Skip to content

Joyfernandas/elitezonej

Repository files navigation

Elite Zone J

Premium Indian tailoring & fashion e-commerce — Next.js 16 App Router storefront with two parallel admin panels sharing one SQLite backend.

Live storefront: web-two-mocha-61.vercel.app Admin panels: local-only (SQLite needs a writable filesystem — Vercel won't work, see Deployment).


Read this first — it isn't vanilla Next.js

This repo runs Next.js 16.2.4 with breaking changes from earlier versions:

What changed Impact
middleware.tsproxy.ts Same behaviour, new filename. Both admin panels are gated by proxy.ts at the repo root.
cookies() is async const c = await cookies() everywhere.
Server actions sit next to pages app/admin/actions/*.ts, app/studio/actions/*.ts.
Native modules need allow-listing next.config.ts has serverExternalPackages: ["better-sqlite3"].

Before writing code: check node_modules/next/dist/docs/ for the canonical Next 16 patterns. Don't rely on Next 14/15 muscle memory.


One brand, two operator surfaces

Both panels read & write the same data/admin.db via the same session cookie (ezj_admin_session). Pick the one whose vibe fits the operator.

/adminAtelier Ledger

Editorial, oxblood-on-cream "leather workbook". Cormorant Garamond italics, mono kickers, hairline rules, zero radii. Designed for the brand owner who wants the back-office to feel like a tailor's notebook, not a SaaS dashboard.

  • 16 routes, 9 server actions
  • app/admin/styles/admin.css carries the design tokens

/studioShopify+Canva-style operator

Pragmatic, productivity-first. Dense tables, drag-to-reorder lists, image uploaders with crop, toast notifications. Designed for staff doing high-volume catalogue/banner/offer work.

  • 20 routes, 9 server actions
  • app/studio/styles/studio.css + react-image-crop for media handling

Same data, two ways of looking at it. You can run them side-by-side or pick one and delete the other; the lib layer doesn't care.


Quick start

npm install                      # installs better-sqlite3, bcryptjs, sharp, puppeteer-core, etc.
npm run dev                      # → http://localhost:3000 (or :3001 if 3000 is busy)

Default credentials (auto-seeded on first DB open — works for both /admin and /studio):

email:    admin@elitezonej.com
password: admin123

These are baked into lib/admin/db.ts (ensureDefaultAdmin). Change the password through /admin/settings after first login — the seed only runs if the account is missing, so your new password sticks.

First boot:

  1. npm run dev
  2. Visit /admin/login (or /studio/login) → log in with the creds above
  3. The DB at data/admin.db is auto-created with:
    • The default owner user
    • 30 SKUs seeded from lib/products.ts
    • 25 mock orders, 18 customers, 6 bookings, 4 promo codes (so dashboards aren't empty)
    • Default homepage blocks, banners, notices

Storefront stays on the static catalog (lib/products.ts); admin edits do not yet propagate to the public homepage. The DB-driven storefront layer (app/components/storefront/) is wired and tested but not the default — see Future work.


Architecture

                              ┌──────────────────┐
                              │  proxy.ts        │  gates /admin/* and /studio/*
                              │  (root)          │  on the SESSION_COOKIE
                              └────────┬─────────┘
                                       │
                 ┌─────────────────────┼─────────────────────┐
                 │                     │                     │
                 ▼                     ▼                     ▼
         ┌────────────┐         ┌────────────┐       ┌──────────────┐
         │ /admin     │         │ /studio    │       │ /            │
         │ atelier    │         │ shopify    │       │ storefront   │
         │ ledger     │         │ canva      │       │ (public)     │
         └─────┬──────┘         └─────┬──────┘       └──────┬───────┘
               │                      │                     │
               └──────────┬───────────┘                     │
                          ▼                                 │
                  ┌───────────────┐                         │
                  │ lib/admin/    │                         │
                  │ ─────────     │                         │
                  │ db.ts         │  better-sqlite3 ◄───────┘  (storefront reads
                  │ auth.ts       │  singleton                  use lib/storefront/
                  │ session.ts    │                             which optionally hits
                  │ repos/*.ts    │                             the same DB)
                  │ schema.sql    │
                  │ schema-v2.sql │
                  └───────┬───────┘
                          ▼
                  ┌───────────────┐
                  │ data/admin.db │   (gitignored, WAL mode)
                  └───────────────┘

Database — better-sqlite3

  • Synchronous. No await on queries. Fits Server Components and Server Actions cleanly.
  • Singleton on globalThis.__ezj_admin_db so dev hot-reload doesn't open a new handle every render.
  • Idempotent bootstrap in lib/admin/db.ts:
    • schema.sql (v1) loaded with CREATE TABLE IF NOT EXISTS on every open.
    • schema-v2.sql applied statement-by-statement so ALTER TABLE ADD COLUMN can be skipped if the column already exists.
    • Empty DB → seeds catalog (seed.ts), fixtures (seed-fixtures.ts), and studio defaults (seed-studio.ts).
  • WAL mode + foreign keys ON. DB file lives at data/admin.db (gitignored).

Auth

  • Hashing: bcryptjs (pure-JS, no native build) at cost 12.
  • Sessions: server-side rows in sessions table; cookie ezj_admin_session holds only the session id (32 random bytes, hex). httpOnly, sameSite=lax, secure in prod, 30-day TTL.
  • Bootstrap: if users is empty, every gate redirects through /admin/setup (or /studio/setup). After bootstrap, the setup page returns 404.
  • Three-layer authorization:
    1. proxy.ts — coarse "logged in or not" gate
    2. app/{admin,studio}/layout.tsx — fetches the session user once via getSessionUser(sid) and passes to children
    3. Per-action requireRole('owner'|'staff') before any mutation

Server actions

Each domain gets one file under app/admin/actions/ or app/studio/actions/. Pattern:

"use server";
import { z } from "zod";
import { revalidatePath } from "next/cache";
import { requireUser } from "../../../lib/admin/session";

const Schema = z.object({ /* … */ });

export async function upsertProduct(formData: FormData) {
  const me = await requireUser();
  const data = Schema.parse(Object.fromEntries(formData));
  // …repo write…
  revalidatePath("/admin/products");
}

The repos/ modules are the only place that touches SQL. Pages and actions call repos; nothing else.

Routing map

app/admin/
  page.tsx                      Dashboard — KPIs, sparkline, low-stock, recent bookings
  login/page.tsx
  setup/page.tsx                First-run owner bootstrap (auto-disabled after)
  products/{,new/,[slug]/}      Catalogue editor — Details · Inventory · Media · Pricing
  fabrics/{,new/,[slug]/}       Fabric SKUs + colourway matrix (hex picker, stock meters)
  inventory/page.tsx            Global SKU × size stock grid
  orders/{,[id]/}               Order list + detail with status flow
  customers/{,[id]/}            Customer list + history
  bespoke/{,[id]/}              Booking inbox — new → contacted → scheduled → done
  categories/page.tsx           Category tree editor
  promotions/{,new/,[code]/}    Promo codes (percent / flat / free_ship)
  content/page.tsx              Hero / Editorial / Process / MFY / Banner / Trust copy
  media/page.tsx                /public/generated explorer
  settings/page.tsx             Brand · Account · Users · Audit log

app/studio/
  page.tsx                      Operator dashboard — open orders, drafts, quick actions
  login/page.tsx
  setup/page.tsx
  products/{,new/,[slug]/}      Catalogue with multi-image upload + crop
  banners/{,new/,[id]/}         Hero banners (desktop + mobile, schedule, target)
  notices/{,new/,[id]/}         Ticker / popup / festive notices
  homepage/{,[id]/}             Composable block builder (drag to reorder)
  categories/{,new/,[id]/}      Category editor with images + visibility toggle
  offers/{,new/,[code]/}        Offer codes with target audience matrix
  flash-sales/{,new/,[id]/}     Countdown banners tied to promo codes
  orders/{,[id]/}               Order workbench
  customers/{,[id]/}
  bespoke/{,[id]/}
  media/page.tsx                Real media library (uploads → /public/admin-uploads)
  settings/page.tsx

Database schema cheat sheet

Table What it stores Key columns
users Admin operators email (unique), password_hash, role (owner/staff)
sessions Active logins id (uuid), user_id, expires_at
products Catalogue (tailored + fabric) slug (PK), kind, status, price, sale_price
inventory Stock per size (product_slug, size) PK, stock, oos_flag
fabric_meta Fabric specs width_inches, gsm, composition, stock_meters_total
fabric_colours Colourways name, hex, stock_meters, image_dir
customers Buyer records email (unique), total_orders, total_spent
orders Order ledger id (WK-####), status, total, currency
order_items Line items order_id, product_slug, qty, unit_price, size, colour
bookings Bespoke leads service, status, assigned_to
categories Nav tree parent_id self-ref, slug, gender, kind, image_path
promotions Promo codes code (PK), type, value, usage_limit, is_featured
offer_targets Promo audience target_type (all/category/product), target_id
flash_sales Countdown banners promo_code FK, ends_at
home_sections Legacy homepage copy key (PK), title, body, image_path
homepage_blocks Composable home blocks type, config_json, sort_order
banners Hero banners image_path, mobile_image_path, text_align, status
notices Tickers / popups type, priority, target_paths
product_images Multi-image gallery product_slug, image_path, sort_order, is_thumbnail, is_hover
product_meta Flags + SEO is_featured, is_trending, is_new_arrival, meta_title
media_assets Upload tracking path (unique), folder, mime, uploaded_by
settings Brand config key (PK), value
audit_log Mutation trail user_id, action, entity, entity_id, payload_json

Schema files: lib/admin/schema.sql (v1), lib/admin/schema-v2.sql (additions).


How to extend

Add a new page in either panel

  1. Create app/{admin|studio}/<route>/page.tsx.
  2. Make it a server component — read with listX() from a repo.
  3. For mutations, drop a server action in app/{admin|studio}/actions/<domain>.ts and pass it as action={…} to a <form>.
  4. The layout already provides the shell — just return the page body.

Add a new schema field

  1. Append to lib/admin/schema-v2.sql (use ALTER TABLE … ADD COLUMN; the applyV2 runner skips columns that already exist).
  2. Update the matching repo + types in lib/admin/types.ts.
  3. Either delete data/admin.db for a clean reseed, or just restart npm run dev — the migration runs idempotently.

Add a new repo

Drop a file under lib/admin/repos/. Keep all db.prepare(...) SQL inside repos — pages and actions should never import getDb() directly.

Add a new homepage block type

  1. Add the type string to homepage_blocks.type (no enum constraint — it's free-form text).
  2. Create the React component under app/components/storefront/blocks/.
  3. Wire it into the switch in app/components/storefront/HomepageRenderer.tsx.
  4. Build a /studio/homepage/[id] editor section for its config_json shape.

Client preview — screenshot deliverable

When you can't deploy and need to show the client what the panels look like:

# 1. Boot the dev server
npm run dev

# 2. Plant a session cookie (one-shot — keeps script unauthenticated-free)
node -e '
  const Db = require("better-sqlite3");
  const { randomBytes } = require("crypto");
  const fs = require("fs");
  const db = new Db("data/admin.db");
  const id = randomBytes(32).toString("hex");
  const exp = new Date(Date.now() + 86400e3).toISOString();
  db.prepare("DELETE FROM sessions").run();
  db.prepare("INSERT INTO sessions (id,user_id,expires_at) VALUES (?,1,?)").run(id, exp);
  fs.writeFileSync("/tmp/ezj_session_id", id);
'

# 3. Capture 41 routes × 2 viewports = 82 PNGs (uses system Chrome via puppeteer-core)
node scripts/admin-screenshots.mjs

# 4. Output: /tmp/admin-screenshots/{admin,studio,storefront}/{desktop,mobile}/*.png

The script reads BASE_URL (defaults to http://localhost:3001) and writes full-page screenshots at 1440×900 @2x and 390×844 (iPhone UA).


Deployment

Vercel — demo mode (in-memory SQLite, no persistence)

better-sqlite3 would normally need a writable filesystem, but lib/admin/db.ts detects process.env.VERCEL === "1" and switches to a :memory: SQLite database. Same schema, same seeds, same default admin@elitezonej.com / admin123 login — it just resets on every cold start (≈15-min idle timeout, or any redeploy).

What you get on Vercel:

  • /studio and /admin deploy and render exactly like local
  • Login with the default credentials works
  • Mutations work during a session — the same lambda instance keeps your edits in RAM until it spins down
  • A second concurrent visitor hitting a different lambda instance gets their own fresh in-memory DB

This is demo-grade, not production. For a real persistent host, see Persistent options below.

Persistent options

If you need real writes that survive cold starts:

Host Why it works
Railway Persistent volume, Node runtime, simple Git-deploy
Fly.io Volumes + always-on machines
Cloudflare Tunnel to your own server Lowest cost, full control, real disk
Self-hosted VPS (DigitalOcean / Hetzner) Vanilla npm run start behind nginx

For any of these, leave VERCEL and IS_SERVERLESS unset so the panel uses the on-disk DB. Mount data/ on a persistent volume.

Storefront-only on Vercel

The public site under /, /collection, /products/[slug], /bespoke reads from the static lib/products.ts and deploys cleanly regardless of the SQLite mode.


Tech stack

Layer Choice
Framework Next.js 16.2 App Router (Turbopack)
UI React 19, hand-written CSS (no UI lib)
Database SQLite via better-sqlite3
Auth bcryptjs + DB-backed sessions
Validation zod in server actions
Image processing sharp (resize/optimize on upload)
Image cropping react-image-crop (studio only)
Screenshots puppeteer-core + system Chrome
Language TypeScript strict

File map (the parts you'll actually touch)

elitezonej/
├── proxy.ts                    Auth gate for /admin/* and /studio/*
├── next.config.ts              serverExternalPackages: ["better-sqlite3"]
├── data/                       SQLite home (gitignored)
│   └── admin.db                Auto-created on first run
│
├── app/
│   ├── page.tsx                Storefront homepage (HeroGrid, CarouselShowcase, EditorialSplit, MadeForYou…)
│   ├── components/             Storefront components (Header, Footer, ProductCard, …)
│   │   └── storefront/blocks/  DB-driven block components (optional alt homepage)
│   │
│   ├── admin/                  Atelier Ledger panel
│   │   ├── layout.tsx          Auth gate + sidebar/topbar shell
│   │   ├── page.tsx            Dashboard
│   │   ├── styles/admin.css    Design tokens (oxblood, cream, Cormorant)
│   │   ├── components/         AdminSidebar, KpiTile, Folio, StatusPill, …
│   │   ├── actions/            Server actions (auth, products, fabrics, orders, …)
│   │   └── <domain>/page.tsx   One folder per operator surface
│   │
│   └── studio/                 Shopify+Canva panel
│       ├── layout.tsx          ToastProvider + StudioShell
│       ├── styles/studio.css
│       ├── components/         StudioShell, ImageUploader, SortableList, Toast, …
│       ├── actions/
│       └── <domain>/page.tsx
│
├── lib/
│   ├── products.ts             Static catalog (storefront source of truth + seed source)
│   ├── admin/
│   │   ├── db.ts               better-sqlite3 singleton + bootstrap
│   │   ├── auth.ts             bcrypt + session helpers
│   │   ├── session.ts          requireUser(), requireRole()
│   │   ├── schema.sql          v1 DDL (idempotent)
│   │   ├── schema-v2.sql       v2 additions (idempotent ALTERs)
│   │   ├── seed.ts             First-run catalog seed from lib/products.ts
│   │   ├── seed-fixtures.ts    25 orders, 18 customers, 6 bookings, 4 promos
│   │   ├── seed-studio.ts      Default homepage blocks, banners, notices
│   │   ├── types.ts            Shared TS types
│   │   ├── kpi.ts              Dashboard aggregations
│   │   ├── format.ts           ₹ formatter, date formatter
│   │   └── repos/              19 repos — one per domain, only files that touch SQL
│   └── storefront/             DB-aware reads for the public site (optional)
│
├── public/
│   ├── generated/              Product imagery (5 angles × 30 SKUs + section images)
│   └── admin-uploads/          Operator uploads (gitignored)
│
└── scripts/
    └── admin-screenshots.mjs   Puppeteer-driven panel screenshot capture

Common gotchas

  • Port 3001 fallback — if anything is bound to 3000 (Brave keeps stale connections open after closing tabs), Next switches to 3001 silently. Update bookmarks and check the dev server output.
  • cookies() is asyncawait cookies() everywhere. cookies().get(...) synchronous use will throw at runtime.
  • DB file is gitignored — every clone gets a fresh seed on first npm run dev. To share state, copy data/admin.db manually (it's a single file).
  • Two cookies, one authezj_admin_session and ezj_studio_session both exist as cookie names in seeds, but the active gate is just ezj_admin_session. Logging into either panel logs you into both.
  • Schema v2 ALTER pattern — never delete a column from schema-v2.sql. The DB might already have it; we silently skip. To remove a column you have to write a v3 migration that does the table-rebuild dance.
  • Storefront ↔ admin disconnect — admin edits today don't change the public homepage; the storefront still reads lib/products.ts. The DB-backed renderer (HomepageRenderer.tsx) exists and works, but app/page.tsx deliberately stays on the static editorial layout. Future task: switch /collection and /products/[slug] to lib/storefront/products.ts.

Future work

  • Wire the public storefront (/, /collection, /products/[slug]) to read from lib/storefront/products.ts so admin edits go live.
  • PDF invoice generation (app/api/admin/orders/[id]/invoice/route.ts is stubbed to return JSON).
  • Email notifications on bookings.status = scheduled and orders.status = shipped.
  • Move data/admin.db to a managed host (Turso / Cloudflare D1) so the panels can run on edge.

Scripts

npm run dev        # Next dev server (turbopack)
npm run build      # production build
npm run start      # production server

No tests yet; verification is manual against the 12-step checklist that lived in the original plan (login → seed → CRUD round-trip on each domain → bespoke booking from /bespoke lands in /admin/bespoke).


Credit & contact

Built for Elite Zone J — Delhi atelier, made-to-measure. Owner contact: see settings table after first-run setup.

About

Elite Zone J — Premium Indian tailoring & fashion e-commerce. Built with Next.js 16 App Router.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors