Make SubmitForm dummied out when not signed in

This commit is contained in:
Jared Schoeny 2025-10-09 23:13:58 -10:00
parent 3e3a58354f
commit d29ef3d3ed
2 changed files with 281 additions and 134 deletions

View File

@ -11,7 +11,7 @@ export default async function SubmitPage() {
<h1 className="text-3xl font-bold tracking-tight">Submit your ROM hack</h1> <h1 className="text-3xl font-bold tracking-tight">Submit your ROM hack</h1>
<p className="mt-2 text-[15px] text-foreground/80">Share your hack so others can discover and play it.</p> <p className="mt-2 text-[15px] text-foreground/80">Share your hack so others can discover and play it.</p>
<div className="mt-8"> <div className="mt-8">
<SubmitForm /> <SubmitForm dummy={!user} />
</div> </div>
{!user && <SubmitAuthOverlay />} {!user && <SubmitAuthOverlay />}
</div> </div>

View File

@ -45,7 +45,11 @@ function SortableCoverItem({ id, index, url, onRemove }: { id: string; index: nu
); );
} }
export default function SubmitForm() { interface SubmitFormProps {
dummy?: boolean;
}
export default function SubmitForm({ dummy = false }: SubmitFormProps) {
const MAX_COVERS = 10; const MAX_COVERS = 10;
const [title, setTitle] = React.useState(""); const [title, setTitle] = React.useState("");
const [author, setAuthor] = React.useState(""); const [author, setAuthor] = React.useState("");
@ -180,48 +184,87 @@ export default function SubmitForm() {
patchUrl: "", patchUrl: "",
}; };
const isDummy = !!dummy;
return ( return (
<div className="grid gap-8 lg:grid-cols-[1fr_.9fr]"> <div className="grid gap-8 lg:grid-cols-[1fr_.9fr]">
<div> <div>
<form className="grid gap-5"> <form className="grid gap-5">
{/* Title */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Title</label> <label className="text-sm text-foreground/80">Title</label>
<input {!isDummy ? (
value={title} <input
onChange={(e) => setTitle(e.target.value)} value={title}
className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" onChange={(e) => setTitle(e.target.value)}
/> className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
/>
) : (
<div
role="textbox"
aria-disabled
className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none"
>
Your hack title
</div>
)}
<div className="mt-1 text-xs text-foreground/60">URL preview: <span className="text-foreground/80">/hack/{slug || "your-title"}</span></div> <div className="mt-1 text-xs text-foreground/60">URL preview: <span className="text-foreground/80">/hack/{slug || "your-title"}</span></div>
</div> </div>
{/* Author */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Author</label> <label className="text-sm text-foreground/80">Author</label>
<input {!isDummy ? (
value={author} <input
onChange={(e) => setAuthor(e.target.value)} value={author}
className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" onChange={(e) => setAuthor(e.target.value)}
/> className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
/>
) : (
<div role="textbox" aria-disabled className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">Your name</div>
)}
</div> </div>
{/* Summary */}
<div className="grid gap-1"> <div className="grid gap-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm text-foreground/80">Short summary</label> <label className="text-sm text-foreground/80">Short summary</label>
<span className={`text-[11px] ${summaryTooLong ? "text-red-300" : "text-foreground/60"}`}>{summary.length}/{summaryLimit}</span> <span className={`text-[11px] ${summaryTooLong ? "text-red-300" : "text-foreground/60"}`}>{summary.length}/{summaryLimit}</span>
</div> </div>
<input {!isDummy ? (
value={summary} <input
onChange={(e) => setSummary(e.target.value)} value={summary}
placeholder="<= 100 characters" onChange={(e) => setSummary(e.target.value)}
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${summaryTooLong ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`} placeholder="<= 100 characters"
/> className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${summaryTooLong ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
/>
) : (
<div
role="textbox"
aria-disabled
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset flex items-center text-foreground/60 select-none ${summaryTooLong ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
>
Short description, max 100 characters.
</div>
)}
</div> </div>
{/* Long description */}
<div className="grid gap-2"> <div className="grid gap-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-sm text-foreground/80">Long description</label> <label className="text-sm text-foreground/80">Long description</label>
<div className="flex items-center gap-1 text-xs"> {!isDummy && (
<button type="button" onClick={() => setShowMdPreview(false)} className={`px-2 py-1 rounded ${!showMdPreview ? "bg-[var(--surface-2)] ring-1 ring-[var(--border)]" : "text-foreground/70"}`}>Write</button> <div className="flex items-center gap-1 text-xs">
<button type="button" onClick={() => setShowMdPreview(true)} className={`px-2 py-1 rounded ${showMdPreview ? "bg-[var(--surface-2)] ring-1 ring-[var(--border)]" : "text-foreground/70"}`}>Preview</button> <button type="button" onClick={() => setShowMdPreview(false)} className={`px-2 py-1 rounded ${!showMdPreview ? "bg-[var(--surface-2)] ring-1 ring-[var(--border)]" : "text-foreground/70"}`}>Write</button>
</div> <button type="button" onClick={() => setShowMdPreview(true)} className={`px-2 py-1 rounded ${showMdPreview ? "bg-[var(--surface-2)] ring-1 ring-[var(--border)]" : "text-foreground/70"}`}>Preview</button>
</div>
)}
</div> </div>
{!showMdPreview ? ( {isDummy ? (
<div className="prose max-w-none h-36 rounded-md bg-[var(--surface-2)] px-3 py-2 ring-1 ring-inset ring-[var(--border)] text-foreground/60 select-none">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{description || "Write a longer markdown description here."}</ReactMarkdown>
</div>
) : !showMdPreview ? (
<textarea <textarea
rows={6} rows={6}
value={description} value={description}
@ -235,15 +278,23 @@ export default function SubmitForm() {
</div> </div>
)} )}
</div> </div>
{/* Version */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Version</label> <label className="text-sm text-foreground/80">Version</label>
<input {!isDummy ? (
value={version} <input
onChange={(e) => setVersion(e.target.value)} value={version}
placeholder="e.g. v1.2.0" onChange={(e) => setVersion(e.target.value)}
className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" placeholder="e.g. v1.2.0"
/> className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
/>
) : (
<div role="textbox" aria-disabled className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">v0.1.0</div>
)}
</div> </div>
{/* Tags */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Tags</label> <label className="text-sm text-foreground/80">Tags</label>
<div className="rounded-md ring-1 ring-inset ring-[var(--border)] bg-[var(--surface-2)] px-2 py-2"> <div className="rounded-md ring-1 ring-inset ring-[var(--border)] bg-[var(--surface-2)] px-2 py-2">
@ -251,18 +302,24 @@ export default function SubmitForm() {
{tags.map((t, i) => ( {tags.map((t, i) => (
<span key={`${t}-${i}`} className="inline-flex items-center gap-1 rounded-full bg-[var(--surface-2)] px-2 py-1 text-xs ring-1 ring-[var(--border)]"> <span key={`${t}-${i}`} className="inline-flex items-center gap-1 rounded-full bg-[var(--surface-2)] px-2 py-1 text-xs ring-1 ring-[var(--border)]">
{t} {t}
<button type="button" onClick={() => removeTagAt(i)} className="ml-1 text-foreground/70 hover:text-foreground">×</button> {!isDummy && (
<button type="button" onClick={() => removeTagAt(i)} className="ml-1 text-foreground/70 hover:text-foreground">×</button>
)}
</span> </span>
))} ))}
<input {!isDummy ? (
value={tagsInput} <input
onChange={(e) => setTagsInput(e.target.value)} value={tagsInput}
onKeyDown={onTagsKeyDown} onChange={(e) => setTagsInput(e.target.value)}
placeholder={tags.length ? "Add tag" : "Add tags (e.g. QoL, Challenge)"} onKeyDown={onTagsKeyDown}
className="flex-1 min-w-[8rem] bg-transparent px-2 text-sm placeholder:text-foreground/50 focus:outline-none" placeholder={tags.length ? "Add tag" : "Add tags (e.g. QoL, Challenge)"}
/> className="flex-1 min-w-[8rem] bg-transparent px-2 text-sm placeholder:text-foreground/50 focus:outline-none"
/>
) : (
<div className="flex-1 min-w-[8rem] px-2 text-sm text-foreground/50 select-none">{tags.length ? "Add tag" : "Add tags (e.g. QoL, Challenge)"}</div>
)}
</div> </div>
{suggestedTags.length > 0 && ( {!isDummy && suggestedTags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1"> <div className="mt-2 flex flex-wrap gap-1">
{suggestedTags.map((s) => ( {suggestedTags.map((s) => (
<button <button
@ -278,32 +335,53 @@ export default function SubmitForm() {
)} )}
</div> </div>
</div> </div>
{/* Cover images */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Cover images</label> <label className="text-sm text-foreground/80">Cover images</label>
<div className="space-y-3"> <div className="space-y-3">
<textarea {!isDummy ? (
rows={2} <textarea
value={newCoversInput} rows={2}
onChange={(e) => setNewCoversInput(e.target.value)} value={newCoversInput}
placeholder="Paste one or multiple URLs (comma or newline separated)" onChange={(e) => setNewCoversInput(e.target.value)}
className="w-full rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" placeholder="Paste one or multiple URLs (comma or newline separated)"
/> className="w-full rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
/>
) : (
<div className="w-full h-14 rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] text-foreground/60 select-none">
Paste one or multiple URLs (comma or newline separated)
</div>
)}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button {!isDummy ? (
type="button" <>
onClick={addFromInput} <button
disabled={coverUrls.length >= MAX_COVERS} type="button"
className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed" onClick={addFromInput}
> disabled={coverUrls.length >= MAX_COVERS}
Add className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground transition-colors hover:bg-black/5 dark:hover:bg-white/10 disabled:opacity-40 disabled:cursor-not-allowed"
</button> >
<button Add
type="button" </button>
onClick={() => setNewCoversInput("")} <button
className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground/80 transition-colors hover:bg-black/5 dark:hover:bg-white/10" type="button"
> onClick={() => setNewCoversInput("")}
Clear className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground/80 transition-colors hover:bg-black/5 dark:hover:bg-white/10"
</button> >
Clear
</button>
</>
) : (
<>
<button type="button" disabled className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground/70 disabled:opacity-40">
Add
</button>
<button type="button" disabled className="inline-flex h-9 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 text-xs font-medium text-foreground/60 disabled:opacity-40">
Clear
</button>
</>
)}
</div> </div>
<div className="text-xs text-foreground/60 flex justify-between"> <div className="text-xs text-foreground/60 flex justify-between">
<p>Images: <span className={overLimit ? "text-red-300 font-bold" : "text-foreground/60"}>{coverUrls.length}</span>/{MAX_COVERS}</p> <p>Images: <span className={overLimit ? "text-red-300 font-bold" : "text-foreground/60"}>{coverUrls.length}</span>/{MAX_COVERS}</p>
@ -313,105 +391,146 @@ export default function SubmitForm() {
{coverUrls.length === 0 ? ( {coverUrls.length === 0 ? (
<p className="text-xs text-foreground/60">No images added yet. Add at least one to preview.</p> <p className="text-xs text-foreground/60">No images added yet. Add at least one to preview.</p>
) : ( ) : (
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}> !isDummy ? (
<SortableContext <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
items={coverUrls.map((url, i) => `${url}-${i}`)} <SortableContext
strategy={verticalListSortingStrategy} items={coverUrls.map((url, i) => `${url}-${i}`)}
> strategy={verticalListSortingStrategy}
>
{coverUrls.map((url, i) => (
<SortableCoverItem
key={`${url}-${i}`}
id={`${url}-${i}`}
index={i}
url={url}
onRemove={() => removeAt(i)}
/>
))}
</SortableContext>
</DndContext>
) : (
<>
{coverUrls.map((url, i) => ( {coverUrls.map((url, i) => (
<SortableCoverItem <StaticCoverItem key={`${url}-${i}`} index={i} url={url} />
key={`${url}-${i}`}
id={`${url}-${i}`}
index={i}
url={url}
onRemove={() => removeAt(i)}
/>
))} ))}
</SortableContext> </>
</DndContext> )
)} )}
</div> </div>
</div> </div>
</div> </div>
{/* Base ROM */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Base ROM</label> <label className="text-sm text-foreground/80">Base ROM</label>
<select {!isDummy ? (
value={baseRom} <select
onChange={(e) => setBaseRom(e.target.value)} value={baseRom}
className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]" onChange={(e) => setBaseRom(e.target.value)}
> className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--ring)]"
{baseRoms.map(({ name, region }) => ( >
<option key={name} value={name}> {baseRoms.map(({ name, region }) => (
{name} ({region}) <option key={name} value={name}>
</option> {name} ({region})
))} </option>
</select> ))}
</select>
) : (
<div className="h-11 rounded-md bg-[var(--surface-2)] px-3 text-sm ring-1 ring-inset ring-[var(--border)] flex items-center text-foreground/60 select-none">{baseRom}</div>
)}
</div> </div>
{/* Box art URL */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Box art URL <span className="text-foreground/60">(optional)</span></label> <label className="text-sm text-foreground/80">Box art URL <span className="text-foreground/60">(optional)</span></label>
<input {!isDummy ? (
value={boxArt} <input
onChange={(e) => setBoxArt(e.target.value)} value={boxArt}
placeholder="https://..." onChange={(e) => setBoxArt(e.target.value)}
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${boxArt && !urlLike(boxArt) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`} placeholder="https://..."
/> className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${boxArt && !urlLike(boxArt) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
/>
) : (
<div className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset flex items-center text-foreground/60 select-none ${boxArt && !urlLike(boxArt) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}>https://...</div>
)}
</div> </div>
{/* Social links */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Social links <span className="text-foreground/60">(optional)</span></label> <label className="text-sm text-foreground/80">Social links <span className="text-foreground/60">(optional)</span></label>
<div className="grid gap-2 sm:grid-cols-3"> <div className="grid gap-2 sm:grid-cols-3">
<input {!isDummy ? (
value={discord} <>
onChange={(e) => setDiscord(e.target.value)} <input
placeholder="Discord invite URL" value={discord}
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${discord && !urlLike(discord) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`} onChange={(e) => setDiscord(e.target.value)}
/> placeholder="Discord invite URL"
<input className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${discord && !urlLike(discord) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
value={twitter} />
onChange={(e) => setTwitter(e.target.value)} <input
placeholder="Twitter/X profile URL" value={twitter}
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${twitter && !urlLike(twitter) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`} onChange={(e) => setTwitter(e.target.value)}
/> placeholder="Twitter/X profile URL"
<input className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${twitter && !urlLike(twitter) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
value={pokecommunity} />
onChange={(e) => setPokecommunity(e.target.value)} <input
placeholder="PokeCommunity thread URL" value={pokecommunity}
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${pokecommunity && !urlLike(pokecommunity) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`} onChange={(e) => setPokecommunity(e.target.value)}
/> placeholder="PokeCommunity thread URL"
className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${pokecommunity && !urlLike(pokecommunity) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}
/>
</>
) : (
<>
<div className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset flex items-center text-foreground/60 select-none ${discord && !urlLike(discord) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}>Discord invite URL</div>
<div className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset flex items-center text-foreground/60 select-none ${twitter && !urlLike(twitter) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}>Twitter/X profile URL</div>
<div className={`h-11 rounded-md px-3 text-sm ring-1 ring-inset flex items-center text-foreground/60 select-none ${pokecommunity && !urlLike(pokecommunity) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : "bg-[var(--surface-2)] ring-[var(--border)]"}`}>PokeCommunity thread URL</div>
</>
)}
</div> </div>
<p className="text-xs text-foreground/60">Use full URLs starting with http:// or https://</p> <p className="text-xs text-foreground/60">Use full URLs starting with http:// or https://</p>
</div> </div>
{/* Upload patch file */}
<div className="grid gap-2"> <div className="grid gap-2">
<label className="text-sm text-foreground/80">Upload patch file</label> <label className="text-sm text-foreground/80">Upload patch file</label>
<input type="file" className="rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm italic text-foreground/50 ring-1 ring-inset ring-[var(--border)] file:bg-black/10 dark:file:bg-[var(--surface-2)] file:text-foreground/80 file:text-sm file:font-medium file:not-italic file:rounded-md file:border-0 file:px-3 file:py-2 file:mr-2 file:cursor-pointer" /> {!isDummy ? (
<input type="file" className="rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm italic text-foreground/50 ring-1 ring-inset ring-[var(--border)] file:bg-black/10 dark:file:bg-[var(--surface-2)] file:text-foreground/80 file:text-sm file:font-medium file:not-italic file:rounded-md file:border-0 file:px-3 file:py-2 file:mr-2 file:cursor-pointer" />
) : (
<div className="rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm italic text-foreground/50 ring-1 ring-inset ring-[var(--border)] select-none">Choose file</div>
)}
<p className="text-xs text-foreground/60">BPS only for verification purposes.</p> <p className="text-xs text-foreground/60">BPS only for verification purposes.</p>
</div> </div>
<div className="flex items-center gap-3">
<button {/* Submit button */}
type="button" {!isDummy && (
disabled={!isValid} <div className="flex items-center gap-3">
className="shine-wrap btn-premium h-11 min-w-[7.5rem] text-sm font-semibold dark:disabled:opacity-70 disabled:cursor-not-allowed disabled:[box-shadow:0_0_0_1px_var(--border)]" <button
> type="button"
<span>Submit</span> disabled={!isValid}
</button> className="shine-wrap btn-premium h-11 min-w-[7.5rem] text-sm font-semibold dark:disabled:opacity-70 disabled:cursor-not-allowed disabled:[box-shadow:0_0_0_1px_var(--border)]"
{!isValid && ( >
<span className="text-xs text-red-600/90">Fill required fields, fix errors, and add at least one cover.</span> <span>Submit</span>
)} </button>
</div> {!isValid && (
<span className="text-xs text-red-600/90">Fill required fields, fix errors, and add at least one cover.</span>
)}
</div>
)}
</form> </form>
</div> </div>
<aside className="flex flex-col gap-5 lg:sticky lg:top-20 self-start"> <aside className="flex flex-col gap-5 lg:sticky lg:top-20 self-start">
<PreviewCard hack={preview} /> <PreviewCard hack={preview} />
<div className="card h-max p-5">
<div className="card h-max p-5"> <div className="text-[15px] font-semibold tracking-tight">Submission tips</div>
<div className="text-[15px] font-semibold tracking-tight">Submission tips</div> <ul className="mt-3 list-disc space-y-2 pl-5 text-sm text-foreground/75">
<ul className="mt-3 list-disc space-y-2 pl-5 text-sm text-foreground/75"> <li>Use a reliable image URL (e.g. `imgur`).</li>
<li>Use a reliable image URL (e.g. `imgur`).</li> <li>Include the exact expected base ROM name.</li>
<li>Include the exact expected base ROM name.</li> <li>Describe notable features, difficulty, and target players.</li>
<li>Describe notable features, difficulty, and target players.</li> </ul>
</ul> </div>
</div> </aside>
</aside>
</div> </div>
); );
} }
@ -420,4 +539,32 @@ function PreviewCard({ hack }: { hack: Hack }) {
return <HackCard hack={hack} clickable={false} />; return <HackCard hack={hack} clickable={false} />;
} }
function StaticCoverItem({ index, url }: { index: number; url: string }) {
return (
<div className="rounded-md">
<div className="h-16 flex items-center justify-between gap-3 p-2 bg-[var(--surface-2)] ring-1 ring-inset ring-[var(--border)]">
<div className="flex items-center gap-3">
<div className="pr-1 text-foreground/40 select-none" title="Drag disabled"></div>
<div className="relative h-12 w-20 overflow-hidden rounded">
<Image src={url} alt={`Cover ${index + 1}`} fill className="object-cover" unoptimized />
</div>
<div className="min-w-0">
<div className="truncate text-xs text-foreground/80">{url}</div>
{index === 0 && <div className="text-[10px] text-emerald-400/90">Primary</div>}
</div>
</div>
<div className="flex items-center gap-1">
<button
type="button"
disabled
className="inline-flex h-8 items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-2 text-xs text-foreground/50"
>
Remove
</button>
</div>
</div>
</div>
);
}