feat: add share and edit links to preset modal, sync URL preset param
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useState, useEffect, useMemo, useRef } from "react"
|
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 Image from "next/image"
|
||||||
import {
|
import {
|
||||||
Gamepad2Icon,
|
Gamepad2Icon,
|
||||||
@@ -201,6 +201,7 @@ export function GamePageClient({
|
|||||||
gameId,
|
gameId,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const [imgError, setImgError] = useState(false)
|
const [imgError, setImgError] = useState(false)
|
||||||
const [stats, setStats] = useState<StatsResponse | null>(null)
|
const [stats, setStats] = useState<StatsResponse | null>(null)
|
||||||
@@ -216,6 +217,32 @@ export function GamePageClient({
|
|||||||
const [reportedPresets, setReportedPresets] = useState<Set<string>>(new Set())
|
const [reportedPresets, setReportedPresets] = useState<Set<string>>(new Set())
|
||||||
const presetsRef = useRef<HTMLDivElement>(null)
|
const presetsRef = useRef<HTMLDivElement>(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) => {
|
const handleDeletePreset = async (presetId: string) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/performance/${presetId}/user-delete`, {
|
const res = await fetch(`/api/performance/${presetId}/user-delete`, {
|
||||||
@@ -751,7 +778,7 @@ export function GamePageClient({
|
|||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={preset.id}
|
key={preset.id}
|
||||||
onClick={() => 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 ${
|
className={`shrink-0 w-72 flex flex-col gap-3 p-4 rounded-xl border transition-colors cursor-pointer hover:border-primary/30 ${
|
||||||
raw
|
raw
|
||||||
? "border-green-500/30 bg-green-500/5"
|
? "border-green-500/30 bg-green-500/5"
|
||||||
@@ -992,7 +1019,8 @@ export function GamePageClient({
|
|||||||
return (
|
return (
|
||||||
<PresetDetailModal
|
<PresetDetailModal
|
||||||
preset={preset}
|
preset={preset}
|
||||||
onClose={() => setSelectedPresetId(null)}
|
gameId={gameId}
|
||||||
|
onClose={handlePresetClose}
|
||||||
onDelete={handleDeletePreset}
|
onDelete={handleDeletePreset}
|
||||||
onReport={handleReportPreset}
|
onReport={handleReportPreset}
|
||||||
hasReported={reportedPresets.has(preset.id)}
|
hasReported={reportedPresets.has(preset.id)}
|
||||||
|
|||||||
+14
-11
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
|
import { Suspense } from "react"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { after } from "next/server"
|
import { after } from "next/server"
|
||||||
import { db } from "@/lib/db/index"
|
import { db } from "@/lib/db/index"
|
||||||
@@ -326,17 +327,19 @@ export default async function GamePage({
|
|||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<GamePageClient
|
<Suspense fallback={<div className="min-h-screen" />}>
|
||||||
game={serializedGame}
|
<GamePageClient
|
||||||
counts={{
|
game={serializedGame}
|
||||||
benchmarks: benchmarkCount,
|
counts={{
|
||||||
presets: presetCount,
|
benchmarks: benchmarkCount,
|
||||||
comments: commentCount,
|
presets: presetCount,
|
||||||
}}
|
comments: commentCount,
|
||||||
platformSupport={platformSupport}
|
}}
|
||||||
presets={serializedPresets}
|
platformSupport={platformSupport}
|
||||||
gameId={game.id}
|
presets={serializedPresets}
|
||||||
/>
|
gameId={game.id}
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
import { AnimatePresence, motion } from "motion/react"
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
import { useSession } from "@/lib/auth-client"
|
import { useSession } from "@/lib/auth-client"
|
||||||
import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries"
|
import type { GameSettingCategory } from "@/lib/db/schema/performanceEntries"
|
||||||
@@ -15,11 +16,14 @@ import {
|
|||||||
ShieldCheckIcon,
|
ShieldCheckIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
|
ShareIcon,
|
||||||
|
PencilIcon,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
import { TiptapRenderer } from "@/components/tiptap-renderer"
|
import { TiptapRenderer } from "@/components/tiptap-renderer"
|
||||||
|
|
||||||
interface Preset {
|
interface Preset {
|
||||||
id: string
|
id: string
|
||||||
|
gameId?: string
|
||||||
hardwareSlug: string
|
hardwareSlug: string
|
||||||
hardwareName: string
|
hardwareName: string
|
||||||
upvotes: number
|
upvotes: number
|
||||||
@@ -45,6 +49,7 @@ interface Preset {
|
|||||||
|
|
||||||
interface PresetDetailModalProps {
|
interface PresetDetailModalProps {
|
||||||
preset: Preset
|
preset: Preset
|
||||||
|
gameId: string
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onDelete: (presetId: string) => void
|
onDelete: (presetId: string) => void
|
||||||
onReport: (
|
onReport: (
|
||||||
@@ -67,11 +72,13 @@ function formatValue(value: string | number | boolean): string {
|
|||||||
|
|
||||||
export function PresetDetailModal({
|
export function PresetDetailModal({
|
||||||
preset,
|
preset,
|
||||||
|
gameId,
|
||||||
onClose,
|
onClose,
|
||||||
onDelete,
|
onDelete,
|
||||||
onReport,
|
onReport,
|
||||||
hasReported,
|
hasReported,
|
||||||
}: PresetDetailModalProps) {
|
}: PresetDetailModalProps) {
|
||||||
|
const router = useRouter()
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
|
|
||||||
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0)
|
const [activeCategoryIndex, setActiveCategoryIndex] = useState(0)
|
||||||
@@ -81,6 +88,7 @@ export function PresetDetailModal({
|
|||||||
"inaccurate" | "spam" | "inappropriate" | "other"
|
"inaccurate" | "spam" | "inappropriate" | "other"
|
||||||
>("inaccurate")
|
>("inaccurate")
|
||||||
const [reportDetails, setReportDetails] = useState("")
|
const [reportDetails, setReportDetails] = useState("")
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const [userVote, setUserVote] = useState<"up" | "down" | null>(null)
|
const [userVote, setUserVote] = useState<"up" | "down" | null>(null)
|
||||||
const [localUpvotes, setLocalUpvotes] = useState(preset.upvotes)
|
const [localUpvotes, setLocalUpvotes] = useState(preset.upvotes)
|
||||||
@@ -90,6 +98,13 @@ export function PresetDetailModal({
|
|||||||
const isAdmin = session?.user?.role === "admin"
|
const isAdmin = session?.user?.role === "admin"
|
||||||
const isAuthenticated = !!session?.user
|
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 categories = preset.settingsJson ?? []
|
||||||
const hasCategories = categories.length > 0
|
const hasCategories = categories.length > 0
|
||||||
const currentCategory = hasCategories ? categories[activeCategoryIndex] : null
|
const currentCategory = hasCategories ? categories[activeCategoryIndex] : null
|
||||||
@@ -349,6 +364,24 @@ export function PresetDetailModal({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{(isOwner || isAdmin) && (
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/game/${gameId}/submit?edit=${preset.id}`)}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-primary border border-primary/20 hover:bg-primary/10 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<PencilIcon className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleShare}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium text-primary border border-primary/20 hover:bg-primary/10 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<ShareIcon className="h-4 w-4" />
|
||||||
|
{copied ? "Link copied!" : "Share"}
|
||||||
|
</button>
|
||||||
|
|
||||||
{session && !hasReported && (
|
{session && !hasReported && (
|
||||||
<>
|
<>
|
||||||
{!showReportForm ? (
|
{!showReportForm ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user