Add optional verification contact field

This commit is contained in:
jschoeny 2026-01-15 19:21:01 -10:00
parent 51f9eaa94b
commit 456832aad3
6 changed files with 71 additions and 4 deletions

View File

@ -30,6 +30,7 @@ export interface HackMetadata {
language: string | null;
is_archive: boolean;
completion_status: Database["public"]["Enums"]["Completion Status"] | null;
verification_contact_info: string | null;
};
images: string[];
tags: string[];
@ -59,12 +60,17 @@ 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,box_art,social_links,created_by,approved,original_author,permission_from,language,is_archive,completion_status")
.select("slug,title,summary,description,base_rom,created_at,updated_at,current_patch,box_art,social_links,created_by,approved,original_author,permission_from,language,is_archive,completion_status,verification_contact_info")
.eq("slug", slug)
.maybeSingle();
if (error || !hack) return null;
// Security: Don't return verification_contact_info if hack is approved
if (hack.approved) {
hack.verification_contact_info = null;
}
// Fetch covers
let images: string[] = [];
const { data: covers } = await supabase

View File

@ -7,7 +7,7 @@ import HackActions from "@/components/Hack/HackActions";
import Markdown from "@/components/Markdown/Markdown";
import Image from "next/image";
import { FaDiscord, FaTwitter, FaGithub, FaTriangleExclamation, FaArrowUpRightFromSquare } from "react-icons/fa6";
import { FiAlertTriangle } from "react-icons/fi";
import { FiAlertTriangle, FiInfo } from "react-icons/fi";
import PokeCommunityIcon from "@/components/Icons/PokeCommunityIcon";
import { createClient, createServiceClient } from "@/utils/supabase/server";
import HackOptionsMenu from "@/components/Hack/HackOptionsMenu";
@ -360,6 +360,23 @@ export default async function HackDetail({ params }: HackDetailProps) {
</div>
)
)}
{isAdmin && hack.verification_contact_info && (
<div className="mx-6 mt-6 rounded-lg border-2 border-blue-500/60 bg-blue-50 dark:bg-blue-900/20 p-4 md:pl-6">
<div className="flex items-center gap-4 md:gap-6">
<div className="flex-shrink-0">
<FiInfo className="text-blue-600 dark:text-blue-400" size={24} />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-blue-900 dark:text-blue-100 mb-2">
Verification Contact Information
</h3>
<div className="text-sm text-blue-800 dark:text-blue-200 whitespace-pre-line">
{hack.verification_contact_info}
</div>
</div>
</div>
</div>
)}
</>
)}

View File

@ -50,6 +50,7 @@ export async function prepareSubmission(formData: FormData) {
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;
const verification_contact_info = (formData.get("verification_contact_info") as string)?.trim() || null;
const is_archive = formData.get("is_archive") === "true";
// For archives, version is not required; for regular hacks, it is
@ -93,6 +94,7 @@ export async function prepareSubmission(formData: FormData) {
patch_url: "",
original_author: original_author || null,
permission_from: permission_from || null,
verification_contact_info: verification_contact_info || null,
current_patch: null, // Archives don't have patches
} as HackInsert;

View File

@ -108,6 +108,7 @@ export default function HackSubmitForm({
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 [verificationContactInfo, setVerificationContactInfo] = React.useState(() => initialDraftRef.current?.verificationContactInfo || "");
const [tags, setTags] = React.useState<string[]>(() => (Array.isArray(initialDraftRef.current?.tags) ? initialDraftRef.current.tags : []));
const [showMdPreview, setShowMdPreview] = React.useState<boolean>(() => !!initialDraftRef.current?.showMdPreview);
const [originalAuthor, setOriginalAuthor] = React.useState<string>(() => {
@ -287,7 +288,7 @@ export default function HackSubmitForm({
const data = JSON.parse(raw);
if (data && typeof data === "object") {
const isEmpty =
!title && !summary && !description && !baseRom && !platform && !version && !language && !completionStatus && !boxArt && !discord && !twitter && !pokecommunity && !github && (!tags || tags.length === 0) && !originalAuthor;
!title && !summary && !description && !baseRom && !platform && !version && !language && !completionStatus && !boxArt && !discord && !twitter && !pokecommunity && !github && !verificationContactInfo && (!tags || tags.length === 0) && !originalAuthor;
if (isEmpty) {
let applied = false;
if (typeof data.title === "string") setTitle(data.title);
@ -316,6 +317,8 @@ export default function HackSubmitForm({
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 (typeof data.verificationContactInfo === "string") setVerificationContactInfo(data.verificationContactInfo);
if (typeof data.verificationContactInfo === "string") applied = applied || !!data.verificationContactInfo;
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
@ -346,7 +349,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.completionStatus || d.boxArt || d.discord || d.twitter || d.pokecommunity || d.github || (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.completionStatus || d.boxArt || d.discord || d.twitter || d.pokecommunity || d.github || d.verificationContactInfo || (Array.isArray(d.tags) && d.tags.length > 0) || (!customCreator && d.originalAuthor)
);
if (hasAny) { hydratedFromDraftRef.current = true; setRestoredDraft(true); }
}, [dummy, draftKey, customCreator]);
@ -369,6 +372,7 @@ export default function HackSubmitForm({
twitter,
pokecommunity,
github,
verificationContactInfo,
tags,
step,
showMdPreview,
@ -402,6 +406,7 @@ export default function HackSubmitForm({
twitter,
pokecommunity,
github,
verificationContactInfo,
tags,
originalAuthor,
customCreator,
@ -441,6 +446,7 @@ export default function HackSubmitForm({
if (twitter) fd.set('twitter', twitter);
if (pokecommunity) fd.set('pokecommunity', pokecommunity);
if (github) fd.set('github', github);
if (verificationContactInfo) fd.set('verification_contact_info', verificationContactInfo);
if (tags.length) fd.set('tags', tags.join(','));
if (isArchive) {
fd.set('is_archive', 'true');
@ -724,6 +730,7 @@ export default function HackSubmitForm({
setTwitter("");
setPokecommunity("");
setGithub("");
setVerificationContactInfo("");
setTags([]);
setNewCoverFiles([]);
setCoverErrors([]);
@ -1146,6 +1153,36 @@ export default function HackSubmitForm({
)}
</div>
</div>
{!profile?.verified && (
<div className="grid gap-2">
<label className="text-sm text-foreground/80">Verification Contact Information <span className="text-foreground/60">(Recommended)</span></label>
<p className="text-xs text-foreground/60">
Help us verify your account by providing contact information from a platform where your message/post history can be verified (e.g., Discord, PokéCommunity). This information will only be visible to admins during the approval process.
</p>
{!isDummy ? (
<textarea
value={verificationContactInfo}
onChange={(e) => setVerificationContactInfo(e.target.value)}
placeholder={`Discord username: @example
I am active in the following servers:
Team Aqua's Hideout, RH Hideout, pret
Here is an invite to my development server:
https://discord.gg/example`}
rows={6}
className="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)] resize-y"
/>
) : (
<div className="rounded-md bg-[var(--surface-2)] px-3 py-2 text-sm ring-1 ring-inset ring-[var(--border)] text-foreground/60 select-none min-h-[6rem]">
Discord: @example<br />
I am active in the following servers: Team Aqua's Hideout, pret, and PokéDev School<br />
Here is an invite to my development server: https://discord.gg/example_server_invite
</div>
)}
<p className="text-xs text-foreground/60">Optional but recommended for faster account verification. Please be detailed.</p>
</div>
)}
</>
)}

View File

@ -161,6 +161,7 @@ export type Database = {
summary: string
title: string
updated_at: string | null
verification_contact_info: string | null
version: string
}
Insert: {
@ -195,6 +196,7 @@ export type Database = {
summary: string
title: string
updated_at?: string | null
verification_contact_info?: string | null
version: string
}
Update: {
@ -229,6 +231,7 @@ export type Database = {
summary?: string
title?: string
updated_at?: string | null
verification_contact_info?: string | null
version?: string
}
Relationships: [

View File

@ -0,0 +1,2 @@
alter table if exists public.hacks
add column if not exists verification_contact_info text;