# Frontend Architecture

ShooterHub's frontend is **vanilla JS + Bootstrap 5.3**, served as-is by Nginx. There is no build step, no bundler, and no framework.

---

## File Structure

```
frontend/
├── index.html            # Landing page / community feed
├── dashboard.html        # User dashboard (stats summary)
├── sessions.html         # Shooting sessions list + detail panel
├── chrono.html           # Chronograph analysis
├── group-size.html       # Target photo annotation tool
├── photos.html           # Group photo gallery
├── reloads.html          # Reloading recipes + batches
├── messages.html         # Private messaging
├── friends.html          # Friendships / follow requests
├── css/
│   └── app.css           # Global styles (minimal, Bootstrap-first)
└── js/
    ├── api.js            # HTTP client (JWT, refresh, helpers)
    ├── utils.js          # Shared UI utilities
    ├── i18n.js           # Client-side translations
    ├── nav.js            # Navbar builder + auth gate + badge polling
    ├── sessions.js       # Sessions page
    ├── chrono.js         # Chrono page
    ├── group-size.js     # Annotation canvas tool
    ├── photos.js         # Photo gallery + lightbox
    ├── reloads.js        # Reloads page
    ├── messages.js       # Messaging page
    └── friends.js        # Friends page
```

---

## Script Load Order

Every HTML page loads scripts in this fixed order:

```html
<script src="/js/api.js"></script>      <!-- 1. HTTP client -->
<script src="/js/utils.js"></script>    <!-- 2. UI helpers -->
<script src="/js/i18n.js"></script>     <!-- 3. Translations -->
<script src="/js/nav.js"></script>      <!-- 4. Navbar / auth gate -->
<script src="/js/PAGE.js"></script>     <!-- 5. Page-specific code -->
```

`nav.js` **blocks unauthenticated access** to protected routes. It checks for a JWT in `localStorage` and redirects to the login modal if absent.

---

## `api.js` — HTTP Client

Central API client. All page scripts call these functions; none call `fetch()` directly.

### Core functions

| Function | Description |
|---|---|
| `apiGet(url)` | GET, returns parsed JSON |
| `apiPost(url, body)` | POST JSON |
| `apiPatch(url, body)` | PATCH JSON |
| `apiDelete(url)` | DELETE |
| `apiFetch(url, opts)` | Raw fetch with JWT header + auto-refresh |

### JWT handling

- Access token stored in `localStorage` as `access`.
- Refresh token stored in `localStorage` as `refresh`.
- On 401, `apiFetch` automatically calls `/api/auth/token/refresh/`, stores the new access token, and retries the original request once.
- On refresh failure (401 again), clears storage and redirects to login.

### `asList(data)`

Unwraps DRF paginated responses:

```js
asList(data)  // → data.results if paginated, data if already an array
```

Used throughout page scripts after every list endpoint call.

---

## `utils.js` — Shared UI Utilities

Shared helpers used by all page scripts. Must be loaded after `api.js`.

### `showToast(msg, type = 'success')`

Injects a Bootstrap toast into `#toastContainer` and auto-dismisses it. Type maps to Bootstrap contextual colours (`success`, `danger`, `warning`, `info`).

Every HTML page includes `<div id="toastContainer"></div>` at the bottom.

### `esc(s)`

HTML-escapes a string. Used when injecting user-controlled text into `innerHTML`.

### Unit system

Units are stored in `localStorage`:

| Function | Key | Values |
|---|---|---|
| `getDistUnit()` / `setDistUnit(u)` | `distUnit` | `'mm'` (default), `'moa'`, `'mrad'` |
| `getVelUnit()` / `setVelUnit(u)` | `velUnit` | `'ms'` (default), `'fps'` |

### `fDist(mm, moa, distM)`

Formats a distance value according to the current unit:
- `mm` → shows `X mm`
- `moa` → shows `X MOA` (requires `moa` param to be non-null)
- `mrad` → converts mm to mrad using `distM` (requires `distM` in metres)

Falls back to mm when MOA/MRAD data is unavailable.

### `applyDistUnitButtons(container)`

Marks the active `[data-dist-unit]` button within a container by adding/removing the Bootstrap `active` class. Call this whenever the unit changes or a modal opens.

```js
applyDistUnitButtons(document.getElementById('myModal'));
```

### `renderPublicToggle(isPublic, { btnId, iconId, labelId, privateClass })`

Updates a "Make public / Make private" toggle button's icon and label. The `privateClass` option sets the button variant when private (default `'btn-outline-secondary'`; pass `'btn-outline-light'` inside dark modals).

---

## `i18n.js` — Translations

Provides `t(key)` for client-side string lookup. Supported languages: `en`, `fr`, `de`, `es`. The active language is read from `localStorage` (set from the user's profile preference after login).

---

## `nav.js` — Navbar + Auth Gate

### Navbar

Builds the `<div id="navbar">` element present on every page. Injects navigation links, the user avatar dropdown, and a login/register modal.

After building the navbar, `nav.js` checks login state and:
- If logged in: fetches unread message count → populates `#navMsgBadge`
- If logged in: fetches pending friend requests → populates `#navFriendsBadge`

### Protected routes

```js
const PROTECTED = ['/sessions.html', '/chrono.html', '/photos.html',
                   '/reloads.html', '/dashboard.html', '/group-size.html',
                   '/messages.html', '/friends.html'];
```

If the current path is in `PROTECTED` and no JWT is present, the login modal opens automatically.

---

## Page Scripts

### `sessions.js`

- Loads session list (PRS / Free Practice / Speed Shooting tabs).
- Clicking a session opens a detail panel inline (right column).
- `openGroupPhotoModal(gpId)` opens a dark `modal-xl` with photo + stats + unit switch.
- Unit switch in the detail panel calls `applyUnits('dist'|'vel', value)` which updates localStorage, re-renders the panel, and syncs unit buttons in the open photo modal if any.

### `chrono.js`

- Loads chronograph analyses, renders velocity stats and shot tables.
- Public/private toggle via `renderPublicToggle`.
- Navigates to `group-size.html?analysis=ID` to link a photo to an analysis.

### `group-size.js` — Annotation Canvas

The most complex page script. Manages an HTML5 `<canvas>` for bullet-hole annotation:

1. **Reference line** — drawn first; establishes px→mm scale from the user's `refLength` input.
2. **POA** — single point, sets the aim reference.
3. **POIs** — multiple bullet-hole points; scaled circles drawn at bullet diameter.

Calculations (group ES, mean radius, centroid, offset from POA, correction) are done client-side in `updateResults()`. Results are displayed using `fDist()` from `utils.js`.

The "Save POIs & compute server-side" button (`saveBtn`) posts annotations to `/api/photos/group-photos/{id}/` and triggers server-side recomputation.

Launched from `photos.html` or `sessions.html` via `?gp=ID` query parameter; the `?from=chrono&analysis=ID` parameter activates the back-button linking flow.

### `photos.js`

- Grid of `GroupPhoto` cards with lazy pagination (`loadMoreBtn`).
- Filter by measured / unmeasured (client-side, no refetch).
- **Lightbox**: dark `modal-xl`, shows full photo + stats bar + unit switch buttons. Stats rendered by `renderLbStats(gp)` using `fDist`.
- Upload modal: posts to `/api/photos/upload/` then creates a `GroupPhoto` record.
- Compute button: calls `/api/photos/group-photos/{id}/compute-group-size/`.

### `messages.js`

- Two-pane: left list (Inbox/Sent tabs), right detail.
- Marking a message read: PATCH `read_at` on open.
- Compose modal: recipient search via `/api/social/members/?q=` with debounced autocomplete. `_recipientId` stores the selected user id.
- Reply: `prefillCompose(recipientDetail, 'Re: ' + subject)` pre-fills the compose modal.

### `friends.js`

- Three tabs: Friends / Requests / Sent requests.
- Add Friend modal: live search with debounced input, send request on click.
- `sendRequest(userId, btn)` handles HTTP 409 (already connected) by showing a badge.
- Accept/Decline: PATCH to `/api/social/friends/{id}/accept/` or `.../decline/`.

### `reloads.js`

- Lists reloading recipes and batches.
- Public/private toggle on batches.

---

## Unit Switch Pattern

The unit switch is consistent across all pages that show ballistic data:

1. HTML: a `<div class="btn-group">` with `<button data-dist-unit="mm|moa|mrad">` buttons.
2. On page load / modal open: `applyDistUnitButtons(container)` marks the active button.
3. On click: `setDistUnit(value)` → `applyDistUnitButtons(container)` → re-render data.
4. `fDist(mm, moa, distM)` formats the value for the current unit.

This pattern lives in `utils.js` so any page can adopt it with minimal code.

---

## Data Flow Summary

```
User action
    │
    ▼
Page script (e.g. sessions.js)
    │  calls
    ▼
api.js (apiGet / apiPost / …)
    │  adds JWT header, handles 401 refresh
    ▼
Django REST API (/api/…)
    │  returns JSON
    ▼
Page script
    │  calls asList(), fDist(), esc(), showToast() from utils.js
    ▼
DOM update (innerHTML / classList / …)
```

---

## Common Patterns

### Pagination

```js
let _nextUrl = null;
let _allItems = [];

async function load(reset = false) {
  if (reset) { _allItems = []; _nextUrl = null; }
  const data = await apiGet(_nextUrl || '/api/endpoint/?page_size=24');
  const items = asList(data);
  _nextUrl = data.next ? new URL(data.next).pathname + new URL(data.next).search : null;
  _allItems = [..._allItems, ...items];
  render(items);
  document.getElementById('loadMoreBtn').style.display = _nextUrl ? '' : 'none';
}
```

### Inline card refresh after mutation

```js
const idx = _allItems.findIndex(x => x.id === id);
_allItems[idx] = { ..._allItems[idx], ...updatedFields };
const old = document.querySelector(`[data-item-id="${id}"]`);
if (old) { old.remove(); renderCards([_allItems[idx]]); }
```

### Delegated event handling

Click handlers for dynamically generated cards are set up as delegated listeners on the static parent, not on each card element:

```js
grid.addEventListener('click', e => {
  const btn = e.target.closest('[data-action="delete"]');
  if (!btn) return;
  deleteItem(btn.dataset.id);
});
```
