Initial skills documentation — 25 categories, all SKILL.md + references + scripts
This commit is contained in:
+200
@@ -0,0 +1,200 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user