Imported from satvikvirmani/cutline (
AGENTS.md). Install upstream withnpx skills add satvikvirmani/cutline. Copyright stays with the author.
Build Brief: "30-Day Cut" Tracker — PWA for Vercel
Instructions for the coding agent: Build this as a complete, deployable Next.js PWA. A reference file 30-day-cut-tracker.html is attached alongside this brief — it is the literal source of truth for UI, CSS variables, fonts, layout, and component behavior. Port its markup/CSS/JS logic into React components and a real storage layer. Do not redesign anything; only re-architect the storage and add PWA/deployment scaffolding.
1. Tech stack
- Framework: Next.js 14+ (App Router, TypeScript)
- Styling: Plain CSS Modules or global CSS using the exact CSS variables from the reference file (no Tailwind needed — the design is already token-based)
- Fonts: Barlow Condensed, Inter, IBM Plex Mono (Google Fonts — use
next/font/googleinstead of a<link>tag) - Local storage: IndexedDB via the
idbnpm package - Cloud storage: Vercel KV (Upstash Redis under the hood) for cross-device persistence
- Auth: Single passcode gate (env var), not a full user system — this is a personal single-user app
- PWA:
@ducanh2912/next-pwa(actively maintained App Router-compatible PWA plugin) for manifest + service worker - Hosting: Vercel
2. Design tokens (copy exactly from reference file)
--paper:#F1F4F2;
--grid-line:#CBD8D2;
--ink:#1C2B27;
--ink-soft:#5B6B65;
--accent-amber:#D9912E;
--accent-green:#3F7A5C;
--accent-coral:#C75646;
--card-bg:#FFFFFF;
--border:#DCE5E0;
Display font = Barlow Condensed (700/800), body = Inter, data/inputs = IBM Plex Mono. Graph-paper grid background on body exactly as in the reference (linear-gradient grid lines at 28px spacing).
3. Pages & routes
/login — passcode entry, sets httpOnly cookie on success
/ — main tracker (protected by middleware)
/api/auth — POST: checks passcode against env var, sets cookie
/api/kv/[key] — GET/PUT/DELETE: proxies to Vercel KV
/manifest.json — PWA manifest (generated by next-pwa or static in /public)
middleware.ts redirects any request without a valid session cookie to /login, except /login and /api/auth themselves.
4. Data model (unchanged from reference file)
type Targets = { calories: number; protein: number; water: number; steps: number };
type DayLog = {
calories: number;
protein: number;
weight: number;
waist: number;
water: number; // glasses filled
workoutDone: boolean;
notes: string;
};
Keys:
targets→Targetsdaylog:YYYY-MM-DD→DayLog
Workout plan mapping by JS Date.getDay() (0=Sun..6=Sat) — copy verbatim from reference file's WORKOUT_PLAN object.
5. Storage layer — implement exactly this contract
Create lib/storage.ts exposing the same shape as the artifact's window.storage so component logic ports with minimal changes:
export async function getItem(key: string): Promise<string | null>
export async function setItem(key: string, value: string): Promise<void>
export async function deleteItem(key: string): Promise<void>
export async function listKeys(prefix?: string): Promise<string[]>
Behavior (offline-first, write-through):
- Read (
getItem): Read from IndexedDB first (instant, works offline). In the background, fetch the same key from/api/kv/[key]; if the remote value differs and is newer (compare aupdatedAttimestamp stored alongside each value), overwrite IndexedDB and notify the UI to re-render. - Write (
setItem): Write to IndexedDB immediately (UI never waits on network). Fire-and-forget aPUT /api/kv/[key]call to sync to Vercel KV. If the network call fails, queue it in an IndexedDBpendingSynctable and retry on next app load oronlineevent. - Delete: Same write-through pattern as
setItem. - List: IndexedDB is the source of truth for listing keys (covers the 30 fixed
daylog:*keys +targets); no need to round-trip to KV for listing.
Store every value as { value: string, updatedAt: number } internally (in both IndexedDB and KV) to support the newest-wins merge above.
Why this shape: every place in the reference file that calls window.storage.get/set should now call getItem/setItem with almost no other code changes — minimize the diff between the working prototype and the production app.
6. Component breakdown (1:1 with reference file's sections)
<Header>— title, "Day N of 30" counter, streak-dot row (click to select day), settings gear + targets panel<TodayProgress>— calorie/protein bars, color logic (green≤105% of calorie target / ≥100% of protein target,amber/coralthresholds exactly as in reference file'sbar()function)<LogEntryForm>— calories/protein/weight/waist inputs, water glass row (click-to-fill), workout toggle (auto-shows planned workout for that weekday), notes textarea, save button with "Saved ✓" feedback<WeekStats>— avg weight / workouts done / days logged for the Mon–Sun week containing the selected day (reusemondayOf()logic verbatim)
Keep all client-side interactivity in "use client" components; the passcode-protected shell page can be a server component.
7. PWA requirements
public/manifest.json:
{
"name": "30-Day Cut Tracker",
"short_name": "30-Day Cut",
"description": "Daily fat-loss, macro and workout tracker",
"start_url": "/",
"display": "standalone",
"background_color": "#F1F4F2",
"theme_color": "#1C2B27",
"icons": [
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
]
}
- Generate two simple icon PNGs (192/512) using the
--accent-amberdot motif from the streak grid as the icon mark — keep it on the--paperbackground. - Add
apple-touch-iconandapple-mobile-web-app-capablemeta tags inapp/layout.tsxfor iOS installability. - Configure
next-pwato precache the app shell and fonts, and serve a basic offline fallback if a page request fails with no network. - Service worker must NOT cache
/api/*routes — those should always hit network when online.
8. Auth (minimal, single user)
- Env var
APP_PASSCODE(set in Vercel dashboard, never committed). /login— single password input, POSTs to/api/auth./api/auth— compares submitted value toprocess.env.APP_PASSCODE; on match, sets an httpOnly, secure,SameSite=Laxcookie (e.g.session=ok, long expiry like 90 days).middleware.ts— checks for that cookie on all routes except/loginand/api/auth; redirects to/loginif missing.- No signup, no multi-user, no password reset flow — this is intentionally minimal.
9. Environment variables (set in Vercel project settings)
| Variable | Source |
|---|---|
KV_REST_API_URL |
Auto-filled when you attach a Vercel KV (Upstash) storage instance to the project |
KV_REST_API_TOKEN |
Same as above |
APP_PASSCODE |
You choose this manually |
10. File structure
app/
layout.tsx
page.tsx (main tracker, protected)
login/page.tsx
api/
auth/route.ts
kv/[key]/route.ts
globals.css (design tokens + grid background)
components/
Header.tsx
TodayProgress.tsx
LogEntryForm.tsx
WeekStats.tsx
lib/
storage.ts (IndexedDB + KV hybrid, contract from §5)
workoutPlan.ts (weekday → workout label map)
dates.ts (fmt, mondayOf, dateFromOffset helpers — port verbatim)
middleware.ts
public/
manifest.json
icon-192.png
icon-512.png
next.config.js (wrapped with next-pwa)
11. Build & deploy steps
npx create-next-app@latest 30-day-cut --typescript --app- Install deps:
npm i idb @ducanh2912/next-pwa @vercel/kv - Port design tokens + fonts into
app/globals.cssandapp/layout.tsxexactly as in the reference HTML. - Build components per §6, wiring them to
lib/storage.tsper §5. - Implement
/login,/api/auth,middleware.tsper §8. - Implement
/api/kv/[key]/route.tsusing@vercel/kvfor GET/PUT/DELETE. - Add manifest + icons +
next-pwaconfig per §7. - Run locally (
npm run dev), test: offline mode (DevTools → Network → Offline, confirm app still loads and logs save locally), then go back online and confirm sync fires. - Push to a new GitHub repo.
- Import the repo into Vercel → in the project's Storage tab, create/attach a Vercel KV database (this auto-populates the KV env vars).
- Add
APP_PASSCODEmanually under Settings → Environment Variables. - Deploy. Visit the URL on phone, use the browser's "Add to Home Screen" to confirm installability.
12. Acceptance checklist
- UI is visually identical to
30-day-cut-tracker.html(colors, fonts, spacing, streak-dot grid, water glasses, toggle switch) - Passcode gate works; wrong passcode rejected; correct passcode persists across sessions
- Logging a day works fully offline, then syncs once back online
- Opening the app on a second device shows previously logged days (cloud sync confirmed)
- Lighthouse PWA audit passes "Installable" criteria
- Clearing browser cache does not lose data (confirms KV is the durable copy, IndexedDB is just a fast cache)