> For the complete documentation index, see [llms.txt](https://docs.lostdev.store/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.lostdev.store/territories/tablet-ui.md).

# tablet ui

React + Vite frontend, in [`web/src`](broken://pages/4436955ae532418b92feaa0ddf58012364609c61), built to `web/dist` and served by `fxmanifest.lua` (`ui_page 'web/dist/index.html'`).

## Structure

```
web/src/
├── App.jsx                      # root: territory HUD + tablet + dev toolbar
├── App.module.css
├── index.css
├── main.jsx                      # Vite/React entrypoint
├── i18n/
│   ├── LocaleContext.jsx         # provider + useTranslation() hook
│   └── translations.js
├── utils/
│   └── nui.js                    # bridge to the Lua client (fetch + postMessage)
├── components/                   # in-game HUD (outside the tablet)
│   ├── TerritoryCard.jsx         # progress card shown when entering a zone
│   └── AttackAlertBanner.jsx     # "UNDER ATTACK" banner
├── dev/
│   └── DevToolbar.jsx            # toolbar shown only in `import.meta.env.DEV`
└── tablet/
    ├── TacticalTablet.jsx        # main tablet component (tabs)
    ├── mockData.js               # sample data (preview before real data)
    ├── factionUtils.js
    └── components/
        ├── TopThree.jsx          # podium of the top 3 factions
        ├── LeaderboardTable.jsx  # faction table
        ├── TerritoryStatusList.jsx # territory list (compact/full)
        ├── TerritoryDetail.jsx   # detail panel + history for one territory
        ├── MyFactionPanel.jsx    # summary of the player's faction
        ├── StatisticsPanel.jsx   # charts/statistics
        ├── ActivityFeed.jsx      # event feed (capture/defend/lose/admin)
        ├── AdminPanel.jsx        # ADMIN tab
        └── Dropdown.jsx          # reusable select component
```

## Lua ↔ React communication

### React → Lua (`postNUI`, in `utils/nui.js`)

```js
fetch(`https://${GetParentResourceName()}/${action}`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ action, ...data }),
})
```

`GetParentResourceName()` is hardcoded to `'ld_territories'` in this file — **if you rename the resource, you must update this constant** (unlike the usual FiveM pattern that uses the client-injected native function).

Convenience APIs exposed:

* `TabletAPI` — open/close the tablet, sounds, extras, and history.
* `AdminAPI` — every admin panel action.

Both are thin wrappers around `postNUI`.

### Lua → React (`SendNUIMessage`, listened to by `setupNUIListener`)

The Lua client always sends this shape:

```lua
SendNUIMessage({ type = 'territories', action = '<name>', data = <payload> })
```

`setupNUIListener` listens on `window.addEventListener('message', ...)`, filters by `data.type === 'territories'`, and re-emits via `TerritoryEvents.emit(action, data)` — a small internal event bus (`TerritoryEvents.on/off/emit`) used by React components to react to updates without prop drilling.

Main events (constants in `TerritoryEvents`, see [Events & Exports](broken://pages/45047c47c6f21c126c33711bdc4c80b000b95d77) for the Lua counterpart):

| JS name                                  | Lua origin                          | Consumed by                                    |
| ---------------------------------------- | ----------------------------------- | ---------------------------------------------- |
| `TERRITORY_UPDATE`                       | `territories:update`                | `App.jsx` → `TerritoryCard`                    |
| `ENTERED_TERRITORY` / `EXITED_TERRITORY` | same                                | `App.jsx` (show/hide HUD)                      |
| `OPEN_LEADERBOARD` / `CLOSE_LEADERBOARD` | `tablet:open`/`close` on the client | `App.jsx` (opens/closes `TacticalTablet`)      |
| `TABLET_DATA`                            | `territories:tabletData`            | `TacticalTablet` (real territories + factions) |
| `TABLET_EXTRAS`                          | `territories:tabletExtras`          | `TacticalTablet` (stats + activity)            |
| `TERRITORY_HISTORY`                      | `territories:territoryHistory`      | `TerritoryDetail`                              |
| `ADMIN_DATA`                             | `territories:admin:receiveData`     | `AdminPanel`                                   |
| `ADMIN_ZONE_CREATED`                     | `territories:admin:zoneCreated`     | `AdminPanel` (creation feedback)               |
| `UNDER_ATTACK`                           | `territories:underAttack`           | `AttackAlertBanner`                            |
| `SET_LOCALE`                             | `territories:setLocale`             | `LocaleContext`                                |

## "Live" mode vs mock

`TacticalTablet.jsx` receives sample `factions`/`territories` (`MOCK_FACTIONS`, `MOCK_TERRITORIES`) as a preview fallback, used **only** before any real data arrives from the server.

As soon as `TABLET_DATA` is received for the first time (`liveData !== null`), the component enters `isLive = true` mode and stops using any mock data — even sections that are still empty, such as activity or stats, show the real empty state instead of fictitious data.

## Tablet tabs

| Tab         | Component                                                                | Content                                                                                                                  |
| ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
| Overview    | `TopThree` + `LeaderboardTable` + `TerritoryStatusList` + `ActivityFeed` | General summary: podium, leaderboard, 5 territories, 5 recent events.                                                    |
| Territories | `TerritoryStatusList` + `TerritoryDetail`                                | Full list + detail/history panel on selection.                                                                           |
| Factions    | `LeaderboardTable` (`detailed` mode)                                     | Full leaderboard.                                                                                                        |
| My Faction  | `MyFactionPanel` + `ActivityFeed` (filtered)                             | Stats and activity for the player's own faction. Shows an empty state if the player has no gang/job.                     |
| Statistics  | `StatisticsPanel`                                                        | Charts derived from `stats.dailyActivity`.                                                                               |
| History     | `ActivityFeed` (full)                                                    | Complete event log.                                                                                                      |
| Admin       | `AdminPanel`                                                             | Only visible if `adminAuthorized === true` (see [Admin Panel](broken://pages/00311225da68fbeb0db4688dff6f5a62a0475741)). |

## Territory HUD (`TerritoryCard.jsx`)

Floating card shown while the player is inside a zone — never captures input (`SetNuiFocus` is never called for it).

Accent color is based on state:

* `contested`
* `cooldown`
* `capturing`
* `neutral`

The owning-faction color is resolved from a fixed map (`ballas`, `vagos`, `families`) with a deterministic hash fallback. This follows the same principle used in `client/blips.lua` on the Lua side, independently re-implemented here in JS; they do not share the same hash implementation.

## Internationalization

`i18n/LocaleContext.jsx` provides `useTranslation()` (`t(key)`).

The active locale comes from Lua via the `SET_LOCALE` event fired on `nui:ready` (see [client-modules.md](broken://pages/7531c26ca5a86edda075378fc784e9e5e5a22b11#mainlua--main-loop-and-nui)), reflecting `Config.Locale`.

## Rebuild

{% tabs %}
{% tab title="npm" %}

```bash
cd web
npm install
npm run build   # generates web/dist/, served by fxmanifest.lua
```

{% endtab %}
{% endtabs %}

`npm run dev` (Vite) can be used outside of FiveM to iterate on the UI with mock data — `postNUI` fails silently outside the CEF client (no real `GetParentResourceName`), which is why pure UI development relies on `MOCK_FACTIONS`/`MOCK_TERRITORIES`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.lostdev.store/territories/tablet-ui.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
