Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/app/(profile)/my-schedule/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { eq } from "drizzle-orm";

import db from "@/db";
import { eventAttendees } from "@/db/schema";
import ListView from "@/features/toggles/ToggleViews/ListView";
import { auth } from "@/lib/auth";
import { getUpcomingEvents } from "@/lib/events";

export default async function MySchedulePage(): Promise<React.ReactElement> {
const session = await auth();
const userId = session?.user?.id;

if (!userId) {
return (
<>
{}
<Box
sx={{
backgroundColor: "secondary.main",
color: "white",
px: 10,
py: 5,
width: "100%",
}}
>
<Typography variant="h4" sx={{ fontWeight: 400 }}>
My Schedule
</Typography>
</Box>

{/* PAGE CONTENT */}
<Box
sx={{
width: "100%",
px: 10,
py: 5,
}}
>
<Typography>Please Sign In</Typography>
</Box>
</>
);
}

const events = await getUpcomingEvents();

const registrations = await db
.select({ eventId: eventAttendees.eventId })
.from(eventAttendees)
.where(eq(eventAttendees.userId, userId));

const registeredSet = new Set(registrations.map((r) => r.eventId));

const registeredEvents = events
.filter((e) => registeredSet.has(e.id))
.map((e) => ({
...e,
isRegistered: true,
}));

return (
<>
{/* FULL-WIDTH HEADER (same as Update Profile) */}
<Box
sx={{
backgroundColor: "secondary.main",
color: "white",
px: 10,
py: 5,
width: "100%",
}}
>
<Typography variant="h4" sx={{ fontWeight: 400 }}>
My Schedule
</Typography>
</Box>

{/* PAGE CONTENT */}
<Box
sx={{
width: "100%",
px: 10,
py: 5,
}}
>
{registeredEvents.length === 0 ? (
<Typography>You are not registered for any events yet.</Typography>
) : (
<ListView events={registeredEvents} />
)}
</Box>
</>
);
}
31 changes: 31 additions & 0 deletions src/components/layout/Header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Toolbar from "@mui/material/Toolbar";
import Typography from "@mui/material/Typography";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useSession } from "next-auth/react";
import * as React from "react";

Expand All @@ -17,6 +18,9 @@ export default function Header(): React.ReactElement {
const canCreate = role === "admin" || role === "manager";
const headerColor = { admin: "secondary.main", manager: "#276636" };
const fontColor = { admin: "#ffffff", manager: "#ffffff" };
const pathname = usePathname();
const isEvents = pathname === "/";
const isMySchedule = pathname === "/my-schedule";
const appBarSx =
role && role in headerColor
? { backgroundColor: headerColor[role as keyof typeof headerColor] }
Expand Down Expand Up @@ -88,6 +92,33 @@ export default function Header(): React.ReactElement {
</Typography>
</Box>
</Box>
<Box sx={{ display: "flex", alignItems: "center", gap: 4, mr: 4 }}>
<Link href="/" style={{ textDecoration: "none" }}>
<Typography
sx={{
fontSize: ".9rem",
fontWeight: isEvents ? 700 : 400,
color: isEvents ? "#22305B" : "#6c6f79",
cursor: "pointer",
}}
>
Events
</Typography>
</Link>

<Link href="/my-schedule" style={{ textDecoration: "none" }}>
<Typography
sx={{
fontSize: ".9rem",
fontWeight: isMySchedule ? 700 : 400,
color: isMySchedule ? "#22305B" : "#6c6f79",
cursor: "pointer",
}}
>
My Schedule
</Typography>
</Link>
</Box>

<Box sx={{ display: "flex", alignItems: "center" }}>
{canCreate && (
Expand Down
4 changes: 4 additions & 0 deletions src/features/home/components/EventCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import LocationPinIcon from "@mui/icons-material/LocationPin";
import PersonIcon from "@mui/icons-material/Person";
import QueryBuilderIcon from "@mui/icons-material/QueryBuilder";
import { Box, Card, CardContent, Typography } from "@mui/material";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import * as React from "react";

Expand Down Expand Up @@ -31,6 +32,8 @@ export default function VolunteerEventCard(
): React.ReactElement {
const { status } = useSession();

const router = useRouter();

const timeRange = getTimeRange(
event.eventDate,
event.startTime,
Expand Down Expand Up @@ -79,6 +82,7 @@ export default function VolunteerEventCard(
setIsRegistered(true);
setRegisteredUsers((prev) => prev + 1);
}
router.refresh();
} catch (error) {
if (error instanceof Error) {
if (error.message === "Event capacity reached") {
Expand Down
32 changes: 32 additions & 0 deletions src/lib/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,35 @@ export async function getUpcomingEvents(): Promise<
)
.orderBy(asc(events.eventDate));
}

export async function getUserRegisteredEvents(
userId: string,
): Promise<(typeof events.$inferSelect)[]> {
const today = new Date();
const threeMonthsFromNow = new Date(today);
threeMonthsFromNow.setMonth(today.getMonth() + 3);

const todayStr = today.toISOString().split("T")[0];
const threeMonthsStr = threeMonthsFromNow.toISOString().split("T")[0];

const registrations = await db
.select({ eventId: eventAttendees.eventId })
.from(eventAttendees)
.where(eq(eventAttendees.userId, userId));

if (registrations.length === 0) return [];

const eventIds = registrations.map((r) => r.eventId);

return db
.select()
.from(events)
.where(
and(
inArray(events.id, eventIds),
gte(events.eventDate, todayStr),
lte(events.eventDate, threeMonthsStr),
),
)
.orderBy(asc(events.eventDate));
}
Loading