diff --git a/docs/superpowers/plans/2026-04-27-devices-admin.md b/docs/superpowers/plans/2026-04-27-devices-admin.md index 060b78a..0007fb4 100644 --- a/docs/superpowers/plans/2026-04-27-devices-admin.md +++ b/docs/superpowers/plans/2026-04-27-devices-admin.md @@ -1580,4 +1580,414 @@ git commit -m "feat: add per-device OG image generator for device detail pages - Renders device name, type badge, and stats on branded dark background - Uses Edge runtime for fast generation - References OG image in device detail page metadata" -``` \ No newline at end of file +``` +--- + +### Task 7: Admin Layout, Sidebar & Auth Guard + +**Files:** +- Create: `app/(admin)/layout.tsx` +- Create: `app/(admin)/admin/page.tsx` +- Create: `components/admin/admin-sidebar.tsx` +- Modify: `components/navbar.tsx` +- Modify: `lib/routes.ts` + +- [ ] **Step 1: Create the admin sidebar component** + +Create `components/admin/admin-sidebar.tsx`: + +```tsx +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { UsersIcon, CpuIcon, Gamepad2Icon } from "lucide-react" + +const adminNavItems = [ + { href: "/admin/users", label: "Users", icon: UsersIcon }, + { href: "/admin/hardware", label: "Hardware", icon: CpuIcon }, + { href: "/admin/games", label: "Games", icon: Gamepad2Icon }, +] + +export function AdminSidebar() { + const pathname = usePathname() + + return ( + + ) +} +``` + +- [ ] **Step 2: Create the admin layout with server-side auth guard** + +Create `app/(admin)/layout.tsx`: + +```tsx +import { redirect } from "next/navigation" +import { headers } from "next/headers" +import { auth } from "@/lib/auth" +import { AdminSidebar } from "@/components/admin/admin-sidebar" +import type { Metadata } from "next" + +export const metadata: Metadata = { + title: { template: "%s | Admin — DeckyVault", default: "Admin — DeckyVault" }, + robots: { index: false, follow: false }, +} + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const session = await auth.api.getSession({ + headers: await headers(), + }) + + if (!session || session.user.role !== "admin") { + redirect("/") + } + + return ( +
+ {/* Header */} +
+
+

Admin

+

+ Manage users, hardware, and games +

+
+
+ + {/* Sidebar + Content */} +
+
+ +
{children}
+
+
+
+ ) +} +``` + +- [ ] **Step 3: Create the admin redirect page** + +Create `app/(admin)/admin/page.tsx`: + +```tsx +import { redirect } from "next/navigation" + +export default function AdminPage() { + redirect("/admin/users") +} +``` + +- [ ] **Step 4: Add admin link to navbar profile dropdown** + +In `components/navbar.tsx`: + +1. Add `ShieldIcon` to the lucide imports: +```tsx +import { + Bookmark, + CircleXIcon, + Gamepad2Icon, + LogOut, + MenuIcon, + ShieldIcon, + User, + XIcon, +} from "lucide-react" +``` + +2. In the **desktop profile dropdown** — right after the `{authRoutes.map(...)}` block and before the `
` sign-out divider, add: +```tsx +{session.user.role === "admin" && ( + <> +
+ setUserMenuOpen(false)} + className="w-full flex items-center gap-2 px-3 py-2 text-sm text-primary/80 hover:text-primary hover:bg-primary/5 transition-colors cursor-pointer" + > + + Admin + + +)} +``` + +3. In the **mobile sidebar** — after `{authRoutes.map(...)}` in the mobile auth section, add: +```tsx +{session.user.role === "admin" && ( + setMobileMenuOpen(false)} + className="flex items-center gap-2 px-3 py-2 rounded-lg text-sm text-primary/80 hover:text-primary hover:bg-primary/5 transition-colors cursor-pointer" + > + + Admin + +)} +``` + +- [ ] **Step 5: Add admin routes to routes file** + +In `lib/routes.ts`, append an `adminRoutes` export (not in the main `routes` nav — admin link only appears in the profile dropdown): + +```ts +export const adminRoutes = [ + { + title: "Users", + href: "/admin/users", + icon: "Users", + }, + { + title: "Hardware", + href: "/admin/hardware", + icon: "Cpu", + }, + { + title: "Games", + href: "/admin/games", + icon: "Gamepad2", + }, +] +``` + +- [ ] **Step 6: Build to verify** + +Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && bun run build --no-lint 2>&1 | tail -20` +Expected: Build succeeds. + +- [ ] **Step 7: Commit** + +```bash +git add app/(admin)/ components/admin/admin-sidebar.tsx components/navbar.tsx lib/routes.ts +git commit -m "feat: add admin layout with sidebar, auth guard, and navbar link + +- Admin layout with server-side auth guard (redirects non-admins) +- Reusable sidebar component with active state detection +- Navbar profile dropdown shows admin link for admin users +- Admin routes are noindexed (robots: false) +- /admin redirects to /admin/users" +``` + +--- + +### Task 8: Admin Users Page + +**Files:** +- Create: `app/(admin)/admin/users/page.tsx` +- Create: `app/(admin)/admin/users/users-client.tsx` + +- [ ] **Step 1: Create the users server component** + +Create `app/(admin)/admin/users/page.tsx`: + +```tsx +import type { Metadata } from "next" +import { UsersClient } from "./users-client" + +export const metadata: Metadata = { + title: "Users", +} + +export default function UsersPage() { + return +} +``` + +- [ ] **Step 2: Create the users client component** + +Create `app/(admin)/admin/users/users-client.tsx` — a full user management table with search, role changing (user/contributor/admin), and ban/unban functionality using better-auth's admin plugin client methods (`authClient.admin.listUsers`, `authClient.admin.setRole`, `authClient.admin.banUser`/`unbanUser`). The component should: + +- Fetch users on mount via `authClient.admin.listUsers({ query: { limit: 100 } })` +- Filter users client-side based on a search input +- Render a table with columns: User (avatar initial + name + email), Role (dropdown select), Status (active/banned dot), Actions (ban/unban button) +- Role dropdown uses `handleRoleChange` calling `authClient.admin.setRole({ userId, role })` +- Ban button calls `authClient.admin.banUser({ userId })` / `authClient.admin.unbanUser({ userId })` +- Style: rounded-xl border, table with text-sm, header row `bg-text/[0.03]`, status dots (green/red), role select with colored borders + +Use the same Tailwind patterns as the rest of the app: `px-4 py-2.5`, `rounded-lg border border-border bg-text/5`, `text-text/50`, focus rings, etc. + +- [ ] **Step 3: Commit** + +```bash +git add app/(admin)/admin/users/ +git commit -m "feat: add admin users management page + +- User list with search by name/email +- Change role (user/contributor/admin) via dropdown +- Ban/unban users with status indicators +- Uses better-auth admin plugin client methods" +``` + +--- + +### Task 9: Admin Hardware Page + +**Files:** +- Create: `app/(admin)/admin/hardware/page.tsx` +- Create: `app/(admin)/admin/hardware/hardware-client.tsx` + +- [ ] **Step 1: Create the hardware server component** + +Create `app/(admin)/admin/hardware/page.tsx`: + +```tsx +import type { Metadata } from "next" +import { HardwareClient } from "./hardware-client" + +export const metadata: Metadata = { + title: "Hardware", +} + +export default function HardwarePage() { + return +} +``` + +- [ ] **Step 2: Create the hardware client component with CRUD** + +Create `app/(admin)/admin/hardware/hardware-client.tsx` — a hardware management page with: + +- **List**: Fetches from `/api/hardware/stats`, renders device cards (icon + name + slug + type badge + benchmark count) with edit/delete buttons +- **Add Device** button at top opens a modal form +- **Edit modal**: Pre-filled form for name, slug (readonly on edit), device type (select handheld/console), sort order, image URL +- **Delete**: Confirmation via `confirm()`, calls `DELETE /api/hardware/:slug` +- **Create**: Modal form, calls `POST /api/hardware` with body `{ slug, name, deviceType, image, sortOrder }` +- **Update**: Modal form, calls `PATCH /api/hardware/:slug` with body `{ name, deviceType, image, sortOrder }` +- Card grid uses same pattern as devices list page +- Modal styled: `fixed inset-0 z-50`, backdrop `bg-black/50`, form `bg-background border border-border rounded-xl p-6 max-w-md` + +- [ ] **Step 3: Commit** + +```bash +git add app/(admin)/admin/hardware/ +git commit -m "feat: add admin hardware management page with CRUD + +- Device list fetched from /api/hardware/stats +- Create/edit devices via modal form +- Delete with confirmation dialog +- Uses existing hardware CRUD API endpoints" +``` + +--- + +### Task 10: Admin Games Page + +**Files:** +- Create: `app/(admin)/admin/games/page.tsx` +- Create: `app/(admin)/admin/games/games-client.tsx` + +- [ ] **Step 1: Create the games server component** + +Create `app/(admin)/admin/games/page.tsx`: + +```tsx +import type { Metadata } from "next" +import { GamesClient } from "./games-client" + +export const metadata: Metadata = { + title: "Games", +} + +export default function GamesPage() { + return +} +``` + +- [ ] **Step 2: Create the games client component** + +Create `app/(admin)/admin/games/games-client.tsx` — a games management page with: + +- **List**: Fetches from `/api/games?limit=50` (existing CRUD endpoint with search support), renders a table +- **Search**: Debounced input, passes `?search=query` to the API +- **Table columns**: Cover image (small thumbnail via `next/image`), Title, Developer, Source (Steam/manual badge), Sync Status (badge), Actions +- **Actions**: "View" link to `/game/:id` (external-link icon), "Delete" button (admin only, calls `DELETE /api/games/:gameId`) +- **Pagination**: "Load more" button or offset-based pagination controls (`?offset=N&limit=50`) +- Style: same table pattern as users page + +- [ ] **Step 3: Commit** + +```bash +git add app/(admin)/admin/games/ +git commit -m "feat: add admin games management page + +- Game list with search and pagination +- Source and sync status badges +- View on site link and delete action +- Uses existing games CRUD API" +``` + +--- + +### Task 11: Sitemap Update & Final SEO + +**Files:** +- Modify: `app/sitemap.ts` + +- [ ] **Step 1: Remove /search from sitemap since it's noindexed** + +In `app/sitemap.ts`, find and remove any entry for `/search` (it's noindexed in its layout metadata, so including it in the sitemap is contradictory). + +- [ ] **Step 2: Commit** + +```bash +git add app/sitemap.ts +git commit -m "fix: remove noindexed /search page from sitemap" +``` + +--- + +### Task 12: Build, Lint & Cleanup + +**Files:** All modified/created files + +- [ ] **Step 1: Run full build** + +Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && bun run build 2>&1` +Expected: Build succeeds with zero errors. + +- [ ] **Step 2: Fix any build errors** + +If the build fails, read the error messages, fix them, and re-run. + +- [ ] **Step 3: Run linter** + +Run: `cd /Users/adrianbonpin/Documents/Code/personal/deckyvault && bun run lint 2>&1` +Expected: No errors. + +- [ ] **Step 4: Fix any lint warnings** + +If lint issues exist, fix them and re-run. + +- [ ] **Step 5: Final commit** + +```bash +git add -A +git commit -m "chore: fix build errors and lint warnings from devices/admin implementation" +```