diff --git a/app/game/[id]/game-page-client.tsx b/app/game/[id]/game-page-client.tsx index 3be2a6c..212a094 100644 --- a/app/game/[id]/game-page-client.tsx +++ b/app/game/[id]/game-page-client.tsx @@ -1,7 +1,7 @@ "use client" import { useCallback, useState, useEffect, useMemo, useRef } from "react" -import { useRouter } from "next/navigation" +import { useRouter, useSearchParams } from "next/navigation" import Image from "next/image" import { Gamepad2Icon, @@ -201,6 +201,7 @@ export function GamePageClient({ gameId, }: Props) { const router = useRouter() + const searchParams = useSearchParams() const { data: session } = useSession() const [imgError, setImgError] = useState(false) const [stats, setStats] = useState(null) @@ -216,6 +217,32 @@ export function GamePageClient({ const [reportedPresets, setReportedPresets] = useState>(new Set()) const presetsRef = useRef(null) + // On mount, auto-open preset from URL + useEffect(() => { + const presetId = searchParams.get("preset") + queueMicrotask(() => { + if (presetId) { + setSelectedPresetId(presetId) + } else { + setSelectedPresetId(null) + } + }) + }, [searchParams]) + + const handlePresetOpen = (presetId: string) => { + setSelectedPresetId(presetId) + const url = new URL(window.location.href) + url.searchParams.set("preset", presetId) + router.replace(url.toString(), { scroll: false }) + } + + const handlePresetClose = () => { + setSelectedPresetId(null) + const url = new URL(window.location.href) + url.searchParams.delete("preset") + router.replace(url.toString(), { scroll: false }) + } + const handleDeletePreset = async (presetId: string) => { try { const res = await fetch(`/api/performance/${presetId}/user-delete`, { @@ -751,7 +778,7 @@ export function GamePageClient({ return ( setSelectedPresetId(preset.id)} + onClick={() => handlePresetOpen(preset.id)} 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" @@ -992,7 +1019,8 @@ export function GamePageClient({ return ( setSelectedPresetId(null)} + gameId={gameId} + onClose={handlePresetClose} onDelete={handleDeletePreset} onReport={handleReportPreset} hasReported={reportedPresets.has(preset.id)} diff --git a/app/game/[id]/page.tsx b/app/game/[id]/page.tsx index 9aef754..429ae05 100644 --- a/app/game/[id]/page.tsx +++ b/app/game/[id]/page.tsx @@ -1,4 +1,5 @@ import type { Metadata } from "next" +import { Suspense } from "react" import { notFound } from "next/navigation" import { after } from "next/server" import { db } from "@/lib/db/index" @@ -326,17 +327,19 @@ export default async function GamePage({ }), }} /> - + }> + + ) } diff --git a/app/game/[id]/preset-detail-modal.tsx b/app/game/[id]/preset-detail-modal.tsx index 74c5f90..1f5ac66 100644 --- a/app/game/[id]/preset-detail-modal.tsx +++ b/app/game/[id]/preset-detail-modal.tsx @@ -2,6 +2,7 @@ import { useState } from "react" import Image from "next/image" +import { useRouter } from "next/navigation" import { AnimatePresence, motion } from "motion/react" import { useSession } from "@/lib/auth-client" import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries" @@ -15,11 +16,14 @@ import { ShieldCheckIcon, XIcon, UserIcon, + ShareIcon, + PencilIcon, } from "lucide-react" import { TiptapRenderer } from "@/components/tiptap-renderer" interface Preset { id: string + gameId?: string hardwareSlug: string hardwareName: string upvotes: number @@ -45,6 +49,7 @@ interface Preset { interface PresetDetailModalProps { preset: Preset + gameId: string onClose: () => void onDelete: (presetId: string) => void onReport: ( @@ -67,11 +72,13 @@ function formatValue(value: string | number | boolean): string { export function PresetDetailModal({ preset, + gameId, onClose, onDelete, onReport, hasReported, }: PresetDetailModalProps) { + const router = useRouter() const { data: session } = useSession() const [activeCategoryIndex, setActiveCategoryIndex] = useState(0) @@ -81,6 +88,7 @@ export function PresetDetailModal({ "inaccurate" | "spam" | "inappropriate" | "other" >("inaccurate") const [reportDetails, setReportDetails] = useState("") + const [copied, setCopied] = useState(false) const [userVote, setUserVote] = useState<"up" | "down" | null>(null) const [localUpvotes, setLocalUpvotes] = useState(preset.upvotes) @@ -90,6 +98,13 @@ export function PresetDetailModal({ const isAdmin = session?.user?.role === "admin" const isAuthenticated = !!session?.user + const handleShare = () => { + const url = `${window.location.origin}/game/${gameId}?preset=${preset.id}` + navigator.clipboard.writeText(url) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + const categories = preset.settingsJson ?? [] const hasCategories = categories.length > 0 const currentCategory = hasCategories ? categories[activeCategoryIndex] : null @@ -349,6 +364,24 @@ export function PresetDetailModal({ )} + {(isOwner || isAdmin) && ( + + )} + + + {session && !hasReported && ( <> {!showReportForm ? (