From edbd812c3019dc6cc25d21a4625192d90f0e802a Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 21 Aug 2026 00:31:36 +0700 Subject: [PATCH] feat: fully dynamic custom field badges (auto-style by value, not key name) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomFieldBadges now auto-detects styling based on VALUE TYPE+CONTENT (number→purple points style, difficulty strings→color-coded, event-like strings→purple, categories→orange, status→color-coded) - DocForm help text made generic (no hardcoded field examples) - Removed typed CTF fields from DocumentMeta (event/challenge/etc) — everything flows through extraFields JSONB for true dynamic behavior --- apps/web/app/[section]/[...slug]/page.tsx | 92 +++++++++++++++++------ apps/web/app/components/DocForm.tsx | 12 +-- 2 files changed, 73 insertions(+), 31 deletions(-) diff --git a/apps/web/app/[section]/[...slug]/page.tsx b/apps/web/app/[section]/[...slug]/page.tsx index ee475a3..7062ca9 100644 --- a/apps/web/app/[section]/[...slug]/page.tsx +++ b/apps/web/app/[section]/[...slug]/page.tsx @@ -45,7 +45,8 @@ function CustomFieldBadges({ // Convert doc to a plain record to get index signature const docRecord: Record = { ...doc } as Record; const customEntries = Object.entries(docRecord).filter( - ([k, v]) => !standardKeys.has(k) && v !== undefined && v !== null && v !== "" && !k.startsWith("_"), + ([k, v]) => + !standardKeys.has(k) && v !== undefined && v !== null && v !== "" && !k.startsWith("_"), ); if (customEntries.length === 0) return null; @@ -55,38 +56,83 @@ function CustomFieldBadges({ {customEntries.map(([key, value]) => { const label = key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); let colorClass = "bg-[#191a1b] border-[#23252a] text-[#d0d6e0]"; - let prefix = ""; + let displayValue: string; - // Auto-style common CTF fields based on key name - if (key === "points") { - prefix = "pts"; + // --- Auto-detect styling based on VALUE TYPE + VALUE CONTENTS --- + // (NOT key name — content creators determine appearance via values, + // not by using specific key names) + + if (typeof value === "number") { + // Numeric values → purple "points"-style badge with + prefix colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; - } else if (key === "difficulty" || key === "level") { + displayValue = `${value}`; + } else if (typeof value === "boolean") { + // Boolean values → green check/X + colorClass = value + ? "bg-green-500/15 border-green-500/25 text-green-400" + : "bg-red-500/15 border-red-500/25 text-red-400"; + displayValue = value ? "Yes" : "No"; + } else if (Array.isArray(value)) { + // Arrays → purple badge, joined values + colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; + displayValue = value.join(", "); + } else if (typeof value === "object" && value !== null) { + // Objects → neutral badge, truncated JSON + displayValue = JSON.stringify(value).slice(0, 40) + + (JSON.stringify(value).length > 40 ? "…" : ""); + } else { + // String values — auto-detect content type for styling const v = String(value).toLowerCase(); - if (v === "easy") - colorClass = "bg-green-500/10 border-green-500/30 text-green-400"; - else if (v === "medium") - colorClass = "bg-yellow-500/10 border-yellow-500/30 text-yellow-400"; - else if (v === "hard") - colorClass = "bg-red-500/10 border-red-500/30 text-red-400"; - else - colorClass = "bg-[#191a1b] border-[#23252a] text-[#d0d6e0]"; - } else if (key === "event" || key === "category") { - colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; - } - const displayValue = Array.isArray(value) - ? value.join(", ") - : typeof value === "object" - ? JSON.stringify(value) - : String(value); + // Difficulty-like values (easy/medium/hard/etc) OR numeric with suffix + const diffMatch = v.match( + /^(easy|simple|beginner)\b|^(medium|intermediate)\b|^(hard|expert|advanced)\b/i + ); + if (diffMatch) { + if (/^easy|simple|beginner/i.test(v)) + colorClass = "bg-green-500/10 border-green-500/30 text-green-400"; + else if (/^medium|intermediate/i.test(v)) + colorClass = "bg-yellow-500/10 border-yellow-500/30 text-yellow-400"; + else if (/^hard|expert|advanced/i.test(v)) + colorClass = "bg-red-500/10 border-red-500/30 text-red-400"; + displayValue = diffMatch[0]; + } + // Event-like values (contains CTF, DEF CON, hack, etc.) + else if (/ctf|def.?con|hack|game|competition|tournament|qualifier/i.test(v)) { + colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; + displayValue = String(value); + } + // Points-like values (number + "pts" suffix) + else if (/(\d+)\s*pts?$/i.test(v)) { + const match = v.match(/(\d+)\s*pts?$/i); + colorClass = "bg-[#5e6ad2]/10 border-[#5e6ad2]/30 text-[#7170ff]"; + displayValue = match ? `${match[1]} pts` : String(value); + } + // Category-like values (pwn/web/crypto/etc) + else if (/^(pwn|web|crypto|misc|forensic|reverse|pwnable|binary|webexploit)$/i.test(v)) { + colorClass = "bg-orange-500/15 border-orange-500/25 text-orange-400"; + displayValue = v.charAt(0).toUpperCase() + v.slice(1); + } + // Status-like values (solved/pending/wip) + else if (/^(solved|pending|wip|in.?progress|completed|todo)$/i.test(v)) { + colorClass = v.includes("solved") || v.includes("completed") + ? "bg-green-500/10 border-green-500/30 text-green-400" + : v.includes("wip") || v.includes("progress") + ? "bg-yellow-500/10 border-yellow-500/30 text-yellow-400" + : "bg-red-500/10 border-red-500/30 text-red-400"; + displayValue = v.charAt(0).toUpperCase() + v.slice(1); + } + else { + displayValue = String(value); + } + } return ( - {prefix ? `${displayValue} ${prefix}` : `${label}: ${displayValue}`} + {displayValue} ); })} diff --git a/apps/web/app/components/DocForm.tsx b/apps/web/app/components/DocForm.tsx index 97e20c6..6ca3e9c 100644 --- a/apps/web/app/components/DocForm.tsx +++ b/apps/web/app/components/DocForm.tsx @@ -244,14 +244,10 @@ export default function DocForm({ mode, slug, secret, initial }: DocFormProps) { Custom metadata fields

- Add extra frontmatter fields. These will be stored in the document's - YAML frontmatter and rendered as badges on the doc page. Examples for - CTF writeups: event,{" "} - challenge,{" "} - category,{" "} - difficulty,{" "} - points. -

+ Add extra frontmatter fields. Content creators name and define their own + metadata here — the system auto-renders any field as a badge on the doc + page based on its value (e.g. numbers → points style, "easy" → green + difficulty badge, etc.).{" "}

{customFields.length > 0 && (
{customFields.map((field, i) => (