frontend architecture and routing
Next.js 16 App Router pages, shared layout, and nuqs for schedule query state.
See Schedule generation, Timeline view, Academic calendar, Compare timetables, Mess menu. State: 3.5. URLs: 9.3.
Entry#
website/app/layout.tsx sets metadata, PWA manifest, Google scripts, Geist fonts, then wraps children in Providers and a sidebar + main column.
flowchart TD Root[app/layout.tsx] --> Prov[Providers] Prov --> QC[QueryClientProvider] QC --> PH[PostHogProvider] PH --> BG[Background] BG --> UC[UserContextProvider] UC --> NQ[NuqsAdapter] NQ --> Nav[Navbar] NQ --> Main[page children] NQ --> Analytics NQ --> Toaster
| Provider | Job |
|---|---|
| QueryClientProvider | /api/* cache, no refetch on focus |
| PostHogProvider | NEXT_PUBLIC_POSTHOG_KEY, host /ph |
| Background | Visual layer |
| UserContextProvider | schedule / editedSchedule |
| NuqsAdapter | nuqs/adapters/next/app |
There is no BrowserRouter. Navigation uses next/link and useRouter.
Routes#
flowchart LR layout[layout.tsx] --> home["/ HomeContent"] layout --> tl["/timeline"] layout --> ac["/academic-calendar"] layout --> ex["/exam-schedule"] layout --> mm["/mess-menu"] layout --> cmp["/compare-timetables"] layout --> doc["/api-doc"] layout --> nf["not-found.tsx"]
| Path | File | UI |
|---|---|---|
/ |
app/page.tsx |
HomeContent |
/timeline |
app/timeline/page.tsx |
TimelineWrapper |
/academic-calendar |
app/academic-calendar/page.tsx |
CalendarContent |
/exam-schedule |
app/exam-schedule/page.tsx |
ExamContent |
/mess-menu |
app/mess-menu/page.tsx |
MenuContent |
/compare-timetables |
app/compare-timetables/page.tsx |
compare page |
/api-doc |
app/api-doc/page.tsx |
Swagger UI |
| API | app/api/*/route.ts |
JSON |
vercel.json only rewrites PostHog. App Router handles the rest. Catch-all 404 is app/not-found.tsx.
Navigation#
Tabs in website/components/layout/navbar.tsx:
export const tabs = [
{ label: "Create Timetable", mobileLabel: "Create", path: "/", icon: Calendar },
{ label: "Timeline", mobileLabel: "Timeline", path: "/timeline", icon: Clock },
{ label: "Mess Menu", mobileLabel: "Menu", path: "/mess-menu", icon: Utensils },
{ label: "Academic Calendar", mobileLabel: "Calendar", path: "/academic-calendar", icon: Calendar },
{ label: "Exam Schedule", mobileLabel: "Exams", path: "/exam-schedule", icon: BookOpen },
{ label: "Compare Timetables", mobileLabel: "Compare", path: "/compare-timetables", icon: GitCompare },
];
flowchart TB tabs[tabs array] tabs --> Desk["Desktop sidebar w-64"] tabs --> Mob["MobileNavbar bottom pill"] tabs --> Swipe["6px edge swipe, delta 50"]
Swipe is off on /timeline so it does not fight the calendar. Desktop is md:flex fixed left. Mobile is md:hidden bottom bar. Main content uses md:ml-64 and pb-24 on small screens.
Active tab: exact match for /, prefix match otherwise.
URL state#
sequenceDiagram participant URL participant nuqs participant Form as ScheduleForm participant Home as HomeContent URL->>nuqs: year batch campus selectedSubjects nuqs->>Form: useQueryState Home->>Home: if params and no cache, auto-generate Home->>Home: if params != cachedScheduleParams, UrlParamsDialog
UrlParamsDialog actions: generate from URL, prefill form, or keep the cached schedule.
Layout#
flowchart TB Body[body] Body --> Flex[flex min-h-screen] Flex --> Side[Navbar 256px] Flex --> Main[main flex-1 md:ml-64] Body --> Bottom[MobileNavbar]
State vs route#
| Route | Context |
|---|---|
/ |
Read/write schedule, persist cachedSchedule |
/timeline |
Read editedSchedule || schedule |
/compare-timetables |
Local page state, can load classConfigs |
/academic-calendar, /exam-schedule, /mess-menu |
Independent queries |
Only home writes the shared schedule. Timeline hydrates from cachedSchedule if context is empty.