# Frontend Recreation TODO

This document outlines all the necessary information to recreate the SvelteKit frontend for the Openplace/Wplace application.

## Overview

Openplace is a collaborative real-time pixel art canvas layered over a world map (similar to r/place). Users can paint pixels, join alliances, view leaderboards, and moderate content. The frontend is built with SvelteKit and integrates with the backend API documented below.

## Tech Stack

- **Framework**: SvelteKit (based on `index.html` module imports)
- **Language**: TypeScript (inferred from backend consistency)
- **Fonts**:
  - PixelifySans (pixel art style font)
  - Geist (modern sans-serif)
  - NotoColorEmoji (for flag emojis)
- **Map Library**: Likely Leaflet or Mapbox (for world map integration)
- **PWA**: Progressive Web App with service worker
- **Build Tool**: Vite (standard for SvelteKit)

## Project Structure

Based on compiled output, the frontend likely has these routes:

```
/                          - Main canvas view
/admin                     - Admin panel
/admin/*                   - Various admin sub-pages
/moderation                - Moderation panel
/maps                      - Map-related pages
404.html                   - 404 error page
```

## API Endpoints Reference

### Authentication (`/auth`)

#### POST `/login`
- **Body**: `{ username: string, password: string }`
- **Response**: `{ success: boolean }`
- **Cookie**: Sets `j` cookie (JWT token, HttpOnly, 30 day expiry)
- **Note**: Auto-registers users if username doesn't exist

#### POST `/auth/logout`
- **Headers**: Requires JWT cookie
- **Response**: `{ success: boolean }`
- **Cookie**: Clears `j` cookie

---

### Pixel Operations (`/pixel`)

#### GET `/:season/tile/random`
- **Purpose**: Get random tile coordinates to start viewing
- **Response**: `{ tileX: number, tileY: number }`

#### GET `/:season/pixel/:tileX/:tileY?x=X&y=Y`
- **Purpose**: Get information about a specific pixel
- **Query Params**:
  - `x` (0-999): pixel X within tile
  - `y` (0-999): pixel Y within tile
- **Response**:
```typescript
{
  colorId: number,
  paintedBy: number,
  paintedAt: string,
  user: {
    id: number,
    name: string,
    level: number,
    equippedFlag: number
  }
}
```

#### GET `/files/:season/tiles/:tileX/:tileY.png`
- **Purpose**: Get rendered tile image (1000x1000 pixels)
- **Response**: PNG image
- **Cache**: 5 minutes (`Cache-Control: public, max-age=300`)

#### POST `/:season/pixel/:tileX/:tileY`
- **Purpose**: Paint one or more pixels
- **Headers**: Requires JWT cookie
- **Body**:
```typescript
{
  colors: number[],    // Array of colorIds (0-63)
  coords: number[][]   // Array of [x, y] coordinates within tile
}
```
- **Response**:
```typescript
{
  success: boolean,
  currentCharges: number,
  maxCharges: number,
  chargesLastUpdatedAt: string,
  pixelsPainted: number,
  level: number
}
```
- **Errors**:
  - 400: Invalid coordinates or colors
  - 403: Not enough charges, color not unlocked, or timed out

---

### User Profile (`/me`)

#### GET `/me`
- **Headers**: Requires JWT cookie
- **Response**:
```typescript
{
  id: number,
  name: string,
  discord: string | null,
  country: string,
  droplets: number,
  currentCharges: number,
  maxCharges: number,
  chargesCooldownMs: number,
  chargesLastUpdatedAt: string,
  pixelsPainted: number,
  level: number,
  equippedFlag: number,
  extraColorsBitmap: string,  // base64 encoded
  flagsBitmap: string | null, // base64 encoded
  showLastPixel: boolean,
  picture: string | null,
  allianceId: number | null,
  allianceRole: string,
  alliance: {
    id: number,
    name: string,
    description: string,
    pixelsPainted: number
  } | null
}
```

#### POST `/me/update`
- **Headers**: Requires JWT cookie
- **Body**:
```typescript
{
  name?: string,          // 3-20 chars
  showLastPixel?: boolean,
  discord?: string        // Optional Discord username
}
```
- **Response**: Updated user profile (same as GET /me)

#### GET `/me/profile-pictures`
- **Headers**: Requires JWT cookie
- **Response**:
```typescript
{
  pictures: Array<{
    id: number,
    url: string
  }>
}
```

---

### Alliance System (`/alliance`)

#### GET `/alliance`
- **Headers**: Requires JWT cookie
- **Response**: User's current alliance details or error if not in alliance

#### POST `/alliance`
- **Purpose**: Create new alliance
- **Headers**: Requires JWT cookie
- **Body**: `{ name: string }`  // 3-30 chars, unique
- **Response**: Alliance details

#### POST `/alliance/update-description`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Body**: `{ description: string }`  // Max 500 chars
- **Response**: Updated alliance

#### GET `/alliance/invites`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Response**:
```typescript
{
  invites: Array<{
    id: string,  // UUID
    createdAt: string
  }>
}
```

#### GET `/alliance/join/:invite`
- **Headers**: Requires JWT cookie
- **Response**: Joins alliance via invite code

#### POST `/alliance/update-headquarters`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Body**: `{ latitude: number, longitude: number }`
- **Response**: Updated alliance

#### GET `/alliance/members/:page`
- **Headers**: Requires JWT cookie
- **Params**: `page` (0-indexed)
- **Response**:
```typescript
{
  members: Array<{
    id: number,
    name: string,
    pixelsPainted: number,
    level: number,
    role: string,
    equippedFlag: number
  }>,
  total: number,
  page: number,
  pageSize: number
}
```

#### GET `/alliance/members/banned/:page`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Response**: Paginated list of banned users

#### POST `/alliance/give-admin`
- **Headers**: Requires JWT cookie (must be alliance owner)
- **Body**: `{ promotedUserId: number }`
- **Response**: 200 OK

#### POST `/alliance/ban`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Body**: `{ bannedUserId: number }`
- **Response**: Updated alliance

#### POST `/alliance/unban`
- **Headers**: Requires JWT cookie (must be alliance admin)
- **Body**: `{ unbannedUserId: number }`
- **Response**: Updated alliance

#### GET `/alliance/leaderboard/:mode`
- **Headers**: Requires JWT cookie
- **Params**: `mode` - "today" | "week" | "month" | "all-time"
- **Response**: Top 50 alliances with pixel counts

---

### Leaderboards (`/leaderboard`)

#### GET `/leaderboard/player/:mode`
- **Params**: `mode` - "today" | "week" | "month" | "all-time"
- **Response**:
```typescript
Array<{
  id: number,
  name: string,
  allianceId: number,
  allianceName: string,
  equippedFlag: number,
  pixelsPainted: number,
  picture?: string,
  discord: string
}>
```

#### GET `/leaderboard/alliance/:mode`
- **Params**: `mode` - "today" | "week" | "month" | "all-time"
- **Response**:
```typescript
Array<{
  id: number,
  name: string,
  pixelsPainted: number
}>
```

#### GET `/leaderboard/country/:mode`
- **Params**: `mode` - "today" | "week" | "month" | "all-time"
- **Response**: Array of countries with pixel counts
- **Note**: Currently returns mock data, needs implementation

#### GET `/leaderboard/region/:mode/:country`
- **Params**:
  - `mode` - "today" | "week" | "month" | "all-time"
  - `country` - Country ID number
- **Response**: Array of regions with pixel counts
- **Note**: Currently returns mock data, needs implementation

#### GET `/leaderboard/region/players/:city/:mode`
- **Params**:
  - `city` - City ID
  - `mode` - "today" | "week" | "month" | "all-time"
- **Response**: Top 50 players in region
- **Note**: City parameter currently unused

#### GET `/leaderboard/region/alliances/:city/:mode`
- **Params**:
  - `city` - City ID
  - `mode` - "today" | "week" | "month" | "all-time"
- **Response**: Top 50 alliances in region
- **Note**: City parameter currently unused

---

### Store (`/store`)

#### POST `/purchase`
- **Headers**: Requires JWT cookie
- **Body**:
```typescript
{
  product: {
    id: 70 | 80 | 100 | 110,
    amount?: number,      // Quantity (default 1)
    variant?: number      // For colors (32-63) or flags (1-251)
  }
}
```
- **Product IDs**:
  - 70: +5 Max Charges (500 droplets)
  - 80: +30 Paint Charges (500 droplets)
  - 100: Unlock Paid Color (2000 droplets) - requires `variant` (32-63)
  - 110: Unlock Flag (20,000 droplets) - requires `variant` (1-251)
- **Response**: `{ success: boolean }`
- **Errors**: 403 if insufficient droplets

#### POST `/flag/equip/:id`
- **Headers**: Requires JWT cookie
- **Params**: `id` - Flag ID (1-251)
- **Response**: `{ success: boolean }`
- **Errors**: 403 if flag not unlocked

---

### Admin Panel (`/admin/*`)

**All admin endpoints require:**
- JWT cookie authentication
- User role = "admin"
- Returns 403 Forbidden otherwise

#### GET `/admin/users?id=USER_ID`
- **Query**: `id` - User ID
- **Response**:
```typescript
{
  id: number,
  name: string,
  droplets: number,
  picture: string | null,
  role: string,
  timeout_until: string,
  ban_reason: null,          // TODO: Not implemented
  reported_times: 0,         // TODO: Not implemented
  timeouts_count: 0,         // TODO: Not implemented
  same_ip_accounts: 0,       // TODO: Not implemented
  alliance_id: number | null,
  alliance_name: string | null,
  pixels_painted: number,
  phone_validated: false,    // TODO: Not implemented
  discord: string | null
}
```

#### GET `/admin/users/notes?userId=USER_ID`
- **Query**: `userId` - User ID
- **Response**:
```typescript
{
  notes: Array<{
    id: number,
    author: {
      role: string,
      id: number,
      name: string
    },
    note: string,
    createdAt: string
  }>
}
```

#### POST `/admin/users/notes`
- **Body**: `{ userId: number, note: string }`
- **Response**: `{}`

#### GET `/admin/users/tickets?id=USER_ID`
- **Query**: `id` - User ID
- **Response**: `{}`  // TODO: Not implemented

#### GET `/admin/users/purchases?userId=USER_ID`
- **Query**: `userId` - User ID
- **Response**: `{}`  // TODO: Not implemented

#### POST `/admin/users/set-user-droplets`
- **Body**: `{ userId: number, droplets: number }`  // Adds droplets (can be negative)
- **Response**: `{ success: boolean }`

#### GET `/admin/tickets`
- **Response**: Open tickets grouped by reported user
```typescript
{
  tickets: Array<{
    id: number,           // Reported user ID
    reportedUser: {
      id: number,
      name: string,
      discord: string,
      country: string,
      banned: boolean
    },
    createdAt: string,
    reports: Array<{
      id: string,        // Ticket ID (UUID)
      latitude: number,
      longitude: number,
      zoom: number,
      reason: string,
      notes: string,
      image: string,
      createdAt: string
    }>
  }>,
  status: 200
}
```

#### GET `/admin/closed-tickets`
- **Response**: Same as `/admin/tickets` but for resolved tickets

#### GET `/admin/open-tickets-count`
- **Response**: `{ tickets: number }`

#### POST `/admin/severe-open-tickets-count`
- **Response**: `{ tickets: number }`

#### POST `/admin/assign-new-tickets`
- **Response**: `{ newTicketsIds: [] }`  // TODO: Not implemented

#### GET `/admin/count-all-tickets`
- **Response**:
```typescript
{
  doxxing: number,
  inappropriate_content: number,
  hate_speech: number,
  bot: number,
  other: number,
  griefing: number,
  total_open_tickets: number
}
```

#### GET `/admin/count-all-reports`
- **Response**: Same as `/admin/count-all-tickets`  // TODO: Uses same data

#### GET `/admin/alliances/:id`
- **Params**: `id` - Alliance ID
- **Response**:
```typescript
{
  id: number,
  name: string,
  pixelsPainted: number
}
```

#### GET `/admin/alliances/:id/full`
- **Params**: `id` - Alliance ID
- **Response**: Full alliance details including members, bans, etc.

#### GET `/admin/alliances/search?q=QUERY`
- **Query**: `q` - Search by name or ID
- **Response**: `{ results: Alliance[] }`  // Top 20 results

---

### Moderation Panel (`/moderator/*`)

**All moderator endpoints require:**
- JWT cookie authentication
- User role = "moderator" or "admin"
- Returns 403 Forbidden otherwise

#### GET `/moderator/tickets`
- **Response**: Same format as `/admin/tickets`

#### GET `/moderator/users/tickets?userId=USER_ID`
- **Query**: `userId` - User ID
- **Response**: All tickets for a specific user

#### GET `/moderator/open-tickets-count`
- **Response**: `{ tickets: number }`

#### POST `/moderator/severe-open-tickets-count`
- **Response**: `{ tickets: number }`

#### POST `/moderator/assign-new-tickets`
- **Response**: `{ newTicketsIds: [] }`  // TODO: Not implemented

#### GET `/moderator/count-my-tickets`
- **Response**: `0`  // TODO: Not implemented

---

## Core Frontend Features to Implement

### 1. Authentication System

**Components:**
- Login form (see `LoginForm` CSS asset)
- Registration flow (combined with login)
- Session management using JWT cookie
- Auto-redirect to login if unauthorized

**Key Implementation Details:**
- Cookie name: `j`
- Cookie is HttpOnly (not accessible via JavaScript)
- 30-day expiration
- Auto-create account on first login with username/password

---

### 2. Main Canvas View

**Components:**
- Interactive world map (Leaflet/Mapbox)
- Tile-based pixel rendering system
- Zoom controls
- Color picker palette (32 free colors + 32 paid colors)
- Brush/paint tool
- Pixel info tooltip on hover/click
- Charge indicator (shows current/max charges)
- Level display

**Technical Requirements:**
- Tiles are 1000x1000 pixels
- Fetch tiles as PNG images: `/files/:season/tiles/:tileX/:tileY.png`
- Cache tiles appropriately (5 min cache header)
- Calculate global coordinates: `globalX = tileX * 1000 + x`, `globalY = tileY * 1000 + y`
- Map global coordinates to lat/lng for world map overlay
- Handle painting multiple pixels in one request
- Show charge regeneration countdown (default: 1 charge per 30 seconds)
- Disable paid colors unless unlocked (check `extraColorsBitmap`)

**Charge System:**
- Default: 20 max charges
- Regenerates 1 charge every 30 seconds (configurable per user)
- Painting consumes charges
- Must calculate current charges: `currentCharges + floor((now - lastUpdate) / cooldownMs)`
- 10% discount when painting in equipped flag's region (TODO: region system not implemented)

**Color Palette (0-63):**
```typescript
// Colors 0-31: Free
// Colors 32-63: Paid (require purchase)
// Color 0: Transparent
// Check if color unlocked: extraColorsBitmap & (1 << (colorId - 32))
```

Full color palette available in backend: `src/utils/colors.ts`

---

### 3. User Profile Page

**Components:**
- Profile avatar with level indicator (see `ProfileAvatarWithLevel` CSS asset)
- Username (editable)
- Discord username (editable)
- Show last pixel toggle
- Droplets balance
- Charges indicator
- Pixels painted count
- Level display
- Equipped flag display
- Alliance affiliation

**Features:**
- Edit profile settings
- View unlocked colors
- View unlocked flags
- View alliance info
- View favorite locations (TODO: not implemented in backend)

---

### 4. Alliance System

**Components:**
- Alliance creation dialog
- Alliance info panel
- Member list (paginated, 50 per page)
- Admin controls (for alliance admins)
- Invite system
- Ban management
- Headquarters map marker

**Features:**
- Create alliance (requires no current alliance)
- Join alliance via invite link
- Leave alliance
- Update description (admins only)
- Set headquarters location on map (admins only)
- Promote members to admin (owner only)
- Ban/unban members (admins only)
- View alliance leaderboard

---

### 5. Leaderboards

**Views:**
- Player leaderboard (top 50)
- Alliance leaderboard (top 50)
- Country leaderboard
- Region leaderboard
- Regional player leaderboard
- Regional alliance leaderboard

**Time Filters:**
- Today
- Week (last 7 days)
- Month (current month)
- All-time

**Display Fields:**
- Rank (1-50)
- Player name / Alliance name
- Equipped flag icon
- Pixels painted
- Alliance affiliation (for players)

---

### 6. Store System

**Products:**
1. **+5 Max Charges** (500 droplets)
   - Increases maxCharges by 5
   - Can purchase multiple

2. **+30 Paint Charges** (500 droplets)
   - Adds 30 to currentCharges (up to max)
   - Can purchase multiple

3. **Unlock Paid Color** (2000 droplets each)
   - Unlocks one of colors 32-63
   - Must select color variant
   - Updates `extraColorsBitmap`

4. **Unlock Flag** (20,000 droplets each)
   - Unlocks one of 251 country flags
   - Must select flag variant (1-251)
   - Updates `flagsBitmap`

**Implementation:**
- Display droplet balance
- Show which colors/flags are already unlocked
- Disable purchase if insufficient droplets
- Confirmation dialog before purchase
- Update UI after successful purchase

**Flag Equipping:**
- Separate endpoint to equip owned flag
- Can only equip flags that are unlocked
- Equipped flag shown on profile and leaderboards

---

### 7. Admin Panel

**Pages:**
- User management
- Ticket management (reports)
- Alliance management
- Statistics dashboard

**User Management:**
- Search users by ID
- View user details
- View user notes
- Add moderator notes
- Set droplets (add/subtract)
- View user tickets
- View purchase history (TODO)

**Ticket Management:**
- View open tickets
- View closed tickets
- Tickets grouped by reported user
- Show ticket details (location, reason, image evidence)
- Assign tickets to moderators (TODO)
- Count tickets by reason

**Alliance Management:**
- Search alliances
- View alliance details
- View full alliance info (members, bans)

---

### 8. Moderation Panel

**Features:**
- View assigned tickets
- View all open tickets
- View user ticket history
- Count severe tickets
- Count my assigned tickets (TODO)

**Ticket Types:**
- Doxxing
- Inappropriate Content
- Hate Speech
- Bot
- Griefing
- Other

**Ticket Details:**
- Reporter info
- Reported user info
- Canvas location (lat/lng, zoom)
- Reason
- Notes
- Evidence image
- Timestamp

---

## Data Models

### User
```typescript
{
  id: number
  name: string
  discord: string | null
  country: string
  email: string | null
  banned: boolean
  timeoutUntil: Date
  role: "user" | "moderator" | "admin"
  pixelsPainted: number
  droplets: number
  maxCharges: number
  currentCharges: number
  chargesCooldownMs: number
  chargesLastUpdatedAt: Date
  extraColorsBitmap: number        // Bitmask for unlocked paid colors
  flagsBitmap: Bytes | null        // Bitmap for unlocked flags
  equippedFlag: number             // Currently equipped flag (0 = none)
  showLastPixel: boolean
  picture: string | null
  level: number                    // floor(sqrt(pixelsPainted / 100)) + 1
  allianceId: number | null
  allianceRole: "member" | "admin" | "owner"
}
```

### Alliance
```typescript
{
  id: number
  name: string                     // Unique, 3-30 chars
  description: string | null       // Max 500 chars
  hqLatitude: number | null
  hqLongitude: number | null
  pixelsPainted: number
  members: User[]
  bannedUsers: BannedUser[]
  invites: AllianceInvite[]
}
```

### Pixel
```typescript
{
  id: number
  tileX: number
  tileY: number
  x: number                        // 0-999
  y: number                        // 0-999
  colorId: number                  // 0-63
  paintedBy: number                // User ID
  paintedAt: Date
}
```

### Tile
```typescript
{
  id: number
  x: number                        // Tile X coordinate
  y: number                        // Tile Y coordinate
  imageData: Bytes | null          // Cached PNG (if applicable)
  pixels: Pixel[]
}
```

### Ticket (Report)
```typescript
{
  id: string                       // UUID
  userId: number                   // Reporter
  reportedUserId: number           // Reported user
  latitude: number                 // Canvas location
  longitude: number
  zoom: number
  reason: "doxxing" | "inappropriate_content" | "hate_speech" | "bot" | "griefing" | "other"
  notes: string
  image: string                    // Evidence image URL/path
  resolved: boolean
  severe: boolean
  createdAt: Date
}
```

### Region
```typescript
{
  id: number
  cityId: number
  name: string
  number: number
  countryId: number
  flagId: number
}
```

---

## Constants and Configuration

### Season
- Default: `"s1"` (Season 1)
- Used in pixel API endpoints: `/:season/pixel/...`

### Color Palette
- 64 total colors (0-63)
- 0-31: Free colors
- 32-63: Paid colors (2000 droplets each)
- Color 0: Transparent/eraser

### Flags
- 251 total country flags (1-251)
- 20,000 droplets each
- Stored as bitmap in `flagsBitmap`

### Charge System
- Default max charges: 20
- Default cooldown: 30,000ms (30 seconds)
- Formula: `floor((now - lastUpdate) / cooldownMs)` charges regenerated

### Level Calculation
```typescript
level = floor(sqrt(pixelsPainted / 100)) + 1
```

### Pagination
- Default page size: 50
- Pages are 0-indexed

### Validation Rules
- Username: 3-20 characters
- Alliance name: 3-30 characters, unique
- Alliance description: Max 500 characters
- Coordinates: x, y must be 0-999 within tile
- Color ID: 0-63

---

## State Management

**Client-side state to manage:**

1. **User State**
   - Current user profile
   - Authentication status
   - Charge count (auto-update based on time)
   - Droplets balance
   - Unlocked colors/flags

2. **Canvas State**
   - Current map position (lat/lng)
   - Zoom level
   - Visible tiles
   - Selected color
   - Brush mode
   - Cached tile images

3. **Alliance State**
   - Current alliance
   - Member list
   - Invites (if admin)
   - Leaderboard

4. **UI State**
   - Active modal/dialog
   - Sidebar open/closed
   - Selected leaderboard mode
   - Selected leaderboard time filter

**Real-time considerations:**
- Pixel updates from other users (consider WebSocket/polling)
- Charge regeneration countdown
- Leaderboard updates

---

## UI/UX Guidelines

### Theme
- Light theme only (from meta tag: `color-scheme: light only`)
- Theme color: `#f8f4f0` (from webmanifest)
- Background: `#ffffff`

### Fonts
- **PixelifySans**: Use for headings, canvas UI elements, retro aesthetic
- **Geist**: Use for body text, modern UI
- **NotoColorEmoji**: Use for flag rendering

### Responsive Design
- Mobile-first approach
- PWA optimized
- Touch-friendly controls for canvas
- Separate mobile/desktop layouts for complex pages (admin panel)

### Key Interactions
- Hover over pixel: Show tooltip with painter info
- Click pixel: Show detailed pixel info modal
- Click map: Pan to location
- Click color: Select for painting
- Click canvas: Paint pixel(s) with selected color
- Right-click/long-press: Color picker (pick color from canvas)

---

## Assets Required

### Images
- Favicon (multiple sizes)
- App icons (192x192, 512x512)
- PWA screenshots
- Flag sprite sheet (flags.webp, flags@2x.webp @ 2x resolution)
- OG image for social sharing

### Audio
- `notification.mp3` - For notification sounds

### Existing Assets (in `/frontend` folder)
- `/img/*` - Various images
- `/maps/*` - Map-related assets
- `/download.png`, `/download.svg` - Download icons
- `PixelifySans-latin.vdc2vUDH.woff2` - Font file
- `css2.css` - Likely Google Fonts CSS

---

## Service Worker & PWA

**Features to implement:**
- Offline canvas viewing (cache tiles)
- Background sync for painted pixels
- Push notifications for alliance updates
- Install prompt handling (see `window.pwaInstallPrompt` in index.html)
- Cache strategy for static assets
- Network-first for API calls
- Cache-first for tile images

**Service Worker Registration:**
```javascript
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}
```

---

## WebSocket / Real-time Updates (Recommended)

While not currently implemented in the backend, the frontend should be designed to support real-time updates:

**Potential WebSocket events:**
- `pixel:painted` - Another user painted a pixel
- `tile:updated` - Tile has new pixels
- `alliance:member_joined` - New alliance member
- `charge:regenerated` - Charge regenerated (client-side timer is fine too)
- `leaderboard:updated` - Leaderboard changed

**Implementation approach:**
1. Start with polling (GET tile images every 5 seconds for visible tiles)
2. Design component architecture to easily swap in WebSocket later
3. Use event emitter pattern for pixel updates

---

## Routing Structure (SvelteKit)

```
src/routes/
├── +page.svelte                    # Main canvas view
├── +layout.svelte                  # Root layout (auth check, header, etc.)
├── admin/
│   ├── +page.svelte                # Admin dashboard
│   ├── users/
│   │   └── +page.svelte            # User management
│   ├── tickets/
│   │   ├── +page.svelte            # Open tickets
│   │   └── closed/+page.svelte     # Closed tickets
│   └── alliances/
│       └── +page.svelte            # Alliance management
├── moderation/
│   ├── +page.svelte                # Moderator dashboard
│   └── tickets/+page.svelte        # Assigned tickets
├── leaderboard/
│   └── +page.svelte                # Leaderboard with tabs
├── profile/
│   └── +page.svelte                # User profile
├── alliance/
│   ├── +page.svelte                # Alliance view/create
│   └── [inviteId]/+page.svelte     # Join alliance via invite
└── store/
    └── +page.svelte                # Store page
```

---

## Component Architecture (Suggested)

### Shared Components
```
src/lib/components/
├── auth/
│   ├── LoginForm.svelte
│   └── AuthGuard.svelte
├── canvas/
│   ├── MapCanvas.svelte
│   ├── TileLayer.svelte
│   ├── ColorPicker.svelte
│   ├── BrushTool.svelte
│   ├── PixelInfo.svelte
│   └── ChargeIndicator.svelte
├── user/
│   ├── ProfileAvatar.svelte
│   ├── ProfileAvatarWithLevel.svelte  # Existing CSS asset
│   ├── UserCard.svelte
│   └── UserStats.svelte
├── alliance/
│   ├── AllianceCard.svelte
│   ├── AllianceMembers.svelte
│   ├── AllianceInvite.svelte
│   └── CreateAlliance.svelte
├── leaderboard/
│   ├── LeaderboardTable.svelte
│   ├── LeaderboardFilters.svelte
│   └── LeaderboardEntry.svelte
├── store/
│   ├── StoreItem.svelte
│   ├── ColorUnlockGrid.svelte
│   └── FlagSelector.svelte
├── admin/
│   ├── UserSearch.svelte
│   ├── UserDetails.svelte
│   ├── TicketList.svelte
│   ├── TicketDetails.svelte
│   └── AllianceSearch.svelte
└── common/
    ├── Button.svelte
    ├── Modal.svelte
    ├── Pagination.svelte
    ├── Toast.svelte
    └── Tooltip.svelte
```

---

## Store (Svelte Stores)

```typescript
// src/lib/stores/auth.ts
export const currentUser = writable<User | null>(null);
export const isAuthenticated = derived(currentUser, $user => !!$user);

// src/lib/stores/canvas.ts
export const selectedColor = writable<number>(1);
export const currentCharges = writable<number>(20);
export const canvasPosition = writable<{lat: number, lng: number, zoom: number}>();
export const visibleTiles = writable<Set<string>>();  // "x,y" tile keys

// src/lib/stores/alliance.ts
export const currentAlliance = writable<Alliance | null>(null);

// src/lib/stores/ui.ts
export const activeModal = writable<string | null>(null);
export const sidebarOpen = writable<boolean>(false);
```

---

## API Client

Create a typed API client for all backend endpoints:

```typescript
// src/lib/api/client.ts
export class ApiClient {
  private baseUrl = '';  // Same origin

  // Auth
  async login(username: string, password: string) { ... }
  async logout() { ... }

  // Pixels
  async getRandomTile() { ... }
  async getPixelInfo(tileX, tileY, x, y) { ... }
  async paintPixels(tileX, tileY, colors, coords) { ... }
  getTileImageUrl(tileX, tileY): string { ... }

  // User
  async getProfile() { ... }
  async updateProfile(data) { ... }

  // Alliance
  async getAlliance() { ... }
  async createAlliance(name) { ... }
  // ... etc

  // Leaderboards
  async getPlayerLeaderboard(mode) { ... }
  // ... etc

  // Store
  async purchase(productId, amount, variant?) { ... }
  async equipFlag(flagId) { ... }

  // Admin (requires admin role)
  async getUser(userId) { ... }
  // ... etc
}

export const api = new ApiClient();
```

---

## Bitmap Utilities (Client-side)

Implement bitmap helper for colors and flags:

```typescript
// src/lib/utils/bitmap.ts
export class WplaceBitmap {
  private bytes: Uint8Array;

  constructor(base64?: string) {
    if (base64) {
      this.bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
    } else {
      this.bytes = new Uint8Array(0);
    }
  }

  get(index: number): boolean {
    const byteIndex = Math.floor(index / 8);
    const bitIndex = index % 8;
    if (byteIndex >= this.bytes.length) return false;
    const realIndex = this.bytes.length - 1 - byteIndex;
    return (this.bytes[realIndex] & (1 << bitIndex)) !== 0;
  }

  toBase64(): string {
    return btoa(String.fromCharCode(...this.bytes));
  }
}

export function isColorUnlocked(colorId: number, extraColorsBitmap: number): boolean {
  if (colorId < 32) return true;
  const mask = 1 << (colorId - 32);
  return (extraColorsBitmap & mask) !== 0;
}
```

---

## Charge Calculation (Client-side)

```typescript
// src/lib/utils/charges.ts
export function calculateCurrentCharges(
  currentCharges: number,
  maxCharges: number,
  lastUpdate: Date,
  cooldownMs: number
): number {
  if (currentCharges >= maxCharges) return currentCharges;

  const timeSinceLastUpdate = Date.now() - lastUpdate.getTime();
  const chargesGenerated = Math.floor(timeSinceLastUpdate / cooldownMs);

  return Math.min(maxCharges, currentCharges + chargesGenerated);
}

export function getNextChargeTime(
  currentCharges: number,
  maxCharges: number,
  lastUpdate: Date,
  cooldownMs: number
): Date | null {
  if (currentCharges >= maxCharges) return null;

  const timeSinceLastUpdate = Date.now() - lastUpdate.getTime();
  const timeUntilNextCharge = cooldownMs - (timeSinceLastUpdate % cooldownMs);

  return new Date(Date.now() + timeUntilNextCharge);
}
```

---

## Level Calculation (Client-side)

```typescript
// src/lib/utils/level.ts
export function calculateLevel(pixelsPainted: number): number {
  return Math.floor(Math.sqrt(pixelsPainted / 100)) + 1;
}

export function getPixelsForNextLevel(currentLevel: number): number {
  return ((currentLevel + 1 - 1) ** 2) * 100;
}

export function getLevelProgress(pixelsPainted: number): number {
  const currentLevel = calculateLevel(pixelsPainted);
  const pixelsForCurrentLevel = ((currentLevel - 1) ** 2) * 100;
  const pixelsForNextLevel = (currentLevel ** 2) * 100;
  const pixelsInCurrentLevel = pixelsPainted - pixelsForCurrentLevel;
  const pixelsNeededForLevel = pixelsForNextLevel - pixelsForCurrentLevel;

  return pixelsInCurrentLevel / pixelsNeededForLevel;
}
```

---

## Color Palette (Client-side)

```typescript
// src/lib/constants/colors.ts
export interface Color {
  rgb: [number, number, number];
  paid: boolean;
}

export const COLOR_PALETTE: Record<number, Color> = {
  0: { rgb: [0, 0, 0], paid: false },      // Transparent
  1: { rgb: [0, 0, 0], paid: false },
  2: { rgb: [60, 60, 60], paid: false },
  // ... (copy from backend src/utils/colors.ts)
  63: { rgb: [205, 197, 158], paid: true }
};

export function getColorHex(colorId: number): string {
  const color = COLOR_PALETTE[colorId];
  if (!color) return '#000000';
  const [r, g, b] = color.rgb;
  return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
}
```

---

## Testing Checklist

### Authentication
- [ ] Login with existing account
- [ ] Register new account (auto-create on login)
- [ ] Logout
- [ ] Session persistence across page reloads
- [ ] Redirect to login on 401

### Canvas
- [ ] Load random tile on first visit
- [ ] Pan and zoom map
- [ ] Render tile images correctly
- [ ] Paint single pixel
- [ ] Paint multiple pixels
- [ ] Color picker selection
- [ ] Charge deduction after painting
- [ ] Charge regeneration countdown
- [ ] Hover tooltip with pixel info
- [ ] Cannot paint without charges
- [ ] Cannot paint with locked color

### Profile
- [ ] View own profile
- [ ] Edit username
- [ ] Edit discord
- [ ] Toggle show last pixel
- [ ] View unlocked colors
- [ ] View unlocked flags
- [ ] Display correct level

### Alliance
- [ ] Create alliance
- [ ] Join alliance via invite
- [ ] Leave alliance
- [ ] Update description (admin)
- [ ] Set HQ location (admin)
- [ ] View members list (paginated)
- [ ] Promote member (owner only)
- [ ] Ban member (admin)
- [ ] Unban member (admin)
- [ ] View alliance leaderboard

### Leaderboards
- [ ] Player leaderboard (all time modes)
- [ ] Alliance leaderboard (all time modes)
- [ ] Correct sorting by pixels painted
- [ ] Display alliance affiliation for players
- [ ] Display equipped flags

### Store
- [ ] Purchase max charges
- [ ] Purchase paint charges
- [ ] Purchase color unlock
- [ ] Purchase flag unlock
- [ ] Equip purchased flag
- [ ] Cannot purchase without droplets
- [ ] Cannot equip non-owned flag

### Admin Panel
- [ ] Search user by ID
- [ ] View user details
- [ ] Add user note
- [ ] Set user droplets
- [ ] View open tickets
- [ ] View closed tickets
- [ ] Count tickets by reason
- [ ] Search alliances
- [ ] View alliance details

### Moderation Panel
- [ ] View assigned tickets
- [ ] View all open tickets
- [ ] Count severe tickets
- [ ] View user ticket history

---

## Known Limitations / TODOs

**Backend TODOs (frontend should account for):**
1. Region system returns placeholder data
2. Country/region leaderboards not fully implemented
3. Ticket assignment system not implemented
4. Purchase history not tracked
5. User ban/timeout system incomplete
6. Phone verification not implemented
7. Same IP account detection not implemented
8. Report counts not implemented

**Frontend recommendations:**
- Add WebSocket support for real-time pixel updates
- Implement efficient tile caching strategy
- Add undo/redo for painting
- Add eyedropper tool (pick color from canvas)
- Add minimap for navigation
- Add search functionality for map locations
- Add notification system for alliance events
- Add dark mode toggle (update meta tag)

---

## Development Setup

1. **Initialize SvelteKit project:**
   ```bash
   npm create svelte@latest frontend
   cd frontend
   npm install
   ```

2. **Install dependencies:**
   ```bash
   npm install -D @sveltejs/adapter-static
   npm install leaflet  # or mapbox-gl
   npm install @types/leaflet -D
   ```

3. **Configure for static build:**
   Update `svelte.config.js` to use `adapter-static`

4. **Environment variables:**
   Create `.env`:
   ```
   PUBLIC_API_URL=http://localhost:3000
   PUBLIC_SEASON=s1
   ```

5. **Development:**
   ```bash
   npm run dev
   ```

6. **Build:**
   ```bash
   npm run build
   ```
   Output to `build/` directory, copy to backend's `frontend/` folder

---

## API Response Error Handling

All endpoints follow consistent error format:

```typescript
{
  error: string,      // Error message
  status: number      // HTTP status code
}
```

**Common status codes:**
- 400: Bad Request (validation error)
- 401: Unauthorized (not logged in)
- 403: Forbidden (insufficient permissions, banned, timed out, or not enough resources)
- 404: Not Found
- 500: Internal Server Error

**Frontend should handle:**
- Display error messages from `error` field
- Redirect to login on 401
- Show appropriate UI feedback for 403 (e.g., "You don't have permission")
- Retry on 500 with exponential backoff

---

## Final Notes

This TODO document provides a comprehensive reference for recreating the frontend. The backend API is fully functional and documented here. The frontend should be built as a SvelteKit static site that communicates with this backend via the documented API endpoints.

Key priorities:
1. Authentication and session management
2. Main canvas view with painting functionality
3. User profile and settings
4. Alliance system
5. Leaderboards
6. Store
7. Admin/moderation panels

The compiled frontend in the current `frontend/` folder can serve as a reference for styling and UX patterns, but the source code needs to be recreated from scratch based on this documentation.
