React 19, Vite 6, TailwindCSS 3. Single-page application deployed as a static site on Render.
App
├── Header (sticky, dark bg)
│ ├── Logo — SVG document mark, black/white
│ ├── Nav links — How it works, Analyze, GitHub
│ ├── ThemeToggle — Dark/light mode, saves to localStorage
│ └── HealthBadge — Backend status + Gemini/Fallback indicator
├── Hero section — Headline, CTA, tech stack pills, load animations
├── How it works — 3-step numbered section
├── Analyzer section — id="analyzer"
│ ├── AnalysisForm
│ │ ├── Tab switcher — Paste text / Upload PDF·TXT
│ │ ├── Textarea — Resume text input (min 80 chars)
│ │ ├── File zone — Drag-area style file picker
│ │ ├── Target role — Text input
│ │ └── Job desc — Textarea (optional)
│ └── ResultPanel
│ ├── EmptyState — Feature preview cards before first submit
│ ├── LoadingSkeleton — Animated pulse while waiting
│ └── Results
│ ├── ScoreRing — SVG ring, colour based on score band
│ ├── Engine badge — "Gemini AI" (violet) or "Local fallback" (grey)
│ ├── ListBlock × 3 — Missing skills / Strengths / Recommendations
│ └── Icons — Per-section inline SVG icons
├── Footer
│ ├── Logo + tagline + copyright
│ └── Links — GitHub, Report issue, Gemini API, Back to top
└── ErrorBoundary — Catches React render errors, shows reset UI
Dark mode — Tailwind class strategy. useTheme() in App.jsx:
- Reads
localStorage.themeon mount - Falls back to
window.matchMedia('(prefers-color-scheme: dark)') - Toggles
darkclass on<html>, writes preference back tolocalStorage
Tab input — AnalysisForm tracks inputMode ("text" | "file"). Switching clears the other input so both are never sent simultaneously (server also rejects this).
Loading state — isLoading is passed to both AnalysisForm (disables submit, shows spinner) and ResultPanel (shows skeleton instead of empty state).
Score ring colour — Dynamic based on score: green ≥ 80, amber 60–79, red < 60. Track colour uses CSS variable --ring-track (slate-200 light / slate-700 dark).
Page load animations — CSS keyframe fadeUp with staggered animation-delay classes (anim-delay-1 through anim-delay-4). Applied to hero content only; no animation on subsequent interactions.
Engine badge — Surfaced from result.engine in the API response. Shows which service produced the result.
All backend calls are in frontend/src/lib/api.js:
analyzeResume({ resumeText, resumeFile, targetRole, jobDescription })— POST/analyzeasmultipart/form-datagetHealth()— GET/health
Backend URL is set at build time via VITE_API_BASE_URL (defaults to http://localhost:8000).
- Component classes use Tailwind utilities directly (no CSS modules)
- Dark mode variants via
dark:prefix on every element - Component-level CSS utility classes (
.field-label,.input-control,.textarea-control,.nav-link,.footer-link) defined insrc/index.cssvia@layer components - CSS variables for SVG colours:
--ring-trackin:root/.dark