Marketing and student-registration site for Academic Improvement Solutions, a South African tutoring service offering CAPS & IEB Mathematics and Physical Sciences lessons (online via Microsoft Teams and one-on-one in person).
It is a single-page React site: one scrolling landing page with anchored sections, plus a 404 route. There is no backend — registrations and enquiries are handed off to WhatsApp.
| Framework | React 18 + TypeScript |
| Build | Vite 5 (@vitejs/plugin-react-swc) |
| Styling | Tailwind CSS 3 with CSS-variable design tokens |
| Components | shadcn/ui (Radix primitives) in src/components/ui/ |
| Forms | react-hook-form + zod |
| Routing | react-router-dom (BrowserRouter, no basename) |
| Unit tests | Vitest + Testing Library (jsdom) |
| E2E | Playwright |
| Hosting | GitHub Pages via GitHub Actions |
@tanstack/react-query is wired up in App.tsx but not yet used — there is no data fetching today.
Requires Node 20+ (CI builds on Node 24). npm and bun lockfiles are both committed; CI uses npm ci.
npm install
npm run dev # http://localhost:8080| Command | Description |
|---|---|
npm run dev |
Vite dev server on port 8080 (HMR error overlay disabled) |
npm run build |
Production build to dist/ |
npm run build:dev |
Build in development mode |
npm run preview |
Serve the built dist/ locally |
npm run lint |
ESLint over the repo |
npm test |
Vitest, single run |
npm run test:watch |
Vitest in watch mode |
Playwright is configured through playwright.config.ts / playwright-fixture.ts; run specs with npx playwright test.
src/
├── main.tsx # React root
├── App.tsx # Providers (react-query, tooltips, toasters) + routes
├── index.css # Tailwind layers + light/dark design tokens
├── pages/
│ ├── Index.tsx # Composes every landing-page section
│ └── NotFound.tsx # Catch-all route
├── components/
│ ├── Navbar.tsx # Fixed nav, anchor links, mobile menu
│ ├── Hero.tsx # Headline + CTAs into #pricing / #contact
│ ├── Services.tsx # Subject/offering cards
│ ├── Pricing.tsx # Online packages A/B/C + in-person per-grade rates
│ ├── Registration.tsx # Validated enrolment form → WhatsApp
│ ├── About.tsx # Founder bio
│ ├── Contact.tsx # WhatsApp / email / Facebook / location cards
│ ├── Footer.tsx
│ ├── NavLink.tsx # react-router NavLink with activeClassName support
│ └── ui/ # shadcn/ui primitives — generated, avoid hand-editing
├── hooks/ # use-toast, use-mobile
├── lib/utils.ts # cn() class merge helper
├── assets/ # Logo and founder image
└── test/ # Vitest setup + example spec
Sections are rendered in a fixed order by pages/Index.tsx and linked by the id on each <section> (#home, #services, #pricing, #register, #about, #contact). Adding a section means creating the component, giving it an id, rendering it in Index.tsx, and adding an entry to navLinks in Navbar.tsx.
Registration.tsx validates the form with a zod schema (student details, parent/guardian details, and three required agreement checkboxes), then formats the answers into a message and opens a https://wa.me/... link in a new tab. Nothing is stored or sent server-side, and the submitted state only reflects that the WhatsApp hand-off was opened — not that a message was actually delivered.
The WhatsApp number lives in two places today: Registration.tsx (in the wa.me URL) and Contact.tsx (as WHATSAPP_NUMBER). Update both when it changes.
Content is hard-coded as arrays at the top of each component — no CMS:
- Prices and packages —
onlinePlansandautumnPlansin Pricing.tsx. Thepopularandrecommendedflags control the badge and card styling. Package labels are duplicated in thepackagesarray in Registration.tsx; keep them in sync. - Subjects / offerings —
servicesin Services.tsx. - Contact details and social links — Contact.tsx.
- Founder bio and credentials — About.tsx.
- Page title, description and Open Graph tags — index.html.
Colours are HSL CSS variables defined in index.css and mapped to Tailwind names in tailwind.config.ts. The palette is deep teal (--primary) with a gold accent (--secondary); headings use Playfair Display, body text uses Inter, both loaded from Google Fonts. A .dark token set exists but no theme toggle is currently mounted, so the site always renders light.
Use the semantic Tailwind classes (bg-primary, text-secondary, border-border, …) rather than literal colours, so a palette change stays a one-file edit.
Every push to main triggers .github/workflows/deploy.yml, which installs with npm ci, runs npm run build, and publishes dist/ to the github-pages branch with JamesIves/github-pages-deploy-action. Lint and tests are not part of the pipeline — run them locally before pushing.
The build uses the default / base path and BrowserRouter without a basename, so the site expects to be served from a domain root (a custom domain or a user/organisation Pages site) rather than from a /<repo>/ project-page subpath. Note also that GitHub Pages has no SPA rewrite: deep links other than / will 404 at the server before the router sees them. Since every section is an anchor on /, this only matters if real routes are added later.