The production website for Anugruja Arts Studio β the studio of Master Artist and Curator Anuradha Govarthanan (20+ years of practice across watercolours, acrylics, oils, Tanjore, Mysore, and Kerala mural styles).
This is a full Next.js App Router application: a public gallery site, a built-in password-protected admin console, a Git-backed CMS pipeline (content is committed to this repo through the GitHub API), and an AI studio assistant ("Chitra") that answers visitor questions from the same content file. No database, no server to babysit β the repo is the CMS.
| Area | What it does |
|---|---|
Public site (src/app/(site)/) |
Home, Art for Sale, Classes & exam coaching (NATA/NID/NIFT/BFAβ¦), About the artist, events, commission & inquiry bridges (WhatsApp / email) |
| Artwork experience | 3D drag carousels, a global lightbox (click any painting to enlarge β optimized variants, so it opens instantly), scroll reveals, reduced-motion support |
Admin console (/admin) |
Password-gated studio console: photo-first upload wizard (pick photos β describe one at a time β crop/rotate/straighten β publish one or the batch), gallery managers, home-shortcut & events editors, brand/SEO settings, live preview editor β every edit is staged and reviewed before one explicit Commit |
| Chat assistant "Chitra" | Floating chat widget backed by OpenRouter (server-side key only). Its knowledge is generated from the same site.json the site renders β new paintings are in the bot's context automatically |
| Image pipeline | next/image + a custom optimizer: on upload, originals are compressed client-side; on display, right-sized variants (640/1080/1920) are served and cached |
- The hero announcement stays in flow. Only
Headerowns the bounded brand/announcement swap after the hero scrolls past; focus, hover and hidden tabs pause it. Reduced motion uses a static announcement. Desktop navigation starts at 1280px; smaller widths use the drawer. - Event galleries reuse
Carousel3D's deck presentation with no autoplay and all event photos, not a cropped three-image mosaic. The shared frame uses natural dimensions fromimageSize()(or image load for new uploads), constrained to the measured stage. Stage measurements must not feed back into the stage's own height. - The collector showcase grows with its content rather than clipping to one viewport. Medium, dimensions and status come from each artwork; certification, shipping insurance and scarcity must not be promised without verified content. Price confirmation and WhatsApp inquiry helpers remain authoritative.
- Keep the hero display title's
1.24line height and descender padding in both responsive rules. Do not restore a filter on the gradient-clipped title. - Autoplay is disabled for reduced motion and lite devices. Keep idle carousels off the shared frame loop. Run
npm run typecheck,npm run buildandnpm run audit:cssafter changing these surfaces.
Everything the site shows β sections, artworks, prices, statuses, events, chatbot knowledge, quick-nav β lives in one file:
content/site.json β the single source of truth
ββββββββββββββββββββββββββ 1. Stage edits in the admin console
β Admin console /admin β (in-memory / localStorage, nothing live yet)
βββββββββββββ¬βββββββββββββ
β 2. One explicit "Commit Changes"
βΌ
ββββββββββββββββββββββββββ 3. POST /api/content β
β GitHub Contents API β commits content/site.json (and any uploaded
β (via /api/github) β images in public/images) to this repository
βββββββββββββ¬βββββββββββββ
β 4. Push to main triggers
βΌ
ββββββββββββββββββββββββββ 5. Vercel rebuilds; site reads site.json
β Live website (Vercel)β at build/request time; the chatbot context
ββββββββββββββββββββββββββ is regenerated from the same file
Consequences of this design:
- Nothing goes live until you commit. Admin edits are staged and fully discardable.
- Content is versioned. Every change is a Git commit β full history, easy revert.
- The chatbot never drifts.
src/lib/chatbot/context.tsbuilds its knowledge from the committedsite.json, so uploaded/edited paintings update the bot automatically.
Artworks live in typed collections (GalleryKey in src/lib/types.ts, described by GALLERY_CATALOG). One of them β Art for Sale β is the commerce collection and the only place the public pages render a price; the rest are showcase galleries (Featured, Commissions, Classes, Watercolour, Workshops, Testimonials, Awards) that never show sales metadata.
Whether a painting is for sale is therefore an explicit per-artwork decision, asked as the first question of the upload flow:
- Just show it (default) β the photo goes to a showcase gallery. No price is asked, none is stored, and no availability badge is published.
- Sell it β it goes to Art for Sale and the form asks for Price (βΉ) and Availability (
Available / Reserved / Sold).
Filenames still auto-suggest a collection (krishna-sale-2.jpg β Art for Sale, student-boat.jpg β Classes & Courses) and a photo with no hint lands in a showcase gallery β never in the sale catalogue by accident. Flipping the switch is non-destructive: it remembers the showcase gallery you had chosen and restores it.
Price privacy: nothing public until an admin saves it. Every artwork carries a priceConfirmedAt stamp that every admin price-edit path (gallery manager, section editor, upload wizard β both this console and the standalone admin portal) sets the moment a price is saved. Visitor-facing surfaces β the Art for Sale grid, the home carousel, the lightbox, the chatbot and the WhatsApp inquiry prefill β render an unconfirmed price as XXXX plus a "Contact the studio for the actual cost" note, while admin screens keep showing the raw figures. The shared rule lives in src/lib/price.ts; the seeded placeholder prices were stripped from content/site.json, and GET /api/content strips unconfirmed prices for anonymous callers, so the raw JSON can never leak what the pages don't show.
The wizard is photo-first, exactly like a phone album:
- Pick one photo or many (file picker or drag & drop). Canvas pre-compression starts in the background immediately.
- Describe them one at a time β the painting fills one side while its own form sits beside it, with a filmstrip to jump between photos and a green tick on finished ones.
- Frame each photo β Crop, rotate & straighten opens a phone-style editor: pinch or scroll to zoom, drag to move, Turn 90Β° for sideways shots, a Straighten slider (Β±15Β°), and shape presets (Whole photo Β· Square Β· Portrait Β· Landscape). Straightening pulls the crop inward to real paint, so no grey wedges can reach the gallery. The geometry lives in
src/lib/photoFraming.tsand is shared by the on-screen preview and the exported file, so what you frame is exactly what uploads. - Publish β nothing needs ticking: the whole queue is included,
Publish this photo to β¦sends just the one on screen, orPublish all N photosopens a review sheet listing every painting with its gallery, price and for-sale status before one commit.
Editing anywhere in the console stages the change immediately β there is no per-item "mark for commit" step. getPendingChanges() in src/context/SiteContext.tsx diffs the working content against the last published baseline (studio details, SEO, every page section, social links, chatbot settings, every gallery and artwork, the events calendar) and the review sheet lists all of it; the console's bar reads All synced only when that diff is genuinely empty. It also runs a safety-net pass over every top-level content key, so a field added later cannot go missing from the list and leave the commit button disabled.
Destructive actions confirm through the console's own dialog (src/components/admin/ConfirmDialog.tsx) rather than window.confirm, which blocks the page's main thread, cannot be styled, and is silently suppressed by browsers after repeated use.
- The console at
/admin(and/login) asks for the studio password before anything renders. - The password is never in this repository β it lives in the host's environment variables (
ADMIN_PASSWORDon Vercel,.env.locallocally). - Sessions are HMAC-SHA256-signed cookies (
src/lib/adminAuth.ts), 1-hour expiry; sign-in is rate-limited with a lockout (src/lib/loginThrottle.ts). - Lost the password? Set a new value in the host's env vars and redeploy β no code change involved. Note that the signing secret is derived from the password, so changing it invalidates every existing session at once (visitors to
/adminwill see the "session expired" notice and simply sign in again).
git clone https://github.com/abhay2008/AnugrujaArtsStudio.git
cd AnugrujaArtsStudio
npm install
# create .env.local from the template and fill in what you need
cp .env.example .env.local
npm run dev # β http://localhost:3000Without any env vars the public site works fully from the committed site.json; the admin gate needs ADMIN_PASSWORD, and committing/uploading through the GitHub API needs a GITHUB_TOKEN.
Useful scripts:
| Script | What it does |
|---|---|
npm run dev |
Dev server (default http://localhost:3000) |
npm run build / npm start |
Production build / serve it |
npm run typecheck |
tsc --noEmit β the check to run before every commit |
npm run lint |
Next.js ESLint |
npm run test:framing |
8 400+ assertions over the crop/rotate/straighten geometry (scripts/test-photo-framing.ts) |
npm run test:chat |
Chat pipeline unit checks (fuzzy lookups, guardrails, pricing replies) |
npm run test:e2e |
End-to-end chat conversation against the local pipeline |
npm run images:optimize |
Rebuild the public/images/opt/* derivatives |
npm run audit:css |
Cross-engine CSS compatibility audit |
Create .env.local (gitignored β never commit real values). Templates: .env.example.
| Variable | Required | Purpose |
|---|---|---|
ADMIN_PASSWORD |
for /admin |
Studio password checked server-side at sign-in |
GITHUB_TOKEN |
for commits/uploads | GitHub fine-grained PAT (Contents: read/write on this repo) β held server-side only |
GITHUB_OWNER / GITHUB_REPO / GITHUB_BRANCH |
optional | Override commit target (defaults to this repo's main) |
OPENROUTER_API_KEY |
for chatbot | Server-side key for the Chitra assistant |
OPENROUTER_MODEL / OPENROUTER_FALLBACK_MODELS |
optional | Chat model + fallback chain (free-tier models rotate β check that the configured ones are still available) |
NEXT_PUBLIC_SITE_URL |
optional | Public URL used for chatbot attribution headers |
CHATBOT_RAG_TOP_K / CHATBOT_RAG_TOKEN_BUDGET |
optional | How much of the catalogue the assistant retrieves per question |
CHATBOT_CACHE_TTL_SECONDS / CHATBOT_CACHE_MAX_ENTRIES |
optional | Response cache β repeat questions are answered with zero API calls, and entries expire when content changes |
NEXT_DIST_DIR |
optional | Point a dev server at its own build directory, so two servers can run from one checkout without sharing a .next cache |
Security rules this project follows:
- Secrets live only in
.env.local(gitignored) and the host's (Vercel) environment settings. - Never paste real tokens or passwords into code, docs, issues, or commit messages.
- If a secret ever touches Git history, treat it as compromised: rotate it and scrub history β do not just delete the file.
The site deploys on Vercel: push to main β automatic build β live. The same environment variables must be configured in the Vercel project settings (they are not read from the repo). Uploaded images commit to public/images/ in this repo, so they deploy with the next build and are served optimized via next/image.
AnugrujaArtsStudio/
βββ content/
β βββ site.json # β Single source of truth for ALL site content
βββ public/
β βββ images/ # 140+ artwork photos, portraits, certificates
βββ src/
β βββ app/
β β βββ (site)/ # Public pages: home, sale, classes, about, β¦
β β βββ admin/ # Admin console (password-gated) + preview editor
β β βββ login/ # Admin sign-in gate
β β βββ api/
β β βββ auth/ # Password check β signed session cookie
β β βββ content/ # GET/POST site content (commit pipeline)
β β βββ upload/ # Image upload β public/images + GitHub commit
β β βββ github/ # GitHub Contents API helpers (commit status, etc.)
β β βββ chat/ # Chitra: OpenRouter proxy, guardrails, rate limit
β βββ components/ # Carousels, lightbox, chat widget
β β βββ admin/ # Console: MassUploadStudio (wizard), ArtworkFramer
β β # (crop/rotate/straighten), GalleryManager,
β β # EventsManager, QuickNavManager, SectionEditor,
β β # AdminShell (commit flow), ConfirmDialog
β βββ context/
β β βββ SiteContext.tsx # Live content store shared by site + admin;
β β # getPendingChanges() drives the review sheet
β βββ lib/
β βββ serverContent.ts # Reads site.json (+ GitHub fallback)
β βββ github.ts # GitHub Contents API client
β βββ adminAuth.ts # HMAC-SHA256 session cookies (1 h)
β βββ loginThrottle.ts # Brute-force lockout for the admin gate
β βββ imageOptimize.ts # Client-side compression + canvas encoding
β βββ photoFraming.ts # Crop / rotate / straighten geometry (pure math)
β βββ types.ts # Data model + GALLERY_CATALOG
β βββ chatbot/ # context.ts, lookup.ts, guardrails.ts, rateLimit.ts
βββ scripts/
β βββ test-photo-framing.ts # Geometry assertions (npm run test:framing)
β βββ test-chatbot.ts # Chat pipeline checks
β βββ e2e-chat.ts # End-to-end chat run
β βββ optimize-images.mjs # Image derivative builder
β βββ audit-css-compat.mjs # Cross-engine CSS audit
βββ .env.example # Template only β real values live in .env.local / Vercel
Abhay Kashyap Undergraduate, Computer Science & Business Systems @ BMSCE Bangalore GitHub Profile
Historical note: the studio's first website (2024, static HTML/jQuery) was handcrafted before modern AI tooling; this repo is its full production successor.