diff --git a/src/app/hack/[slug]/edit/page.tsx b/src/app/hack/[slug]/edit/page.tsx
index 974e436..56a9e30 100644
--- a/src/app/hack/[slug]/edit/page.tsx
+++ b/src/app/hack/[slug]/edit/page.tsx
@@ -18,7 +18,7 @@ export default async function EditHackPage({ params }: EditPageProps) {
const { data: hack } = await supabase
.from("hacks")
- .select("slug,title,summary,description,base_rom,version,language,box_art,social_links,created_by")
+ .select("slug,title,summary,description,base_rom,language,box_art,social_links,created_by,current_patch")
.eq("slug", slug)
.maybeSingle();
if (!hack) return notFound();
@@ -45,13 +45,23 @@ export default async function EditHackPage({ params }: EditPageProps) {
.eq("hack_slug", slug);
const tags = (tagRows || []).map((r: any) => r.tags?.name).filter(Boolean) as string[];
+ let version = "";
+ if (hack.current_patch) {
+ const { data: currentPatch } = await supabase
+ .from("patches")
+ .select("version")
+ .eq("id", hack.current_patch)
+ .maybeSingle();
+ version = currentPatch?.version || "";
+ }
+
const initial = {
title: hack.title,
summary: hack.summary,
description: hack.description,
base_rom: hack.base_rom,
language: hack.language,
- version: hack.version,
+ version: version || "Pre-release",
box_art: hack.box_art,
social_links: (hack.social_links as unknown) as { discord?: string; twitter?: string; pokecommunity?: string } | null,
tags,
@@ -67,10 +77,15 @@ export default async function EditHackPage({ params }: EditPageProps) {
{hack.title}
-
-
- Back to hack
-
+
diff --git a/src/app/hack/[slug]/edit/patch/page.tsx b/src/app/hack/[slug]/edit/patch/page.tsx
new file mode 100644
index 0000000..d7de3da
--- /dev/null
+++ b/src/app/hack/[slug]/edit/patch/page.tsx
@@ -0,0 +1,63 @@
+import { redirect, notFound } from "next/navigation";
+import { createClient } from "@/utils/supabase/server";
+import HackPatchForm from "@/components/Hack/HackPatchForm";
+import Link from "next/link";
+import { FaChevronLeft } from "react-icons/fa6";
+
+interface EditPatchPageProps {
+ params: Promise<{ slug: string }>;
+}
+
+export default async function EditPatchPage({ params }: EditPatchPageProps) {
+ const { slug } = await params;
+ const supabase = await createClient();
+ const { data: { user } } = await supabase.auth.getUser();
+ if (!user) {
+ redirect(`/login?redirectTo=%2Fhack%2F${encodeURIComponent(slug)}%2Fedit%2Fpatch`);
+ }
+
+ const { data: hack } = await supabase
+ .from("hacks")
+ .select("slug,base_rom,created_by,title,current_patch")
+ .eq("slug", slug)
+ .maybeSingle();
+ if (!hack) return notFound();
+ if (hack.created_by !== user!.id) return notFound();
+
+ const { data: patchRows } = await supabase
+ .from("patches")
+ .select("id,version")
+ .eq("parent_hack", slug)
+ .order("created_at", { ascending: true });
+ const existingVersions = (patchRows || []).map((p: any) => p.version as string);
+
+ const currentPatch = patchRows?.find((p: any) => p.id === hack.current_patch);
+ const currentVersion = currentPatch?.version;
+
+ return (
+
+
+ Upload new version for
+ {hack.title}
+
+
+
+
+
+
+
+
+
+ Back to hack
+
+
+
+ );
+}
+
+
diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx
index 2dd6c5b..9964871 100644
--- a/src/app/hack/[slug]/page.tsx
+++ b/src/app/hack/[slug]/page.tsx
@@ -21,7 +21,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
const supabase = await createClient();
const { data: hack, error } = await supabase
.from("hacks")
- .select("slug,title,summary,description,base_rom,version,created_at,updated_at,downloads,current_patch,box_art,social_links,created_by")
+ .select("slug,title,summary,description,base_rom,created_at,updated_at,downloads,current_patch,box_art,social_links,created_by")
.eq("slug", slug)
.maybeSingle();
if (error || !hack) return notFound();
@@ -62,16 +62,18 @@ export default async function HackDetail({ params }: HackDetailProps) {
// Resolve a short-lived signed patch URL (if current_patch exists)
let signedPatchUrl = "";
+ let patchVersion = "";
if (hack.current_patch != null) {
const { data: patch } = await supabase
.from("patches")
- .select("id,bucket,filename")
+ .select("id,bucket,filename,version")
.eq("id", hack.current_patch as number)
.maybeSingle();
if (patch) {
const client = getMinioClient();
const bucket = patch.bucket || PATCHES_BUCKET;
signedPatchUrl = await client.presignedGetObject(bucket, patch.filename, 60 * 5);
+ patchVersion = patch.version || "";
}
}
@@ -79,7 +81,7 @@ export default async function HackDetail({ params }: HackDetailProps) {
{hack.title}
- {hack.version}
+ {patchVersion || "Pre-release"}
By {author}
diff --git a/src/app/hack/actions.ts b/src/app/hack/actions.ts
index f322fdf..c02e133 100644
--- a/src/app/hack/actions.ts
+++ b/src/app/hack/actions.ts
@@ -2,6 +2,7 @@
import { createClient } from "@/utils/supabase/server";
import type { TablesInsert } from "@/types/db";
+import { getMinioClient, PATCHES_BUCKET } from "@/utils/minio/server";
export async function updateHack(args: {
slug: string;
@@ -160,3 +161,41 @@ export async function saveHackCovers(args: { slug: string; coverUrls: string[] }
}
+export async function presignNewPatchVersion(args: { slug: string; version: string; objectKey?: string }) {
+ const supabase = await createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+ if (!user) return { ok: false, error: "Unauthorized" } as const;
+
+ // Ensure hack exists and belongs to user
+ const { data: hack, error: hErr } = await supabase
+ .from("hacks")
+ .select("slug, created_by")
+ .eq("slug", args.slug)
+ .maybeSingle();
+ if (hErr) return { ok: false, error: hErr.message } as const;
+ if (!hack) return { ok: false, error: "Hack not found" } as const;
+ if (hack.created_by !== user.id) return { ok: false, error: "Forbidden" } as const;
+
+ // Enforce unique version per hack
+ const { data: existing } = await supabase
+ .from("patches")
+ .select("id")
+ .eq("parent_hack", args.slug)
+ .eq("version", args.version)
+ .limit(1)
+ .maybeSingle();
+ if (existing) return { ok: false, error: "That version already exists for this hack." } as const;
+
+ const safeVersion = args.version.replace(/[^a-zA-Z0-9._-]+/g, "-");
+ const objectKey = args.objectKey || `${args.slug}-${safeVersion}.bps`;
+
+ const client = getMinioClient();
+ // 10 minutes to upload
+ const url = await client.presignedPutObject(PATCHES_BUCKET, objectKey, 60 * 10);
+
+ return { ok: true, presignedUrl: url, objectKey } as const;
+}
+
+
diff --git a/src/app/submit/actions.ts b/src/app/submit/actions.ts
index 3fa48e5..7fe3bfd 100644
--- a/src/app/submit/actions.ts
+++ b/src/app/submit/actions.ts
@@ -163,6 +163,16 @@ export async function confirmPatchUpload(args: { slug: string; objectKey: string
if (!hack) return { ok: false, error: "Hack not found" } as const;
if (hack.created_by !== user.id) return { ok: false, error: "Forbidden" } as const;
+ // Enforce unique version per hack defensively (avoid race with presign step)
+ const { data: existing, error: vErr } = await supabase
+ .from("patches")
+ .select("id")
+ .eq("parent_hack", args.slug)
+ .eq("version", args.version)
+ .maybeSingle();
+ if (vErr) return { ok: false, error: vErr.message } as const;
+ if (existing) return { ok: false, error: "That version already exists for this hack." } as const;
+
// Create patch row
const { data: patch, error: pErr } = await supabase
.from("patches")
diff --git a/src/components/Discover/DiscoverBrowser.tsx b/src/components/Discover/DiscoverBrowser.tsx
index 7e1f423..40516bb 100644
--- a/src/components/Discover/DiscoverBrowser.tsx
+++ b/src/components/Discover/DiscoverBrowser.tsx
@@ -35,7 +35,7 @@ export default function DiscoverBrowser() {
const { data: rows } = await supabase
.from("hacks")
- .select("slug,title,summary,description,base_rom,version,downloads,created_by,patch_url,updated_at")
+ .select("slug,title,summary,description,base_rom,downloads,created_by,updated_at,current_patch")
.order(orderBy, { ascending: false });
const slugs = (rows || []).map((r) => r.slug);
const { data: coverRows } = await supabase
@@ -77,6 +77,20 @@ export default function DiscoverBrowser() {
arr.push(r.tags.name);
tagsBySlug.set(r.hack_slug, arr);
});
+
+ let mappedVersions = new Map
();
+ await Promise.all((rows || []).map(async (r) => {
+ if (r.current_patch) {
+ const { data: currentPatch } = await supabase
+ .from("patches")
+ .select("version")
+ .eq("id", r.current_patch)
+ .maybeSingle();
+ mappedVersions.set(r.slug, currentPatch?.version || "Pre-release");
+ } else {
+ mappedVersions.set(r.slug, "Pre-release");
+ }
+ }));
// Fetch all tags with category to build UI groups
const { data: allTagRows } = await supabase
.from("tags")
@@ -95,7 +109,7 @@ export default function DiscoverBrowser() {
tags: tagsBySlug.get(r.slug) || [],
downloads: r.downloads,
baseRomId: r.base_rom,
- version: r.version,
+ version: mappedVersions.get(r.slug) || "Pre-release",
summary: r.summary,
description: r.description,
}));
diff --git a/src/components/Hack/HackOptionsMenu.tsx b/src/components/Hack/HackOptionsMenu.tsx
index e5a37f7..c3eb82f 100644
--- a/src/components/Hack/HackOptionsMenu.tsx
+++ b/src/components/Hack/HackOptionsMenu.tsx
@@ -57,38 +57,13 @@ export default function HackOptionsMenu({ slug, canEdit }: HackOptionsMenuProps)
className="block w-full px-3 py-2 text-left text-sm data-focus:bg-black/5 dark:data-focus:bg-white/10">
Edit
- >}
- {/*
-
- {canEdit && }
- {canEdit && (
-