# Project Create a production-ready **Next.js 14 (App Router)** admin CMS called **“FitAdmin”**. Tech stack & conventions: • TypeScript, Tailwind CSS, shadcn/ui • React-hook-form + Zod for every form • SWR (stale-while-revalidate) for data fetching & optimistic cache updates • zustand for global UI state (toasts, modals, sidebar collapse) • Heroicons 2 (React) for general icons; lucide-react only if missing • @dnd-kit/core for drag-and-drop re-ordering • Deploy-ready on Vercel (no `next export`) • All pages live under `/admin`; public routes stay untouched • Light/dark mode toggle (use next-theme) # Backend (already running) Base URL is **/api** (same origin). ## Exercises GET /api/exercises → list (optionally `?tag=legs`) POST /api/exercises → create (body: {name, media[], tags[]}) GET /api/exercises/:id → read one PUT /api/exercises/:id → update (same shape as create) DELETE /api/exercises/:id → remove ## Program Days GET /api/days → list `{dayNumber, title}` GET /api/days/:num → hydrated Day doc (see schema) PUT /api/days/:num → replace entire Day doc Day Schema (returned by GET /api/days/:num) ```ts interface Media { uri: string cdn_url?: string stream_url?: string } interface Exercise { _id: string; name: string; media: Media[]; tags: string[] } interface Set { name: string; details: string | null; exercises: Exercise[]; rawOptions: string[] } interface Block { title: string; type: "SET" | "SUPERSET" | "STRENGTH"; explanation?: string|null; sets: Set[] } interface ProgramDay { _id: string; dayNumber: number; title: string; blocks: Block[] } /admin (Dashboard) 2 summary cards: Total Exercises, Total Program Days (hit /api/exercises & /api/days) Quick links to “New Exercise” & “Edit Program” 2 /admin/exercises Table view (TanStack Table) with columns: Name, Tags, #Media, Actions Search by name (client), filter by tag (server: pass ?tag=) “Add Exercise” button ⟶ /admin/exercises/new 3 /admin/exercises/new and /admin/exercises/[id] Re-usable <ExerciseForm> component (react-hook-form + Zod) Fields: Name (text), Tags (multi-select combobox, free-type), Media list editor • Each Media row = uri, cdn_url, stream_url (inline editable) • Add / remove rows Submit → POST or PUT, then SWR mutate list, toast success, redirect back 4 /admin/program Landing page that lists all days as cards. Click a card → /admin/program/1, etc. 5 /admin/program/[dayNumber] Fetch GET /api/days/:num Show Day header (editable title) Blocks accordion (one per Block). Inside each Block: Block metadata • Title, Type (select), Explanation (textarea) • Drag handle to re-order blocks (dnd-kit) Sets list (inner drag-and-drop) • For each Set: Name, Details, Drag handle • Exercises multiselect (combobox pulls from /api/exercises, shows name + tags) • Any text still living in rawOptions[] should surface as a warning badge. “Add Block” / “Add Set” buttons with sensible defaults. Sticky Save button → PUT /api/days/:num with full updated document. • Validate with Zod before PUT. • On success: toast & SWR mutate. 6 Modal / Drawer Components Re-usable <ConfirmDelete> modal Toast notifications (sonner or shadcn/ui’s use-toast) UX polish Breadcrumbs on every admin page. Loading skeletons & empty-states using shadcn/ui Skeleton & Empty patterns. Form autosave hint: if user navigates away with unsaved changes, warn via beforeunload. Security (outline only) Protect all /admin/** routes with a simple bearer token check (process.env.ADMIN_KEY) inside middleware.ts. If the header Authorization: Bearer … is wrong → redirect to /login (generate but leave implementation empty for now). Code organisation lib/api.ts — tiny fetch wrapper that injects ADMIN_KEY header & SWR hooks: useExercises, useExercise(id), useProgramDay(num) components/ExerciseForm.tsx, components/ProgramDayEditor.tsx, components/BlockEditor.tsx, etc. stores/ui.ts contain zustand slice for sidebar, theme toggle. Deliverables All pages/components fully typed, no any. Tailwind classes follow shadcn defaults; add a light shadow & rounded-2xl to all cards. README with local dev (bun dev, needs MONGO_URI, DB_NAME, ADMIN_KEY) & deploy steps. --- https://rommyapi.up.railway.app/api/days Existing BE: // index.tsx – Bun 1.2 runtime | Hono v4 | MongoDB import { Hono } from "hono@4"; import { cors } from "hono/cors"; import { MongoClient, Db, ObjectId } from "mongodb"; // ───────────────────────────────────────── // 1. CORS + app // ───────────────────────────────────────── const app = new Hono(); app.use("/*", cors()); app.get("/api/health", (c) => c.json({ status: "ok" })); // ───────────────────────────────────────── // 2. Lazy Mongo connection helper // (reads env vars you set in Railway) // ───────────────────────────────────────── const uri = process.env.MONGO_URI ?? ""; const name = process.env.DB_NAME ?? "fitness"; let db: Db | null = null; const getDB = async () => { if (db) return db; const client = new MongoClient(uri); await client.connect(); db = client.db(name); return db; }; // ───────────────────────────────────────── // 3. API routes // ───────────────────────────────────────── // ——— program days ——— // ——— program days ——— app.get("/api/days", async (c) => { const db = await getDB(); // 1. pull *all* day documents (already small – only eight) const days = await db .collection("programDays") .find() .sort({ dayNumber: 1 }) .toArray(); // 2. collect every exerciseId across the whole programme const ids: (string | ObjectId)[] = []; for (const d of days) { for (const block of d.blocks) { for (const set of block.sets) { for (const id of set.exerciseIds ?? []) { if (!ids.includes(id)) ids.push(id); } } } } // 3. fetch all exercises in a single query const exDocs = await db .collection("exercises") .find({ _id: { $in: ids } }) .toArray(); const exById = new Map(exDocs.map((e) => [e._id, e])); // 4. hydrate every day (mutating the in-memory copies, *not* the DB) for (const d of days) { for (const block of d.blocks) { for (const set of block.sets) { set.exercises = (set.exerciseIds ?? []) .map((id: string | ObjectId) => exById.get(id)) .filter(Boolean); delete set.exerciseIds; } } } return c.json(days); }); app.get("/api/days/raw/:num", async (c) => { const num = Number(c.req.param("num")); const doc = await (await getDB()) .collection("programDays") .findOne({ dayNumber: num }); return doc ? c.json(doc) : c.notFound(); }); app.get("/api/days/:num", async (c) => { const dayNumber = Number(c.req.param("num")); const db = await getDB(); // 1. grab the day document const day = await db.collection("programDays").findOne({ dayNumber }); if (!day) return c.notFound(); // 2. gather all unique exerciseIds, preserving first-seen order const ids: (string | ObjectId)[] = []; for (const block of day.blocks) { for (const set of block.sets) { for (const id of set.exerciseIds ?? []) { if (!ids.includes(id)) ids.push(id); } } } // 3. fetch every exercise in a single query const exercises = await db .collection("exercises") .find({ _id: { $in: ids } }) .toArray(); const map = new Map(exercises.map((e) => [e._id, e])); // 4. replace exerciseIds with full docs, keeping order for (const block of day.blocks) { for (const set of block.sets) { set.exercises = (set.exerciseIds ?? []) .map((id) => map.get(id)) .filter(Boolean); delete set.exerciseIds; // drop the id list if you don’t need it } } return c.json(day); }); // ——— exercises ——— app.get("/api/exercises", async (c) => { const tag = c.req.query("tag"); const filter = tag ? { tags: tag } : {}; const docs = await ( await getDB() ) .collection("exercises") .find(filter, { projection: { _id: 1, name: 1, tags: 1 } }) .sort({ name: 1 }) .toArray(); return c.json(docs); }); app.get("/api/exercises/:id", async (c) => { const id = c.req.param("id"); const _id = id.length === 24 ? new ObjectId(id) : id; // ObjectId or UUID const doc = await (await getDB()).collection("exercises").findOne({ _id }); return doc ? c.json(doc) : c.notFound(); }); // 👇 replace the old root handler with this: app.get("/", (c) => c.html(/* html */ ` <!doctype html> <html lang="en"> <head> <meta charset="utf-8"/> <title>Fitness DB Explorer</title> <style> body{font-family:system-ui, sans-serif;margin:0;padding:1.2rem;line-height:1.4} h1{margin-top:0} nav{display:flex;gap:.6rem;margin-bottom:1rem} button{padding:.4rem .8rem;border:1px solid #888;background:#f5f5f5;cursor:pointer} button:hover{background:#eee} pre{white-space:pre-wrap;background:#fafafa;border:1px solid #ddd;padding:.8rem;border-radius:4px} #out{min-height:300px} </style> </head> <body> <h1>🏋️♂️ Fitness DB Explorer</h1> <nav> <button onclick="fetchDays()">List Days</button> <button onclick="fetchExercises()">List Exercises</button> Tag: <input id="tag" placeholder="legs" style="width:6rem"> <button onclick="fetchExercises(document.getElementById('tag').value)">↻</button> </nav> <label>Manual GET: </label> <input id="manual" style="width:50%" placeholder="/api/days/1"> <button onclick="manualGet()">Go</button> <pre id="out">Click a button to query the API…</pre> <script> async function show(res) { const pretty = JSON.stringify(res, null, 2) document.getElementById('out').textContent = pretty } async function fetchDays() { const res = await fetch('/api/days').then(r => r.json()) show(res) } async function fetchExercises(tag='') { const url = tag ? '/api/exercises?tag='+encodeURIComponent(tag) : '/api/exercises' const res = await fetch(url).then(r => r.json()) show(res) } async function manualGet() { const path = document.getElementById('manual').value.trim() || '/' try { const res = await fetch(path) const text = res.headers.get('content-type')?.includes('json') ? JSON.stringify(await res.json(), null, 2) : await res.text() document.getElementById('out').textContent = text } catch (err) { document.getElementById('out').textContent = '⚠️ '+ err } } </script> </body> </html> `) ); // ───────────────────────────────────────── // 4. Bun server export (Railway picks it up) // ───────────────────────────────────────── Bun.serve({ port: Number(process.env.PORT ?? 3000), fetch: app.fetch, });
Loading...
The code returns the following error:
The file "/components/ui/sonner" cannot be found (imported in "/app/layout"). Make sure the file or alias exists in the correct location.
Revise the code to address the error.
I was previewing: /admin
Loading...
Turn this into api routes we use on the admin frontend.
the mongo uri i smongodb://mongo:lmqgfeaQgluJXobKBVFcXiPGrveBuyiw@shortline.proxy.rlwy.net:55983