diff --git a/CHANGELOG.md b/CHANGELOG.md index 766daab..b3a71d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,49 @@ 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.3.0] - 2026-03-04 + +### Added + +#### Game References + +- **Game References section** (`/game-references`) — 12 new reference pages backed by the `/api/game-references` endpoints, all with search, pagination, and loading skeletons +- **Blips** (`/game-references/blips`) — Image grid of all minimap blip icons with ID labels; secondary tab for the blip color palette with hex swatches +- **Checkpoints** (`/game-references/checkpoints`) — Image grid of all checkpoint types with section filter tabs (standard 0–49 / type 44–46 variant) +- **Markers** (`/game-references/markers`) — Image grid of all 44 `DRAW_MARKER` types with ID and name labels +- **Ped Models** (`/game-references/ped-models`) — Image grid of all pedestrian models with category filter chips, prop/component counts, and pagination +- **Weapon Models** (`/game-references/weapon-models`) — Card grid grouped by weapon type with expandable detail panels (hash key, model hash key, DLC, description, components, tints) +- **Data Files** (`/game-references/data-files`) — Searchable table of all resource manifest `data_file` keys with file type, root element, mounter, and example columns +- **Game Events** (`/game-references/game-events`) — Searchable table of client-side game events with descriptions +- **Gamer Tags** (`/game-references/gamer-tags`) — Table of head display component IDs and names +- **HUD Colors** (`/game-references/hud-colors`) — Toggle between a color swatch grid and a full RGBA/hex table for all ~234 HUD color indices +- **Net Game Events** (`/game-references/net-game-events`) — Searchable table of `GTA_EVENT_IDS` enum entries with sequential IDs +- **Pickup Hashes** (`/game-references/pickup-hashes`) — Searchable table of `ePickupHashes` enum entries with numeric hash values +- **Zones** (`/game-references/zones`) — Searchable table of all 1300+ map zones with zone name ID, zone name, and description +- **Game References Hub** (`/game-references`) — Landing page with a hero section, live stats row (category count + total entries from `/api/game-references/summary`), and a 4-column responsive card grid; each card displays a per-category entry count badge fetched from the summary endpoint +- **Game References Layout** — Shared SEO metadata and Open Graph tags for the `/game-references` route group + +### Changed + +#### JSON Validator + +- **Modular Architecture** — Rewrote the monolithic 913-line `validator-content.tsx` into a fully modular plugin-based system + - `types.ts` — Shared TypeScript interfaces (`ValidatorType`, `IssueSeverity`, `ValidationIssue`, `ValidationResult`, `ValidatorConfig`, `ValidatorPlaceholder`) + - `base-validator.ts` — Abstract `BaseValidator` class with shared `parseJson`, `formatJson`, `createIssue`, and `getConfig` methods + - `validators/generic.ts` — `GenericJsonValidator` for plain JSON syntax validation + - `validators/txadmin-embed.ts` — `TxAdminEmbedValidator` with Discord embed structure and character limit enforcement + - `validators/txadmin-config.ts` — `TxAdminConfigValidator` with hex color, button structure, and status string/color pairing validation + - `registry.ts` — `ValidatorRegistry` singleton for registering, retrieving, and querying all validators +- **Componentized UI** — Split rendering into focused, reusable components under `core/validator/components/` + - `ValidatorHeader` — Title, mode badge, and Format / Clear / Validate action buttons with keyboard shortcut hints + - `EditorPanel` — JSON input textarea with invalid-state styling and `Ctrl+Enter` hint + - `ResultsPanel` — Animated results display with severity badges, issue path/message/suggestion rendering, formatted output copy, and validation metadata footer (character count, line count, validation time) + - `ValidatorSidebar` — Collapsible sidebar (expanded `w-72` / collapsed `w-20`) with validation mode selector, quick templates, click-to-insert placeholder variables, and resource links +- **Validation Metadata** — Results now include `validationTime`, `characterCount`, and `lineCount` surfaced in the results panel footer +- **Type rename** — Internal validator type `txadmin-embed-config` renamed to `txadmin-config` for consistency + +--- + ## [1.2.0] - 2026-02-14 ### Added diff --git a/app/game-references/_components/page-shell.tsx b/app/game-references/_components/page-shell.tsx new file mode 100644 index 0000000..fa55ae0 --- /dev/null +++ b/app/game-references/_components/page-shell.tsx @@ -0,0 +1,257 @@ +"use client"; + +import Link from "next/link"; +import { ChevronLeft, Search, X, type LucideIcon } from "lucide-react"; +import { Button } from "@ui/components/button"; +import { Input } from "@ui/components/input"; +import { cn } from "@utils/functions/cn"; + +// --------------------------------------------------------------------------- +// PageShell +// --------------------------------------------------------------------------- + +interface GameReferencePageShellProps { + icon: LucideIcon; + iconColor: string; + iconBg: string; + title: string; + description: React.ReactNode; + badge?: string; + controls?: React.ReactNode; + children: React.ReactNode; +} + +export function GameReferencePageShell({ + icon: Icon, + iconColor, + iconBg, + title, + description, + badge, + controls, + children, +}: GameReferencePageShellProps) { + return ( +
+ {/* Ambient background */} +
+
+
+
+
+ + {/* Header */} +
+
+ {/* Breadcrumb */} + + + Game References + + +
+ {/* Icon */} +
+ +
+ + {/* Title + description */} +
+
+

+ {title} +

+ {badge && ( + + {badge} + + )} +
+

+ {description} +

+
+
+ + {/* Tabs / filters / search */} + {controls &&
{controls}
} +
+
+ + {/* Main content */} +
{children}
+
+ ); +} + +// --------------------------------------------------------------------------- +// ReferenceSearch — consistent search bar used across all pages +// --------------------------------------------------------------------------- + +interface ReferenceSearchProps { + value: string; + placeholder: string; + onChange: (v: string) => void; + onSearch: () => void; + onClear: () => void; +} + +export function ReferenceSearch({ + value, + placeholder, + onChange, + onSearch, + onClear, +}: ReferenceSearchProps) { + return ( +
+
+ + onChange(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onSearch()} + /> + {value && ( + + )} +
+ +
+ ); +} + +// --------------------------------------------------------------------------- +// ReferencePagination — consistent pagination used across all pages +// --------------------------------------------------------------------------- + +interface ReferencePaginationProps { + offset: number; + limit: number; + total: number; + hasMore: boolean; + onPrev: () => void; + onNext: () => void; +} + +export function ReferencePagination({ + offset, + limit, + total, + hasMore, + onPrev, + onNext, +}: ReferencePaginationProps) { + if (!hasMore && offset === 0) return null; + return ( +
+ + + {offset + 1}–{Math.min(offset + limit, total)} of {total} + + +
+ ); +} + +// --------------------------------------------------------------------------- +// ReferenceFilterChips — pill-style filter buttons +// --------------------------------------------------------------------------- + +interface ReferenceFilterChipsProps { + allLabel?: string; + options: string[]; + value: string; + onChange: (v: string) => void; +} + +export function ReferenceFilterChips({ + allLabel = "All", + options, + value, + onChange, +}: ReferenceFilterChipsProps) { + if (options.length === 0) return null; + return ( +
+ + {[...options].sort().map((opt) => ( + + ))} +
+ ); +} + +// --------------------------------------------------------------------------- +// ReferenceTabs — horizontal tab switcher +// --------------------------------------------------------------------------- + +interface Tab { + id: string; + label: string; + count?: number; +} + +interface ReferenceTabsProps { + tabs: Tab[]; + active: string; + onChange: (id: string) => void; +} + +export function ReferenceTabs({ tabs, active, onChange }: ReferenceTabsProps) { + return ( +
+ {tabs.map((tab) => ( + + ))} +
+ ); +} diff --git a/app/game-references/blips/page.tsx b/app/game-references/blips/page.tsx new file mode 100644 index 0000000..39724c8 --- /dev/null +++ b/app/game-references/blips/page.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Crosshair } from "lucide-react"; +import { + GameReferencePageShell, + ReferenceSearch, + ReferencePagination, + ReferenceTabs, +} from "../_components/page-shell"; + +interface Blip { id: number; name: string; imageUrl: string } +interface BlipColor { id: number; name: string; hex: string } +interface BlipsApiResponse { + success: boolean; count: number; + data: { blips: Blip[]; colors: BlipColor[] }; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function BlipsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + const [activeTab, setActiveTab] = useState<"blips" | "colors">("blips"); + + const url = `${API_URL}/api/game-references/blips?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const blips = data?.data?.blips ?? []; + const colors = data?.data?.colors ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + + setActiveTab(t as "blips" | "colors")} + /> + {activeTab === "blips" && ( + + )} +
+ } + > + {isPending ? ( +
+ {Array.from({ length: 48 }).map((_, i) => ( +
+ ))} +
+ ) : activeTab === "blips" ? ( + <> +

+ Showing {blips.length} of{" "} + {metadata?.total ?? blips.length} blips +

+
+ {blips.map((blip) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {blip.name} + {blip.id} +
+ ))} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + ) : ( +
+ {colors.map((color) => ( +
+
+
+
{color.name}
+
{color.id} · {color.hex}
+
+
+ ))} +
+ )} + + ); +} \ No newline at end of file diff --git a/app/game-references/checkpoints/page.tsx b/app/game-references/checkpoints/page.tsx new file mode 100644 index 0000000..1eeca27 --- /dev/null +++ b/app/game-references/checkpoints/page.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { MapPin } from "lucide-react"; +import { + GameReferencePageShell, ReferenceSearch, ReferencePagination, ReferenceFilterChips, +} from "../_components/page-shell"; + +interface Checkpoint { id: string; label: string; section: "standard" | "type-44-46"; imageUrl: string } +interface ApiResponse { + success: boolean; count: number; data: Checkpoint[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function CheckpointsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [section, setSection] = useState<"" | "standard" | "type-44-46">(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/checkpoints?search=${encodeURIComponent(search)}§ion=${section}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, section, offset]); + + const checkpoints = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + const handleSection = (s: string) => { setSection(s as "" | "standard" | "type-44-46"); setOffset(0); }; + + const sectionLabels: Record = { + "": "All", + standard: "Standard (0–49)", + "type-44-46": "Type 44–46", + }; + + return ( + All checkpoint types for use with CREATE_CHECKPOINT.} + badge={metadata ? `${metadata.total.toLocaleString()} types` : undefined} + controls={ +
+
+ {(["", "standard", "type-44-46"] as const).map((s) => ( + + ))} +
+ +
+ } + > + {isPending ? ( +
+ {Array.from({ length: 30 }).map((_, i) => ( +
+ ))} +
+ ) : ( + <> +

+ Showing {checkpoints.length} of{" "} + {metadata?.total ?? checkpoints.length} checkpoints +

+
+ {checkpoints.map((cp, i) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {cp.id} + {cp.id} + {cp.label && {cp.label}} +
+ ))} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/data-files/page.tsx b/app/game-references/data-files/page.tsx new file mode 100644 index 0000000..eed2172 --- /dev/null +++ b/app/game-references/data-files/page.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { FileCode } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination } from "../_components/page-shell"; + +interface DataFile { key: string; fileType: string; rootElement: string; mounter: string; example: string } +interface ApiResponse { + success: boolean; count: number; data: DataFile[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function DataFilesPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/data-files?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const files = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + + } + > + {isPending ? ( +
+ {Array.from({ length: 15 }).map((_, i) => ( +
+ ))} +
+ ) : ( + <> +

+ Showing {files.length} of{" "} + {metadata?.total ?? files.length} data file types +

+
+ + + + + + + + + + + {files.map((f) => ( + + + + + + + ))} + +
KeyFile TypeRoot ElementMounter
{f.key}{f.fileType}{f.rootElement}{f.mounter}
+
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/game-events/page.tsx b/app/game-references/game-events/page.tsx new file mode 100644 index 0000000..d00f151 --- /dev/null +++ b/app/game-references/game-events/page.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Zap } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination } from "../_components/page-shell"; + +interface GameEvent { name: string; description: string } +interface ApiResponse { + success: boolean; count: number; data: GameEvent[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function GameEventsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/game-events?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const events = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + + } + > + {isPending ? ( +
+ {Array.from({ length: 12 }).map((_, i) => ( +
+ ))} +
+ ) : ( + <> +

+ Showing {events.length} of{" "} + {metadata?.total ?? events.length} events +

+
+ {events.map((ev) => ( +
+
{ev.name}
+ {ev.description && ( +

{ev.description}

+ )} +
+ ))} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/gamer-tags/page.tsx b/app/game-references/gamer-tags/page.tsx new file mode 100644 index 0000000..50df53b --- /dev/null +++ b/app/game-references/gamer-tags/page.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Tag } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch } from "../_components/page-shell"; + +interface GamerTagComponent { id: number; name: string } +interface ApiResponse { + success: boolean; count: number; data: GamerTagComponent[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +export default function GamerTagsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + + const url = `${API_URL}/api/game-references/gamer-tags?search=${encodeURIComponent(search)}`; + const { data, isPending } = useFetch(url, {}, [search]); + + const components = data?.data ?? []; + const total = data?.metadata?.total; + + const handleSearch = () => setSearch(inputValue); + const handleClear = () => { setInputValue(""); setSearch(""); }; + + return ( + Head display component IDs for use with SET_MULTIPLAYER_HANGER_COLOUR and related natives.} + badge={total ? `${total.toLocaleString()} components` : undefined} + controls={ + + } + > + {isPending ? ( +
+ {Array.from({ length: 15 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ + + + + + + + + {components.map((comp) => ( + + + + + ))} + +
IDComponent Name
{comp.id}{comp.name}
+
+ )} + + ); +} \ No newline at end of file diff --git a/app/game-references/hud-colors/page.tsx b/app/game-references/hud-colors/page.tsx new file mode 100644 index 0000000..1cbd62b --- /dev/null +++ b/app/game-references/hud-colors/page.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Palette } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination, ReferenceTabs } from "../_components/page-shell"; + +interface HUDColor { index: number; name: string; r: number; g: number; b: number; a: number; hex: string } +interface ApiResponse { + success: boolean; count: number; data: HUDColor[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function HUDColorsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + const [view, setView] = useState<"swatches" | "table">("swatches"); + + const url = `${API_URL}/api/game-references/hud-colors?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const colors = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + All HUD colour indices with RGBA values and hex codes. Use with HUD_COLOUR_* constants.} + badge={metadata ? `${metadata.total.toLocaleString()} colors` : undefined} + controls={ +
+ setView(v as "swatches" | "table")} + /> + +
+ } + > + {isPending ? ( + view === "swatches" ? ( +
+ {Array.from({ length: 40 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
{Array.from({ length: 15 }).map((_, i) =>
)}
+ ) + ) : view === "swatches" ? ( + <> +
+ {colors.map((c) => ( +
+
+ {c.index} +
+ ))} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + ) : ( + <> +
+ + + + + + + + + + + + {colors.map((c) => ( + + + + + + + + ))} + +
IdxNameSwatchHexRGBA
{c.index}{c.name}
{c.hex}{c.r}, {c.g}, {c.b}, {c.a}
+
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/layout.tsx b/app/game-references/layout.tsx new file mode 100644 index 0000000..901f062 --- /dev/null +++ b/app/game-references/layout.tsx @@ -0,0 +1,42 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; +import { HomeLayout } from "fumadocs-ui/layouts/home"; +import { baseOptions } from "@/app/layout.config"; + +export const metadata: Metadata = { + title: { + template: "%s | Game References | FixFX", + default: "Game References | FixFX", + }, + description: + "FiveM & RedM game reference data including blips, checkpoints, markers, ped models, weapon models, zones, HUD colors, and more.", + keywords: [ + "FiveM game references", + "FiveM blips", + "FiveM markers", + "FiveM ped models", + "FiveM weapon models", + "FiveM zones", + "FiveM HUD colors", + "CitizenFX reference", + "GTA5 modding reference", + ], + alternates: { + canonical: "https://fixfx.wiki/game-references", + }, + openGraph: { + title: "Game References | FixFX", + description: + "FiveM & RedM game reference data — blips, checkpoints, markers, ped models, weapon models, zones, HUD colors, and more.", + url: "https://fixfx.wiki/game-references", + type: "website", + }, +}; + +export default function GameReferencesLayout({ + children, +}: { + children: ReactNode; +}) { + return {children}; +} diff --git a/app/game-references/markers/page.tsx b/app/game-references/markers/page.tsx new file mode 100644 index 0000000..f6ceec0 --- /dev/null +++ b/app/game-references/markers/page.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Layers } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch } from "../_components/page-shell"; + +interface Marker { id: number; name: string; imageUrl: string } +interface ApiResponse { + success: boolean; count: number; data: Marker[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +export default function MarkersPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + + const url = `${API_URL}/api/game-references/markers?search=${encodeURIComponent(search)}`; + const { data, isPending } = useFetch(url, {}, [search]); + + const markers = data?.data ?? []; + const total = data?.metadata?.total; + + const handleSearch = () => setSearch(inputValue); + const handleClear = () => { setInputValue(""); setSearch(""); }; + + return ( + All 3D marker types available via DRAW_MARKER (IDs 0–43).} + badge={total ? `${total} marker types` : undefined} + controls={ + + } + > + {isPending ? ( +
+ {Array.from({ length: 44 }).map((_, i) => ( +
+ ))} +
+ ) : ( +
+ {markers.map((marker) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {marker.name} +
+
{marker.id}
+ {marker.name && ( +
{marker.name}
+ )} +
+
+ ))} +
+ )} + + ); +} \ No newline at end of file diff --git a/app/game-references/net-game-events/page.tsx b/app/game-references/net-game-events/page.tsx new file mode 100644 index 0000000..7a0e5d6 --- /dev/null +++ b/app/game-references/net-game-events/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Network } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination } from "../_components/page-shell"; + +interface NetGameEvent { id: number; name: string } +interface ApiResponse { + success: boolean; count: number; data: NetGameEvent[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function NetGameEventsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/net-game-events?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const events = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + All GTA_EVENT_IDS enum entries with sequential IDs for network event handling.} + badge={metadata ? `${metadata.total.toLocaleString()} events` : undefined} + controls={ + + } + > + {isPending ? ( +
+ {Array.from({ length: 20 }).map((_, i) =>
)} +
+ ) : ( + <> +

+ Showing {events.length} of{" "} + {metadata?.total ?? events.length} events +

+
+ + + + + + + + + {events.map((ev) => ( + + + + + ))} + +
IDEvent Name
{ev.id}{ev.name}
+
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/page.tsx b/app/game-references/page.tsx new file mode 100644 index 0000000..6ced88f --- /dev/null +++ b/app/game-references/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +import Link from "next/link"; +import { + Crosshair, + MapPin, + Layers, + Users, + Sword, + FileCode, + Zap, + Tag, + Palette, + Network, + Hash, + Map, + ArrowRight, + BookOpen, + Database, +} from "lucide-react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; + +const REFERENCES = [ + { + slug: "blips", + label: "Map Blips", + description: "All minimap blip icons with IDs and the full blip colour palette.", + icon: Crosshair, + color: "from-blue-500/20 to-blue-600/5 border-blue-500/20", + iconColor: "text-blue-500", + tag: "blips", + }, + { + slug: "checkpoints", + label: "Checkpoints", + description: "Checkpoint types for CREATE_CHECKPOINT — standard and type 44–46 variants.", + icon: MapPin, + color: "from-emerald-500/20 to-emerald-600/5 border-emerald-500/20", + iconColor: "text-emerald-500", + tag: "checkpoints", + }, + { + slug: "markers", + label: "Markers", + description: "All 44 DRAW_MARKER types with IDs and name labels.", + icon: Layers, + color: "from-orange-500/20 to-orange-600/5 border-orange-500/20", + iconColor: "text-orange-500", + tag: "markers", + }, + { + slug: "ped-models", + label: "Ped Models", + description: "Pedestrian model names with category filters, prop and component counts.", + icon: Users, + color: "from-violet-500/20 to-violet-600/5 border-violet-500/20", + iconColor: "text-violet-500", + tag: "ped-models", + }, + { + slug: "weapon-models", + label: "Weapon Models", + description: "Weapon model names grouped by type with hash keys, DLC info, components, and tints.", + icon: Sword, + color: "from-red-500/20 to-red-600/5 border-red-500/20", + iconColor: "text-red-500", + tag: "weapon-models", + }, + { + slug: "data-files", + label: "Data Files", + description: "All resource manifest data_file keys with file type, root element, and mounter details.", + icon: FileCode, + color: "from-cyan-500/20 to-cyan-600/5 border-cyan-500/20", + iconColor: "text-cyan-500", + tag: "data-files", + }, + { + slug: "game-events", + label: "Game Events", + description: "Client-side game events with descriptions for use in resource scripting.", + icon: Zap, + color: "from-yellow-500/20 to-yellow-600/5 border-yellow-500/20", + iconColor: "text-yellow-500", + tag: "game-events", + }, + { + slug: "gamer-tags", + label: "Gamer Tags", + description: "Head display component IDs for SET_MULTIPLAYER_HANGER_COLOUR and related natives.", + icon: Tag, + color: "from-pink-500/20 to-pink-600/5 border-pink-500/20", + iconColor: "text-pink-500", + tag: "gamer-tags", + }, + { + slug: "hud-colors", + label: "HUD Colors", + description: "All ~234 HUD colour indices with RGBA values and hex codes — swatch grid and table view.", + icon: Palette, + color: "from-fuchsia-500/20 to-fuchsia-600/5 border-fuchsia-500/20", + iconColor: "text-fuchsia-500", + tag: "hud-colors", + }, + { + slug: "net-game-events", + label: "Net Game Events", + description: "GTA_EVENT_IDS enum entries with sequential IDs for network event handling.", + icon: Network, + color: "from-sky-500/20 to-sky-600/5 border-sky-500/20", + iconColor: "text-sky-500", + tag: "net-game-events", + }, + { + slug: "pickup-hashes", + label: "Pickup Hashes", + description: "ePickupHashes enum entries with numeric hash values for pickup scripting.", + icon: Hash, + color: "from-lime-500/20 to-lime-600/5 border-lime-500/20", + iconColor: "text-lime-500", + tag: "pickup-hashes", + }, + { + slug: "zones", + label: "Zones", + description: "All 1300+ map zones with zone name IDs and descriptions for area detection.", + icon: Map, + color: "from-teal-500/20 to-teal-600/5 border-teal-500/20", + iconColor: "text-teal-500", + tag: "zones", + }, +]; + +interface SummaryData { + success: boolean; + counts: Record; + total: number; +} + +export default function GameReferencesPage() { + const { data: summary } = useFetch( + `${API_URL}/api/game-references/summary`, + {}, + [], + ); + + const counts = summary?.counts ?? {}; + const totalEntries = summary?.total; + + return ( +
+ {/* Ambient background */} +
+
+
+
+
+
+ + {/* Hero */} +
+
+
+
+ + + Game References + +
+ +

+ FiveM{" "} + + Reference Data + +

+ +

+ Searchable reference tables for GTA V and FiveM internals — blips, + markers, ped models, weapon models, HUD colours, zones, and more. + Everything you need for scripting, mapped and indexed. +

+ + {/* Stats row */} +
+
+ + + + {REFERENCES.length} + {" "} + reference categories + +
+ {totalEntries !== undefined && ( +
+ + + + {totalEntries.toLocaleString()}+ + {" "} + total entries + +
+ )} +
+
+
+
+ + {/* Grid */} +
+
+ {REFERENCES.map(({ slug, label, description, icon: Icon, color, iconColor, tag }) => { + const count = counts[tag]; + return ( + + {/* Icon */} +
+ +
+ + {/* Text */} +
+
+

+ {label} +

+ {count !== undefined && ( + + {count.toLocaleString()} + + )} +
+

+ {description} +

+
+ + {/* Footer */} +
+ Explore + +
+ + ); + })} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/game-references/ped-models/page.tsx b/app/game-references/ped-models/page.tsx new file mode 100644 index 0000000..132b3fa --- /dev/null +++ b/app/game-references/ped-models/page.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Users } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination, ReferenceFilterChips } from "../_components/page-shell"; + +interface PedModel { name: string; category: string; props: number; components: number; imageUrl: string } +interface ApiResponse { + success: boolean; count: number; data: PedModel[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean; categories: string[] }; +} + +const LIMIT = 50; + +export default function PedModelsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [category, setCategory] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/ped-models?search=${encodeURIComponent(search)}&category=${encodeURIComponent(category)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, category, offset]); + + const peds = data?.data ?? []; + const metadata = data?.metadata; + const categories = metadata?.categories ?? []; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + const handleCategory = (cat: string) => { setCategory(cat); setOffset(0); }; + + return ( + All pedestrian models available in GTA V / FiveM. Use model names with GET_HASH_KEY or REQUEST_MODEL.} + badge={metadata ? `${metadata.total.toLocaleString()} models` : undefined} + controls={ +
+ + +
+ } + > + {isPending ? ( +
+ {Array.from({ length: LIMIT }).map((_, i) => ( +
+ ))} +
+ ) : ( + <> +

+ Showing {peds.length} of{" "} + {metadata?.total ?? peds.length} peds + {category && <> in "{category}"} +

+
+ {peds.map((ped) => ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {ped.name} +
+
{ped.name}
+
{ped.props}p · {ped.components}c
+
+
+ ))} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/pickup-hashes/page.tsx b/app/game-references/pickup-hashes/page.tsx new file mode 100644 index 0000000..5548077 --- /dev/null +++ b/app/game-references/pickup-hashes/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Hash } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination } from "../_components/page-shell"; + +interface PickupHash { name: string; hash: string } +interface ApiResponse { + success: boolean; count: number; data: PickupHash[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function PickupHashesPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/pickup-hashes?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const pickups = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + All ePickupHashes enum entries with numeric hash values. Use with CREATE_PICKUP_ROTATE and related natives.} + badge={metadata ? `${metadata.total.toLocaleString()} pickups` : undefined} + controls={ + + } + > + {isPending ? ( +
+ {Array.from({ length: 20 }).map((_, i) =>
)} +
+ ) : ( + <> +

+ Showing {pickups.length} of{" "} + {metadata?.total ?? pickups.length} pickup hashes +

+
+ + + + + + + + + {pickups.map((p, i) => ( + + + + + ))} + +
Pickup NameHash Value
{p.name}{p.hash}
+
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/weapon-models/page.tsx b/app/game-references/weapon-models/page.tsx new file mode 100644 index 0000000..30472ed --- /dev/null +++ b/app/game-references/weapon-models/page.tsx @@ -0,0 +1,149 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Sword } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination, ReferenceFilterChips } from "../_components/page-shell"; + +interface WeaponModel { + name: string; hash: string; modelHashKey: string; dlc: string; + description: string; imageUrl: string; group: string; + components: string[]; tints: string[]; +} +interface ApiResponse { + success: boolean; count: number; data: WeaponModel[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean; groups: string[] }; +} + +const LIMIT = 50; + +export default function WeaponModelsPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [group, setGroup] = useState(""); + const [offset, setOffset] = useState(0); + const [expanded, setExpanded] = useState(null); + + const url = `${API_URL}/api/game-references/weapon-models?search=${encodeURIComponent(search)}&group=${encodeURIComponent(group)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, group, offset]); + + const weapons = data?.data ?? []; + const metadata = data?.metadata; + const groups = metadata?.groups ?? []; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + const handleGroup = (g: string) => { setGroup(g); setOffset(0); setExpanded(null); }; + + return ( + + + +
+ } + > + {isPending ? ( +
+ {Array.from({ length: 9 }).map((_, i) =>
)} +
+ ) : ( + <> +

+ Showing {weapons.length} of{" "} + {metadata?.total ?? weapons.length} weapons + {group && <> in "{group}"} +

+
+ {weapons.map((weapon) => { + const key = weapon.hash || weapon.name; + const isExpanded = expanded === key; + const hasDetails = weapon.description || weapon.components?.length > 0 || weapon.tints?.length > 0; + return ( +
+
+ {weapon.imageUrl && ( + /* eslint-disable-next-line @next/next/no-img-element */ + {weapon.name} + )} +
+
{weapon.name}
+
{weapon.hash}
+
+ {weapon.group && ( + {weapon.group} + )} + {weapon.dlc && weapon.dlc !== "base" && ( + {weapon.dlc} + )} +
+
+
+ {hasDetails && ( +
+ + {isExpanded && ( +
+ {weapon.modelHashKey && ( +
Model Hash Key: {weapon.modelHashKey}
+ )} + {weapon.description && ( +

{weapon.description}

+ )} + {weapon.components?.length > 0 && ( +
+
Components ({weapon.components.length})
+
+ {weapon.components.map((c) => {c})} +
+
+ )} + {weapon.tints?.length > 0 && ( +
+
Tints ({weapon.tints.length})
+
+ {weapon.tints.map((t) => {t})} +
+
+ )} +
+ )} +
+ )} +
+ ); + })} +
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/game-references/zones/page.tsx b/app/game-references/zones/page.tsx new file mode 100644 index 0000000..2bbc854 --- /dev/null +++ b/app/game-references/zones/page.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState } from "react"; +import { useFetch } from "@core/useFetch"; +import { API_URL } from "@/packages/utils/src/constants/link"; +import { Map } from "lucide-react"; +import { GameReferencePageShell, ReferenceSearch, ReferencePagination } from "../_components/page-shell"; + +interface Zone { id: number; zoneNameId: string; zoneName: string; description: string } +interface ApiResponse { + success: boolean; count: number; data: Zone[]; + metadata: { total: number; limit: number; offset: number; hasMore: boolean }; +} + +const LIMIT = 100; + +export default function ZonesPage() { + const [search, setSearch] = useState(""); + const [inputValue, setInputValue] = useState(""); + const [offset, setOffset] = useState(0); + + const url = `${API_URL}/api/game-references/zones?search=${encodeURIComponent(search)}&limit=${LIMIT}&offset=${offset}`; + const { data, isPending } = useFetch(url, {}, [search, offset]); + + const zones = data?.data ?? []; + const metadata = data?.metadata; + + const handleSearch = () => { setSearch(inputValue); setOffset(0); }; + const handleClear = () => { setInputValue(""); setSearch(""); setOffset(0); }; + + return ( + All GTA V map zone identifiers and names. Use with GET_NAME_OF_ZONE and related natives.} + badge={metadata ? `${metadata.total.toLocaleString()} zones` : undefined} + controls={ + + } + > + {isPending ? ( +
+ {Array.from({ length: 15 }).map((_, i) =>
)} +
+ ) : ( + <> +

+ Showing {zones.length} of{" "} + {metadata?.total ?? zones.length} zones +

+
+ + + + + + + + + + {zones.map((zone) => ( + + + + + + ))} + +
Zone IDNameDescription
{zone.zoneNameId}{zone.zoneName}{zone.description}
+
+ setOffset(Math.max(0, offset - LIMIT))} + onNext={() => setOffset(offset + LIMIT)} + /> + + )} + + ); +} \ No newline at end of file diff --git a/app/layout.config.tsx b/app/layout.config.tsx index a9e4584..cf8e6dc 100644 --- a/app/layout.config.tsx +++ b/app/layout.config.tsx @@ -12,6 +12,7 @@ import { Server, Palette, Braces, +v MessageCircle, } from "lucide-react"; export const baseOptions: HomeLayoutProps = { @@ -44,6 +45,12 @@ export const baseOptions: HomeLayoutProps = { icon: , url: "/", }, + { + type: "main", + text: "Chat", + icon: , + url: "/chat", + }, { type: "menu", text: "Blog", @@ -181,16 +188,18 @@ export const baseOptions: HomeLayoutProps = { menu: { banner: (
- -

Fixie

+ +

+ Game References +

), }, - icon: , - text: "Chat with Fixie", + icon: , + text: "Game References", description: - "Fixie is a powerful AI assistant that can help you with all your CFX needs.", - url: "/chat", + "Explore various game references and resources.", + url: "/game-references", }, { menu: { @@ -275,7 +284,7 @@ export const baseOptions: HomeLayoutProps = { description: "Validate JSON syntax and txAdmin Discord bot embed configurations.", url: "/validator", - }, + } ], }, ], diff --git a/app/validator/layout.tsx b/app/validator/layout.tsx index d1a474c..e430872 100644 --- a/app/validator/layout.tsx +++ b/app/validator/layout.tsx @@ -1,7 +1,7 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: "JSON Validator | FixFX", + title: "JSON Validator", description: "Validate JSON syntax and txAdmin Discord bot embed configurations. Check your embed JSON and config JSON for errors before deploying.", keywords: [ diff --git a/packages/ui/src/core/validator/base-validator.ts b/packages/ui/src/core/validator/base-validator.ts new file mode 100644 index 0000000..fc0ef37 --- /dev/null +++ b/packages/ui/src/core/validator/base-validator.ts @@ -0,0 +1,80 @@ +/** + * Abstract base class for all validators + */ + +import { + ValidatorType, + ValidationResult, + ValidationIssue, + ValidatorConfig, +} from "./types"; + +export abstract class BaseValidator { + protected type: ValidatorType; + protected config: ValidatorConfig; + + constructor(type: ValidatorType, config: ValidatorConfig) { + this.type = type; + this.config = config; + } + + /** + * Main validation method - must be implemented by subclasses + */ + abstract validate(json: string): ValidationResult; + + /** + * Parse JSON and handle errors + */ + protected parseJson(json: string): { parsed?: unknown; error?: string } { + try { + return { parsed: JSON.parse(json) }; + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : "Failed to parse JSON syntax", + }; + } + } + + /** + * Format JSON nicely + */ + protected formatJson(json: string): string { + try { + const parsed = JSON.parse(json); + return JSON.stringify(parsed, null, 2); + } catch { + return json; + } + } + + /** + * Helper to create an issue + */ + protected createIssue( + path: string, + message: string, + severity: "error" | "warning" | "info" = "error", + code?: string, + suggestion?: string + ): ValidationIssue { + return { path, message, severity, code, suggestion }; + } + + /** + * Get validator configuration + */ + getConfig(): ValidatorConfig { + return this.config; + } + + /** + * Get validator type + */ + getType(): ValidatorType { + return this.type; + } +} diff --git a/packages/ui/src/core/validator/components/editor-panel.tsx b/packages/ui/src/core/validator/components/editor-panel.tsx new file mode 100644 index 0000000..c7554cd --- /dev/null +++ b/packages/ui/src/core/validator/components/editor-panel.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { FC, RefObject } from "react"; +import { FileJson } from "lucide-react"; +import { cn } from "@utils/functions/cn"; + +interface EditorPanelProps { + value: string; + onChange: (value: string) => void; + isInvalid?: boolean; + textareaRef: RefObject; +} + +export const EditorPanel: FC = ({ + value, + onChange, + isInvalid = false, + textareaRef, +}) => { + return ( +
+ {/* Panel Header */} +
+
+ + Input +
+ Ctrl+Enter to validate +
+ + {/* Editor */} +
+