From d136fd4af5eebe6dd1daea68dd7d83bcf453c4ea Mon Sep 17 00:00:00 2001 From: TheRealToxicDev Date: Sat, 14 Feb 2026 01:04:22 -0700 Subject: [PATCH] feat(add): add validator and more --- CHANGELOG.md | 38 + app/api/route.ts | 5 + app/layout.config.tsx | 18 + app/validator/layout.tsx | 34 + app/validator/page.tsx | 27 + .../src/core/validator/validator-content.tsx | 912 ++++++++++++++++++ 6 files changed, 1034 insertions(+) create mode 100644 app/validator/layout.tsx create mode 100644 app/validator/page.tsx create mode 100644 packages/ui/src/core/validator/validator-content.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f825a..766daab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ All notable changes to FixFX will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-02-14 + +### Added + +#### JSON Validator + +- **Validator Page** (`/validator`) - Full-featured JSON validator with txAdmin support + - Generic JSON syntax validation with formatted output + - txAdmin Discord embed JSON validation with field-level issue reporting + - txAdmin embed config JSON validation (status strings, colors, buttons) + - Collapsible sidebar with validation mode selector, quick templates, and txAdmin placeholder reference + - Click-to-insert txAdmin placeholders (`{{serverName}}`, `{{statusString}}`, etc.) + - Format/prettify and clear actions with keyboard shortcut (Ctrl+Enter) + - Client-side fallback validation when the backend API is unreachable + - Mobile-responsive layout with dropdown validation type selector + - Suspense loading state with progress indicator +- **Validator Layout** - SEO metadata for `/validator` with Open Graph tags +- **Navigation** - Added JSON Validator to the Resources menu in the nav bar with Braces icon +- **API Route Documentation** - Added validator endpoint to the API index route + +### Changed + +#### Data Fetching & Artifacts + +- **`useFetch` Hook** - Migrated from manual `useState`/`useEffect`/`AbortController` to TanStack Query (`useQuery`) + - Automatic request deduplication, caching, and background refetching + - Simplified error handling with typed errors (`E = Error`) + - Query keys derived from URL and dependency array for proper cache invalidation + - Removed manual abort controller management (handled by TanStack Query) +- **`GitHubFetcher`** - Migrated from Axios to native `fetch` API + - Removed `axios` dependency entirely + - Consolidated request logic into a single private `request()` method + - Uses native `AbortController` with configurable timeout + - Proper rate limit tracking via `Headers.get()` instead of raw header objects + - Improved error handling for 304 Not Modified responses + - Cleaner POST/PUT/DELETE methods delegating to the shared request method +- **Query Provider** - Added TanStack Query provider for `useFetch` integration + ## [1.1.0] - 2026-01-26 ### Added diff --git a/app/api/route.ts b/app/api/route.ts index e5e18e6..f5ad91f 100644 --- a/app/api/route.ts +++ b/app/api/route.ts @@ -19,6 +19,11 @@ export async function GET() { path: "/api/artifacts?platform=windows&product=fivem", description: "FiveM/RedM server artifacts", }, + { + name: "validator", + path: "/api/validator/validate", + description: "JSON validator with txAdmin embed support", + }, ], }); } diff --git a/app/layout.config.tsx b/app/layout.config.tsx index 577ef23..a9e4584 100644 --- a/app/layout.config.tsx +++ b/app/layout.config.tsx @@ -11,6 +11,7 @@ import { X, Server, Palette, + Braces, } from "lucide-react"; export const baseOptions: HomeLayoutProps = { @@ -258,6 +259,23 @@ export const baseOptions: HomeLayoutProps = { "Download official FixFX logos, icons, and brand guidelines.", url: "/brand", }, + { + menu: { + banner: ( +
+ +

+ JSON Validator +

+
+ ), + }, + icon: , + text: "JSON Validator", + description: + "Validate JSON syntax and txAdmin Discord bot embed configurations.", + url: "/validator", + }, ], }, ], diff --git a/app/validator/layout.tsx b/app/validator/layout.tsx new file mode 100644 index 0000000..d1a474c --- /dev/null +++ b/app/validator/layout.tsx @@ -0,0 +1,34 @@ +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "JSON Validator | FixFX", + description: + "Validate JSON syntax and txAdmin Discord bot embed configurations. Check your embed JSON and config JSON for errors before deploying.", + keywords: [ + "JSON validator", + "txAdmin", + "Discord embed", + "FiveM", + "txAdmin Discord bot", + "embed config", + "JSON syntax checker", + ], + alternates: { + canonical: "https://fixfx.wiki/validator", + }, + openGraph: { + title: "JSON Validator | FixFX", + description: + "Validate JSON syntax and txAdmin Discord bot embed configurations.", + url: "https://fixfx.wiki/validator", + type: "website", + }, +}; + +export default function ValidatorLayout({ + children, +}: { + children: React.ReactNode; +}) { + return
{children}
; +} diff --git a/app/validator/page.tsx b/app/validator/page.tsx new file mode 100644 index 0000000..2b3ea01 --- /dev/null +++ b/app/validator/page.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { Suspense } from "react"; +import { ValidatorContent } from "@ui/core/validator/validator-content"; +import { Card } from "@ui/components/card"; +import { Progress } from "@ui/components/progress"; + +export default function ValidatorPage() { + return ( + + + +

+ Loading validator... +

+
+ + } + > + +
+ ); +} + +export const dynamic = "force-dynamic"; diff --git a/packages/ui/src/core/validator/validator-content.tsx b/packages/ui/src/core/validator/validator-content.tsx new file mode 100644 index 0000000..ce18df1 --- /dev/null +++ b/packages/ui/src/core/validator/validator-content.tsx @@ -0,0 +1,912 @@ +"use client"; + +import * as React from "react"; +import { useCallback, useEffect, useState, useRef } from "react"; +import { ScrollArea } from "@/packages/ui/src/components/scroll-area"; +import { Badge } from "@/packages/ui/src/components/badge"; +import { Button } from "@/packages/ui/src/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/packages/ui/src/components/card"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "@/packages/ui/src/components/alert"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/packages/ui/src/components/tooltip"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/packages/ui/src/components/tabs"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/packages/ui/src/components/accordion"; +import { Separator } from "@/packages/ui/src/components/separator"; +import { cn } from "@/packages/utils/src/functions/cn"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { motion, AnimatePresence } from "motion/react"; +import { + CheckCircle2, + XCircle, + AlertTriangle, + Info, + Copy, + Check, + Braces, + FileJson, + Eraser, + Play, + ChevronLeft, + ChevronRight, + Bot, + Sparkles, + ArrowRight, + Code2, + ExternalLink, +} from "lucide-react"; +import Link from "next/link"; +import { FixFXIcon } from "@/packages/ui/src/icons"; + +// Types +interface ValidationIssue { + path: string; + message: string; + severity: "error" | "warning" | "info"; +} + +interface ValidationResult { + valid: boolean; + type: string; + issues: ValidationIssue[]; + formatted?: string; + parseError?: string; +} + +interface ValidatorInfoResponse { + types: { value: string; label: string; description: string }[]; + placeholders: { name: string; description: string }[]; + limits: { + embed: Record; + config: Record; + }; +} + +type ValidationType = "generic" | "txadmin-embed" | "txadmin-embed-config"; + +// Sample JSONs for quick-start +const SAMPLE_EMBED_JSON = `{ + "title": "{{serverName}}", + "url": "{{serverBrowserUrl}}", + "description": "Server status embed configured with txAdmin.", + "fields": [ + { + "name": "> STATUS", + "value": "\`\`\`\\n{{statusString}}\\n\`\`\`", + "inline": true + }, + { + "name": "> PLAYERS", + "value": "\`\`\`\\n{{serverClients}}/{{serverMaxClients}}\\n\`\`\`", + "inline": true + }, + { + "name": "> UPTIME", + "value": "\`\`\`\\n{{uptime}}\\n\`\`\`", + "inline": true + } + ] +}`; + +const SAMPLE_CONFIG_JSON = `{ + "onlineString": "🟢 Online", + "onlineColor": "#0BA70B", + "partialString": "🟡 Partial", + "partialColor": "#FFF100", + "offlineString": "🔴 Offline", + "offlineColor": "#A70B28", + "buttons": [ + { + "emoji": "1062338355909640233", + "label": "Connect", + "url": "{{serverJoinUrl}}" + } + ] +}`; + +export function ValidatorContent() { + const [jsonInput, setJsonInput] = useState(""); + const [validationType, setValidationType] = + useState("generic"); + const [result, setResult] = useState(null); + const [validatorInfo, setValidatorInfo] = + useState(null); + const [isValidating, setIsValidating] = useState(false); + const [copiedFormatted, setCopiedFormatted] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const textareaRef = useRef(null); + + // Fetch validator info on mount + useEffect(() => { + fetch(`${API_URL}/api/validator/info`) + .then((res) => res.json()) + .then((data) => setValidatorInfo(data)) + .catch(console.error); + }, []); + + const validate = useCallback(async () => { + if (!jsonInput.trim()) return; + + setIsValidating(true); + try { + const response = await fetch(`${API_URL}/api/validator/validate`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + json: jsonInput, + type: validationType, + }), + }); + + const data = await response.json(); + setResult(data.result); + } catch (err) { + // Fallback: client-side JSON parse check + try { + const parsed = JSON.parse(jsonInput); + const formatted = JSON.stringify(parsed, null, 4); + setResult({ + valid: true, + type: validationType, + issues: [], + formatted, + }); + } catch (parseErr) { + setResult({ + valid: false, + type: validationType, + issues: [], + parseError: + parseErr instanceof Error + ? parseErr.message + : "Invalid JSON syntax", + }); + } + } finally { + setIsValidating(false); + } + }, [jsonInput, validationType]); + + const handleFormat = useCallback(() => { + try { + const parsed = JSON.parse(jsonInput); + setJsonInput(JSON.stringify(parsed, null, 4)); + } catch { + // If it can't be parsed, trigger validation to show the error + validate(); + } + }, [jsonInput, validate]); + + const handleClear = useCallback(() => { + setJsonInput(""); + setResult(null); + }, []); + + const handleCopyFormatted = useCallback(() => { + if (result?.formatted) { + navigator.clipboard.writeText(result.formatted); + setCopiedFormatted(true); + setTimeout(() => setCopiedFormatted(false), 2000); + } + }, [result]); + + const loadSample = useCallback( + (type: ValidationType) => { + setValidationType(type); + if (type === "txadmin-embed") { + setJsonInput(SAMPLE_EMBED_JSON); + } else if (type === "txadmin-embed-config") { + setJsonInput(SAMPLE_CONFIG_JSON); + } + setResult(null); + }, + [], + ); + + // Keyboard shortcut: Ctrl+Enter to validate + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { + e.preventDefault(); + validate(); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [validate]); + + const severityIcon = (severity: string) => { + switch (severity) { + case "error": + return ; + case "warning": + return ; + case "info": + return ; + default: + return null; + } + }; + + const severityBadgeVariant = (severity: string) => { + switch (severity) { + case "error": + return "destructive" as const; + case "warning": + return "warning" as const; + case "info": + return "info" as const; + default: + return "secondary" as const; + } + }; + + const errorCount = + result?.issues.filter((i) => i.severity === "error").length ?? 0; + const warningCount = + result?.issues.filter((i) => i.severity === "warning").length ?? 0; + const infoCount = + result?.issues.filter((i) => i.severity === "info").length ?? 0; + + return ( +
+ {/* Ambient background */} +
+
+
+
+
+ + {/* Sidebar */} +
+ {/* Sidebar Header */} +
+ {!sidebarCollapsed && ( + + + JSON Validator + + )} + +
+ + + {!sidebarCollapsed ? ( +
+ {/* Validation Type */} +
+

+ Validation Mode +

+
+ {[ + { + value: "generic" as ValidationType, + label: "Generic JSON", + icon: , + color: "text-gray-400", + }, + { + value: "txadmin-embed" as ValidationType, + label: "txAdmin Embed", + icon: , + color: "text-green-500", + }, + { + value: "txadmin-embed-config" as ValidationType, + label: "txAdmin Config", + icon: , + color: "text-amber-500", + }, + ].map((type) => ( + + ))} +
+
+ + + + {/* Quick Templates */} +
+

+ Quick Templates +

+
+ + +
+
+ + + + {/* Placeholders Reference */} +
+

+ txAdmin Placeholders +

+
+ {( + validatorInfo?.placeholders ?? [ + { name: "serverName", description: "Server name" }, + { + name: "serverClients", + description: "Players online", + }, + { + name: "serverMaxClients", + description: "Max players", + }, + { name: "statusString", description: "Status text" }, + { name: "uptime", description: "Server uptime" }, + { + name: "nextScheduledRestart", + description: "Next restart", + }, + { name: "serverJoinUrl", description: "Join URL" }, + { + name: "serverBrowserUrl", + description: "Browser URL", + }, + { name: "serverCfxId", description: "Cfx.re ID" }, + { name: "statusColor", description: "Status color" }, + ] + ).map((p) => ( + + + + + + +

{p.description}

+

+ Click to insert at cursor +

+
+
+
+ ))} +
+
+ + + + {/* Useful Links */} +
+

+ Useful Links +

+ +
+
+ ) : ( + /* Collapsed sidebar icons */ +
+ + {[ + { + value: "generic" as ValidationType, + icon: , + label: "Generic JSON", + }, + { + value: "txadmin-embed" as ValidationType, + icon: , + label: "txAdmin Embed", + }, + { + value: "txadmin-embed-config" as ValidationType, + icon: , + label: "txAdmin Config", + }, + ].map((type) => ( + + + + + {type.label} + + ))} + +
+ )} +
+
+ + {/* Main Content */} +
+ {/* Top Bar */} +
+
+
+ +

JSON Validator

+
+ + {validationType === "generic" + ? "Generic" + : validationType === "txadmin-embed" + ? "txAdmin Embed" + : "txAdmin Config"} + +
+ +
+ {/* Mobile validation type selector */} +
+ +
+ + + + + + + Format/prettify JSON + + + + + + + + + Clear editor + + + + +
+
+ + {/* Editor + Results */} +
+ {/* JSON Input */} +
+
+
+ + Input +
+ + Ctrl+Enter to validate + +
+
+