Files
my_openplace/frontend-backup/TODO.md
T
2025-10-02 02:40:11 -07:00

36 KiB

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:
{
  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:
{
  colors: number[],    // Array of colorIds (0-63)
  coords: number[][]   // Array of [x, y] coordinates within tile
}
  • Response:
{
  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:
{
  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:
{
  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:
{
  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:
{
  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:
{
  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:
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:
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:
{
  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:
{
  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:
{
  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
{
  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:
{
  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:
{
  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):

// 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

{
  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

{
  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

{
  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

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

Ticket (Report)

{
  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

{
  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

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:

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/service-worker.js');
}

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)

// 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:

// 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:

// 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)

// 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)

// 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)

// 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:

    npm create svelte@latest frontend
    cd frontend
    npm install
    
  2. Install dependencies:

    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:

    npm run dev
    
  6. Build:

    npm run build
    

    Output to build/ directory, copy to backend's frontend/ folder


API Response Error Handling

All endpoints follow consistent error format:

{
  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.