Imported from Karan-parmar-007/portfolio-frontend (
AGENTS.md). Install upstream withnpx skills add Karan-parmar-007/portfolio-frontend. Copyright stays with the author.
Portfolio Frontend — Agent Context
Read this file first, then the relevant spec in docs/portfolio-rewrite/, before changing any code or answering an architecture question about this repo.
What this is
A Vite + React 19 + TypeScript SPA with two faces:
- Public portfolio — a faithful clone of https://v4.brittanychiang.com/, fully driven by the portfolio API. Nothing on screen is hardcoded.
- Owner-only admin CMS at
/admin, lazily loaded, gated by SSO identity.
Styling is Tailwind CSS v4 via @tailwindcss/vite, with design tokens declared
once in an @theme block in src/index.css.
Specs — the authoritative source
| Topic | Document |
|---|---|
| Overview, phases, global rules | README |
| Structure, types, data flow, routing | 05 |
| Palette, fonts, section-by-section spec | 06 |
| Admin CMS spec | 07 |
| Endpoints and response shapes | 03 |
| Auth model | 04 |
If code and spec disagree, the spec wins — or update the spec in the same change and say why.
The old app at frontend/my-website-frontent is a reference for visual language and the API contract only. It is untyped JSX with duplicated layouts and dead modules. Do not copy files from it.
Two backends, two clients
| Client | Base URL | Purpose |
|---|---|---|
lib/apiClient.ts |
${VITE_PORTFOLIO_API_URL}/v1 → localhost:8001/api/v1 |
portfolio content, public reads and admin writes |
lib/ssoClient.ts |
${VITE_SSO_API_URL}/v1 → localhost:8000/api/v1 |
login, logout, refresh only |
Both use withCredentials: true. Tokens are HttpOnly cookies set by the SSO — this
app never sees, stores, or sends a token itself.
apiClientattachesX-CSRF-Tokenfrom thecsrf_tokencookie on mutating requests, and on401refreshes via the SSO then retries once, coalescing concurrent refreshes.ssoClientattaches CSRF but has no refresh interceptor.GET /auth/sessionis excluded from the refresh interceptor — it returns200for anonymous callers, so retrying it would loop.
Dev ports: this app runs on 5174 (strictPort). 5173 belongs to the SSO
frontend. The portfolio origin must be present in the SSO's CORS_ORIGINS.
Layout
src/
├── main.tsx # QueryClientProvider, AuthProvider, Toaster, StrictMode
├── App.tsx # RouterProvider
├── router.tsx # route table; /admin is a React.lazy chunk
├── index.css # @import tailwindcss + @theme tokens + base layer
├── types/ # one interface per API response — no any, no casts
├── lib/ # env, apiClient, ssoClient, queryKeys, mediaUrl, cn, date, csrf
├── api/ # thin typed functions, one per endpoint group
├── hooks/ # React Query hooks + UI hooks (scroll spy, reveal, reduced motion)
├── context/ # AuthContext
├── components/
│ ├── layout/ # SiteLayout, Nav, MobileMenu, SocialRail, EmailRail, Footer
│ ├── sections/ # Hero, About, Experience, Education, FeaturedProjects,
│ │ # OtherProjects, Contact — presentational, props only
│ ├── project/ # cards, filters, search
│ ├── common/ # NumberedHeading, Reveal, IntroLoader, MediaImage, states
│ ├── admin/ # OwnerGate, AdminLayout, tables, dialogs, fields
│ └── ui/ # token-styled primitives (Radix based)
└── pages/ # route components; compose sections, own routing concerns
Sections receive data as props and never fetch. Pages fetch. HomePage issues
exactly one request (GET /portfolio/overview) and should stay under 80 lines —
the old home.jsx was 617.
Non-negotiable rules
TypeScript
stricton. Noany, no@ts-ignore, no non-null assertions on API data.- Every API response has an interface in
src/types/, written from 03 field for field.
Content
- Nothing user-facing is hardcoded. Not the skills list, not the bio, not the
section titles' data, not project names, not social links, not dashboard counts.
The About skills come from
about_skills(show_in_about); the old app inlined them. - Section numbers (
01.,02., ...) are computed from which sections have data, and the same computed list feeds both the nav and the headings.
Styling
- No hex literal outside
src/index.css. Use the@themetokens. - Every interactive element has a visible
focus-visiblering in--color-green. Neveroutline-nonewithout a replacement. - Every animation is gated by
usePrefersReducedMotion.
Data
- React Query owns all server state. No
useEffect+fetch. - Every query uses a key from
lib/queryKeys.ts. - Every admin mutation invalidates its granular key and
queryKeys.overview, because the home page is served from the aggregate endpoint. - Archive filters live in the URL search params so they are shareable.
States
- Every data surface has loading (layout-matched skeleton), empty, and error states. The old site rendered nothing on failure; that is a bug, not a pattern.
IntroLoaderis a non-blocking overlay: queries start immediately, it runs once per session, and it is skipped under reduced motion.
Uploads
- Build a
FormDataand pass it as the body. Never setContent-Type— the browser must add the multipart boundary. The old app hardcoded it and broke it.
Admin
- Exactly one
AdminLayoutand one sidebar item array. The old admin duplicated its sidebar across seven pages. react-hook-form+zodfor every form. No uncontrolledFormData(e.target).sonnertoasts for all feedback. Neveralert().- No public auth links anywhere in the site chrome.
/admin/loginis reachable only by typing it.
Dependencies
- Every installed package is used. An unused dependency is a review failure.
- Do not add
framer-motion(usemotion),tw-animate-css, or a drag-and-drop library (reordering uses move-up/move-down buttons).
Environment
VITE_PORTFOLIO_API_URL=http://localhost:8001/api
VITE_SSO_API_URL=http://localhost:8000/api
VITE_SSO_FRONTEND_URL=http://localhost:5173
Read them only through lib/env.ts, which throws on a missing value at startup
rather than producing undefined in a URL.
Commands
npm run dev # localhost:5174
npm run build # tsc -b && vite build — must be clean
npm run lint # must be clean
npm run preview # localhost:4174
Requires the portfolio backend on 8001 and the SSO backend on 8000 to be running.
Common pitfalls
- Running on 5173 and colliding with the SSO frontend.
- Forgetting
withCredentials, so cookies are never sent and admin calls 401. - Setting
Content-Typeon a multipart request. - Letting the refresh interceptor fire on
GET /auth/session, causing a loop. - Fetching inside a section component instead of the page.
- Hardcoding section numbers so removing Education leaves a gap.
- Rendering admin chrome before
OwnerGateresolves, briefly exposing it. - Hex colours in components instead of tokens.
- Building the experience section as an accordion — v4 uses vertical tabs with a sliding green indicator, and that is the spec.
- Reintroducing base64 image handling. Images are URLs from
GET /api/v1/media/{id}viamediaUrl(). - Forgetting to invalidate
queryKeys.overviewafter an admin write, so the home page shows stale content. - Adding a public login or signup link. There is none.