Files

201 lines
9.6 KiB
Markdown

# Shark Game Frontend Patterns — API-Connected HTML
Created Jul 8, 2026. Three frontend pages (`index.html`, `draft-room.html`, `league.html`) converted from static mockups to real API consumers against the FastAPI backend at port 8083. All served through the backend's catch-all route at `shark.iamgmb.com`.
## Architecture
```
Browser → shak.iamgmb.com → Caddy reverse_proxy → FastAPI (port 8083)
├── /api/* endpoints
└── serve /var/www/shark-game/*.html
```
Frontend makes relative fetch calls (`/api/auth/login`, `/api/leagues/1/draft`, etc.) — no hardcoded API base URL. Same-origin because Caddy proxies the whole domain through the backend.
## API Helper Pattern (all pages)
```javascript
const API_BASE = ''; // same-origin
function getToken() { return localStorage.getItem('shark_token'); }
function getUser() { try { return JSON.parse(localStorage.getItem('shark_user') || '{}'); } catch { return {}; } }
async function apiCall(path, options = {}) {
const token = getToken();
const headers = { 'Content-Type': 'application/json', ...options.headers };
if (token) headers['Authorization'] = 'Bearer ' + token;
const res = await fetch(API_BASE + path, { ...options, headers });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || data.message || 'Request failed');
return data;
}
```
**Key decisions:**
- `localStorage` for token storage (not sessionStorage — survives tab close)
- Token stored as `shark_token`, user object as `shark_user` (JSON serialized)
- Error extraction favors `data.detail` (FastAPI's default) then `data.message`
- `API_BASE = ''` for same-origin — works with both Caddy split-routing and full-proxy patterns
- No `fetch` wrapper retries — caller handles errors and shows them to user
## Auth Flow (index.html)
### Registration → Post-Auth Redirect
```
User fills form → handleSignUp()
→ POST /api/auth/register { email, password, display_name, phone? }
→ Store token + user in localStorage
→ GET /api/leagues (list user's leagues)
→ 0 leagues: show league creation + join UI
→ 1 league: window.location.href = 'draft-room.html?league_id=...'
→ 2+ leagues: show league picker cards
```
Login follows the same post-auth flow. The `onAuthSuccess()` function is shared between registration and login.
**Mode toggle:** Sign Up / Log In share a submit button. Toggling swaps visibility of `#registerFields` vs `#loginFields` and changes the button text / toggle link text.
### League Management on Landing Page
- **Create league:** `POST /api/leagues { name }` → redirects to draft-room.html
- **Join league:** `POST /api/leagues/join { invite_code }` → redirects to draft-room.html
- **Invite codes** are 8-char uppercase hex; input auto-uppercases, requires 8+ chars
- **League list cards** show name, member count, status, invite code; clicking enters that league
### Session persistence
`DOMContentLoaded` checks `getToken()`. If present, skips auth form and goes straight to `onAuthSuccess()`. This means returning users don't re-login unless they explicitly log out (which clears both `shark_token` and `shark_user`).
## Draft Room (draft-room.html)
### URL-driven league selection
```
?league_id=123 → load that league
no ?league_id → show league picker overlay (GET /api/leagues, user clicks one)
```
The overlay shows a list of league cards with member counts and status. If a user navigates to draft-room.html without a league, they pick one here.
### Data loading pattern
```javascript
async function loadAll() {
const [league, board, scores, regions] = await Promise.all([
apiCall('/api/leagues/' + leagueId),
apiCall('/api/leagues/' + leagueId + '/draft'),
apiCall('/api/leagues/' + leagueId + '/scores'),
apiCall('/api/regions'),
]);
// Store state, render all tabs
}
```
**Four parallel API calls at load time.** All tabs share this state — switching tabs doesn't re-fetch unless leaderboard hasn't loaded yet (lazy load on first tab switch).
### Tab Structure
| Tab | Contents | Data Source |
|-----|----------|-------------|
| **Draft** | Grid of 12 region cards, draft order bar, status | `draftBoard.available_regions`, `draftBoard.picks`, `draftBoard.current_pick` |
| **My Team** | 2-column grid of your drafted regions | `draftBoard.picks` filtered by `user_id` |
| **Leaderboard** | Sorted score entries with medals | `leaderboardData` (loaded lazily on first tab switch) |
| **League** | Name, status, invite code, members, draft start | `leagueData` |
### Draft Board Rendering
- Combines `available_regions` (undrafted) with regions from `draftBoard.picks` (already drafted)
- Sorts: undrafted first (alphabetical by name), then drafted
- Each card shows: emoji, name, sighting/bite/fatality stats from `regionsMap`
- **Your pick:** amber glow border + shadow, gold "Draft" button, only clickable when it's your turn
- **Drafted by others:** grayed out, shows "Taken by {name}", disabled button
- **Your drafted regions:** amber glow, "Drafted ✓", own label, disabled
### Draft Action
```javascript
async function makePick(regionId) {
const result = await apiCall('/api/leagues/' + leagueId + '/draft/pick', {
method: 'POST',
body: JSON.stringify({ region_id: regionId }),
});
await loadAll(); // Full reload after pick
}
```
**After every pick the entire page state reloads** (`loadAll()`). This is simple and correct for a turn-based game with 2-8 players. The API enforces turn order and duplicate protection server-side.
### Status Bar Messages
| Condition | Display |
|-----------|---------|
| Draft complete | "🏆 Draft Complete!" + green complete badge |
| Status = 'draft' (not started) | "⏳ Waiting for commissioner to start draft" + pending badge |
| Your turn | "🎯 YOUR PICK!" in amber |
| Someone else's turn | "⏳ Waiting for {name}" in teal |
### Draft Order Bar
Horizontal scroll of all members showing who has picked (dimmed), who's currently picking (amber border), and who is "you" (teal border). Order picks per iteration capacity (snake logic handled server-side).
### Turn Enforcement
The button is only clickable when `currentDraftingUserId === myUserId` AND `draftBoard.status === 'active'` AND `!draftBoard.draft_complete`. Server also enforces this — the POST returns 403 if it's not your turn.
### Bottom Bar (Your Team)
Horizontal scroll of picks chips showing emoji + region name + round. Remaining picks indicator ("+ 3 picks remaining"). Total score in top-right score box updated from leaderboard data.
### Commissioner Actions (League Info Tab)
- **Start draft button** shown only when: user is commissioner AND status is 'draft' AND ≥2 members
- **Need more members message** shown when commissioner but <2 members
- **"Waiting for commissioner" message** shown when non-commissioner and status is 'draft'
- **"Draft is live"** shown when status is 'active'
### Invite Code Copy
```javascript
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(code);
// Show "Copied!" for 2 seconds
} else {
// Fallback: create temporary textarea, select, execCommand('copy')
}
```
## League Settings Page (league.html)
Standalone page at `/web/league.html?league_id=...`. If no `league_id` in URL, auto-loads the user's first league (redirect fallback). Contains:
- League details card (name, status badge, season start, member count)
- Invite code with copy-to-clipboard
- Full member list with draft order
- Commissioner actions (same as draft room's League tab)
- Navigation links back to draft room and home
## CSS Theme
All pages share a consistent ocean theme:
- `--deep-ocean: #0a1628` (body background)
- `--amber: #f59e0b` (accent color, buttons)
- `--teal-accent: #0ea5e9` (secondary accent, borders)
- `--sonar-green: #22c55e` (score display)
- `--alert-red: #dc2626` (danger/warning elements)
- Wave overlay with `radial-gradient` and `waveShift` animation
- Sonar ping rings with `sonarPing` animation
- Card backgrounds with `backdrop-filter: blur(8px)` and top gradient border
- Responsive grid: 3 columns default, 2 columns below 360px
## Pitfalls
- **Token expiry is 30 days with no refresh endpoint** — the user gets 401 on any API call after expiry. Handle by catching 401 and redirecting to login. Currently no global 401 handler — individual `apiCall` callers show the error in the inline error box.
- **Data staleness after draft pick** — after `makePick`, `loadAll()` re-fetches everything. This is a full page refresh pattern. For a live draft with polling, add a `setInterval` that calls `GET /api/leagues/{id}/draft` every 5 seconds.
- **`history.replaceState`** used to set URL params silently — used in league picker overlay to set `?league_id=` without a page reload.
- **Same-origin fetch**: `API_BASE = ''` works only because frontend and API are served from the same domain (Caddy reverse proxy or full-proxy pattern). If frontend were on a different origin, CORS would need `Access-Control-Allow-Origin` on the backend.
- **Invite code expiry:** Backend rejects join attempts when league status is not 'draft'. The frontend shows the code even after draft starts — the user gets an API error when trying to join. Consider hiding the code after draft starts.
- **Button text swap pattern** — the auth submit button changes text from "Join the Frenzy" to "Registering..." and back. Use `innerHTML` restore pattern (save original in a `<span>` wrapper) rather than re-assigning `textContent`.
- **No CSRF protection** — token-in-header pattern is sufficient for this scale but CSP headers would be a good add.