Frontend Architecture¶
Understanding the structure, patterns, and key technical decisions behind the Next.js frontend.
System Overview¶
The ai-tutor-ui is a Next.js 16 application that serves as the student-facing and admin-facing interface for the AI tutoring platform. It communicates exclusively with the FastAPI backend over HTTP.
Key Characteristics¶
- App Router: Uses Next.js App Router (not the legacy Pages Router)
- TypeScript strict mode: All code is fully typed;
strict: truein tsconfig - Component-driven: UI built from shadcn/ui primitives composed into feature components
- Course-level i18n: Localization scoped to course content, not the app shell
- Token-based auth: JWT access + refresh tokens managed client-side
- Markdown + math: Lesson content renders GitHub-flavored Markdown with LaTeX math via KaTeX
Technology Stack¶
| Concern | Choice |
|---|---|
| Framework | Next.js 16 (App Router) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS + CSS custom properties (HSL theming) |
| Component library | shadcn/ui (Radix UI primitives + Tailwind) |
| Forms | React Hook Form + Zod |
| i18n | Custom useCourseTranslations hook (next-intl installed but not used for app shell) |
| Testing | Vitest + React Testing Library + MSW |
| Markdown | react-markdown + remark-gfm + remark-math + rehype-katex |
| Icons | Lucide React |
Architecture Diagram¶
graph TB
Browser[Browser]
NextJS[Next.js App Router]
Pages[Pages & Layouts]
Components[React Components]
Hooks[Custom Hooks]
APILayer[API Layer - lib/api/]
AuthClient[Auth Client - apiFetch]
Contexts[React Contexts]
Backend[FastAPI Backend :9898]
Browser --> NextJS
NextJS --> Pages
Pages --> Components
Components --> Hooks
Components --> Contexts
Components --> APILayer
APILayer --> AuthClient
AuthClient --> Backend
Project Structure¶
ai-tutor-ui/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout (fonts, providers, toaster)
│ ├── page.tsx # Home/landing page
│ ├── signin/ # Sign in page
│ ├── signup/ # Registration page
│ ├── signup-onboarding/ # Post-registration onboarding
│ ├── dashboard/ # Student dashboard
│ ├── courses/[id]/ # Course detail + modules
│ ├── notebook/ # Student notebook
│ ├── admin/ # Admin panel (courses, users, prompts, analytics, etc.)
│ ├── share/ # Public shared chat sessions
│ ├── chat-debug/ # Chat debugging tools
│ └── globals.css # CSS custom properties (theme tokens)
├── components/
│ ├── ui/ # shadcn/ui base components (button, card, dialog...)
│ ├── auth/ # Authentication components (RoleGuard)
│ ├── admin/ # Admin panel components
│ ├── dashboard/ # Dashboard components (CourseCarousel)
│ ├── features/ # Feature-specific components (CourseDialog, ModuleTree...)
│ ├── home/ # Landing page components
│ ├── notebook/ # Notebook components
│ ├── onboarding/ # Onboarding flow components
│ └── *.tsx # Shared components (Header, MessageContent, LessonView...)
├── hooks/ # Custom React hooks
│ ├── use-debounce.ts
│ └── use-toast.ts
├── lib/
│ ├── api/ # API layer — one file per resource (courses.ts, users.ts, modules.ts...)
│ ├── authClient.ts # Core API client: apiFetch(), token management
│ ├── api-client.ts # Server-side API helpers (public course listing)
│ ├── contexts/ # React contexts (RoleProvider, LanguageDirectionContext)
│ ├── i18n/ # Internationalization (useCourseTranslations, config, CourseI18nProvider)
│ ├── validations/ # Zod schemas for form validation (auth, course, module, content, onboarding)
│ ├── utils.ts # Shared utilities (cn() for class merging)
│ └── *.ts # Feature utilities (lesson-parser, parse-chat-response, theme-config, courseStyles)
├── types/ # TypeScript type definitions (auth, course, role, notebook, interactive-ui, llm-provider)
├── messages/ # i18n translation files (en.json, ar.json)
├── tests/ # Test infrastructure (setup.ts, test-utils.tsx, mocks/)
└── public/ # Static assets
API Layer¶
All backend communication flows through a single client in lib/authClient.ts.
Core Client: apiFetch¶
apiFetch<T>(path, options) is the single entry point for every API call. It handles:
- Attaching the JWT access token as a
Bearerheader - Automatic token refresh when a 401 is returned
- JSON serialization of request bodies
- Structured error handling via the
ApiErrorclass
// ApiError carries the HTTP status and parsed response body
class ApiError extends Error {
status: number;
data: unknown;
}
Tokens are stored in localStorage under two keys:
ai_tutor_access_token— short-lived JWT for API requestsai_tutor_refresh_token— long-lived token used to obtain a new access token
Resource Modules¶
lib/api/ contains one file per backend resource. Each file exports plain async functions that call apiFetch:
lib/api/
├── courses.ts # listCourses(), getCourse(id), createCourse(data)...
├── users.ts # getMe(), updateUser(id, data)...
├── modules.ts # listModules(courseId), createModule(data)...
├── content.ts # getContentItem(id), createContent(data)...
└── prompts.ts # listPrompts(), createPrompt(data)...
Components import directly from these modules. There's no global state layer for server data — components fetch what they need and manage loading/error state locally or via React context.
Authentication & Authorization¶
Token Flow¶
- User signs in or registers
- Backend returns an access token and a refresh token
- Both tokens are written to
localStorage - Every
apiFetchcall reads the access token and attaches it asAuthorization: Bearer <token> - On a 401 response,
apiFetchcalls the refresh endpoint, stores the new tokens, and retries the original request
Role System¶
RoleProvider (in lib/contexts/) wraps the entire app. On mount it fetches the current user's roles from the backend and makes them available via the useRoles() hook.
The role hierarchy from lowest to highest privilege:
RoleGuard is a component wrapper for conditional rendering based on role checks:
<RoleGuard requireInstructor>
<AdminControls />
</RoleGuard>
{/* Or with an explicit role list */}
<RoleGuard requiredRoles={["instructor", "department_admin"]}>
<AdminControls />
</RoleGuard>
RoleProvider also listens for storage events so that role state stays consistent across browser tabs.
Theming & Styling¶
CSS Custom Properties¶
app/globals.css defines all color tokens as HSL values:
:root {
--primary: 38 92% 50%;
--secondary: 210 40% 96.1%;
--muted: 210 40% 96.1%;
--accent: 38 100% 86%;
--destructive: 0 84.2% 60.2%;
}
.dark {
--primary: 217.2 91.2% 59.8%;
/* ... */
}
Dark mode is toggled by adding the .dark class to the <html> element.
Tailwind + shadcn/ui¶
Tailwind is configured with semantic color names that map to the CSS custom properties:
// tailwind.config.js
colors: {
primary: "hsl(var(--primary))",
secondary: "hsl(var(--secondary))",
// ...
}
shadcn/ui components live in components/ui/ as copied source files (not an npm package). They use class-variance-authority (cva) for variant-based styling, clsx for conditional classes, and tailwind-merge to resolve conflicts. The cn() utility in lib/utils.ts combines all three.
Fonts¶
- Inter — body text
- Patrick Hand — decorative/display use
- IBM Plex Sans Arabic — RTL content
Custom Animations¶
globals.css defines custom keyframes used across the app: accordion-down, accordion-up, fade-in, and loader-reveal.
Internationalization (i18n)¶
Course-Level Localization¶
The app shell (navigation, buttons, labels) is in English. Localization applies at the course level: each course has a language_code, and content within that course is rendered in that language.
This means i18n is not configured globally in Next.js. Instead, components that render course content call useCourseTranslations(languageCode).
useCourseTranslations¶
const { t, isRTL, direction, formatNumber } = useCourseTranslations(languageCode);
t("lesson", "progress") // looks up messages/en.json or messages/ar.json
isRTL // true for Arabic, Hebrew, Farsi, Urdu
direction // "rtl" | "ltr"
formatNumber(42) // "٤٢" for ar locale, "42" otherwise
Translation files live in messages/en.json and messages/ar.json.
RTL Support¶
RTL layout is applied conditionally based on isRTL. The patterns to follow:
flex-row-reversewithisRTLto swap flex item order- Logical CSS properties (
ms-*,me-*,ps-*,pe-*) instead of physical (ml-*,mr-*) rtl-flipclass on directional icons (chevrons, arrows)
See ai-tutor-ui/docs/i18n-rtl-guide.md for the full RTL pattern reference.
Design Patterns¶
Form Validation¶
Forms use React Hook Form with Zod schemas defined in lib/validations/. The schema is the single source of truth for both runtime validation and TypeScript types:
// lib/validations/auth.ts
const signInSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type SignInData = z.infer<typeof signInSchema>;
Component Composition¶
Feature components are built by composing shadcn/ui primitives from components/ui/. The pattern is:
components/ui/— unstyled or minimally styled primitives (Button, Card, Dialog...)components/features/— domain components that compose primitives (CourseDialog, ModuleTree...)app/pages — assemble feature components into full pages
Markdown Rendering¶
Lesson content and chat messages are rendered with react-markdown. The pipeline:
remark-gfm— GitHub-flavored Markdown (tables, strikethrough, task lists)remark-math— parses$...$and$$...$$math delimitersrehype-katex— renders parsed math as KaTeX HTML
Key Design Decisions¶
localStorage vs httpOnly Cookies for Tokens¶
Chose: localStorage
Pros: - Simple to implement in a client-rendered SPA - No server-side cookie handling needed - Works across subdomains without configuration
Cons: - Accessible to JavaScript (XSS risk) - Requires manual token attachment on every request
Mitigation: Short-lived access tokens (30 min), automatic refresh
Course-Level vs App-Level i18n¶
Chose: Course-level localization
Reasoning: The app UI is English-only. Only course content (lesson text, quiz labels, chat responses) needs localization. Applying next-intl globally would add complexity without benefit for the app shell. The useCourseTranslations hook keeps localization scoped to where it's actually needed.
shadcn/ui vs a Full Component Library¶
Chose: shadcn/ui
Pros: - Components are copied into the project — full ownership, no version lock-in - Built on Radix UI primitives (accessible by default) - Tailwind-native styling fits the existing setup - Easy to customize without fighting library internals
Cons: - Updates require manual re-copying of components - More files in the repo
App Router vs Pages Router¶
Chose: App Router
Reasoning: Next.js recommends App Router for new projects. It enables React Server Components, nested layouts, and colocated loading/error states. The project uses it primarily for its layout and routing capabilities rather than server-side data fetching.