> For the complete documentation index, see [llms.txt](https://dirkscripts.gitbook.io/dirkscripts-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://dirkscripts.gitbook.io/dirkscripts-documentation/resources/dirk-cfx-react/script-config.md).

# Script Config (React)

This page covers the **React/NUI side** of the script config system. For the **Lua side** (registration, watchers, DB persistence), see [dirk\_lib → scriptConfig](https://github.com/DirkDigglerz/dirk_docs/blob/main/resources/dirk-lib/modules/scriptConfig/README.md).

***

## Overview

The script config system lets you build a full admin configuration panel for any FiveM resource with:

* Live settings that apply without server restart
* Undo / redo with full edit history
* Version conflict detection (optimistic concurrency)
* Change audit log with admin attribution
* JSON editor for bulk/advanced edits
* Reset to defaults with confirmation

The architecture flows like this:

```
schema.json + defaults.json
        ↓
dirk_lib server (registerScriptConfig → MySQL)
        ↕ NUI callbacks (get / update / history / reset)
dirk-cfx-react (createScriptConfig → useForm → SettingsPanel)
        ↕ React component sections
Admin UI in-game
```

***

## Step-by-Step Setup

### 1. Define Your Schema

Create `schema.json` in your resource root. This is a JSON Schema that defines every setting, its type, and its default value.

```json
{
  "type": "object",
  "properties": {
    "basic": {
      "type": "object",
      "properties": {
        "debug": { "type": "boolean", "default": false },
        "interactionType": { "type": "string", "default": "target" },
        "maxPlayers": { "type": "number", "default": 32 }
      }
    },
    "rewards": {
      "type": "array",
      "x-arrayKey": "name",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "label": { "type": "string" },
          "amount": { "type": "number", "default": 1 }
        }
      }
    }
  }
}
```

Schema extensions:

| Extension       | Purpose                                         |
| --------------- | ----------------------------------------------- |
| `x-serverOnly`  | Never sent to client — server-only secrets      |
| `x-renamedFrom` | Auto-migrate old key path to new key path       |
| `x-arrayKey`    | Merge arrays by identity field instead of index |

{% hint style="info" %}
`defaults.json` is optional — if provided, it supplies the initial default values. Otherwise defaults are derived from the `"default"` fields in `schema.json`.
{% endhint %}

### 2. Register on the Lua Side

See [dirk\_lib → scriptConfig](https://github.com/DirkDigglerz/dirk_docs/blob/main/resources/dirk-lib/modules/scriptConfig/README.md) for full details. The minimal setup:

**`src/server/scriptConfig.lua`**

```lua
CreateThread(function()
  lib.scriptConfig(schema, function(src)
    return IsPlayerAceAllowed(src, 'admin')
  end)
end)
```

**`src/client/scriptConfig.lua`**

```lua
local _ = lib.scriptConfig  -- triggers lazy-load + KVP cache
```

### 3. Create the TypeScript Config Store

In your resource's `web/src/stores/`, create a file that defines your settings types, Zod schema, and calls `createScriptConfig`:

```tsx
import { z } from "zod";
import { createScriptConfig } from "dirk-cfx-react";

// ── Zod Schema ────────────────────────────────────────────────

const BasicSchema = z.object({
  debug: z.boolean(),
  interactionType: z.string(),
  maxPlayers: z.number(),
});

const RewardSchema = z.object({
  name: z.string(),
  label: z.string(),
  amount: z.number(),
});

export const ScriptConfigSchema = z.object({
  basic: BasicSchema,
  rewards: z.array(RewardSchema),
});

export type Settings = z.infer<typeof ScriptConfigSchema>;

// ── Defaults ──────────────────────────────────────────────────

export const defaultScriptConfig: Settings = {
  basic: {
    debug: false,
    interactionType: "target",
    maxPlayers: 32,
  },
  rewards: [],
};

// ── Store ─────────────────────────────────────────────────────

export const {
  store: useScriptConfig,
  updateSettings,
  getHistory,
  useScriptConfigHooks,
} = createScriptConfig<Settings>(defaultScriptConfig);
```

### 4. Build the Admin Panel

Create `web/src/components/Admin/main.tsx` with your nav items and section routing:

```tsx
import { useNuiEvent, SettingsPanel, type NavItem } from "dirk-cfx-react";
import { Settings, Sword } from "lucide-react";
import { useState } from "react";
import { useScriptConfigHooks, defaultScriptConfig, ScriptConfigSchema } from "@/stores/useScriptConfig";
import { BasicSection } from "./BasicSection";
import { RewardsSection } from "./RewardsSection";

const NAV_ITEMS: NavItem[] = [
  { id: "basic", icon: Settings, label: "Basic" },
  { id: "rewards", icon: Sword, label: "Rewards" },
];

export function AdminPanel() {
  const [open, setOpen] = useState(false);
  useScriptConfigHooks();

  useNuiEvent("OPEN_ADMIN_SECTION", () => setOpen(true));

  return (
    <SettingsPanel
      navItems={NAV_ITEMS}
      title="My Resource"
      subtitle="Admin Settings"
      open={open}
      onClose={() => setOpen(false)}
      defaultScriptConfig={defaultScriptConfig}
      schema={ScriptConfigSchema}
      resetConfirmText="my_resource"
    >
      {(activeTab) => (
        <>
          {activeTab === "basic" && <BasicSection />}
          {activeTab === "rewards" && <RewardsSection />}
        </>
      )}
    </SettingsPanel>
  );
}
```

### 5. Build Section Components

Each admin section uses `useFormField` and `useFormActions` from `dirk-cfx-react` to bind to the form state managed by `SettingsPanel`:

```tsx
import { useFormField, useFormActions, AdminPageTitle, InputContainer } from "dirk-cfx-react";
import { Settings } from "lucide-react";
import { Flex, NumberInput, Switch, Select, useMantineTheme } from "@mantine/core";
import type { Settings as SettingsType } from "@/stores/useScriptConfig";

export function BasicSection() {
  const theme = useMantineTheme();
  const color = theme.colors[theme.primaryColor][theme.primaryShade as number];
  const basic = useFormField<SettingsType, "basic">("basic");
  const { setValue } = useFormActions<SettingsType>();

  return (
    <Flex direction="column" gap="sm" p="sm">
      <AdminPageTitle title="Basic" icon={Settings} color={color} />

      <InputContainer title="Debug Mode" description="Enable debug commands and logging">
        <Switch
          checked={basic.debug}
          onChange={(e) => setValue("basic.debug", e.currentTarget.checked)}
        />
      </InputContainer>

      <InputContainer title="Interaction Type" description="How players interact with objects">
        <Select
          value={basic.interactionType}
          onChange={(v) => setValue("basic.interactionType", v)}
          data={["target", "interact", "textui"]}
        />
      </InputContainer>

      <InputContainer title="Max Players" description="Maximum concurrent players">
        <NumberInput
          value={basic.maxPlayers}
          onChange={(v) => setValue("basic.maxPlayers", v)}
          min={1}
          max={256}
        />
      </InputContainer>
    </Flex>
  );
}
```

***

## API Reference

### `createScriptConfig<T>(defaultValue)`

Factory function that creates a Zustand store and NUI integration for your settings type. Call once at module level.

```tsx
const { store, updateSettings, getHistory, useScriptConfigHooks } =
  createScriptConfig<MySettings>(defaultScriptConfig);
```

**Returns:**

| Property               | Type                                            | Description                                                |
| ---------------------- | ----------------------------------------------- | ---------------------------------------------------------- |
| `store`                | `StoreApi<T>`                                   | Zustand store — use as a hook for reactive reads           |
| `updateSettings`       | `(data: Partial<T>) => Promise<NuiResponse<T>>` | Send partial update to server via NUI                      |
| `getHistory`           | `(params?) => Promise<HistoryResponse>`         | Fetch paginated change history                             |
| `useScriptConfigHooks` | `() => void`                                    | Call inside your admin component to register NUI listeners |

### `SettingsPanel`

The main admin UI shell. Renders a full-screen modal with sidebar navigation, undo/redo, save/discard, history, JSON editor, and reset.

```tsx
<SettingsPanel
  navItems={NAV_ITEMS}
  title="My Resource"
  open={isOpen}
  defaultScriptConfig={defaults}
  schema={zodSchema}
>
  {(activeTab) => <>{/* render active section */}</>}
</SettingsPanel>
```

| Prop                  | Type                               | Required | Description                                                   |
| --------------------- | ---------------------------------- | -------- | ------------------------------------------------------------- |
| `navItems`            | `NavItem[]`                        | yes      | Sidebar navigation items `{ id, icon, label }`                |
| `title`               | `string`                           | yes      | Panel title                                                   |
| `subtitle`            | `string`                           | no       | Panel subtitle                                                |
| `open`                | `boolean`                          | yes      | Whether the panel is visible                                  |
| `onClose`             | `() => void`                       | no       | Close handler (defaults to `fetchNui("CLOSE_ADMIN_SECTION")`) |
| `children`            | `(activeTab: string) => ReactNode` | yes      | Render function receiving the active tab ID                   |
| `defaultScriptConfig` | `T`                                | yes      | Default settings object for reset                             |
| `schema`              | `{ safeParse }`                    | no       | Zod schema for JSON editor validation                         |
| `resetConfirmText`    | `string`                           | no       | Text user must type to confirm reset                          |
| `width`               | `string`                           | no       | Panel width (default: `"70vh"`)                               |
| `height`              | `string`                           | no       | Panel height (default: `"85vh"`)                              |

**Built-in sidebar features:**

* **Undo / Redo** — Step through edit history
* **Save** — Calls `updateSettings` with all pending changes, shows unsaved count badge
* **History** — Paginated audit log modal with search/filter
* **Discard** — Revert to last saved state
* **Manual Edit (JSON)** — Full JSON editor with schema validation
* **Reset Defaults** — Wipe to defaults with confirmation modal

### `useForm`

Advanced form state hook with deep nesting, undo/redo, and Zod validation. Used internally by `SettingsPanel` but can be used standalone.

```tsx
const form = useForm<MyFormData>({
  initialValues: { ... },
  validate: zodSchema,
});
```

**Key methods:**

| Method                    | Description                                             |
| ------------------------- | ------------------------------------------------------- |
| `setValue(path, value)`   | Set a nested value (e.g. `"basic.debug"`)               |
| `getValue(path)`          | Get a nested value                                      |
| `reset()`                 | Reset to initial values                                 |
| `reinitialize(newValues)` | Reset with new initial values                           |
| `undo()`                  | Undo last change                                        |
| `redo()`                  | Redo undone change                                      |
| `validate()`              | Run validation, returns `{ success, errors }`           |
| `getInputProps(path)`     | Returns `{ value, onChange, error }` for Mantine inputs |
| `isDirty()`               | Whether any values have changed                         |
| `getChangedFields()`      | Array of paths that have been modified                  |

### Context Hooks

These hooks access the form state within `FormProvider` (set up by `SettingsPanel`):

| Hook                      | Returns                    | Description                       |
| ------------------------- | -------------------------- | --------------------------------- |
| `useFormField(path)`      | `T[path]`                  | Reactive value at a nested path   |
| `useFormFields(...paths)` | `[T[p1], T[p2], ...]`      | Reactive values at multiple paths |
| `useFormActions()`        | `{ setValue, reset, ... }` | All form mutation methods         |
| `useFormError(path)`      | `string \| undefined`      | Validation error for a field      |
| `useFormErrors()`         | `Record<string, string>`   | All current validation errors     |

***

## NUI Callback Reference

The settings system communicates with `dirk_lib` through these NUI callbacks (handled automatically):

| Callback                      | Direction | Purpose                                         |
| ----------------------------- | --------- | ----------------------------------------------- |
| `NUI_READY`                   | UI → Lua  | Signals that the NUI is loaded and ready        |
| `GET_SETTINGS`                | UI → Lua  | Fetches server config (currency, colours, etc.) |
| `GET_LOCALES`                 | UI → Lua  | Fetches locale strings                          |
| `GET_ITEMS`                   | UI → Lua  | Fetches inventory item definitions              |
| `OPEN_ADMIN_SECTION`          | Lua → UI  | Opens the admin panel                           |
| `CLOSE_ADMIN_SECTION`         | UI → Lua  | Closes the admin panel                          |
| `UPDATE_SCRIPT_SETTINGS`      | UI → Lua  | Sends settings update with expected version     |
| `GET_SCRIPT_SETTINGS_HISTORY` | UI → Lua  | Fetches paginated change history                |

{% hint style="info" %}
All NUI callbacks are registered automatically by `dirk_lib`'s scriptConfig module. You don't need to set them up manually.
{% endhint %}

***

## Version Conflict Handling

When two admins edit settings simultaneously:

1. Admin A opens the panel → receives settings + `client_version` hash
2. Admin B saves a change → server's version hash updates
3. Admin A tries to save → sends their `expectedVersion`
4. Server detects mismatch → returns `success: false` with `latestData`
5. UI automatically merges the latest data back into the form
6. Admin A can review and re-save

This prevents silent overwrites and ensures all changes are tracked.
