From 1e42cdd39e492adc66a4bbc7ef8057b1d7497f5d Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Mon, 27 Apr 2026 16:26:05 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20preset=20UX=20=E2=80=94=20gradient=20fad?= =?UTF-8?q?e=20edges,=20Tiptap=20notes=20renderer,=20two-column=20modal=20?= =?UTF-8?q?layout=20with=20stats/settings,=20upvote/downvote=20buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/game/[id]/game-page-client.tsx | 41 +- app/game/[id]/preset-detail-modal.tsx | 695 +++++++++++++------------- components/tiptap-renderer.tsx | 52 ++ 3 files changed, 422 insertions(+), 366 deletions(-) create mode 100644 components/tiptap-renderer.tsx diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx index 00f5374..70620c3 100644 --- a/app/game/[id]/game-page-client.tsx +++ b/app/game/[id]/game-page-client.tsx @@ -12,8 +12,8 @@ import { SparklesIcon, DatabaseIcon, Plus, - ChevronLeftIcon, - ChevronRightIcon, + ThumbsUpIcon, + ThumbsDownIcon, } from "lucide-react" import Link from "next/link" import { useSession } from "@/lib/auth-client" @@ -383,7 +383,8 @@ export function GamePageClient({ {game.title} ) : ( -
- {/* Left scroll button */} - - {/* Right scroll button */} - +
+ {/* Left fade edge */} +
+ {/* Right fade edge */} +
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 ${ + className={`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" @@ -803,9 +794,15 @@ export function GamePageClient({ {preset.hardwareName}

-
- - {preset.upvotes} +
+ + + {preset.upvotes} + + + + {preset.downvotes} +
diff --git a/app/game/[id]/preset-detail-modal.tsx b/app/game/[id]/preset-detail-modal.tsx index 8c2a81f..55879ad 100644 --- a/app/game/[id]/preset-detail-modal.tsx +++ b/app/game/[id]/preset-detail-modal.tsx @@ -6,15 +6,17 @@ import { AnimatePresence, motion } from "motion/react" import { useSession } from "@/lib/auth-client" import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries" import { - TrendingUpIcon, - TrendingDownIcon, + ThumbsUpIcon, + ThumbsDownIcon, FlagIcon, TrashIcon, - ChevronDownIcon, + ChevronLeftIcon, + ChevronRightIcon, ShieldCheckIcon, XIcon, UserIcon, } from "lucide-react" +import { TiptapRenderer } from "@/components/tiptap-renderer" interface Preset { id: string @@ -72,9 +74,7 @@ export function PresetDetailModal({ }: PresetDetailModalProps) { const { data: session } = useSession() - const [openCategories, setOpenCategories] = useState>( - new Set(), - ) + const [activeCategoryIndex, setActiveCategoryIndex] = useState(0) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [showReportForm, setShowReportForm] = useState(false) const [reportReason, setReportReason] = useState< @@ -82,19 +82,52 @@ export function PresetDetailModal({ >("inaccurate") const [reportDetails, setReportDetails] = useState("") + const [userVote, setUserVote] = useState<"up" | "down" | null>(null) + const [localUpvotes, setLocalUpvotes] = useState(preset.upvotes) + const [localDownvotes, setLocalDownvotes] = useState(preset.downvotes) + const isOwner = session?.user?.id === preset.userId const isAdmin = session?.user?.role === "admin" + const isAuthenticated = !!session?.user - const toggleCategory = (index: number) => { - setOpenCategories((prev) => { - const next = new Set(prev) - if (next.has(index)) { - next.delete(index) - } else { - next.add(index) + const categories = preset.settingsJson ?? [] + const hasCategories = categories.length > 0 + const currentCategory = hasCategories ? categories[activeCategoryIndex] : null + + const goPrevCategory = () => { + setActiveCategoryIndex((prev) => (prev > 0 ? prev - 1 : categories.length - 1)) + } + + const goNextCategory = () => { + setActiveCategoryIndex((prev) => (prev < categories.length - 1 ? prev + 1 : 0)) + } + + const handleUpvote = async () => { + if (!isAuthenticated || userVote === "up") return + try { + const res = await fetch(`/api/performance/${preset.id}/upvote`, { method: "POST" }) + if (res.ok) { + if (userVote === "down") setLocalDownvotes((d) => d - 1) + setLocalUpvotes((u) => u + 1) + setUserVote("up") } - return next - }) + } catch (err) { + console.error("Failed to upvote:", err) + } + } + + const handleDownvote = async () => { + if (!isAuthenticated || userVote === "down") return + try { + const res = await fetch(`/api/performance/${preset.id}/downvote`, { method: "POST" }) + if (res.ok) { + if (userVote === "up") setLocalUpvotes((u) => u - 1) + setLocalDownvotes((d) => d + 1) + setUserVote("down") + } + } catch (err) { + console.error("Failed to downvote:", err) + } } const handleReportSubmit = () => { @@ -132,7 +165,7 @@ export function PresetDetailModal({ {/* Modal card */} e.stopPropagation()} > {/* Header */} -
+
-
- {preset.userImage ? ( - { - ) : ( - - )} -
-
-
- - {preset.userName || "Anonymous"} - - {preset.verifiedAt && ( - - - - )} -
- - {formatDate(preset.createdAt)} - -
+

+ {preset.hardwareName} +

- {/* Stats Row */} -
-
- - Upvotes - -
- - - {preset.upvotes} - -
-
-
- - Downvotes - -
- - - {preset.downvotes} - -
-
-
- - Avg FPS - -
- - {preset.fpsAvg ?? "—"} - - {preset.fpsAvg !== null && - preset.fpsLow !== null && - preset.fpsHigh !== null && ( - - ({Math.round(preset.fpsLow)} - – - {Math.round(preset.fpsHigh)}) - - )} -
-
-
- - {/* Metadata Grid */} -
- - - - - -
- - {/* Settings Table */} - {preset.settingsJson && - preset.settingsJson.length > 0 && ( -
-

- Settings ({preset.settingsCount}) -

-
- {preset.settingsJson.map( - (category, idx) => ( -
- - - {openCategories.has( - idx, - ) && ( - -
- {category.settings.map( - ( - setting, - sIdx, - ) => ( -
- - { - setting.title - } - - - {formatValue( - setting.value, - )} - -
- ), - )} -
-
- )} -
-
- ), + {/* Two-column body */} +
+ {/* Left panel */} +
+ {/* User info */} +
+
+ {preset.userImage ? ( + {preset.userName + ) : ( + )}
-
- )} - - {/* User Notes */} - {preset.userNotes && ( -
-

- Notes -

-

- {preset.userNotes} -

-
- )} - - {/* Actions */} -
- {(isOwner || isAdmin) && ( - <> - {!showDeleteConfirm ? ( - - ) : ( +
- - Are you sure? + + {preset.userName || "Anonymous"} - - -
- )} - - )} - - {session && !hasReported && ( - <> - {!showReportForm ? ( - - ) : ( -
- -