diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx index bffa1ff..00f5374 100644 --- a/app/game/[id]/game-page-client.tsx +++ b/app/game/[id]/game-page-client.tsx @@ -12,11 +12,16 @@ import { SparklesIcon, DatabaseIcon, Plus, + ChevronLeftIcon, + ChevronRightIcon, } from "lucide-react" import Link from "next/link" import { useSession } from "@/lib/auth-client" import { FaSteam } from "react-icons/fa" -import { motion } from "motion/react" +import { motion, AnimatePresence } from "motion/react" + +import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries" +import { PresetDetailModal } from "./preset-detail-modal" import { BookmarkButton } from "@/components/saved-games/bookmark-button" @@ -67,6 +72,8 @@ interface Preset { hardwareSlug: string hardwareName: string upvotes: number + downvotes: number + settingsJson: GameSettingCategory[] | null settingsCount: number fpsAvg: number | null fpsLow: number | null @@ -76,6 +83,12 @@ interface Preset { 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 } @@ -207,6 +220,38 @@ export function GamePageClient({ frameGen: "all", }) const [loading, setLoading] = useState(true) + const [selectedPresetId, setSelectedPresetId] = useState(null) + const [reportedPresets, setReportedPresets] = useState>(new Set()) + const presetsRef = useRef(null) + + const handleDeletePreset = async (presetId: string) => { + try { + const res = await fetch(`/api/performance/${presetId}/user-delete`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }) + if (res.ok) { + setSelectedPresetId(null) + } + } catch (err) { + console.error("Failed to delete preset:", err) + } + } + + const handleReportPreset = async (presetId: string, reason: "inaccurate" | "spam" | "inappropriate" | "other", details?: string) => { + try { + const res = await fetch(`/api/performance/${presetId}/report`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason, details }), + }) + if (res.ok) { + setReportedPresets((prev) => new Set(prev).add(presetId)) + } + } catch (err) { + console.error("Failed to report preset:", err) + } + } const handleImgError = useCallback(() => setImgError(true), []) @@ -322,6 +367,7 @@ export function GamePageClient({ }, []) return ( + <>
{/* Section 1: Hero Header */} - {/* Section 4: Statistics Dashboard */} + {/* Section 4: Community Presets */} +
+
+

+ Community Presets +

+ + {filteredPresets.length} preset + {filteredPresets.length !== 1 ? "s" : ""} + +
+ + {filteredPresets.length === 0 ? ( +
+ +

+ No presets match the selected filters +

+
+ ) : ( +
+ {/* Left scroll button */} + + {/* Right scroll button */} + +
+ {[...filteredPresets] + .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + .map((preset) => { + const raw = isRawPerformerPreset(preset) + const fpsColor = getFpsColor(preset) + return ( + setSelectedPresetId(preset.id)} + className={`flex-shrink-0 w-72 flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${ + raw + ? "border-green-500/30 bg-green-500/5" + : "border-border bg-text/3" + }`} + > + {/* Header */} +
+
+
+

+ {generatePresetName(preset)} +

+ {raw && ( + + + Raw + + )} + {isPoorPerformancePreset(preset) && preset.fpsAvg !== null && ( + + ⚠ Slow + + )} +
+

+ {preset.hardwareName} +

+
+
+ + {preset.upvotes} +
+
+ + {/* Settings count */} +
+ {preset.settingsCount} settings +
+ + {/* FPS */} + {preset.fpsAvg !== null && ( +
+ + {preset.fpsAvg} + + + {" "} + avg + + {preset.fpsLow !== null && ( + + {" "} + ({preset.fpsLow}– + {preset.fpsHigh}) + + )} +
+ )} + + {/* Technology tags */} +
+ {preset.upscalerType && + preset.upscalerType !== + "none" && ( + + {preset.upscalerType.toUpperCase()} + {preset.upscalerVersion ? ` ${preset.upscalerVersion}` : ""} + + )} + {preset.frameGenMethod && + preset.frameGenMethod !== + "none" && ( + + {preset.frameGenMethod === + "fsr_fg" + ? "FSR FG" + : preset.frameGenMethod === + "dlss_fg" + ? "DLSS FG" + : preset.frameGenMethod} + + )} +
+ + {/* Proton + OS */} +
+ {preset.protonVersion && ( + + Proton{" "} + {preset.protonVersion} + + )} + {preset.osVersion && ( + + SteamOS {preset.osVersion} + + )} +
+
+ ) + })} +
+
+ )} +
+
+ + {/* Section 5: Statistics Dashboard */} +
{/* Row 1 — Featured */} @@ -792,156 +1010,23 @@ export function GamePageClient({ )}
- - {/* Section 5: Community Presets */} - -
-
-

- Community Presets -

- - {filteredPresets.length} preset - {filteredPresets.length !== 1 ? "s" : ""} - -
- - {filteredPresets.length === 0 ? ( -
- -

- No presets match the selected filters -

-
- ) : ( -
- {filteredPresets.map((preset) => { - const raw = isRawPerformerPreset(preset) - const fpsColor = getFpsColor(preset) - return ( -
- {/* Header */} -
-
-
-

- {generatePresetName(preset)} -

- {raw && ( - - - Raw - - )} - {isPoorPerformancePreset(preset) && preset.fpsAvg !== null && ( - - ⚠ Slow - - )} -
-

- {preset.hardwareName} -

-
-
- - {preset.upvotes} -
-
- - {/* Settings count */} -
- {preset.settingsCount} settings -
- - {/* FPS */} - {preset.fpsAvg !== null && ( -
- - {preset.fpsAvg} - - - {" "} - avg - - {preset.fpsLow !== null && ( - - {" "} - ({preset.fpsLow}– - {preset.fpsHigh}) - - )} -
- )} - - {/* Technology tags */} -
- {preset.upscalerType && - preset.upscalerType !== - "none" && ( - - {preset.upscalerType.toUpperCase()} - {preset.upscalerVersion ? ` ${preset.upscalerVersion}` : ""} - - )} - {preset.frameGenMethod && - preset.frameGenMethod !== - "none" && ( - - {preset.frameGenMethod === - "fsr_fg" - ? "FSR FG" - : preset.frameGenMethod === - "dlss_fg" - ? "DLSS FG" - : preset.frameGenMethod} - - )} -
- - {/* Proton + OS */} -
- {preset.protonVersion && ( - - Proton{" "} - {preset.protonVersion} - - )} - {preset.osVersion && ( - - SteamOS {preset.osVersion} - - )} -
-
- ) - })} -
- )} -
-
+ + {selectedPresetId && (() => { + const preset = filteredPresets.find((p) => p.id === selectedPresetId) + if (!preset) return null + return ( + setSelectedPresetId(null)} + onDelete={handleDeletePreset} + onReport={handleReportPreset} + hasReported={reportedPresets.has(preset.id)} + /> + ) + })()} + + ) } diff --git a/app/globals.css b/app/globals.css index 1ec7d4e..d836716 100644 --- a/app/globals.css +++ b/app/globals.css @@ -17,3 +17,11 @@ /* Fonts */ --font-sans: var(--font-lexend); } + +.scrollbar-hide { + scrollbar-width: none; + -ms-overflow-style: none; +} +.scrollbar-hide::-webkit-scrollbar { + display: none; +}