-
Notifications
You must be signed in to change notification settings - Fork 2
File Structure
This page explains how the TTS frontend codebase is organised and what each folder is for. If you are new to React, read the React primer section first — it will make everything else click faster.
TTS is a React app written in TypeScript. React lets you build UIs out of reusable components — functions that return HTML-like syntax called JSX (or TSX in TypeScript).
A component is just a function that returns what should appear on screen:
function Hello({ name }: { name: string }) {
return <p>Hello, {name}!</p>
}You can nest components like HTML tags:
<Hello name="Ana" />State is data that can change over time. When it changes, React re-renders the component.
const [count, setCount] = useState(0)
// count is the current value
// setCount is the function you call to change itUse this to run code after the component renders — for example, to react when a value changes:
useEffect(() => {
document.title = `Count: ${count}`
}, [count]) // runs every time `count` changesIf the dependency array is empty [], the effect runs only once when the component first mounts.
Passing props through many layers of components gets messy. A Context lets you share data across the whole component tree without prop-drilling.
// 1. Create the context
const ThemeContext = createContext('light')
// 2. Wrap your tree with a Provider
<ThemeContext.Provider value="dark">
<App />
</ThemeContext.Provider>
// 3. Read it anywhere inside the tree
const theme = useContext(ThemeContext)TTS uses the SWR library to fetch data from the backend. It handles loading state, caching, and re-fetching automatically:
const { data, isLoading } = useSWR('/api/courses', fetcher)A custom hook is a function whose name starts with use. It wraps useState, useEffect, etc. into a reusable unit:
function useCourseUnits(majorId: number | null) {
const { data, isLoading } = useSWR(
majorId ? `/api/courses/${majorId}` : null,
fetcher
)
return { courseUnits: data ?? [], loading: isLoading }
}tts-fe/
├── src/
│ ├── @types/ # TypeScript type definitions
│ ├── api/ # Functions that call the backend
│ ├── components/ # Reusable UI pieces
│ ├── contexts/ # Global state shared across the app
│ ├── hooks/ # Custom React hooks
│ ├── pages/ # One file per route/page
│ ├── styles/ # Global CSS (Tailwind config lives here)
│ └── utils/ # Pure utility functions (no React)
├── public/ # Static assets (images, icons)
├── .env # Environment variables (not committed)
└── vite.config.ts # Build configuration
Contains the shared type definitions used across the whole app. If you see a type like CourseUnit or CourseOption and want to know its shape, look here.
Example: src/@types/index.d.ts exports all the main types.
When to touch this: When you add a new data structure that more than one file will use.
Each file in here groups related API calls. These are plain async functions — no React, no hooks. They use fetch (or a thin wrapper) to call the Django backend.
// src/api/courses.ts
export async function getCoursesByMajorId(majorId: number) {
const res = await fetch(`/api/courses/${majorId}`)
return res.json()
}When to touch this: When you need to call a new or changed backend endpoint.
Hooks wrap API calls and state logic so components stay clean. A hook fetches data, tracks loading state, and returns the result — the component just calls the hook.
// src/hooks/useCourseUnits.tsx
export default function useCourseUnits(majorId: number | null) {
const { data, isLoading } = useSWR(
majorId ? getCoursesByMajorId(majorId) : null
)
return { courseUnits: data ?? [], loading: isLoading }
}When to touch this: When you need to fetch data and want to reuse that logic in more than one component.
Contexts hold state that many components need at the same time — things like the currently selected timetable option, the list of picked courses, or the theme.
Each context has two files:
-
FooContext.ts— creates the context object and its TypeScript type -
FooProvider.tsx— the Provider component that holds the actualuseStateand passes data down
CombinedProvider.tsx wraps all providers together so main.tsx stays clean.
When to touch this: When you add state that needs to be shared across pages or deeply nested components.
This is the biggest folder. Components are reusable pieces of UI. They receive data via props (or contexts) and render HTML.
The folder is organised by feature area:
components/
├── planner/ # The main timetable planner UI
│ ├── sidebar/ # Left panel (course picker, option switcher)
│ └── schedule/ # The timetable grid
├── ui/ # Generic reusable UI (Button, Dialog, Skeleton…)
├── admin/ # Admin dashboard components
└── svgs/ # SVG illustrations used in the UI
When to touch this: Almost always. Every visible UI change lives here.
Each file here corresponds to a URL route. Pages are thin — they compose components together and pass top-level data down. Business logic belongs in hooks or contexts, not in pages.
pages/
├── PlannerPage.tsx # /planner
├── AdminPage.tsx # /admin
└── LoginPage.tsx # /login
When to touch this: When you add a new route, or need to change what a page renders at the top level.
Utility functions with no React dependency — date formatting, local storage helpers, analytics tracking, etc.
When to touch this: When you have logic that doesn't depend on React state and could be tested in isolation.
A typical data flow looks like this:
pages/PlannerPage.tsx
└── components/planner/sidebar/CoursePicker.tsx
└── hooks/useCourseUnits.tsx ← fetches data
└── api/courses.ts ← calls the backend
And shared state flows like this:
contexts/CombinedProvider.tsx
└── contexts/MultipleOptionsContext.tsx
└── (any component) useContext(MultipleOptionsContext)
High-level component map:

