https://portfolio-nu-silk-13.vercel.app/
A production-focused personal portfolio for Ruththiragayan Sutharsan: a responsive, accessible Next.js experience with a celestial visual system, data-driven content, restrained GSAP motion, a validated contact pipeline, and a private content-management workspace.
Add current desktop and mobile captures here after personal avatar assets and content are supplied.
Next.js App Router, React, strict TypeScript, Tailwind CSS, GSAP/ScrollTrigger, Lucide, Zod, React Hook Form, Resend, Vitest/Testing Library, Playwright, ESLint, and Prettier.
src/app— routes, metadata, contact API, robots, sitemapsrc/components/layout— site-wide navigation and footersrc/components/sections— server-rendered homepage compositionsrc/components/animations— small reusable client-side motion boundariessrc/components/ui— reusable presentation primitivessrc/features/avatar— avatar assets, visual layers, animation configuration, and GSAP scenesrc/features/contact— form UI, shared schema/types, server-only delivery servicesrc/features/projectsandsrc/features/blog— domain types, records, and selectorssrc/features/auth,src/features/content, andsrc/db— administrator sessions, managed portfolio content, repositories, and the Drizzle schemasrc/components/manage— focused client controls for the private workspacedrizzleandscripts— versioned PostgreSQL migration and administrator seed commandsrc/config/site.ts— personal configuration and optional linkssrc/config/environment.ts— centralized server-only environment parsingsrc/data— shared skills and journey contentsrc/lib— shared pure helperspublic/media/avatar— user-supplied avatar assetstestsande2e— unit/component and browser smoke tests
Dependency flow is app → layout/sections → features → database/shared UI/config/lib. Server Components are the default. Public repositories return published records only; protected pages and APIs validate the opaque database-backed administrator session independently.
Public: /, /projects, /projects/[slug], /blogs, /blogs/[slug], /resume, and /privacy. The only authentication entry is the intentionally unlinked /login. Protected routes include /manage, the blog workspace, /manage/content, /manage/media, /manage/resume, and /manage/settings.
Management APIs are /api/auth/login, /api/auth/logout, /api/manage/blogs, /api/manage/blogs/[id], /api/manage/preview/[id], /api/manage/content, /api/manage/media, and /api/manage/resumes. They are not public content APIs.
AvatarScene owns the client lifecycle and creates one GSAP match-media context. avatar.config.ts centralizes asset paths, breakpoints, ScrollTrigger behavior, timing, and video frame assumptions; AvatarDecorations owns the celestial layers. Desktop scrubs the transformation video through the page, while mobile and reduced-motion modes retain the portrait fallback. Cleanup reverts the media context, timeline, listener, timer, and video playback.
The request flow is ContactForm → contactSchema → /api/contact → contact.service → Resend. The Zod schema and discriminated response type are shared safely. Provider access and private environment values are isolated behind server-only modules, and the route never returns provider internals. Development still validates without sending; production reports missing delivery configuration.
Requires Node 20+ and pnpm 10.
corepack enable
pnpm install
cp .env.example .env.local
pnpm db:migrate
pnpm admin:seed
pnpm devOpen http://localhost:3000.
pnpm dev
pnpm build
pnpm start
pnpm lint
pnpm typecheck
pnpm test
pnpm test:watch
pnpm test:e2e
pnpm format
pnpm format:check
pnpm db:generate
pnpm db:migrate
pnpm admin:seedCopy .env.example to .env.local. Production email requires RESEND_API_KEY, CONTACT_TO_EMAIL, and CONTACT_FROM_EMAIL. The from address must use a Resend-verified domain. Without them, development validates and logs only name, subject, and message length; production returns a clear configuration error.
Turnstile is optional. Set NEXT_PUBLIC_TURNSTILE_SITE_KEY and TURNSTILE_SECRET_KEY together, add a Turnstile widget/token to the form when enabling it, and register the deployed hostname in Cloudflare. Partial email or Turnstile configuration fails clearly. Server verification is enforced whenever the key pair is present.
- Edit names, URLs, public email, site origin, and navigation in
src/config/site.ts. - Add verified project records in
src/features/projects/projects.data.ts. - Sign in by manually opening
/login, then manage posts and homepage copy in the private workspace. Do not add this route to public navigation. - Manage private PDF uploads and choose the public download from Resume in the administrator workspace.
- Follow
public/media/avatar/README.mdfor portrait layers and transitions.
Optional URLs are checked before rendering links, so empty values remain non-broken.
- Provision PostgreSQL (Vercel Postgres/Neon or another pooled, TLS-enabled PostgreSQL service), set
DATABASE_URL, and runpnpm db:migrateagainst it. - Locally set
DATABASE_URL,ADMIN_SEED_EMAIL, and a random password of at least 14 characters, then runpnpm admin:seed. Remove the two seed credential variables afterward; the password is stored only as a bcrypt hash. - Optionally create a Vercel Blob store and set
BLOB_READ_WRITE_TOKEN. Without it, media management reports that uploads are unavailable; URL fields remain usable. - For résumé management, set
NEXT_PUBLIC_SUPABASE_URLand the server-onlySUPABASE_SERVICE_ROLE_KEY. The private bucket named bySUPABASE_RESUME_BUCKETis created on the first upload with PDF-only and 10 MB limits. - Import the repository in Vercel, keep the detected Next.js settings, configure production environment variables, and deploy. Set
siteUrlinsrc/config/site.tsto the final origin.
Database migrations should run as a controlled release step, not concurrently in every serverless function. Back up PostgreSQL using the provider's scheduled backups and point-in-time recovery; keep an independent export before destructive schema work. Blob objects require a separate provider backup/retention policy.
- Create: open Blogs → New post, enter the required title, unique slug, excerpt, Markdown body, author, and category, then save a draft or publish.
- Draft and preview: Save draft, then use Open secure preview. Preview stays under the session-protected
/managetree. - Publish/unpublish: use Publish or Unpublish in the editor. Unpublishing clears the public publication timestamp and removes the post from public queries and sitemap generation.
- Delete: choose Delete post and accept the explicit irreversible confirmation.
- Homepage:
/manage/contentchanges content that belongs to the public portfolio; layout, animation, tokens, and component behavior remain code-owned. - Résumé:
/manage/resumeuploads PDF files to a private Supabase bucket. Select one file as public;/resumeissues a short-lived signed download for only that selection. - Media: upload an allowed image up to 5 MB with meaningful alt text. Deleting an in-use asset can break its saved URL, so remove references first.
Passwords are bcrypt-hashed. Login creates a fresh random opaque session token, stores only its SHA-256 digest in PostgreSQL, and sends the raw token only in an HttpOnly, SameSite=Lax cookie (Secure in production). Protected layouts validate non-expired sessions; every management API separately returns 401 or 403. Login attempts are persisted and limited per IP/account window. Mutations enforce same-origin requests, Zod validation, unique database slugs, and generic errors. Markdown is rendered without raw HTML and passed through rehype-sanitize. Public repositories select published posts only. Robots and sitemap output exclude login, management, preview, and API routes.
There is no public registration route. /login is absent from the navbar, footer, mobile menu, sitemap, and other public discovery UI.
- Visit
/logindirectly; verify password visibility, invalid credentials, busy state, and keyboard focus. - Visit
/managein a private window; verify redirect to/loginwith a local return URL. - Sign in, create a draft, and confirm it is absent from
/blogs, its public slug, page metadata, and sitemap. - Open its secure preview, publish it, then verify the public list/detail, metadata, cover alt text, tags, date, reading time, and sitemap.
- Attempt a duplicate slug and invalid URL; verify useful validation without internal errors.
- Edit, unpublish, and delete with confirmation. Verify logout invalidates the session and management API calls return
401. - At mobile widths, keyboard through management navigation, forms, preview, filters, and confirmation controls.
CI runs lockfile installation, lint, strict typecheck, unit tests, and production build. Playwright covers route and interaction smoke tests.
The site includes semantic regions, a skip link, visible focus, labeled controls, mobile-menu focus containment, live form feedback, strong contrast, and keyboard-accessible navigation. Reduced-motion mode removes pinned/scrubbed animation and repeated movement. Mobile uses a short static layout instead of an extended pin.
- Personal avatar layers, transition videos, résumé, social URLs, email, real projects, and published posts await owner-supplied data.
- Turnstile requires adding Cloudflare’s client widget before setting its secret.
- Visual project/blog imagery will appear only after content records and assets are supplied.