"use client"
import { useEffect, useRef, useState } from "react"
import { motion, Reorder, useDragControls } from "motion/react"
import {
Send,
Loader2,
AlertCircle,
Monitor,
Gauge,
SlidersHorizontal,
Terminal,
FileText,
ImagePlus,
X,
GripVertical,
Upload,
ImageIcon,
} from "lucide-react"
import { TiptapEditor } from "@/components/tiptap-editor"
import type { SettingCategory } from "@/components/wizard/settings-editor"
import type { PerformanceData } from "./performance-step"
import type { EnvironmentData } from "./environment-step"
import { UPSCALER_TYPE_OPTIONS, FRAME_GEN_OPTIONS } from "./environment-step"
export interface ReviewData {
hardwareSlug: string
hardwareName: string
hardwareWattHours: number | null
hardwareDeviceType: string | null
gameVersionLabel: string
antiCheat: {
antiCheatRelevant: boolean
antiCheatName: string
antiCheatStatus: "none" | "supported" | "unsupported" | "unknown"
}
performance: PerformanceData
settings: SettingCategory[]
environment: EnvironmentData
}
interface ReviewStepProps {
data: ReviewData
userNotes: string
onUserNotesChange: (notes: string) => void
onSubmit: () => void
isSubmitting: boolean
error: string | null
screenshotFiles: File[]
onScreenshotFilesChange: (files: File[]) => void
submitPhase: "idle" | "uploading" | "saving" | "success" | "error"
}
function SectionHeader({
icon: Icon,
label,
}: {
icon: React.ElementType
label: string
}) {
return (
{label}
)
}
function SummaryRow({
label,
value,
}: {
label: string
value: React.ReactNode
}) {
return (
{label}
{value}
)
}
function formatNumber(val: number | undefined): string {
if (val === undefined || val === null) return "Not set"
return String(val)
}
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
function ScreenshotCard({
file,
url,
index,
onRemove,
}: {
file: File
url: string
index: number
onRemove: () => void
}) {
const dragControls = useDragControls()
return (
{/* Image */}
{/* eslint-disable-next-line @next/next/no-img-element */}

{/* Overlay gradient */}
{/* Drag handle (top-left) */}
{ e.preventDefault(); dragControls.start(e) }}
>
{/* Remove button (top-right) */}
{/* Index badge */}
#{index + 1}
{/* File info (bottom-right) */}
{formatFileSize(file.size)}
{/* Filename bar */}
)
}
export function ReviewStep({
data,
userNotes,
onUserNotesChange,
onSubmit,
isSubmitting,
error,
screenshotFiles,
onScreenshotFilesChange,
submitPhase,
}: ReviewStepProps) {
const {
hardwareName,
gameVersionLabel,
antiCheat,
performance,
environment,
settings,
} = data
// Stable URL mapping so reordering doesn't flicker
const urlMapRef = useRef(new Map())
const [previewUrls, setPreviewUrls] = useState([])
const [isDragOver, setIsDragOver] = useState(false)
useEffect(() => {
const map = urlMapRef.current
// Create URLs for new files
for (const file of screenshotFiles) {
if (!map.has(file)) {
map.set(file, URL.createObjectURL(file))
}
}
// Revoke URLs for removed files
for (const [file, url] of Array.from(map.entries())) {
if (!screenshotFiles.includes(file)) {
URL.revokeObjectURL(url)
map.delete(file)
}
}
setPreviewUrls(screenshotFiles.map((f) => map.get(f)!))
}, [screenshotFiles])
useEffect(() => {
const map = urlMapRef.current
return () => {
for (const url of map.values()) {
URL.revokeObjectURL(url)
}
map.clear()
}
}, [])
const handleFiles = (files: FileList | null) => {
if (!files) return
const incoming = Array.from(files).filter((f) =>
/image\/(jpeg|png|webp)/.test(f.type),
)
const current = screenshotFiles || []
const combined = [...current, ...incoming].slice(0, 2)
onScreenshotFilesChange?.(combined)
}
const removeFile = (file: File) => {
const newFiles = (screenshotFiles || []).filter((f) => f !== file)
onScreenshotFilesChange?.(newFiles)
}
const handleReorder = (newFiles: File[]) => {
onScreenshotFilesChange(newFiles)
}
const upscalerLabel = (() => {
if (
!data.environment.upscalerType ||
data.environment.upscalerType === "none"
)
return "None"
const opt = UPSCALER_TYPE_OPTIONS.find(
(o) => o.value === data.environment.upscalerType,
)
const base = opt?.label ?? data.environment.upscalerType
return data.environment.upscalerVersion
? `${base} ${data.environment.upscalerVersion}`
: base
})()
const frameGenLabel = (() => {
if (
!environment.frameGenMethod ||
environment.frameGenMethod === "none"
)
return "None"
const opt = FRAME_GEN_OPTIONS.find(
(o) => o.value === environment.frameGenMethod,
)
return opt?.label ?? environment.frameGenMethod
})()
const canUploadMore =
(!screenshotFiles || screenshotFiles.length < 2) &&
submitPhase !== "uploading" &&
submitPhase !== "saving"
return (
Review & Submit
Review your submission details below. Add any additional
notes and click Submit when ready.
{/* Summary Cards */}
{/* Setup: Hardware + Version + Anti-Cheat */}
{antiCheat.antiCheatRelevant && (
<>
>
)}
{!antiCheat.antiCheatRelevant && (
)}
{/* Performance */}
{
const wh = data.hardwareWattHours
const tdp = performance.tdpWatts
if (
wh &&
tdp &&
tdp > 0 &&
data.hardwareDeviceType === "handheld"
) {
const hours = wh / tdp
const mins = Math.round(hours * 60)
return `~${hours.toFixed(1)}h (${mins} min)`
}
return "Not available — requires TDP and a handheld device"
})()}
/>
{/* Environment */}
{environment.launchOptions && (
{environment.launchOptions}
}
/>
)}
{environment.youtubeVideoId &&
/^[a-zA-Z0-9_-]{11}$/.test(environment.youtubeVideoId) ? (
) : (
)}
{/* Settings */}
{settings.length === 0 ? (
No settings configured
) : (
{settings.map((cat) => (
{cat.category}
{cat.settings.map((s) => (
{s.title}: {String(s.value)}
))}
))}
)}
{/* Screenshots */}
{(submitPhase === "uploading" ||
submitPhase === "saving") && (
Uploading...
)}
{data.settings.length === 0 ? (
Add game settings to enable screenshot upload
) : (
{/* Screenshot grid with drag-to-reorder */}
{screenshotFiles && screenshotFiles.length > 0 && (
{screenshotFiles.map((file, idx) => (
removeFile(file)}
/>
))}
)}
{/* Upload area */}
{canUploadMore ? (
) : (
Maximum 2 screenshots reached
)}
)}
{/* User Notes */}
onUserNotesChange(JSON.stringify(json))}
placeholder='Add any extra details about your experience...'
/>
{/* Error */}
{error && (
{error}
)}
{/* Submit Button */}
)
}