Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
257 changes: 257 additions & 0 deletions app/game-references/_components/page-shell.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative min-h-screen bg-fd-background">
{/* Ambient background */}
<div className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-transparent" />
<div className="absolute -top-24 left-1/4 h-80 w-80 rounded-full bg-primary/8 blur-[110px] opacity-40" />
<div className="absolute top-1/2 -right-20 h-64 w-64 rounded-full bg-primary/5 blur-[90px] opacity-30" />
</div>

{/* Header */}
<div className="border-b border-fd-border bg-fd-card/70 backdrop-blur-sm">
<div className="mx-auto max-w-7xl px-4 pt-4 pb-5 sm:px-6 lg:px-8">
{/* Breadcrumb */}
<Link
href="/game-references"
className="mb-4 inline-flex items-center gap-1 text-xs font-medium text-fd-muted-foreground transition-colors hover:text-fd-foreground"
>
<ChevronLeft className="h-3.5 w-3.5" />
Game References
</Link>

<div className="flex items-start gap-4">
{/* Icon */}
<div
className={cn(
"flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-xl border bg-gradient-to-br",
iconBg,
)}
>
<Icon className={cn("h-6 w-6", iconColor)} />
</div>

{/* Title + description */}
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-2.5">
<h1 className="text-2xl font-bold tracking-tight text-fd-foreground">
{title}
</h1>
{badge && (
<span className="inline-flex items-center rounded-full border border-fd-border bg-fd-muted px-2.5 py-0.5 text-xs font-medium text-fd-muted-foreground tabular-nums">
{badge}
</span>
)}
</div>
<p className="mt-1 text-sm leading-relaxed text-fd-muted-foreground">
{description}
</p>
</div>
</div>

{/* Tabs / filters / search */}
{controls && <div className="mt-4 space-y-3">{controls}</div>}
</div>
</div>

{/* Main content */}
<div className="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">{children}</div>
</div>
);
}

// ---------------------------------------------------------------------------
// 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 (
<div className="flex gap-2">
<div className="relative max-w-sm flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-fd-muted-foreground" />
<Input
className="pl-9 pr-9"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && onSearch()}
/>
{value && (
<button
className="absolute right-3 top-1/2 -translate-y-1/2 text-fd-muted-foreground transition-colors hover:text-fd-foreground"
onClick={onClear}
>
<X className="h-4 w-4" />
</button>
)}
</div>
<Button onClick={onSearch}>Search</Button>
</div>
);
}

// ---------------------------------------------------------------------------
// 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 (
<div className="mt-6 flex items-center justify-center gap-3">
<Button variant="outline" size="sm" disabled={offset === 0} onClick={onPrev}>
Previous
</Button>
<span className="text-sm tabular-nums text-fd-muted-foreground">
{offset + 1}–{Math.min(offset + limit, total)} of {total}
</span>
<Button variant="outline" size="sm" disabled={!hasMore} onClick={onNext}>
Next
</Button>
</div>
);
}

// ---------------------------------------------------------------------------
// 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 (
<div className="flex flex-wrap gap-1.5">
<Button
variant={value === "" ? "default" : "outline"}
size="sm"
onClick={() => onChange("")}
>
{allLabel}
</Button>
{[...options].sort().map((opt) => (
<Button
key={opt}
variant={value === opt ? "default" : "outline"}
size="sm"
onClick={() => onChange(opt)}
>
{opt}
</Button>
))}
</div>
);
}

// ---------------------------------------------------------------------------
// 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 (
<div className="flex gap-1 rounded-lg border border-fd-border bg-fd-muted/50 p-1 w-fit">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => onChange(tab.id)}
className={cn(
"rounded-md px-3 py-1.5 text-sm font-medium transition-all",
active === tab.id
? "bg-fd-card text-fd-foreground shadow-sm"
: "text-fd-muted-foreground hover:text-fd-foreground",
)}
>
{tab.label}
{tab.count !== undefined && (
<span className="ml-1.5 rounded-full bg-fd-muted px-1.5 py-0.5 text-xs tabular-nums">
{tab.count}
</span>
)}
</button>
))}
</div>
);
}
Loading
Loading