From 1a058f12c2f02393a9241c2e8b5b3bd0ebec01c6 Mon Sep 17 00:00:00 2001 From: Jared Schoeny Date: Tue, 23 Dec 2025 20:40:16 -1000 Subject: [PATCH] Add GitHub to `hack.social_links` --- src/app/hack/[slug]/edit/page.tsx | 7 +- src/app/hack/[slug]/page.tsx | 7 +- src/app/hack/actions.ts | 7 +- src/app/submit/actions.ts | 4 +- src/components/Hack/HackEditForm.tsx | 36 +++++++-- src/components/Hack/HackSubmitForm.tsx | 104 ++++++++++++++++++------- 6 files changed, 127 insertions(+), 38 deletions(-) diff --git a/src/app/hack/[slug]/edit/page.tsx b/src/app/hack/[slug]/edit/page.tsx index a45f21e..6581d5d 100644 --- a/src/app/hack/[slug]/edit/page.tsx +++ b/src/app/hack/[slug]/edit/page.tsx @@ -76,7 +76,12 @@ export default async function EditHackPage({ params }: EditPageProps) { language: hack.language, version: isArchive ? "Archive" : (version || "Pre-release"), box_art: hack.box_art, - social_links: (hack.social_links as unknown) as { discord?: string; twitter?: string; pokecommunity?: string } | null, + social_links: (hack.social_links as unknown) as { + discord?: string; + twitter?: string; + pokecommunity?: string; + github?: string; + } | null, tags, coverKeys, signedCoverUrls, diff --git a/src/app/hack/[slug]/page.tsx b/src/app/hack/[slug]/page.tsx index ccb0b73..462b0a6 100644 --- a/src/app/hack/[slug]/page.tsx +++ b/src/app/hack/[slug]/page.tsx @@ -8,7 +8,7 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeSlug from "rehype-slug"; import Image from "next/image"; -import { FaDiscord, FaTwitter, FaTriangleExclamation, FaArrowUpRightFromSquare } from "react-icons/fa6"; +import { FaDiscord, FaTwitter, FaGithub, FaTriangleExclamation, FaArrowUpRightFromSquare } from "react-icons/fa6"; import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon"; import { createClient, createServiceClient } from "@/utils/supabase/server"; import HackOptionsMenu from "@/components/Hack/HackOptionsMenu"; @@ -486,6 +486,11 @@ export default async function HackDetail({ params }: HackDetailProps) { )} + {((hack.social_links as unknown) as { github?: string })?.github && ( + + + + )} )} diff --git a/src/app/hack/actions.ts b/src/app/hack/actions.ts index e98ae0d..b23a886 100644 --- a/src/app/hack/actions.ts +++ b/src/app/hack/actions.ts @@ -18,7 +18,12 @@ export async function updateHack(args: { language?: string; version?: string; box_art?: string | null; - social_links?: { discord?: string; twitter?: string; pokecommunity?: string } | null; + social_links?: { + discord?: string; + twitter?: string; + pokecommunity?: string; + github?: string; + } | null; tags?: string[]; }) { const supabase = await createClient(); diff --git a/src/app/submit/actions.ts b/src/app/submit/actions.ts index 457b5e1..39bb266 100644 --- a/src/app/submit/actions.ts +++ b/src/app/submit/actions.ts @@ -45,6 +45,7 @@ export async function prepareSubmission(formData: FormData) { const discord = (formData.get("discord") as string)?.trim(); const twitter = (formData.get("twitter") as string)?.trim(); const pokecommunity = (formData.get("pokecommunity") as string)?.trim(); + const github = (formData.get("github") as string)?.trim(); const tags = (formData.get("tags") as string)?.split(",").map((t) => t.trim()).filter(Boolean) || []; const original_author = (formData.get("original_author") as string)?.trim() || null; const permission_from = (formData.get("permission_from") as string)?.trim() || null; @@ -64,11 +65,12 @@ export async function prepareSubmission(formData: FormData) { const slug = await ensureUniqueSlug(baseSlug, supabase); const social_links: HackInsert["social_links"] = - discord || twitter || pokecommunity + discord || twitter || pokecommunity || github ? { discord: discord || undefined, twitter: twitter || undefined, pokecommunity: pokecommunity || undefined, + github: github || undefined, } : null; diff --git a/src/components/Hack/HackEditForm.tsx b/src/components/Hack/HackEditForm.tsx index e71b8e9..ef7fe56 100644 --- a/src/components/Hack/HackEditForm.tsx +++ b/src/components/Hack/HackEditForm.tsx @@ -22,7 +22,12 @@ interface HackEditFormProps { language: string; version: string; box_art: string | null; - social_links: { discord?: string; twitter?: string; pokecommunity?: string } | null; + social_links: { + discord?: string; + twitter?: string; + pokecommunity?: string; + github?: string; + } | null; tags: string[]; coverKeys: string[]; // storage keys for covers in order signedCoverUrls?: string[]; // optional signed URLs aligned to keys @@ -43,6 +48,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { const [discord, setDiscord] = React.useState(initial.social_links?.discord || ""); const [twitter, setTwitter] = React.useState(initial.social_links?.twitter || ""); const [pokecommunity, setPokecommunity] = React.useState(initial.social_links?.pokecommunity || ""); + const [github, setGithub] = React.useState(initial.social_links?.github || ""); const [tags, setTags] = React.useState(initial.tags || []); // Baseline state used for change detection and reverting @@ -56,6 +62,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { discord: initial.social_links?.discord || "", twitter: initial.social_links?.twitter || "", pokecommunity: initial.social_links?.pokecommunity || "", + github: initial.social_links?.github || "", }); type CoverItem = @@ -113,7 +120,8 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { const discordChanged = discord !== baseline.discord; const twitterChanged = twitter !== baseline.twitter; const pokeChanged = pokecommunity !== baseline.pokecommunity; - const contentChanged = titleChanged || summaryChanged || descriptionChanged || languageChanged || boxArtChanged || tagsChanged || discordChanged || twitterChanged || pokeChanged; + const githubChanged = github !== baseline.github; + const contentChanged = titleChanged || summaryChanged || descriptionChanged || languageChanged || boxArtChanged || tagsChanged || discordChanged || twitterChanged || pokeChanged || githubChanged; const newItemsCount = coverItems.filter((i) => i.type === "new").length; const currentExistingKeys = coverItems.filter((i): i is { type: "existing"; key: string; url: string } => i.type === "existing").map((i) => i.key); @@ -155,7 +163,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { async function onSaveMeta() { setSaving(true); try { - const social = discord || twitter || pokecommunity ? { discord: discord || undefined, twitter: twitter || undefined, pokecommunity: pokecommunity || undefined } : null; + const social = discord || twitter || pokecommunity || github ? { discord: discord || undefined, twitter: twitter || undefined, pokecommunity: pokecommunity || undefined, github: github || undefined } : null; const updateArgs: any = { slug }; if (titleChanged) updateArgs.title = title.trim(); if (summaryChanged) updateArgs.summary = summary.trim(); @@ -163,7 +171,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { if (languageChanged) updateArgs.language = language; if (boxArtChanged) updateArgs.box_art = boxArt ? boxArt.trim() : null; if (tagsChanged) updateArgs.tags = tags.slice(); - if (discordChanged || twitterChanged || pokeChanged) updateArgs.social_links = social; // may be null to clear + if (discordChanged || twitterChanged || pokeChanged || githubChanged) updateArgs.social_links = social; // may be null to clear const { ok, error } = await updateHack(updateArgs); if (!ok) throw new Error(error || "Save failed"); @@ -178,6 +186,7 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { discord, twitter, pokecommunity, + github, }); } catch (e: any) { alert(e.message || "Save failed"); @@ -232,8 +241,19 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { {contentChanged && ( @@ -439,6 +459,10 @@ export default function HackEditForm({ slug, initial }: HackEditFormProps) { setPokecommunity(e.target.value)} placeholder="PokeCommunity thread URL" className={`flex-1 h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 ${pokecommunity && !urlLike(pokecommunity) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : pokeChanged ? 'ring-[var(--ring)] bg-[var(--surface-2)]' : 'bg-[var(--surface-2)] ring-[var(--border)]'}`} /> {pokeChanged && } +
+ setGithub(e.target.value)} placeholder="GitHub repository URL" className={`flex-1 h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 ${github && !urlLike(github) ? "ring-red-600/40 bg-red-500/10 dark:ring-red-400/40 dark:bg-red-950/20" : githubChanged ? 'ring-[var(--ring)] bg-[var(--surface-2)]' : 'bg-[var(--surface-2)] ring-[var(--border)]'}`} /> + {githubChanged && } +
diff --git a/src/components/Hack/HackSubmitForm.tsx b/src/components/Hack/HackSubmitForm.tsx index 2f40781..80d4a24 100644 --- a/src/components/Hack/HackSubmitForm.tsx +++ b/src/components/Hack/HackSubmitForm.tsx @@ -14,6 +14,8 @@ import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import rehypeSlug from "rehype-slug"; import { RxDragHandleDots2 } from "react-icons/rx"; +import { FaDiscord, FaTwitter, FaGithub } from "react-icons/fa6"; +import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon"; import { useAuthContext } from "@/contexts/AuthContext"; import { useBaseRoms } from "@/contexts/BaseRomContext"; import TagSelector from "@/components/Submit/TagSelector"; @@ -104,6 +106,7 @@ export default function HackSubmitForm({ const [discord, setDiscord] = React.useState(() => initialDraftRef.current?.discord || ""); const [twitter, setTwitter] = React.useState(() => initialDraftRef.current?.twitter || ""); const [pokecommunity, setPokecommunity] = React.useState(() => initialDraftRef.current?.pokecommunity || ""); + const [github, setGithub] = React.useState(() => initialDraftRef.current?.github || ""); const [tags, setTags] = React.useState(() => (Array.isArray(initialDraftRef.current?.tags) ? initialDraftRef.current.tags : [])); const [showMdPreview, setShowMdPreview] = React.useState(() => !!initialDraftRef.current?.showMdPreview); const [originalAuthor, setOriginalAuthor] = React.useState(() => { @@ -282,7 +285,7 @@ export default function HackSubmitForm({ const data = JSON.parse(raw); if (data && typeof data === "object") { const isEmpty = - !title && !summary && !description && !baseRom && !platform && !version && !language && !boxArt && !discord && !twitter && !pokecommunity && (!tags || tags.length === 0) && !originalAuthor; + !title && !summary && !description && !baseRom && !platform && !version && !language && !boxArt && !discord && !twitter && !pokecommunity && !github && (!tags || tags.length === 0) && !originalAuthor; if (isEmpty) { let applied = false; if (typeof data.title === "string") setTitle(data.title); @@ -307,6 +310,8 @@ export default function HackSubmitForm({ if (typeof data.twitter === "string") applied = applied || !!data.twitter; if (typeof data.pokecommunity === "string") setPokecommunity(data.pokecommunity); if (typeof data.pokecommunity === "string") applied = applied || !!data.pokecommunity; + if (typeof data.github === "string") setGithub(data.github); + if (typeof data.github === "string") applied = applied || !!data.github; if (Array.isArray(data.tags)) setTags(data.tags.filter((t: any) => typeof t === "string")); if (Array.isArray(data.tags)) applied = applied || data.tags.length > 0; // Only load originalAuthor from draft if customCreator is not provided @@ -337,7 +342,7 @@ export default function HackSubmitForm({ if (!d || typeof d !== "object") return; // Don't count originalAuthor if customCreator is provided const hasAny = Boolean( - d.title || d.summary || d.description || d.baseRom || d.platform || d.version || d.language || d.boxArt || d.discord || d.twitter || d.pokecommunity || (Array.isArray(d.tags) && d.tags.length > 0) || (!customCreator && d.originalAuthor) + d.title || d.summary || d.description || d.baseRom || d.platform || d.version || d.language || d.boxArt || d.discord || d.twitter || d.pokecommunity || d.github || (Array.isArray(d.tags) && d.tags.length > 0) || (!customCreator && d.originalAuthor) ); if (hasAny) { hydratedFromDraftRef.current = true; setRestoredDraft(true); } }, [dummy, draftKey, customCreator]); @@ -358,6 +363,7 @@ export default function HackSubmitForm({ discord, twitter, pokecommunity, + github, tags, step, showMdPreview, @@ -389,6 +395,7 @@ export default function HackSubmitForm({ discord, twitter, pokecommunity, + github, tags, originalAuthor, customCreator, @@ -404,7 +411,7 @@ export default function HackSubmitForm({ const urlLike = (s: string) => !s || /^https?:\/\//i.test(s); - const allSocialValid = [discord, twitter, pokecommunity].every((s) => !s || urlLike(s)); + const allSocialValid = [discord, twitter, pokecommunity, github].every((s) => !s || urlLike(s)); const step1Valid = !!title.trim() && !!platform && !!baseRom.trim() && !!language.trim() && (isArchive ? !!originalAuthor.trim() : true); const step2Valid = (isArchive ? true : !!version.trim()) && !!summary.trim() && !summaryTooLong && !!description.trim() && tags.length > 0; @@ -426,6 +433,7 @@ export default function HackSubmitForm({ if (discord) fd.set('discord', discord); if (twitter) fd.set('twitter', twitter); if (pokecommunity) fd.set('pokecommunity', pokecommunity); + if (github) fd.set('github', github); if (tags.length) fd.set('tags', tags.join(',')); if (isArchive) { fd.set('isArchive', 'true'); @@ -619,8 +627,8 @@ export default function HackSubmitForm({ tags: sortOrderedTags(tags.map((name, index) => ({ name, order: index + 1 }))), ...(boxArt ? { boxArt } : {}), socialLinks: - discord || twitter || pokecommunity - ? { discord: discord || undefined, twitter: twitter || undefined, pokecommunity: pokecommunity || undefined } + discord || twitter || pokecommunity || github + ? { discord: discord || undefined, twitter: twitter || undefined, pokecommunity: pokecommunity || undefined, github: github || undefined } : undefined, createdAt: new Date().toISOString(), patchUrl: "", @@ -687,6 +695,7 @@ export default function HackSubmitForm({ setDiscord(""); setTwitter(""); setPokecommunity(""); + setGithub(""); setTags([]); setNewCoverFiles([]); setCoverErrors([]); @@ -997,37 +1006,76 @@ export default function HackSubmitForm({
-
+

Use full URLs starting with http:// or https://

+
{!isDummy ? ( <> - setDiscord(e.target.value)} - placeholder="Discord invite URL" - 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)]"}`} - /> - setTwitter(e.target.value)} - placeholder="Twitter/X profile URL" - 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)]"}`} - /> - 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)]"}`} - /> +
+
+ +
+ setDiscord(e.target.value)} + placeholder="Discord invite URL" + className={`flex-1 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)]"}`} + /> +
+
+
+ +
+ setTwitter(e.target.value)} + placeholder="Twitter/X profile URL" + className={`flex-1 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)]"}`} + /> +
+
+
+ +
+ setPokecommunity(e.target.value)} + placeholder="PokeCommunity thread URL" + className={`flex-1 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)]"}`} + /> +
+
+
+ +
+ setGithub(e.target.value)} + placeholder="GitHub repository URL" + className={`flex-1 h-11 rounded-md px-3 text-sm ring-1 ring-inset focus:outline-none focus:ring-2 focus:ring-[var(--ring)] ${github && !urlLike(github) ? "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
-
Twitter/X profile URL
-
PokeCommunity thread URL
+
+ + Discord invite URL +
+
+ + Twitter/X profile URL +
+
+ + PokeCommunity thread URL +
+
+ + GitHub repository URL +
)}
-

Use full URLs starting with http:// or https://

)}