Hack page: let creators hide the recommended emulators (#106)
Some checks failed
Deploy Supabase Migrations to Production / migrate (push) Has been cancelled

This commit is contained in:
Jared Schoeny
2026-10-03 21:56:14 -06:00
committed by GitHub
parent a8a50dcc89
commit 4cf595eb45
6 changed files with 33 additions and 6 deletions

View File

@@ -34,6 +34,8 @@ export interface HackMetadata {
verification_contact_info: string | null;
/** null while the creator is still drafting; set once they submit for review. */
submitted_at: string | null;
/** Off when the creator lists their own emulators in the description. */
show_emulators: boolean;
};
/** null until the creator fills in the AI disclosure form. */
ai: AiDisclosure | null;
@@ -71,7 +73,7 @@ export async function getHackMetadata(slug: string): Promise<HackMetadata | null
const { data: hack, error } = await supabase
.from("hacks")
.select(`slug,title,summary,description,base_rom,created_at,updated_at,current_patch,custom_version_name,box_art,social_links,created_by,approved,original_author,permission_from,language,is_archive,completion_status,verification_contact_info,submitted_at,${AI_SELECT}`)
.select(`slug,title,summary,description,base_rom,created_at,updated_at,current_patch,custom_version_name,box_art,social_links,created_by,approved,original_author,permission_from,language,is_archive,completion_status,verification_contact_info,submitted_at,show_emulators,${AI_SELECT}`)
.eq("slug", slug)
.maybeSingle();

View File

@@ -45,6 +45,7 @@ export async function updateHack(args: {
original_author?: string;
permission_from?: string;
verification_contact_info?: string | null;
show_emulators?: boolean;
/** Replaces the whole AI disclosure and marks it confirmed now. */
ai?: { levels: AiLevels; note: string | null };
}) {
@@ -96,6 +97,7 @@ export async function updateHack(args: {
if (args.version !== undefined) updatePayload.version = args.version;
if (args.box_art !== undefined) updatePayload.box_art = args.box_art;
if (args.social_links !== undefined) updatePayload.social_links = args.social_links;
if (args.show_emulators !== undefined) updatePayload.show_emulators = args.show_emulators;
if (args.original_author !== undefined || args.permission_from !== undefined) {
// A custom display name alone (original_author without permission_from) isn't an upload on someone's behalf.
if (!hack.permission_from) {

View File

@@ -17,6 +17,7 @@ export interface DraftDetailsValues {
completion_status: Completion | null;
box_art: string | null;
social_links: Social | null;
show_emulators: boolean;
original_author: string | null;
/** Set on hacks uploaded on someone else's behalf; shows the creator fields. An original_author alone is just a custom display name. */
permission_from: string | null;
@@ -62,6 +63,7 @@ type Form = {
completion: Completion | "";
boxArt: string;
social: Record<keyof Social, string>;
emulators: boolean;
creator: string;
permission: string;
contact: string;
@@ -79,13 +81,14 @@ function toForm(values: DraftDetailsValues): Form {
pokecommunity: values.social_links?.pokecommunity ?? "",
github: values.social_links?.github ?? "",
},
emulators: values.show_emulators,
creator: values.original_author ?? "",
permission: values.permission_from ?? "",
contact: values.verification_contact_info ?? "",
};
}
/** Wraps the rail and owns the details sheet for the fields that are not edited in place: base ROM, language, completion, box art, links, and the creator and review fields. Saves (or stages, on listed hacks) on the footer button. */
/** Wraps the rail and owns the details sheet for the fields that are not edited in place: base ROM, language, completion, box art, links, the emulator list, and the creator and review fields. Saves (or stages, on listed hacks) on the footer button. */
export default function DraftDetails({ values, baseLocked, children }: DraftDetailsProps) {
const [open, setOpen] = React.useState(false);
// What the sheet opens with: the last saved or applied values, so Cancel drops only this session's edits.
@@ -126,7 +129,7 @@ function DetailsSheet({ initial, baseLocked, behalf, askContact, onApplied, onCl
const { save, live } = useDraftEditing();
const [saving, setSaving] = React.useState(false);
const [form, setForm] = React.useState(initial);
const { base, language, completion, boxArt, social, creator, permission, contact } = form;
const { base, language, completion, boxArt, social, emulators, creator, permission, contact } = form;
const set = <K extends keyof Form>(key: K, value: Form[K]) => setForm((prev) => ({ ...prev, [key]: value }));
const invalid = !urlLike(boxArt) || SOCIAL.some((s) => !urlLike(social[s.key])) || (behalf && (!creator.trim() || !permission.trim()));
@@ -140,6 +143,7 @@ function DetailsSheet({ initial, baseLocked, behalf, askContact, onApplied, onCl
...(completion ? { completion_status: completion } : {}),
box_art: boxArt.trim() || null,
social_links: Object.keys(links).length ? links : null,
show_emulators: emulators,
...(behalf ? { original_author: creator, permission_from: permission } : {}),
...(askContact ? { verification_contact_info: contact } : {}),
});
@@ -191,6 +195,13 @@ function DetailsSheet({ initial, baseLocked, behalf, askContact, onApplied, onCl
<Field label="Completion status">
<Select value={completion} onChange={(v) => set("completion", v as Completion)} placeholder="Choose one" options={COMPLETION.map((c) => ({ value: c, label: c }))} />
</Field>
<div className="flex flex-col gap-1.5">
<label className="flex cursor-pointer items-center gap-2.5 text-sm font-medium">
<input type="checkbox" checked={emulators} onChange={(e) => set("emulators", e.target.checked)} className="h-4 w-4 accent-[var(--rose-deep)]" />
Show recommended emulators
</label>
<span className="text-xs text-text-3">Hide these to list your own in the About section.</span>
</div>
<Field label="Box art URL" hint="Optional. Shown in the rail with a download link.">
<input value={boxArt} onChange={(e) => set("boxArt", e.target.value)} placeholder="https://…" className={`${FIELD} h-10 ${urlLike(boxArt) ? "" : "border-error"}`} />
</Field>

View File

@@ -321,7 +321,7 @@ export default function HackDetailView({ metadata, downloads, canEdit, canUpload
</header>
{/* Wraps the whole body so the review card can open the details sheet too. */}
<RailEditor editing={editing} values={{ base_rom: hack.base_rom, language: hack.language ?? "English", completion_status: hack.completion_status, box_art: hack.box_art, social_links: social, original_author: hack.original_author, permission_from: hack.permission_from, ...(hack.approved ? {} : { verification_contact_info: hack.verification_contact_info }) }} baseLocked={patchId !== null || !!editor?.hasPatches}>
<RailEditor editing={editing} values={{ base_rom: hack.base_rom, language: hack.language ?? "English", completion_status: hack.completion_status, box_art: hack.box_art, social_links: social, show_emulators: hack.show_emulators, original_author: hack.original_author, permission_from: hack.permission_from, ...(hack.approved ? {} : { verification_contact_info: hack.verification_contact_info }) }} baseLocked={patchId !== null || !!editor?.hasPatches}>
<div className="grid items-start gap-6 md:grid-cols-[minmax(0,1fr)_300px] md:grid-rows-[auto_1fr] md:[grid-template-areas:'main_patch'_'main_rail']">
<div className="order-1 md:order-none md:[grid-area:patch]">
{editing ? (
@@ -406,8 +406,14 @@ export default function HackDetailView({ metadata, downloads, canEdit, canUpload
)}
</RailGroup>
{!isInformationalArchive && (
<RailGroup title="How to play">
{!isInformationalArchive && editing && !hack.show_emulators && (
<RailGroup title="How to play" action={<EditDetailsLink />}>
<p className="text-[13px] leading-[1.45] text-text-3">Hidden. Players won’t see recommended emulators, so list yours in About.</p>
</RailGroup>
)}
{!isInformationalArchive && hack.show_emulators && (
<RailGroup title="How to play" action={editing && <EditDetailsLink />}>
<p className="mb-2.5 text-[13px] leading-[1.45] text-text-2">
Load the patched ROM in {baseRom ? `a ${baseRom.platform} emulator` : "an emulator"}, or on real hardware with a flash cart. Creators recommend:
</p>

View File

@@ -240,6 +240,7 @@ export type Database = {
rejected_by: string | null
rejected_reason: string | null
search: unknown
show_emulators: boolean
slug: string
social_links: Json | null
submitted_at: string | null
@@ -287,6 +288,7 @@ export type Database = {
rejected_by?: string | null
rejected_reason?: string | null
search?: unknown
show_emulators?: boolean
slug: string
social_links?: Json | null
submitted_at?: string | null
@@ -334,6 +336,7 @@ export type Database = {
rejected_by?: string | null
rejected_reason?: string | null
search?: unknown
show_emulators?: boolean
slug?: string
social_links?: Json | null
submitted_at?: string | null

View File

@@ -0,0 +1,3 @@
-- Creators can hide the recommended emulators on their hack page and list their own in the description.
alter table public.hacks
add column show_emulators boolean not null default true;