# Community Presets UX + SEO Overhaul Design Spec ## Overview Two major improvements to DeckyVault: 1. **Community Presets UX Overhaul** — View settings, delete/report, horizontal layout, reorder above graphs 2. **SEO Robustness** — Dynamic metadata, sitemap expansion, JSON-LD, dynamic OG images for game pages --- ## Area 1: Community Presets UX Overhaul ### 1.1 Layout Change — Horizontal Scroll Row **Current:** 3-column vertical grid (`grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`) **Proposed:** Single horizontal scroll row (`flex overflow-x-auto`) with cards ordered newest-first (left-to-right). Cards are date-sorted by `createdAt` descending, so the newest preset appears leftmost. Implementation: - Use `flex overflow-x-auto gap-4 pb-4` on the container - Each card is a `flex-shrink-0 w-72` (or similar fixed width) - Hide scrollbar via CSS (`scrollbar-width: none` + `::-webkit-scrollbar { display: none }`) - Add left/right scroll buttons (chevrons) overlaid at the edges for discoverability ### 1.2 Section Reorder — Presets Above Statistics **Current:** Section order in `game-page-client.tsx`: 1. Hero Header 2. About + Details 3. Device Selector + Filters 4. Statistics Dashboard (charts) 5. Community Presets **Proposed:** 1. Hero Header 2. About + Details 3. Device Selector + Filters 4. **Community Presets** ← moved up 5. Statistics Dashboard ← moved down Simply reorder the two `` sections in the JSX. ### 1.3 View Settings — AnimatePresence Modal **Current:** Preset cards show summary info (FPS, upvotes, tech tags, settings count) but the actual `settingsJson` content is not displayed anywhere. **Proposed:** Clicking a preset card opens a modal using `AnimatePresence` + `layoutId` for a smooth animated transition from the card to the expanded modal. The modal shows: - **Settings Table**: Each `GameSettingCategory` rendered as a collapsible section with category name as header, and settings as a two-column table (setting name → value) - **Full Metadata**: Device name, Proton version, SteamOS version, FPS (avg/min/max), Upscaler, Frame Gen, Launch Options, User Notes - **Community info**: Submitter (if we include userId in the preset data), upvotes/downvotes, verified badge, creation date - **Actions**: Upvote/downvote buttons, Report button, Delete button (owner/admin only) Animation detail: - Each card has a `layoutId={preset.id}` on the card container - The modal wrapper uses the same `layoutId` when replacing the card - `AnimatePresence mode="wait"` wraps the transition - The card's click handler sets `selectedPresetId` state - When `selectedPresetId` is set, render the modal overlay with `motion.div layoutId={selectedPresetId}` ### 1.4 Delete — Soft Delete for Owners + Admins **Current API state:** - Admin soft-delete via `DELETE /api/performance/:id` (sets `isRemoved: true`) - Owner hard-delete via `DELETE /api/performance/:id/user-delete` (permanently removes row) **Proposed changes:** #### 1.4.1 Change user-delete to soft delete Modify the `/api/performance/:id/user-delete` endpoint to set `isRemoved: true` instead of hard-deleting. This aligns owner deletes with admin deletes. Add an optional `reason` body parameter that sets `removedReason`. #### 1.4.2 Delete UI in preset modal In the preset detail modal: - **Owner sees**: A "Delete" button (red, secondary style). Clicking it shows a confirmation dialog: "Are you sure you want to remove this preset? This can be undone by an admin." On confirm, calls `DELETE /api/performance/:id/user-delete`. - **Admin sees**: Same "Delete" button on any preset (not just their own). - **Other users**: No delete button. ### 1.5 Report Flow **New database table: `reports`** | Column | Type | Notes | |---|---|---| | id | text PK | UUID auto-generated | | entryId | text FK → performance_entries.id | onDelete: cascade | | reporterId | text FK → user.id | onDelete: cascade | | reason | enum(inaccurate, spam, inappropriate, other) | Required | | details | text | Optional free-text | | status | enum(open, reviewed, dismissed) | Default: open | | createdAt | timestamp | Default: now() | **New API route: `POST /api/performance/:id/report`** - Auth required (any user role) - Body: `{ reason: "inaccurate" | "spam" | "inappropriate" | "other", details?: string }` - Prevents duplicate reports (one report per user per entry) - Returns the created report **Report UI in preset modal:** - Every preset card/modal shows a "Report" icon button (flag icon) - Clicking it opens a small popover/dropdown with reason selection - On submit, calls the API and shows a toast "Report submitted. Thank you." - If user already reported this entry, show "Already reported" instead ### 1.6 Preset Data Enrichment The server component currently does not pass `userId`, `settingsJson` (full content), or `userNotes` to the client. To support the modal view: **Changes to `app/game/[id]/page.tsx` serialized presets:** Add these fields to the `serializedPresets` mapping: - `settingsJson` — the full `GameSettingCategory[]` array - `userNotes` — string | null - `userId` — the owner's user ID (needed for delete permission check) - `userName` — the owner's display name - `userImage` — the owner's avatar URL - `launchOptions` — string | null - `downvotes` — number - `verifiedAt` — string | null The SQL query in `page.tsx` needs to join on `user` table to get `userName` and `userImage`. ### 1.7 Client-Side Preset Type Update Update the `Preset` interface in `game-page-client.tsx` to include the new fields: ```typescript interface Preset { id: string hardwareSlug: string hardwareName: string upvotes: number downvotes: number settingsJson: GameSettingCategory[] | null settingsCount: number fpsAvg: number | null fpsLow: number | null fpsHigh: number | null upscalerType: string | null upscalerVersion: string | null frameGenMethod: string | null protonVersion: string | null osVersion: string | null launchOptions: string | null userNotes: string | null userId: string userName: string | null userImage: string | null verifiedAt: string | null createdAt: string } ``` --- ## Area 2: SEO Overhaul ### 2.1 Game Details Page — Dynamic Metadata **Current:** `export const metadata = { title: "Game" }` — no game-specific data in metadata. **Proposed:** Replace with `generateMetadata` function: ```typescript export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise { const { id } = await params // ... resolve game (reuse existing logic) return { title: `${game.title} - Benchmarks & Settings`, description: game.description ? `${game.description.slice(0, 160)}` : `Find benchmarks, community presets, and performance settings for ${game.title} on Steam Deck.`, openGraph: { title: `${game.title} - Benchmarks & Settings | DeckyVault`, description: game.description?.slice(0, 200) ?? `Benchmarks and settings for ${game.title}`, url: `https://deckyvault.xyz/game/${game.id}`, images: [{ url: `/game/${game.id}/opengraph-image`, width: 1200, height: 630 }], type: "website", }, twitter: { card: "summary_large_image", title: `${game.title} - Benchmarks & Settings | DeckyVault`, description: game.description?.slice(0, 200) ?? `Benchmarks and settings for ${game.title}`, images: [`/game/${game.id}/opengraph-image`], }, } } ``` ### 2.2 Game Details Page — JSON-LD Structured Data Add a `