tr]:last:border-b-0",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
+ return (
+
+ )
+}
+
+function TableHead({ className, ...props }: React.ComponentProps<"th">) {
+ return (
+ [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCell({ className, ...props }: React.ComponentProps<"td">) {
+ return (
+ | [role=checkbox]]:translate-y-[2px]",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function TableCaption({
+ className,
+ ...props
+}: React.ComponentProps<"caption">) {
+ return (
+
+ )
+}
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+}
diff --git a/src/hooks/use-attendance.ts b/src/hooks/use-attendance.ts
new file mode 100644
index 0000000..9d4398e
--- /dev/null
+++ b/src/hooks/use-attendance.ts
@@ -0,0 +1,193 @@
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
+import axiosClient from "@/lib/axios"
+import { apiEndpoints } from "@/lib/api-endpoints"
+import type { Participant } from "@/types/events"
+import { toast } from "sonner"
+
+// --- Fetch Participants ---
+
+export function useParticipants(eventId: string, scheduleId: string) {
+ return useQuery({
+ queryKey: ["participants", eventId, scheduleId],
+ queryFn: async () => {
+ // Fetch participants for a specific schedule
+ // Note: Endpoint name GET_EVENT_SCHEDULES is confusing but based on usage it seems to be for this
+ const response = await axiosClient.get(
+ apiEndpoints.GET_EVENT_SCHEDULES(eventId, scheduleId)
+ )
+ return response.data as Participant[]
+ },
+ enabled: !!eventId && !!scheduleId,
+ })
+}
+
+// --- Attendance Mutations ---
+
+interface MarkAttendanceParams {
+ studentId: string
+ scheduleId: string
+ // Type of marking action
+ type: "CHECKIN" | "CHECKOUT" | "BOTH"
+ // Is it a Team Event?
+ isTeam: boolean
+ // Marking Type (Solo vs Duo - determines the endpoint logic)
+ markingType: "SOLO" | "DUO"
+}
+
+export function useMarkAttendance() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({
+ studentId,
+ scheduleId,
+ type,
+ isTeam,
+ markingType,
+ }: MarkAttendanceParams) => {
+ let endpoint = ""
+
+ if (markingType === "DUO") {
+ // DUO Logic: Separate IN and OUT endpoints
+ if (isTeam) {
+ if (type === "CHECKIN")
+ endpoint = apiEndpoints.CHECKIN_TEAM_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ else if (type === "CHECKOUT")
+ endpoint = apiEndpoints.CHECKOUT_TEAM_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ } else {
+ // Individual
+ if (type === "CHECKIN")
+ endpoint = apiEndpoints.CHECKIN_INDIVIDUAL_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ else if (type === "CHECKOUT")
+ endpoint = apiEndpoints.CHECKOUT_INDIVIDUAL_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ }
+ } else {
+ // SOLO Logic: One endpoint marks "BOTH" (PRESENT)
+ if (isTeam) {
+ endpoint = apiEndpoints.MARK_TEAM_ATTENDANCE(studentId, scheduleId)
+ } else {
+ endpoint = apiEndpoints.MARK_INDIVIDUAL_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ }
+ }
+
+ if (!endpoint) throw new Error("Invalid marking configuration")
+
+ const response = await axiosClient.post(endpoint)
+ return response.data
+ },
+ onSuccess: (_, variables) => {
+ toast.success("Attendance Marked")
+ // Invalidate queries to refresh the list
+ // We invalidate all participants for this schedule
+ queryClient.invalidateQueries({
+ queryKey: ["participants", variables.scheduleId], // Invalidate broadly or specifically
+ })
+ queryClient.invalidateQueries({
+ queryKey: ["participants"], // Just to be safe for now
+ })
+ },
+ onError: (error) => {
+ console.error("Failed to mark attendance", error)
+ toast.error("Failed to mark attendance")
+ },
+ })
+}
+
+// --- Unmark Attendance Mutations ---
+
+interface UnmarkAttendanceParams {
+ studentId: string
+ scheduleId: string
+ type: "CHECKIN" | "CHECKOUT" | "BOTH"
+ isTeam: boolean
+ markingType: "SOLO" | "DUO"
+}
+
+export function useUnmarkAttendance() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: async ({
+ studentId,
+ scheduleId,
+ type,
+ isTeam,
+ markingType,
+ }: UnmarkAttendanceParams) => {
+ let endpoint = ""
+
+ if (markingType === "DUO") {
+ // DUO Logic
+ if (isTeam) {
+ if (type === "CHECKIN")
+ endpoint = apiEndpoints.UNMARK_TEAM_CHECKIN(studentId, scheduleId)
+ else if (type === "CHECKOUT")
+ endpoint = apiEndpoints.UNMARK_TEAM_CHECKOUT(studentId, scheduleId)
+ } else {
+ // Individual
+ if (type === "CHECKIN")
+ endpoint = apiEndpoints.UNMARK_INDIVIDUAL_CHECKIN(
+ studentId,
+ scheduleId
+ )
+ else if (type === "CHECKOUT")
+ endpoint = apiEndpoints.UNMARK_INDIVIDUAL_CHECKOUT(
+ studentId,
+ scheduleId
+ )
+ }
+ } else {
+ // SOLO Logic
+ if (isTeam) {
+ endpoint = apiEndpoints.UNMARK_TEAM_ATTENDANCE(studentId, scheduleId)
+ } else {
+ endpoint = apiEndpoints.UNMARK_INDIVIDUAL_ATTENDANCE(
+ studentId,
+ scheduleId
+ )
+ }
+ }
+
+ if (!endpoint) throw new Error("Invalid unmarking configuration")
+
+ // Unmarking usually is also a POST or DELETE? Assuming DELETE or POST based on typical conventions,
+ // but generated endpoints imply specific URLs. Most likely these are POST or DELETE.
+ // Checking api-endpoints.ts... it just gives the URL string.
+ // Typically 'unMark' implies a state change, likely DELETE or POST.
+ // I'll stick with POST for now unless I see otherwise in the backend docs (which I don't have).
+ // Or maybe DELETE? `axiosClient.delete(endpoint)`
+ // Let's assume POST for /unMark/... pattern unless commonly RESTful.
+ // Actually, safest is usually POST for RPC-style "unMark" URLs.
+ const response = await axiosClient.post(endpoint)
+ return response.data
+ },
+ onSuccess: (_, { scheduleId }) => {
+ toast.success("Attendance Unmarked")
+ queryClient.invalidateQueries({
+ queryKey: ["participants", scheduleId],
+ })
+ queryClient.invalidateQueries({
+ queryKey: ["participants"],
+ })
+ },
+ onError: (error) => {
+ console.error("Failed to unmark attendance", error)
+ toast.error("Failed to unmark attendance")
+ },
+ })
+}
diff --git a/src/hooks/use-events.ts b/src/hooks/use-events.ts
new file mode 100644
index 0000000..e6f39e4
--- /dev/null
+++ b/src/hooks/use-events.ts
@@ -0,0 +1,17 @@
+import { useQuery } from "@tanstack/react-query"
+import axiosClient from "@/lib/axios"
+import { apiEndpoints } from "@/lib/api-endpoints"
+import type { Event } from "@/types/events"
+
+export function useOrganizerEvents() {
+ return useQuery({
+ queryKey: ["events"],
+ queryFn: async () => {
+ // Fetch organizer events
+ const response = await axiosClient.get(apiEndpoints.GET_ORGANIZER_EVENTS)
+ // We assume the response data is the list of events directly or in a standard wrapper
+ // Adjust this if your API returns { data: [...] } or similiar
+ return response.data as Event[]
+ },
+ })
+}
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index 6308b61..d933a18 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -8,103 +8,216 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
-import { Route as rootRouteImport } from './routes/__root'
-import { Route as ScannerRouteImport } from './routes/scanner'
-import { Route as EventsRouteImport } from './routes/events'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as LoginIndexRouteImport } from './routes/login/index'
+import { Route as rootRouteImport } from "./routes/__root"
+import { Route as ScannerRouteImport } from "./routes/scanner"
+import { Route as EventsRouteImport } from "./routes/events"
+import { Route as IndexRouteImport } from "./routes/index"
+import { Route as LoginIndexRouteImport } from "./routes/login/index"
+import { Route as EventsIndexRouteImport } from "./routes/events/index"
+import { Route as EventsEventIdRouteImport } from "./routes/events/$eventId"
+import { Route as EventsEventIdIndexRouteImport } from "./routes/events/$eventId/index"
+import { Route as EventsEventIdSchedulesScheduleIdRouteImport } from "./routes/events/$eventId/schedules/$scheduleId"
const ScannerRoute = ScannerRouteImport.update({
- id: '/scanner',
- path: '/scanner',
+ id: "/scanner",
+ path: "/scanner",
getParentRoute: () => rootRouteImport,
} as any)
const EventsRoute = EventsRouteImport.update({
- id: '/events',
- path: '/events',
+ id: "/events",
+ path: "/events",
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+ id: "/",
+ path: "/",
getParentRoute: () => rootRouteImport,
} as any)
const LoginIndexRoute = LoginIndexRouteImport.update({
- id: '/login/',
- path: '/login/',
+ id: "/login/",
+ path: "/login/",
getParentRoute: () => rootRouteImport,
} as any)
+const EventsIndexRoute = EventsIndexRouteImport.update({
+ id: "/",
+ path: "/",
+ getParentRoute: () => EventsRoute,
+} as any)
+const EventsEventIdRoute = EventsEventIdRouteImport.update({
+ id: "/$eventId",
+ path: "/$eventId",
+ getParentRoute: () => EventsRoute,
+} as any)
+const EventsEventIdIndexRoute = EventsEventIdIndexRouteImport.update({
+ id: "/",
+ path: "/",
+ getParentRoute: () => EventsEventIdRoute,
+} as any)
+const EventsEventIdSchedulesScheduleIdRoute =
+ EventsEventIdSchedulesScheduleIdRouteImport.update({
+ id: "/schedules/$scheduleId",
+ path: "/schedules/$scheduleId",
+ getParentRoute: () => EventsEventIdRoute,
+ } as any)
export interface FileRoutesByFullPath {
- '/': typeof IndexRoute
- '/events': typeof EventsRoute
- '/scanner': typeof ScannerRoute
- '/login/': typeof LoginIndexRoute
+ "/": typeof IndexRoute
+ "/events": typeof EventsRouteWithChildren
+ "/scanner": typeof ScannerRoute
+ "/events/$eventId": typeof EventsEventIdRouteWithChildren
+ "/events/": typeof EventsIndexRoute
+ "/login/": typeof LoginIndexRoute
+ "/events/$eventId/": typeof EventsEventIdIndexRoute
+ "/events/$eventId/schedules/$scheduleId": typeof EventsEventIdSchedulesScheduleIdRoute
}
export interface FileRoutesByTo {
- '/': typeof IndexRoute
- '/events': typeof EventsRoute
- '/scanner': typeof ScannerRoute
- '/login': typeof LoginIndexRoute
+ "/": typeof IndexRoute
+ "/scanner": typeof ScannerRoute
+ "/events": typeof EventsIndexRoute
+ "/login": typeof LoginIndexRoute
+ "/events/$eventId": typeof EventsEventIdIndexRoute
+ "/events/$eventId/schedules/$scheduleId": typeof EventsEventIdSchedulesScheduleIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
- '/': typeof IndexRoute
- '/events': typeof EventsRoute
- '/scanner': typeof ScannerRoute
- '/login/': typeof LoginIndexRoute
+ "/": typeof IndexRoute
+ "/events": typeof EventsRouteWithChildren
+ "/scanner": typeof ScannerRoute
+ "/events/$eventId": typeof EventsEventIdRouteWithChildren
+ "/events/": typeof EventsIndexRoute
+ "/login/": typeof LoginIndexRoute
+ "/events/$eventId/": typeof EventsEventIdIndexRoute
+ "/events/$eventId/schedules/$scheduleId": typeof EventsEventIdSchedulesScheduleIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
- fullPaths: '/' | '/events' | '/scanner' | '/login/'
+ fullPaths:
+ | "/"
+ | "/events"
+ | "/scanner"
+ | "/events/$eventId"
+ | "/events/"
+ | "/login/"
+ | "/events/$eventId/"
+ | "/events/$eventId/schedules/$scheduleId"
fileRoutesByTo: FileRoutesByTo
- to: '/' | '/events' | '/scanner' | '/login'
- id: '__root__' | '/' | '/events' | '/scanner' | '/login/'
+ to:
+ | "/"
+ | "/scanner"
+ | "/events"
+ | "/login"
+ | "/events/$eventId"
+ | "/events/$eventId/schedules/$scheduleId"
+ id:
+ | "__root__"
+ | "/"
+ | "/events"
+ | "/scanner"
+ | "/events/$eventId"
+ | "/events/"
+ | "/login/"
+ | "/events/$eventId/"
+ | "/events/$eventId/schedules/$scheduleId"
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
- EventsRoute: typeof EventsRoute
+ EventsRoute: typeof EventsRouteWithChildren
ScannerRoute: typeof ScannerRoute
LoginIndexRoute: typeof LoginIndexRoute
}
-declare module '@tanstack/react-router' {
+declare module "@tanstack/react-router" {
interface FileRoutesByPath {
- '/scanner': {
- id: '/scanner'
- path: '/scanner'
- fullPath: '/scanner'
+ "/scanner": {
+ id: "/scanner"
+ path: "/scanner"
+ fullPath: "/scanner"
preLoaderRoute: typeof ScannerRouteImport
parentRoute: typeof rootRouteImport
}
- '/events': {
- id: '/events'
- path: '/events'
- fullPath: '/events'
+ "/events": {
+ id: "/events"
+ path: "/events"
+ fullPath: "/events"
preLoaderRoute: typeof EventsRouteImport
parentRoute: typeof rootRouteImport
}
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
+ "/": {
+ id: "/"
+ path: "/"
+ fullPath: "/"
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/login/': {
- id: '/login/'
- path: '/login'
- fullPath: '/login/'
+ "/login/": {
+ id: "/login/"
+ path: "/login"
+ fullPath: "/login/"
preLoaderRoute: typeof LoginIndexRouteImport
parentRoute: typeof rootRouteImport
}
+ "/events/": {
+ id: "/events/"
+ path: "/"
+ fullPath: "/events/"
+ preLoaderRoute: typeof EventsIndexRouteImport
+ parentRoute: typeof EventsRoute
+ }
+ "/events/$eventId": {
+ id: "/events/$eventId"
+ path: "/$eventId"
+ fullPath: "/events/$eventId"
+ preLoaderRoute: typeof EventsEventIdRouteImport
+ parentRoute: typeof EventsRoute
+ }
+ "/events/$eventId/": {
+ id: "/events/$eventId/"
+ path: "/"
+ fullPath: "/events/$eventId/"
+ preLoaderRoute: typeof EventsEventIdIndexRouteImport
+ parentRoute: typeof EventsEventIdRoute
+ }
+ "/events/$eventId/schedules/$scheduleId": {
+ id: "/events/$eventId/schedules/$scheduleId"
+ path: "/schedules/$scheduleId"
+ fullPath: "/events/$eventId/schedules/$scheduleId"
+ preLoaderRoute: typeof EventsEventIdSchedulesScheduleIdRouteImport
+ parentRoute: typeof EventsEventIdRoute
+ }
}
}
+interface EventsEventIdRouteChildren {
+ EventsEventIdIndexRoute: typeof EventsEventIdIndexRoute
+ EventsEventIdSchedulesScheduleIdRoute: typeof EventsEventIdSchedulesScheduleIdRoute
+}
+
+const EventsEventIdRouteChildren: EventsEventIdRouteChildren = {
+ EventsEventIdIndexRoute: EventsEventIdIndexRoute,
+ EventsEventIdSchedulesScheduleIdRoute: EventsEventIdSchedulesScheduleIdRoute,
+}
+
+const EventsEventIdRouteWithChildren = EventsEventIdRoute._addFileChildren(
+ EventsEventIdRouteChildren
+)
+
+interface EventsRouteChildren {
+ EventsEventIdRoute: typeof EventsEventIdRouteWithChildren
+ EventsIndexRoute: typeof EventsIndexRoute
+}
+
+const EventsRouteChildren: EventsRouteChildren = {
+ EventsEventIdRoute: EventsEventIdRouteWithChildren,
+ EventsIndexRoute: EventsIndexRoute,
+}
+
+const EventsRouteWithChildren =
+ EventsRoute._addFileChildren(EventsRouteChildren)
+
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
- EventsRoute: EventsRoute,
+ EventsRoute: EventsRouteWithChildren,
ScannerRoute: ScannerRoute,
LoginIndexRoute: LoginIndexRoute,
}
diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx
index 24c70ab..3c30000 100644
--- a/src/routes/__root.tsx
+++ b/src/routes/__root.tsx
@@ -1,9 +1,9 @@
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"
-import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
-import { TanStackDevtools } from "@tanstack/react-devtools"
+// import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools"
+// import { TanStackDevtools } from "@tanstack/react-devtools"
import { Toaster } from "@/components/ui/sonner"
-import TanStackQueryDevtools from "../integrations/tanstack-query/devtools"
+// import TanStackQueryDevtools from "../integrations/tanstack-query/devtools"
import type { QueryClient } from "@tanstack/react-query"
@@ -16,7 +16,7 @@ export const Route = createRootRouteWithContext()({
<>
- ()({
},
TanStackQueryDevtools,
]}
- />
+ /> */}
>
),
})
diff --git a/src/routes/events.tsx b/src/routes/events.tsx
index d69bd38..f8563a2 100644
--- a/src/routes/events.tsx
+++ b/src/routes/events.tsx
@@ -1,80 +1,9 @@
-import { useState, useMemo } from "react"
-import { createFileRoute } from "@tanstack/react-router"
-import { CalendarDays } from "lucide-react"
-import { EventCard } from "@/components/events/event-card"
-import { EventsFilter } from "@/components/events/events-filter"
-import { MOOCK_EVENTS } from "@/components/events/mock-data"
-import { Sidebar, SidebarTrigger, useSwipeToOpen } from "@/components/sidebar"
-import type { DayFilter } from "@/types/events"
+import { createFileRoute, Outlet } from "@tanstack/react-router"
export const Route = createFileRoute("/events")({
- component: EventsPage,
+ component: EventsLayout,
})
-function EventsPage() {
- const [searchQuery, setSearchQuery] = useState("")
- const [selectedDay, setSelectedDay] = useState("All")
-
- // Sidebar state with swipe support
- const { isOpen, setIsOpen, close } = useSwipeToOpen()
-
- const filteredEvents = useMemo(() => {
- return MOOCK_EVENTS.filter((event) => {
- const matchesSearch =
- event.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- event.organizer.toLowerCase().includes(searchQuery.toLowerCase())
- const matchesDay = selectedDay === "All" || event.day === selectedDay
-
- return matchesSearch && matchesDay
- })
- }, [searchQuery, selectedDay])
-
- return (
-
- {/* Sidebar - opens on swipe from left edge or menu button click */}
-
-
- {/* Main content - offset by sidebar width on desktop */}
-
- {/* Header with menu button (mobile only) */}
-
-
- setIsOpen(true)} />
-
-
-
- Events
-
-
-
- {/* Filter section - constrained width */}
-
-
-
-
- {/* Cards grid - fills available width */}
-
- {filteredEvents.length > 0 ? (
- filteredEvents.map((event) => (
-
- ))
- ) : (
-
-
- No events found matching your criteria.
-
-
- )}
-
-
-
- )
+function EventsLayout() {
+ return
}
diff --git a/src/routes/events/$eventId.tsx b/src/routes/events/$eventId.tsx
new file mode 100644
index 0000000..e212940
--- /dev/null
+++ b/src/routes/events/$eventId.tsx
@@ -0,0 +1,9 @@
+import { createFileRoute, Outlet } from "@tanstack/react-router"
+
+export const Route = createFileRoute("/events/$eventId")({
+ component: EventLayout,
+})
+
+function EventLayout() {
+ return
+}
diff --git a/src/routes/events/$eventId/index.tsx b/src/routes/events/$eventId/index.tsx
new file mode 100644
index 0000000..bc75253
--- /dev/null
+++ b/src/routes/events/$eventId/index.tsx
@@ -0,0 +1,124 @@
+import { createFileRoute, Link } from "@tanstack/react-router"
+import { ArrowLeft, CalendarDays, Building2, Loader2 } from "lucide-react"
+import { ScheduleCard } from "@/components/events/schedule-card"
+import { MOCK_SCHEDULES } from "@/components/events/mock-data"
+import { useOrganizerEvents } from "@/hooks/use-events"
+import { useEventStore } from "@/store/event-store"
+import { useEffect, useMemo } from "react"
+
+export const Route = createFileRoute("/events/$eventId/")({
+ component: EventDetailPage,
+})
+
+function EventDetailPage() {
+ const { eventId } = Route.useParams()
+ const { setSelectedEvent } = useEventStore()
+
+ // API Fetch
+ const { data: events, isLoading, isError } = useOrganizerEvents()
+
+ const event = useMemo(() => {
+ return events?.find((e) => e.id === eventId)
+ }, [events, eventId])
+
+ // Mock schedules for now as endpoint is ambiguous
+ const schedules = MOCK_SCHEDULES[eventId] ?? []
+
+ // Update store on load
+ useEffect(() => {
+ if (event) {
+ setSelectedEvent(event)
+ }
+ }, [event, setSelectedEvent])
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (isError || (!isLoading && !event)) {
+ return (
+
+
+ {isError ? "Failed to load event" : "Event not found"}
+
+
+ ← Back to Events
+
+
+ )
+ }
+
+ // Safe to assert event is defined here due to check above
+ const safeEvent = event!
+
+ return (
+
+
+ {/* Header */}
+
+
+ {/* Event Info */}
+
+
+ {safeEvent.name}
+
+
+
+
+
+ {safeEvent.organizer}
+
+
+
+ {safeEvent.day}
+
+
+
+
+ {/* Schedules Grid */}
+
+ Schedules
+
+ {schedules.length > 0 ? (
+
+ {schedules.map((schedule) => (
+
+ ))}
+
+ ) : (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/routes/events/$eventId/schedules/$scheduleId.tsx b/src/routes/events/$eventId/schedules/$scheduleId.tsx
new file mode 100644
index 0000000..2877d79
--- /dev/null
+++ b/src/routes/events/$eventId/schedules/$scheduleId.tsx
@@ -0,0 +1,367 @@
+import { createFileRoute, Link } from "@tanstack/react-router"
+import { useState, useMemo, useEffect } from "react"
+import { ArrowLeft, Search, Users, QrCode } from "lucide-react"
+import { useEventStore } from "@/store/event-store"
+import { SoloAttendanceScanner } from "@/components/events/solo-attendance-scanner"
+import { DuoAttendanceScanner } from "@/components/events/duo-attendance-scanner"
+import { MOCK_SCHEDULES, MOOCK_EVENTS } from "@/components/events/mock-data"
+import { Input } from "@/components/ui/input"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { AttendanceActions } from "@/components/events/attendance-actions"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import type { Participant } from "@/types/events"
+
+export const Route = createFileRoute("/events/$eventId/schedules/$scheduleId")({
+ component: ScheduleParticipantPage,
+})
+
+import {
+ useParticipants,
+ useMarkAttendance,
+ useUnmarkAttendance,
+} from "@/hooks/use-attendance"
+import { Loader2 } from "lucide-react"
+
+// ... imports ...
+
+function ScheduleParticipantPage() {
+ const { eventId, scheduleId } = Route.useParams()
+ const { selectedEvent, setSelectedEvent, setSelectedSchedule } =
+ useEventStore()
+
+ // State for search and pagination
+ const [searchQuery, setSearchQuery] = useState("")
+ const [page, setPage] = useState(1)
+ const itemsPerPage = 10
+
+ // API Fetch
+ const {
+ data: participantsData,
+ isLoading: isLoadingParticipants,
+ isError: isErrorParticipants,
+ } = useParticipants(eventId, scheduleId)
+ const { mutate: markAttendance } = useMarkAttendance()
+ const { mutate: unmarkAttendance } = useUnmarkAttendance()
+
+ if (isErrorParticipants) {
+ // Simple error handling
+ console.error("Failed to fetch participants")
+ }
+
+ // Simulate Data Fetching for Event/Schedule (or use store if already populated)
+ // We use store or mock fetch for metadata as before, but participants come from API
+ const event = MOOCK_EVENTS.find((e) => e.id === eventId)
+ const schedule = MOCK_SCHEDULES[eventId]?.find((s) => s.id === scheduleId)
+
+ // Local state for participants to handle optimistic updates or just syncing
+ const [participants, setParticipants] = useState([])
+ const [showScanner, setShowScanner] = useState(false)
+
+ useEffect(() => {
+ // Reset or Fetch data
+ if (event) setSelectedEvent(event)
+ if (schedule) setSelectedSchedule(schedule)
+
+ // Sync participants from API
+ if (participantsData) {
+ setParticipants(participantsData)
+ }
+ }, [
+ eventId,
+ scheduleId,
+ event,
+ schedule,
+ setSelectedEvent,
+ setSelectedSchedule,
+ participantsData,
+ ])
+
+ // Filter Logic
+ const filteredParticipants = useMemo(() => {
+ return participants.filter(
+ (p) =>
+ p.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ p.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ (p.teamName &&
+ p.teamName.toLowerCase().includes(searchQuery.toLowerCase()))
+ )
+ }, [participants, searchQuery])
+
+ // Pagination Logic
+ const totalPages = Math.ceil(filteredParticipants.length / itemsPerPage)
+ const paginatedParticipants = filteredParticipants.slice(
+ (page - 1) * itemsPerPage,
+ page * itemsPerPage
+ )
+
+ // Handlers
+ const handleMark = (
+ participantId: string,
+ type: "CHECKIN" | "CHECKOUT" | "BOTH"
+ ) => {
+ if (!schedule) return
+
+ const studentId = participantId // Assuming participantId is the studentId
+ const isTeam = schedule.type === "GROUP"
+ const markingType = schedule.markingType
+
+ markAttendance({
+ studentId,
+ scheduleId,
+ type,
+ isTeam,
+ markingType,
+ })
+ }
+
+ const handleUnmark = (
+ participantId: string,
+ type: "CHECKIN" | "CHECKOUT" | "BOTH"
+ ) => {
+ if (!schedule) return
+
+ const studentId = participantId
+ const isTeam = schedule.type === "GROUP"
+ const markingType = schedule.markingType
+
+ unmarkAttendance({
+ studentId,
+ scheduleId,
+ type,
+ isTeam,
+ markingType,
+ })
+ }
+
+ if (isLoadingParticipants) {
+ return (
+
+
+
+ )
+ }
+
+ // If no schedule found (e.g. direct link)
+ if (!schedule) {
+ return (
+ Loading or Schedule Not Found...
+ )
+ }
+
+ const isGroup = schedule.type === "GROUP"
+ const isDuo = schedule.markingType === "DUO"
+
+ return (
+
+
+ {/* Navigation */}
+
+
+
+
+
+
+ {selectedEvent?.name}
+
+
+ {schedule.title}
+
+
+
+
+ {/* Controls: Search */}
+
+
+
+ setSearchQuery(e.target.value)}
+ />
+
+
+
+ {/* Participant Table */}
+
+
+
+
+ Participant
+ {/* Removed Team Column */}
+
+ Status
+
+
+ Actions
+
+
+
+
+ {paginatedParticipants.length > 0 ? (
+ paginatedParticipants.map((p) => (
+
+
+
+
+ {p.name}
+
+ {p.email}
+
+ {/* Team Name badge moved here */}
+ {isGroup && p.teamName && (
+
+
+
+ {p.teamName}
+
+
+ )}
+
+
+
+
+
+ {isDuo ? (
+ // DUO Status badges
+
+
+ IN
+
+
+ OUT
+
+
+ ) : (
+ // SOLO Status badge
+
+ {p.attendanceStatus ? "PRESENT" : "ABSENT"}
+
+ )}
+
+
+
+ handleMark(p.id, type)}
+ onUnmark={(type) => handleUnmark(p.id, type)}
+ />
+
+
+ ))
+ ) : (
+
+
+ No participants found.
+
+
+ )}
+
+
+
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+
+ Page {page} of {totalPages}
+
+
+
+ )}
+ {/* Floating Scanner Button */}
+
+
+ {/* Scanner Modal */}
+ {showScanner &&
+ (isDuo ? (
+ setShowScanner(false)}
+ isTeamEvent={isGroup}
+ />
+ ) : (
+ setShowScanner(false)}
+ isTeamEvent={isGroup}
+ />
+ ))}
+
+
+ )
+}
diff --git a/src/routes/events/index.tsx b/src/routes/events/index.tsx
new file mode 100644
index 0000000..f4675b4
--- /dev/null
+++ b/src/routes/events/index.tsx
@@ -0,0 +1,117 @@
+import { useState, useMemo } from "react"
+import { createFileRoute, useNavigate } from "@tanstack/react-router"
+import { CalendarDays, LogOut, Loader2 } from "lucide-react"
+import { EventCard } from "@/components/events/event-card"
+import { EventsFilter } from "@/components/events/events-filter"
+import { useOrganizerEvents } from "@/hooks/use-events"
+import { Button } from "@/components/ui/button"
+import type { DayFilter } from "@/types/events"
+
+export const Route = createFileRoute("/events/")({
+ component: EventsPage,
+})
+
+function EventsPage() {
+ const navigate = useNavigate()
+ const [searchQuery, setSearchQuery] = useState("")
+ const [selectedDay, setSelectedDay] = useState("All")
+
+ const { data: events, isLoading, isError } = useOrganizerEvents()
+
+ const filteredEvents = useMemo(() => {
+ if (!events) return []
+ return events.filter((event) => {
+ const matchesSearch =
+ event.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
+ event.organizer.toLowerCase().includes(searchQuery.toLowerCase())
+ const matchesDay = selectedDay === "All" || event.day === selectedDay
+
+ return matchesSearch && matchesDay
+ })
+ }, [searchQuery, selectedDay, events])
+
+ const handleLogout = () => {
+ // TODO: Implement actual logout logic
+ navigate({ to: "/login" })
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (isError) {
+ return (
+
+ Failed to load events.
+
+
+ )
+ }
+
+ return (
+
+ {/* Main content - Centered max width container */}
+
+ {/* Header */}
+
+
+
+
+ Events
+
+
+
+ {/* Logout Button */}
+
+
+
+ {/* Filter section */}
+
+
+
+
+ {/* Cards grid */}
+
+ {filteredEvents.length > 0 ? (
+ filteredEvents.map((event) => (
+
+ ))
+ ) : (
+
+
+ No events found matching your criteria.
+
+
+ )}
+
+
+
+ )
+}
diff --git a/src/store/event-store.ts b/src/store/event-store.ts
new file mode 100644
index 0000000..89cd019
--- /dev/null
+++ b/src/store/event-store.ts
@@ -0,0 +1,29 @@
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
+import type { Event, Schedule } from "@/types/events"
+
+interface EventStoreState {
+ selectedEvent: Event | null
+ setSelectedEvent: (event: Event | null) => void
+
+ selectedSchedule: Schedule | null
+ setSelectedSchedule: (schedule: Schedule | null) => void
+
+ // Optional: Cache for participants if needed, but Query is better for this
+ // kept simple for now
+}
+
+export const useEventStore = create()(
+ persist(
+ (set) => ({
+ selectedEvent: null,
+ setSelectedEvent: (event) => set({ selectedEvent: event }),
+
+ selectedSchedule: null,
+ setSelectedSchedule: (schedule) => set({ selectedSchedule: schedule }),
+ }),
+ {
+ name: "event-store", // unique name for localStorage
+ }
+ )
+)
diff --git a/src/types/events.ts b/src/types/events.ts
index ad21bed..e119c57 100644
--- a/src/types/events.ts
+++ b/src/types/events.ts
@@ -4,6 +4,35 @@ export interface Event {
name: string
organizer: string
day: "20 Feb" | "21 Feb"
+ type: "INDIVIDUAL" | "GROUP" // <--- Added for badge on Event Card
+ markingType?: "SOLO" | "DUO" // <--- Optional, good for context
+}
+
+// Schedule entity type
+export interface Schedule {
+ id: string
+ title: string
+ venue: string
+ startTime: string
+ endTime: string
+ date: string
+ eventId: string
+ type: "INDIVIDUAL" | "GROUP"
+ markingType: "SOLO" | "DUO"
+}
+
+// Participant entity type
+export interface Participant {
+ id: string
+ name: string
+ email: string
+ teamName?: string // Only for Group events
+ // Attendance status
+ // For SOLO: "PRESENT" | "ABSENT"
+ // For DUO: checkInStatus, checkOutStatus
+ checkInStatus: boolean
+ checkOutStatus: boolean
+ attendanceStatus: boolean // For SOLO marking, typically maps to this
}
// Event card props
|