horey sheet
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
|
||||
# Build output
|
||||
dist
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Git
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# IDE
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs
|
||||
|
||||
# Test
|
||||
coverage
|
||||
.nyc_output
|
||||
|
||||
# Misc
|
||||
*.md
|
||||
!README.md
|
||||
.husky
|
||||
.github
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Docker environment configuration
|
||||
# Copy this to .env when using docker-compose
|
||||
|
||||
# Application
|
||||
PORT=3000
|
||||
|
||||
# Database (MySQL container)
|
||||
MYSQL_ROOT_PASSWORD=rootpassword
|
||||
MYSQL_DATABASE=openplace
|
||||
MYSQL_USER=openplace
|
||||
MYSQL_PASSWORD=openplacepassword
|
||||
MYSQL_PORT=3306
|
||||
|
||||
# Database URL (used by Prisma)
|
||||
DATABASE_URL="mysql://openplace:openplacepassword@mysql:3306/openplace"
|
||||
|
||||
# JWT Secret (CHANGE THIS IN PRODUCTION!)
|
||||
JWT_SECRET="your-secret-key-change-in-production"
|
||||
@@ -0,0 +1,113 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Openplace is an unofficial open-source backend for wplace (a collaborative pixel art canvas), built with TypeScript, tinyhttp, Prisma, and MySQL. The system manages user authentication, pixel painting with charge-based rate limiting, alliances, leaderboards, and moderation features.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Essential Commands
|
||||
- `pnpm dev` - Run development server with hot reload (watches src/ directory)
|
||||
- `pnpm build` - Compile TypeScript to dist/
|
||||
- `pnpm start` - Run production build from dist/
|
||||
- `pnpm test` - Run tests once
|
||||
- `pnpm test:watch` - Run tests in watch mode
|
||||
- `pnpm lint` - Check for linting issues
|
||||
- `pnpm lint:fix` - Auto-fix linting issues
|
||||
|
||||
### Database Commands
|
||||
- `pnpm db:push` - Push schema changes to database and regenerate Prisma client
|
||||
- `pnpm db:generate` - Regenerate Prisma client only
|
||||
- `pnpm db:migrate` - Create and run a new migration
|
||||
- `pnpm seed` - Seed database with initial data
|
||||
|
||||
### Requirements
|
||||
- Node.js >= 24
|
||||
- pnpm >= 10
|
||||
- MySQL/MariaDB
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend Structure
|
||||
|
||||
**Entry Point**: [src/index.ts](src/index.ts) - Sets up tinyhttp app with middleware (CORS, cookie parser, JSON body parsing, logging) and registers all route modules.
|
||||
|
||||
**Authentication Flow**: JWT tokens stored in `j` cookie → validated by [src/middleware/auth.ts](src/middleware/auth.ts) → checks session validity in database → attaches `req.user` with `{ id, sessionId }`
|
||||
|
||||
**Database**: Prisma ORM with MySQL. Global instance exported from [src/config/database.ts](src/config/database.ts) and injected into request via middleware.
|
||||
|
||||
**Route Organization**: Each feature module exports a function that registers routes on the tinyhttp app:
|
||||
- [src/routes/auth.ts](src/routes/auth.ts) - Registration, login, logout
|
||||
- [src/routes/pixel.ts](src/routes/pixel.ts) - Pixel painting, tile generation, pixel info
|
||||
- [src/routes/alliance.ts](src/routes/alliance.ts) - Alliance CRUD, invites, bans, leaderboards
|
||||
- [src/routes/me.ts](src/routes/me.ts) - User profile, favorite locations, settings
|
||||
- [src/routes/admin.ts](src/routes/admin.ts) - User management, bans, timeouts, tickets
|
||||
- [src/routes/moderator.ts](src/routes/moderator.ts) - Moderation actions
|
||||
- [src/routes/leaderboard.ts](src/routes/leaderboard.ts) - Global leaderboards
|
||||
- [src/routes/store.ts](src/routes/store.ts) - Purchase colors and flags
|
||||
|
||||
**Service Layer**: Business logic isolated in service classes:
|
||||
- [src/services/pixel.ts](src/services/pixel.ts) - `PixelService` handles pixel painting with charge validation, color unlocking checks, tile image generation using @napi-rs/canvas, and level calculation
|
||||
- [src/services/alliance.ts](src/services/alliance.ts) - `AllianceService` handles alliance creation, member management, bans
|
||||
- [src/services/user.ts](src/services/user.ts) - `UserService` handles user profile updates, favorites
|
||||
|
||||
**Core Systems**:
|
||||
|
||||
1. **Charge System** ([src/utils/charges.ts](src/utils/charges.ts)): Rate-limiting mechanism where users have `maxCharges` (default 20) that regenerate every `chargesCooldownMs` (default 30s). Painting consumes charges. Function `calculateChargeRecharge()` computes current charge based on time elapsed.
|
||||
|
||||
2. **Bitmap System** ([src/utils/bitmap.ts](src/utils/bitmap.ts)): `WplaceBitMap` class stores boolean flags as packed bytes for efficient storage (used for unlocked colors, flags). Stored in database as Bytes, converted to base64 for API responses.
|
||||
|
||||
3. **Color Palette** ([src/utils/colors.ts](src/utils/colors.ts)): Defines available colors with RGB values and whether they're paid. `checkColorUnlocked()` validates if user has purchased a color by checking bitmap.
|
||||
|
||||
4. **Tile System**: Canvas divided into 1000x1000 tiles. Each pixel has coordinates `(tileX, tileY, x, y)`. Tile images dynamically generated on-demand from pixel data.
|
||||
|
||||
5. **Regions** ([src/config/regions.ts](src/config/regions.ts)): Maps coordinates to geographic regions/countries. Users get 10% charge discount when painting in their equipped flag's region. **Currently returns placeholder data - implementation needed.**
|
||||
|
||||
### Database Schema
|
||||
|
||||
Key models in [prisma/schema.prisma](prisma/schema.prisma):
|
||||
|
||||
- **User**: Core user data, charge state, pixels painted, level, alliance membership, equipped flag, unlocked colors bitmap
|
||||
- **Pixel**: Individual pixel with coordinates (tileX, tileY, x, y), colorId, paintedBy userId, timestamp
|
||||
- **Tile**: Metadata for 1000x1000 tile regions, has many Pixels
|
||||
- **Alliance**: Groups with members, bans, invites, HQ coordinates, total pixels painted
|
||||
- **Session**: JWT session tracking with expiration
|
||||
- **Ticket**: Moderation reports with evidence
|
||||
- **UserNote**: Moderator notes on users
|
||||
|
||||
### Frontend
|
||||
|
||||
Pre-built SvelteKit frontend in [frontend/](frontend/) directory (served as static files, not part of development workflow). Backend serves 404.html for unmatched routes.
|
||||
|
||||
## Key Implementation Patterns
|
||||
|
||||
1. **Route Pattern**: Routes validate input → call service method → return JSON or handle service errors via `handleServiceError()`
|
||||
|
||||
2. **Service Pattern**: Services receive Prisma client in constructor, contain business logic, throw descriptive errors that are caught by error handler middleware
|
||||
|
||||
3. **Bulk Pixel Insert**: Painting uses raw SQL `INSERT ... ON DUPLICATE KEY UPDATE` for performance when updating multiple pixels
|
||||
|
||||
4. **Level Calculation**: `Math.floor(Math.sqrt(pixelsPainted / 100)) + 1`
|
||||
|
||||
5. **Validation**: Separate validator functions in [src/validators/](src/validators/) for common input patterns (seasons, coordinates, pagination)
|
||||
|
||||
6. **Error Responses**: Standardized via `createErrorResponse()` and HTTP_STATUS constants in [src/utils/response.ts](src/utils/response.ts)
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Copy `.env.example` to `.env` and configure:
|
||||
- `DATABASE_URL` - MySQL connection string (format: `mysql://user:password@host/database`)
|
||||
- `JWT_SECRET` - Secret key for JWT signing
|
||||
- `PORT` - Server port (default 3000)
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The project is a work-in-progress with incomplete features (see README.md warnings)
|
||||
- Region lookup system is stubbed and returns placeholder data - needs implementation
|
||||
- Authentication uses JWT cookies named `j`
|
||||
- All API responses use JSON format
|
||||
- The backend is designed to work with the wplace.live frontend protocol
|
||||
- Production deployment requires SSL/HTTPS (enforced by design)
|
||||
- Use `pnpm` as package manager (not npm)
|
||||
@@ -0,0 +1,222 @@
|
||||
# Docker Setup Guide
|
||||
|
||||
This guide explains how to build and run Openplace using Docker.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker Engine 20.10+
|
||||
- Docker Compose 2.0+
|
||||
|
||||
### Basic Usage
|
||||
|
||||
1. **Build and start all services:**
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
2. **View logs:**
|
||||
```bash
|
||||
docker-compose logs -f app
|
||||
```
|
||||
|
||||
3. **Stop services:**
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
4. **Stop and remove volumes (deletes database):**
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
The easiest way to configure the application is to copy `.env.docker` to `.env`:
|
||||
|
||||
```bash
|
||||
cp .env.docker .env
|
||||
```
|
||||
|
||||
Then edit `.env` to customize:
|
||||
|
||||
- `PORT` - Application port (default: 3000)
|
||||
- `MYSQL_*` - Database configuration
|
||||
- `JWT_SECRET` - **IMPORTANT:** Change this in production!
|
||||
|
||||
### Production Deployment
|
||||
|
||||
For production, you should:
|
||||
|
||||
1. **Change the JWT secret:**
|
||||
```env
|
||||
JWT_SECRET="your-secure-random-secret-here"
|
||||
```
|
||||
|
||||
2. **Change database passwords:**
|
||||
```env
|
||||
MYSQL_ROOT_PASSWORD="secure-root-password"
|
||||
MYSQL_PASSWORD="secure-app-password"
|
||||
```
|
||||
|
||||
3. **Use a reverse proxy (nginx/traefik) for SSL/HTTPS**
|
||||
|
||||
4. **Set up regular database backups**
|
||||
|
||||
## Docker Commands
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build the application image
|
||||
docker-compose build
|
||||
|
||||
# Build without cache
|
||||
docker-compose build --no-cache
|
||||
```
|
||||
|
||||
#### Using the precompiled frontend
|
||||
|
||||
If you want to serve the legacy bundle in `frontend-backup/` instead of rebuilding `frontend-src`, set the build argument while building the image:
|
||||
|
||||
```bash
|
||||
USE_FRONTEND_BACKUP=true docker-compose build
|
||||
```
|
||||
|
||||
With `USE_FRONTEND_BACKUP=true` the Dockerfile skips the frontend build step and copies the existing `frontend-backup/` files into `/app/frontend` inside the image.
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
# Start in foreground
|
||||
docker-compose up
|
||||
|
||||
# Start in background
|
||||
docker-compose up -d
|
||||
|
||||
# Start only specific services
|
||||
docker-compose up -d mysql
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
```bash
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# View app logs only
|
||||
docker-compose logs -f app
|
||||
|
||||
# Check service status
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### Database Management
|
||||
|
||||
```bash
|
||||
# Access MySQL shell
|
||||
docker-compose exec mysql mysql -u openplace -p
|
||||
|
||||
# Run migrations
|
||||
docker-compose exec app pnpm db:push
|
||||
|
||||
# Seed database
|
||||
docker-compose exec app pnpm seed
|
||||
|
||||
# Backup database
|
||||
docker-compose exec mysql mysqldump -u openplace -popenplacepassword openplace > backup.sql
|
||||
|
||||
# Restore database
|
||||
docker-compose exec -T mysql mysql -u openplace -popenplacepassword openplace < backup.sql
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
|
||||
```bash
|
||||
# Restart services
|
||||
docker-compose restart
|
||||
|
||||
# Restart specific service
|
||||
docker-compose restart app
|
||||
|
||||
# View resource usage
|
||||
docker stats openplace-app openplace-mysql
|
||||
|
||||
# Clean up unused images
|
||||
docker image prune
|
||||
```
|
||||
|
||||
## Standalone Docker Build
|
||||
|
||||
If you prefer to build without docker-compose:
|
||||
|
||||
```bash
|
||||
# Build image
|
||||
docker build -t openplace:latest .
|
||||
|
||||
# Run container (requires existing MySQL)
|
||||
docker run -d \
|
||||
--name openplace \
|
||||
-p 3000:3000 \
|
||||
-e DATABASE_URL="mysql://user:pass@host:3306/openplace" \
|
||||
-e JWT_SECRET="your-secret" \
|
||||
openplace:latest
|
||||
```
|
||||
|
||||
## Development with Docker
|
||||
|
||||
For development, you may want to mount your source code:
|
||||
|
||||
```bash
|
||||
docker-compose -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
Or use the regular local development setup:
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Database connection fails
|
||||
|
||||
Wait a few seconds for MySQL to initialize on first run. Check logs:
|
||||
```bash
|
||||
docker-compose logs mysql
|
||||
```
|
||||
|
||||
### Port already in use
|
||||
|
||||
Change the port in `.env`:
|
||||
```env
|
||||
PORT=3001
|
||||
```
|
||||
|
||||
### Permission issues
|
||||
|
||||
On Linux, you may need to adjust file permissions:
|
||||
```bash
|
||||
sudo chown -R $(id -u):$(id -g) .
|
||||
```
|
||||
|
||||
### Clear everything and restart
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Docker setup consists of:
|
||||
|
||||
- **openplace-app**: Node.js application container running the backend
|
||||
- **openplace-mysql**: MySQL 8.0 database container
|
||||
- **mysql-data**: Persistent volume for database storage
|
||||
- **openplace-network**: Bridge network for container communication
|
||||
|
||||
The application automatically runs database migrations on startup.
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
ARG USE_FRONTEND_BACKUP=false
|
||||
|
||||
# Build stage
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10 --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files for backend
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install backend dependencies
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# Copy source code and prisma schema
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma client
|
||||
RUN pnpm db:generate
|
||||
|
||||
# Build TypeScript backend
|
||||
RUN pnpm build
|
||||
|
||||
# Build frontend (or reuse compiled backup)
|
||||
WORKDIR /app
|
||||
ARG USE_FRONTEND_BACKUP
|
||||
RUN if [ "$USE_FRONTEND_BACKUP" = "true" ]; then \
|
||||
rm -rf /app/frontend && mkdir -p /app/frontend && cp -R /app/frontend-backup/. /app/frontend/; \
|
||||
else \
|
||||
cd frontend-src && npm install && npm run build; \
|
||||
fi
|
||||
|
||||
# Create login.html from join.html if it doesn't exist
|
||||
RUN if [ -f /app/frontend/join.html ] && [ ! -f /app/frontend/login.html ]; then \
|
||||
cp /app/frontend/join.html /app/frontend/login.html; \
|
||||
fi
|
||||
|
||||
|
||||
# Production stage
|
||||
FROM node:24-alpine
|
||||
|
||||
ARG USE_FRONTEND_BACKUP
|
||||
|
||||
# Install pnpm
|
||||
RUN corepack enable && corepack prepare pnpm@10 --activate
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# Install production dependencies only
|
||||
RUN pnpm install --frozen-lockfile --prod
|
||||
|
||||
# Copy prisma schema for migrations
|
||||
COPY prisma ./prisma
|
||||
|
||||
# Generate Prisma client
|
||||
RUN pnpm db:generate
|
||||
|
||||
# Copy built application from builder stage
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Copy built frontend from builder stage
|
||||
COPY --from=builder /app/frontend ./frontend
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3000/api/v1/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
|
||||
|
||||
# Start the application
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,72 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: openplace-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-openplace}
|
||||
MYSQL_USER: ${MYSQL_USER:-openplace}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-openplacepassword}
|
||||
# No ports exposed - only accessible within Docker network
|
||||
volumes:
|
||||
- mysql-data:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-rootpassword}"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- openplace-network
|
||||
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
container_name: openplace-adminer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${ADMINER_PORT:-8080}:8080"
|
||||
environment:
|
||||
ADMINER_DEFAULT_SERVER: mysql
|
||||
depends_on:
|
||||
- mysql
|
||||
networks:
|
||||
- openplace-network
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
USE_FRONTEND_BACKUP: ${USE_FRONTEND_BACKUP:-false}
|
||||
container_name: openplace-app
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${PORT:-3000}:3000"
|
||||
environment:
|
||||
PORT: 3000
|
||||
DATABASE_URL: "mysql://${MYSQL_USER:-openplace}:${MYSQL_PASSWORD:-openplacepassword}@mysql:3306/${MYSQL_DATABASE:-openplace}"
|
||||
JWT_SECRET: ${JWT_SECRET:-change-this-secret-in-production}
|
||||
NODE_ENV: production
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- openplace-network
|
||||
command: >
|
||||
sh -c "
|
||||
echo 'Waiting for database to be ready...' &&
|
||||
sleep 5 &&
|
||||
echo 'Running database migrations...' &&
|
||||
pnpm db:push &&
|
||||
echo 'Starting application...' &&
|
||||
node dist/index.js
|
||||
"
|
||||
|
||||
volumes:
|
||||
mysql-data:
|
||||
|
||||
networks:
|
||||
openplace-network:
|
||||
driver: bridge
|
||||
@@ -0,0 +1,127 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
|
||||
<link href="/_app/immutable/assets/0.DQCxyt33.css" rel="stylesheet">
|
||||
<link rel="modulepreload" href="/_app/immutable/entry/start.CqSbdZXc.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/B4HM4TqG.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/4WsUhDWi.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BDALf20I.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/4k6DpCgf.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BUhRjcOt.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/entry/app.CuVZ6Ons.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/x1RL6Wqy.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DM9nRpoa.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/B2cHk4HI.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/Bke_korE.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/ChY_8ULT.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BrZ10JY-.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/nodes/0.DIpSCqpd.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DffDvEhl.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/DklPLC_x.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BvbG2Lay.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/CZW2bcQi.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/BNZUboE0.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/cUtKXcx3.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/nodes/5.lvNarnfM.js">
|
||||
<link rel="modulepreload" href="/_app/immutable/chunks/CYItkO2S.js">
|
||||
|
||||
<meta property="og:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content="https://wplace.live/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
|
||||
/>
|
||||
<meta
|
||||
itemprop="description"
|
||||
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Wplace is a collaborative, real-time pixel canvas layered over the world map, where anyone can paint and create art together."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://wplace.live/img/og-image.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="keywords" content="wplace, pixel art, real-time, game, world map, art" />
|
||||
<meta name="apple-mobile-web-app-title" content="Wplace" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' https://challenges.cloudflare.com blob:"
|
||||
/>
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "WebApplication",
|
||||
"name": "Wplace",
|
||||
"url": "https://wplace.live"
|
||||
}
|
||||
</script>
|
||||
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href="/img/favicon-96x96.png"
|
||||
sizes="96x96"
|
||||
/>
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href="/img/apple-touch-icon.png"
|
||||
/>
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents"><!--[--><!--[--><!----><span class="hidden">Version: 1759175263375</span> <!--[!--><!----><div class="flex h-full flex-col items-center justify-center gap-6"><a href="/"><div class="flex items-center gap-1.5 "><img class="pixelated size-20" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAAXNSR0IArs4c6QAAABJQTFRFAQEBAAAAHGHnRcxVStlbMXLnk8SHtQAAAAF0Uk5TAEDm2GYAAABMSURBVHjadc9JCgAhDERRa7r/lZs0ikawdv+tkvEYALS07U2QawmOTo1oQBKr8/cUMLY7JLEPYLW0oISSNLtgiojRBfv0AuB67vH3B+FjAY/0rrGiAAAAAElFTkSuQmCC" alt="Wplace logo"/> <!--[--><span class="text-base-content font-pixel text-5xl">wplace</span><!--]--></div><!----></a> <p class="max-w-3xl text-center font-medium sm:text-xl">Not found</p> <a class="btn btn-primary btn-lg" href="/">Go to map</a></div><!----><!--]--><!----> <section aria-label="Notifications alt+T" tabindex="-1" aria-live="polite" aria-relevant="additions text" aria-atomic="false" class="svelte-tppj9g"><!--[!--><!--]--></section><!----><!----><!--]--> <!--[!--><!--]--><!--]-->
|
||||
|
||||
<script>
|
||||
{
|
||||
__sveltekit_1jtafcq = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/_app/immutable/entry/start.CqSbdZXc.js"),
|
||||
import("/_app/immutable/entry/app.CuVZ6Ons.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element, {
|
||||
node_ids: [0, 5],
|
||||
data: [null,null],
|
||||
form: null,
|
||||
error: null
|
||||
});
|
||||
});
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
window.addEventListener('beforeinstallprompt', (event) => {
|
||||
event.preventDefault();
|
||||
window.pwaInstallPrompt = event;
|
||||
});
|
||||
</script>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user