Commit 588975c5 authored by Mahmoud Aglan's avatar Mahmoud Aglan

fix: floating nav bar, inset header, px-6 margins everywhere

Bottom nav is now floating with rounded corners and shadow.
Header is now a floating pill with rounded corners.
All pages use px-6 (24px) margins for proper inset from edges.
Cards, buttons, and sections all have visible spacing from screen edges.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 5512a11d
================================================================================
STOCKFISH CHESS BOT API - COMPLETE REFERENCE
================================================================================
BASE URL: https://stockfishapi.caprover.al-arcade.com
================================================================================
ENDPOINTS
================================================================================
--------------------------------------------------------------------------------
1. GET MOVE FROM BOT
--------------------------------------------------------------------------------
POST /api/chess/move
Description:
Get a chess move from a specific bot personality. The bot will play according
to its configured skill level, depth, contempt, and blunder probability.
Response includes simulated human-like think time.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the current board position",
"bot_id": "string (required) - ID of the bot to play against",
"time_limit_ms": 0 (optional, int) - time limit in ms. If 0 or omitted, uses depth-based search
}
Response (200 OK):
{
"best_move": "e2e4", // UCI move notation (from-square + to-square, e.g. e2e4, g1f3, e7e8q for promotion)
"evaluation": 0.35, // centipawn evaluation / 100. Positive = white advantage. 999.0 = white mates, -999.0 = black mates
"depth": 10, // search depth reached
"nodes": 125000, // nodes searched
"think_time_ms": 1500, // total time including artificial delay (simulates human thinking)
"pv": "e2e4 e7e5 g1f3" // principal variation (best line), space-separated UCI moves
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
400: {"error": "bot_id is required"}
404: {"error": "bot not found"}
500: {"error": "engine error: ..."}
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", "bot_id": "nour"}'
--------------------------------------------------------------------------------
2. ANALYZE POSITION
--------------------------------------------------------------------------------
POST /api/chess/analyze
Description:
Deep multi-line analysis of a position at full Stockfish strength (skill 20).
Returns multiple candidate moves ranked by evaluation.
Headers:
Content-Type: application/json
Request Body:
{
"fen": "string (required) - FEN notation of the position to analyze",
"depth": 18, (optional, int 1-30, default 18) - search depth
"lines": 3 (optional, int 1-5, default 3) - number of candidate moves to return
}
Response (200 OK):
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"depth": 18,
"lines": [
{
"rank": 1,
"move": "e7e5", // best move in UCI notation
"evaluation": -0.25, // eval from engine perspective (positive = side to move is better)
"depth": 18,
"pv": "e7e5 g1f3 b8c6 ..." // full principal variation
},
{
"rank": 2,
"move": "c7c5",
"evaluation": -0.15,
"depth": 18,
"pv": "c7c5 g1f3 d7d6 ..."
}
]
}
Errors:
400: {"error": "invalid request body"}
400: {"error": "fen is required"}
500: {"error": "engine error: ..."}
Timeout: 30 seconds max per analysis request.
Example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/analyze \
-H "Content-Type: application/json" \
-d '{"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", "depth": 20, "lines": 3}'
--------------------------------------------------------------------------------
3. LIST BOTS
--------------------------------------------------------------------------------
GET /api/chess/bots
Description:
Returns all available bot personalities sorted by difficulty (easiest first).
Response (200 OK):
{
"bots": [
{
"id": "amina",
"name": "Amina",
"name_ar": "أمينة المبتدئة",
"style": "beginner",
"style_ar": "مبتدئة",
"bio": "Just learning chess! Makes mistakes but tries her best.",
"bio_ar": "لسه بتتعلم شطرنج! بتغلط كتير بس بتحاول.",
"elo_min": 400,
"elo_max": 600,
"skill_level": 1,
"depth": 3,
"contempt": 0,
"blunder_chance": 0.30,
"think_time_min_ms": 500,
"think_time_max_ms": 2000,
"opening_book": [],
"avatar_id": "bot-amina",
"portrait_url": "/portraits/amina.png"
},
...
]
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/bots
--------------------------------------------------------------------------------
4. POOL STATS
--------------------------------------------------------------------------------
GET /api/chess/stats
Description:
Returns the current Stockfish process pool status.
Response (200 OK):
{
"pool_alive": 6, // number of Stockfish processes currently running
"pool_idle": 4 // number of those processes currently idle (available)
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/api/chess/stats
--------------------------------------------------------------------------------
5. HEALTH CHECK
--------------------------------------------------------------------------------
GET /health
Description:
Verifies the engine is operational by running a depth-1 search on the
starting position. Used by CapRover/Docker health checks.
Response (200 OK):
{
"status": "healthy",
"engine": "stockfish-18",
"pool_alive": 6,
"pool_idle": 4
}
Response (503 Service Unavailable):
{
"status": "unhealthy",
"error": "acquire process: context deadline exceeded"
}
Example:
curl https://stockfishapi.caprover.al-arcade.com/health
================================================================================
AVAILABLE BOTS (sorted by difficulty)
================================================================================
ID | Name | Arabic Name | Style | Style (AR) | ELO | Skill | Depth | Blunder% | Think Time (ms) | Portrait
--------------|-----------------|------------------|--------------|-------------|-----------|-------|-------|----------|-----------------|------------------
amina | Amina | أمينة المبتدئة | beginner | مبتدئة | 400-600 | 1 | 3 | 30% | 500-2000 | /portraits/amina.png
tarek | Tarek | طارق المتحفظ | defensive | دفاعي | 800-1000 | 5 | 6 | 15% | 1000-3000 | /portraits/tarek.png
nour | Nour | نور المهاجمة | aggressive | هجومية | 1000-1200 | 8 | 10 | 8% | 800-3000 | /portraits/nour.png
omar | Omar | عمر الاستراتيجي | positional | استراتيجي | 1200-1400 | 11 | 12 | 4% | 1500-4000 | /portraits/omar.png
layla | Layla | ليلى المبدعة | creative | إبداعية | 1400-1600 | 14 | 14 | 2% | 1000-5000 | /portraits/layla.png
ziad | Ziad | زياد الصلب | solid | صلب | 1600-1800 | 17 | 16 | 1% | 2000-6000 | /portraits/ziad.png
grandmaster | Grandmaster Bot | الجراند ماستر | near_perfect | شبه مثالي | 2000-2200 | 20 | 20 | 0% | 3000-8000 | /portraits/grandmaster.png
================================================================================
BOT BEHAVIOR DETAILS
================================================================================
BLUNDER MECHANISM:
Each move request, the bot rolls against its blunder_chance probability.
If it "blunders", the engine searches at depth=1 with skill_level=0,
producing a weak/random move. Otherwise it plays at its configured strength.
THINK TIME SIMULATION:
After the engine returns a move, the API adds artificial delay to simulate
human-like thinking. The delay is random between think_time_min and think_time_max.
Total response time = engine_time + artificial_delay.
If the client disconnects (context cancelled), the delay is aborted.
CONTEMPT:
Positive contempt = bot plays more aggressively, avoids draws.
Negative contempt = bot is happy to draw, plays defensively.
Range: -100 to 100.
SKILL LEVEL:
Stockfish's internal skill parameter (0-20).
0 = weakest, introduces random errors.
20 = full strength, no artificial weakening.
OPENING BOOKS (metadata only, not enforced by engine):
Listed per bot for frontend display. The engine does not use opening books;
it calculates from the given FEN position directly.
AVATAR IDs:
Each bot has an avatar_id field for frontend use (e.g. "bot-amina", "bot-nour").
Map these to your avatar image assets.
PORTRAITS:
Each bot has a portrait_url field pointing to a 512x512 pixel image.
Portraits are served publicly at: https://stockfishapi.caprover.al-arcade.com/portraits/{bot_id}.png
Supported formats: PNG, JPG, WebP.
Upload via admin panel: /admin/bots/edit/{id} -> Portrait upload form.
Recommended dimensions: 512x512 pixels (square).
These are character portraits/avatars for display in the chess UI.
STYLE LABELS (Arabic):
Each bot has a "style_ar" field with the Arabic translation of its play style.
Use this for bilingual (EN/AR) UI display.
Examples: "مبتدئة" (beginner), "دفاعي" (defensive), "هجومية" (aggressive),
"استراتيجي" (positional), "إبداعية" (creative), "صلب" (solid),
"شبه مثالي" (near_perfect).
================================================================================
MOVE NOTATION
================================================================================
All moves use UCI (Universal Chess Interface) long algebraic notation:
- Normal move: source_square + destination_square (e.g. "e2e4", "g1f3")
- Pawn promotion: source + destination + piece_letter (e.g. "e7e8q" for queen promotion)
- Castling: king's start + king's end (e.g. "e1g1" for white kingside, "e1c1" for queenside)
- En passant: normal pawn capture notation (e.g. "e5d6")
Piece letters for promotion: q=queen, r=rook, b=bishop, n=knight
================================================================================
FEN (Forsyth-Edwards Notation) FORMAT
================================================================================
A FEN string describes a complete board position in a single line:
"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
| | | | | |
| | | | | +-- fullmove number
| | | | +----- halfmove clock (50-move rule)
| | | +---------- en passant target square ("-" if none)
| | +----------------- castling availability (KQkq or "-")
| +--------------------------- active color: "w" or "b"
+------------------------------------- piece placement (rank 8 to rank 1, "/" separated)
Piece letters: K=king, Q=queen, R=rook, B=bishop, N=knight, P=pawn
Uppercase = white, lowercase = black
Numbers = consecutive empty squares
Starting position FEN:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
================================================================================
EVALUATION VALUES
================================================================================
The "evaluation" field in responses:
- Measured in pawns (centipawns / 100)
- Positive = white advantage
- Negative = black advantage
- +0.50 means white is half a pawn ahead
- +999.0 = white has forced checkmate
- -999.0 = black has forced checkmate
- Values near 0.0 = roughly equal position
================================================================================
RATE LIMITING
================================================================================
- 60 requests per minute per IP address
- Applies to all endpoints
- X-Forwarded-For header is respected (for reverse proxy setups)
- When exceeded: 429 Too Many Requests {"error": "rate limit exceeded"}
================================================================================
CORS
================================================================================
- Access-Control-Allow-Origin: * (all origins allowed)
- Allowed Methods: GET, POST, OPTIONS
- Allowed Headers: Content-Type, Authorization, apikey
- Preflight cache: 86400 seconds (24 hours)
================================================================================
MANAGEMENT API (Full Control)
================================================================================
Base: https://stockfishapi.caprover.al-arcade.com/api/manage
Authentication:
All /api/manage/* endpoints require an API key via header:
X-API-Key: sk-alarc-stockfish-mgmt-2024
OR:
Authorization: Bearer sk-alarc-stockfish-mgmt-2024
Rate Limiting: None on management endpoints (auth-gated).
--------------------------------------------------------------------------------
SYSTEM INFO
--------------------------------------------------------------------------------
GET /api/manage/info
Returns full system overview + all available endpoint list.
Response:
{
"engine": "stockfish-18",
"version": "1.0.0",
"bot_count": 7,
"pool": { "alive": 6, "idle": 4, "max_size": 12, "idle_timeout": 300 },
"settings": { "port": "80", "stockfish_path": "/usr/local/bin/stockfish" },
"endpoints": { ... all endpoints listed ... }
}
--------------------------------------------------------------------------------
BOT CRUD
--------------------------------------------------------------------------------
GET /api/manage/bots
List all bots with count.
GET /api/manage/bots/{id}
Get a single bot by ID.
POST /api/manage/bots
Create a new bot. Send full bot JSON in body.
Required: id, name, style
Returns 201 on success, 409 if ID already exists.
Body:
{
"id": "yasmin",
"name": "Yasmin",
"name_ar": "ياسمين",
"style": "tricky",
"style_ar": "ماكرة",
"bio": "Sets traps and waits for you to fall in.",
"bio_ar": "بتحط فخاخ وبتستنى تقع فيها.",
"elo_min": 1100,
"elo_max": 1300,
"skill_level": 9,
"depth": 11,
"contempt": 20,
"blunder_chance": 0.06,
"think_time_min_ms": 1000,
"think_time_max_ms": 3500,
"opening_book": ["sicilian", "french"],
"avatar_id": "bot-yasmin",
"portrait_url": "/portraits/yasmin.png"
}
PATCH /api/manage/bots/{id}
Partial update. Only send fields you want to change.
Body: { "skill_level": 12, "blunder_chance": 0.05, "style_ar": "ذكية" }
PUT /api/manage/bots/{id}
Full replace. Overwrites entire bot with new data.
Body: same as POST but ID comes from URL.
DELETE /api/manage/bots/{id}
Delete a bot and its portrait files.
--------------------------------------------------------------------------------
BULK OPERATIONS
--------------------------------------------------------------------------------
POST /api/manage/bots/bulk
Create multiple bots at once.
Body: [ {bot1}, {bot2}, ... ]
Response: { "created": ["id1","id2"], "skipped": ["id3 (already exists)"] }
DELETE /api/manage/bots/bulk
Delete multiple bots at once.
Body: { "ids": ["amina", "tarek"] }
Response: { "deleted": ["amina","tarek"], "not_found": [] }
GET /api/manage/bots/export
Download all bots as JSON file (Content-Disposition: attachment).
POST /api/manage/bots/import?overwrite=true
Import bots from JSON array. With overwrite=true, existing bots are replaced.
Body: [ {bot1}, {bot2}, ... ]
Response: { "imported": ["id1"], "skipped": ["id2 (exists)"], "total": 8 }
--------------------------------------------------------------------------------
PORTRAIT MANAGEMENT
--------------------------------------------------------------------------------
POST /api/manage/bots/{id}/portrait
Upload a portrait image (512x512 px).
Content-Type: multipart/form-data
Field name: "portrait"
Accepted: .png, .jpg, .jpeg, .webp (max 10MB)
curl example:
curl -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-F "portrait=@amina_512x512.png"
Response: { "status": "uploaded", "bot_id": "amina", "portrait_url": "/portraits/amina.png", "size_bytes": 245000 }
DELETE /api/manage/bots/{id}/portrait
Delete all portrait files for a bot.
Response: { "status": "deleted", "bot_id": "amina" }
Portraits are served publicly (no auth):
GET https://stockfishapi.caprover.al-arcade.com/portraits/{bot_id}.png
--------------------------------------------------------------------------------
ENGINE / POOL
--------------------------------------------------------------------------------
GET /api/manage/pool
Full pool stats with utilization percentage.
Response:
{
"pool_alive": 6,
"pool_idle": 4,
"pool_max_size": 12,
"pool_utilization": "16.7%",
"idle_timeout_sec": 300
}
POST /api/manage/test-move
Test the engine directly. Supports raw mode (custom params) or bot mode.
Bot mode:
{ "fen": "...", "bot_id": "nour" }
Raw mode (bypass bot config, use custom engine params):
{
"fen": "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1",
"raw_mode": true,
"depth": 25,
"skill_level": 20,
"contempt": 0,
"time_limit_ms": 5000,
"multi_pv": 3
}
Response:
{
"best_move": "e7e5",
"evaluation": -0.12,
"depth": 25,
"nodes": 5000000,
"pv": "e7e5 g1f3 b8c6",
"engine_time_ms": 3200,
"request": { "fen": "...", "depth": 25, "skill_level": 20, "contempt": 0, "multi_pv": 3 }
}
POST /api/manage/analyze
Deep analysis (up to depth 40, up to 10 lines).
{ "fen": "...", "depth": 30, "lines": 5 }
Response: { "fen": "...", "depth": 30, "lines": [...], "engine_time_ms": 12000 }
--------------------------------------------------------------------------------
SETTINGS
--------------------------------------------------------------------------------
GET /api/manage/settings
{ "port": "80", "pool_size": 12, "idle_timeout_sec": 300, "stockfish_path": "/usr/local/bin/stockfish" }
PATCH /api/manage/settings
Update settings (partial). Some require restart.
Body: { "pool_size": 16, "idle_timeout_sec": 600 }
Response: { "status": "updated", "settings": {...}, "note": "some changes require restart" }
--------------------------------------------------------------------------------
LOGS
--------------------------------------------------------------------------------
GET /api/manage/logs?limit=50
Get recent request logs (max 100).
Response:
{
"logs": [
{ "Timestamp": "2024-01-15 14:30:22", "Method": "POST", "Path": "/api/chess/move", "Status": 200, "Duration": "45ms", "IP": "1.2.3.4" },
...
],
"count": 50
}
================================================================================
MANAGEMENT API - QUICK REFERENCE (curl examples)
================================================================================
# Auth header (use in all requests)
AUTH="-H 'X-API-Key: sk-alarc-stockfish-mgmt-2024'"
# System info
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/info \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# List bots
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Get single bot
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots/nour \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Create bot
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"id":"yasmin","name":"Yasmin","name_ar":"ياسمين","style":"tricky","style_ar":"ماكرة","skill_level":9,"depth":11,"contempt":20,"blunder_chance":0.06,"think_time_min_ms":1000,"think_time_max_ms":3500,"elo_min":1100,"elo_max":1300}' | jq .
# Update bot (partial)
curl -s -X PATCH https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"skill_level":2,"blunder_chance":0.25}' | jq .
# Replace bot (full)
curl -s -X PUT https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"name":"Amina","name_ar":"أمينة","style":"beginner","style_ar":"مبتدئة","skill_level":1,"depth":3}' | jq .
# Delete bot
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/yasmin \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Bulk create
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/bulk \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '[{"id":"bot1","name":"Bot One","style":"test","skill_level":5,"depth":5},{"id":"bot2","name":"Bot Two","style":"test","skill_level":10,"depth":10}]' | jq .
# Bulk delete
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/bulk \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"ids":["bot1","bot2"]}' | jq .
# Export all bots (download)
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/bots/export \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" -o bots_backup.json
# Import bots (with overwrite)
curl -s -X POST "https://stockfishapi.caprover.al-arcade.com/api/manage/bots/import?overwrite=true" \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d @bots_backup.json | jq .
# Upload portrait
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-F "portrait=@amina.png" | jq .
# Delete portrait
curl -s -X DELETE https://stockfishapi.caprover.al-arcade.com/api/manage/bots/amina/portrait \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Pool stats
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/pool \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Test move (bot mode)
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/test-move \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","bot_id":"grandmaster"}' | jq .
# Test move (raw mode - custom engine params)
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/test-move \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","raw_mode":true,"depth":30,"skill_level":20,"contempt":0}' | jq .
# Deep analysis
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/manage/analyze \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1","depth":30,"lines":5}' | jq .
# Get settings
curl -s https://stockfishapi.caprover.al-arcade.com/api/manage/settings \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
# Update settings
curl -s -X PATCH https://stockfishapi.caprover.al-arcade.com/api/manage/settings \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" \
-H "Content-Type: application/json" \
-d '{"pool_size":16,"idle_timeout_sec":600}' | jq .
# View logs
curl -s "https://stockfishapi.caprover.al-arcade.com/api/manage/logs?limit=20" \
-H "X-API-Key: sk-alarc-stockfish-mgmt-2024" | jq .
================================================================================
ADMIN PANEL
================================================================================
URL: https://stockfishapi.caprover.al-arcade.com/admin
Login: username "admin", password "Alarcade123#"
Features:
- Dashboard: live pool stats, bot count, request metrics
- Bot management: create, edit, delete bots at runtime
- Pool monitoring: alive/idle process counts
- Test Move: test any bot with a custom FEN position
- Request Logs: last 100 requests with method, path, status, duration, IP
- Settings: view/modify port, pool size, idle timeout
Note: Runtime changes (bot edits, new bots) persist only until container restart.
Core bot definitions are compiled into the binary.
================================================================================
INFRASTRUCTURE
================================================================================
- Engine: Stockfish 18 (compiled from source, NNUE enabled)
- Runtime: Go 1.22, single static binary
- Process Pool: 12 Stockfish processes (configurable via POOL_SIZE env)
- Pre-warmed: half the pool at startup for instant first requests
- Idle Reaper: kills processes unused for 300s (configurable via IDLE_TIMEOUT_SEC)
- Platform: Docker on CapRover (Ubuntu 22.04 base)
- Architecture: x86-64-sse41-popcnt (broad server CPU compatibility)
- Health Check: every 30s, 10s timeout, 3 retries, 15s start period
- Port: 80 inside container (CapRover handles HTTPS/reverse proxy)
================================================================================
ENVIRONMENT VARIABLES
================================================================================
PORT=80 # HTTP listen port
STOCKFISH_PATH=/usr/local/bin/stockfish # path to Stockfish binary
POOL_SIZE=12 # max concurrent Stockfish processes
IDLE_TIMEOUT_SEC=300 # kill idle processes after this many seconds
================================================================================
INTEGRATION EXAMPLES
================================================================================
--- JavaScript/Fetch ---
// Get a move from "nour" bot
const response = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/move', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
bot_id: 'nour'
})
});
const data = await response.json();
console.log(data.best_move); // e.g. "e7e5"
// Analyze a position
const analysis = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
fen: 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
depth: 20,
lines: 3
})
});
const result = await analysis.json();
result.lines.forEach(line => {
console.log(`#${line.rank}: ${line.move} (eval: ${line.evaluation})`);
});
// List all bots
const botsResp = await fetch('https://stockfishapi.caprover.al-arcade.com/api/chess/bots');
const botsData = await botsResp.json();
botsData.bots.forEach(bot => {
console.log(`${bot.name} (${bot.elo_min}-${bot.elo_max} ELO) - ${bot.style}`);
});
--- Python ---
import requests
# Get move
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/move', json={
'fen': 'rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1',
'bot_id': 'omar'
})
move = resp.json()
print(f"Best move: {move['best_move']}, Eval: {move['evaluation']}")
# Analyze
resp = requests.post('https://stockfishapi.caprover.al-arcade.com/api/chess/analyze', json={
'fen': 'r1bqkbnr/pppppppp/2n5/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2',
'depth': 22,
'lines': 5
})
for line in resp.json()['lines']:
print(f"#{line['rank']}: {line['move']} (eval {line['evaluation']:.2f})")
--- cURL ---
# Quick move
curl -s -X POST https://stockfishapi.caprover.al-arcade.com/api/chess/move \
-H "Content-Type: application/json" \
-d '{"fen":"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1","bot_id":"amina"}' | jq .
# Health check
curl -s https://stockfishapi.caprover.al-arcade.com/health | jq .
================================================================================
TYPICAL GAME FLOW (for client integration)
================================================================================
1. GET /api/chess/bots -> show bot selection to user
2. User picks a bot (e.g. "nour") and starts a game
3. Client tracks FEN locally (or uses a chess library)
4. When it's the bot's turn:
POST /api/chess/move with current FEN and bot_id
5. Apply the returned best_move to the board
6. Repeat from step 4 until checkmate/stalemate/draw
7. Optionally: POST /api/chess/analyze for post-game analysis
Important notes for client implementation:
- Always send the FULL FEN including castling rights, en passant, move counters
- The API does NOT validate if the FEN is a legal position
- The API does NOT track game state - it's stateless (one move per request)
- Handle the artificial think_time_ms for UX (show "thinking..." animation)
- Handle 429 rate limit gracefully (retry after 1 second)
- The response time can be up to 8000ms+ for grandmaster bot (due to simulated think time)
================================================================================
ERROR HANDLING
================================================================================
All errors return JSON with an "error" field:
400 Bad Request - malformed JSON, missing required fields
404 Not Found - invalid bot_id
429 Too Many Requests - rate limit exceeded (60/min/IP)
500 Internal Server Error - Stockfish process crash, pool exhaustion, timeout
503 Service Unavailable - health check failed (engine not responding)
Recommended retry strategy:
- 429: wait 1-2 seconds, retry
- 500: wait 2-5 seconds, retry up to 3 times
- 503: service is down, alert user
================================================================================
RESPONSE TIME EXPECTATIONS
================================================================================
Bot | Typical Response Time (includes simulated think time)
----------------|------------------------------------------------------
amina | 500ms - 2000ms
tarek | 1000ms - 3000ms
nour | 800ms - 3000ms
omar | 1500ms - 4000ms
layla | 1000ms - 5000ms
ziad | 2000ms - 6000ms
grandmaster | 3000ms - 8000ms
Analysis endpoint: 1-30 seconds depending on depth (no artificial delay).
Health check: < 5 seconds.
Set client timeouts accordingly (recommend 15s for moves, 35s for analysis).
import { chromium } from 'playwright'
import { mkdirSync } from 'fs'
const BASE_URL = 'https://el3ab-player.caprover.al-arcade.com'
const VIEWPORT = { width: 390, height: 844 }
const EMAIL = 'testplayer1@el3ab.com'
const PASSWORD = 'test123456'
async function main() {
mkdirSync('screenshots', { recursive: true })
const browser = await chromium.launch()
const context = await browser.newContext({
viewport: VIEWPORT,
deviceScaleFactor: 2,
locale: 'ar',
})
const page = await context.newPage()
// Login
await page.goto(`${BASE_URL}/login`, { waitUntil: 'networkidle', timeout: 15000 })
await page.waitForTimeout(1000)
await page.fill('input[type="email"]', EMAIL)
await page.fill('input[type="password"]', PASSWORD)
await page.click('button[type="submit"]')
await page.waitForTimeout(3000)
// Navigate to bot game directly
await page.goto(`${BASE_URL}/game/bot/amina`, { waitUntil: 'networkidle', timeout: 15000 })
await page.waitForTimeout(2000)
await page.screenshot({ path: 'screenshots/bot-match-01-start.png' })
console.log('captured: initial board')
// Target squares specifically within the chess board grid (data-testid="chess-board")
const squares = page.locator('[data-testid="chess-board"] > div')
const squareCount = await squares.count()
console.log(`found ${squareCount} squares on the board`)
// The board is an 8x8 grid. For white's perspective (not flipped):
// Grid index = row*8 + col, where row 0 = rank 8 (top), row 7 = rank 1 (bottom)
// e2 = file e (col 4), rank 2 (row 6) -> index: 6*8 + 4 = 52
// e4 = file e (col 4), rank 4 (row 4) -> index: 4*8 + 4 = 36
// Click e2 (select white pawn)
console.log('clicking e2 (index 52)...')
await squares.nth(52).click()
await page.waitForTimeout(800)
await page.screenshot({ path: 'screenshots/bot-match-02-selected-e2.png' })
console.log('captured: selected e2')
// Click e4 (move pawn)
console.log('clicking e4 (index 36)...')
await squares.nth(36).click()
await page.waitForTimeout(800)
await page.screenshot({ path: 'screenshots/bot-match-03-moved-e4.png' })
console.log('captured: moved e4')
// Wait for bot to respond (up to 12 seconds)
console.log('waiting for bot to respond...')
await page.waitForTimeout(10000)
await page.screenshot({ path: 'screenshots/bot-match-04-bot-responded.png' })
console.log('captured: after bot response')
// Play another move: d2-d4
// d2 = file d (col 3), rank 2 (row 6) -> index: 6*8 + 3 = 51
// d4 = file d (col 3), rank 4 (row 4) -> index: 4*8 + 3 = 35
console.log('clicking d2 (index 51)...')
await squares.nth(51).click()
await page.waitForTimeout(800)
console.log('clicking d4 (index 35)...')
await squares.nth(35).click()
await page.waitForTimeout(800)
await page.screenshot({ path: 'screenshots/bot-match-05-moved-d4.png' })
console.log('captured: moved d4')
// Wait for bot second response
console.log('waiting for bot second response...')
await page.waitForTimeout(10000)
await page.screenshot({ path: 'screenshots/bot-match-06-bot-responded-2.png' })
console.log('captured: after bot second response')
// Play Nf3
// g1 = file g (col 6), rank 1 (row 7) -> index: 7*8 + 6 = 62
// f3 = file f (col 5), rank 3 (row 5) -> index: 5*8 + 5 = 45
console.log('clicking g1 (index 62)...')
await squares.nth(62).click()
await page.waitForTimeout(800)
console.log('clicking f3 (index 45)...')
await squares.nth(45).click()
await page.waitForTimeout(800)
// Wait for bot
console.log('waiting for bot third response...')
await page.waitForTimeout(10000)
await page.screenshot({ path: 'screenshots/bot-match-07-after-3-moves.png' })
console.log('captured: after 3 moves each')
await browser.close()
console.log('done - bot match test complete')
}
main()
......@@ -9,14 +9,11 @@ const PASSWORD = 'test123456'
const pages = [
{ name: '03-home', path: '/' },
{ name: '04-play', path: '/play' },
{ name: '05-matchmaking', path: '/matchmaking' },
{ name: '06-profile', path: '/profile' },
{ name: '07-tournaments', path: '/tournaments' },
{ name: '08-friends', path: '/friends' },
{ name: '09-leaderboard', path: '/leaderboard' },
{ name: '10-notifications', path: '/notifications' },
{ name: '11-shop', path: '/shop' },
{ name: '12-settings', path: '/settings' },
{ name: '05-bot-select', path: '/bot-select' },
{ name: '06-game-bot', path: '/game/bot/amina' },
{ name: '07-profile', path: '/profile' },
{ name: '08-leaderboard', path: '/leaderboard' },
{ name: '09-shop', path: '/shop' },
]
async function main() {
......
......@@ -5,9 +5,9 @@ import { ToastContainer } from '../ui/ToastContainer'
export function AppShell() {
return (
<div className="flex flex-col min-h-dvh">
<div className="flex flex-col min-h-dvh bg-[#0a0a12]">
<Header />
<main className="flex-1 pb-20 overflow-y-auto">
<main className="flex-1 pb-24 overflow-y-auto">
<Outlet />
</main>
<BottomNav />
......
......@@ -15,8 +15,8 @@ export function BottomNav() {
const navigate = useNavigate()
return (
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-surface-1/90 backdrop-blur-xl border-t border-border">
<div className="flex items-center justify-around px-2 py-2 max-w-lg mx-auto">
<nav className="fixed bottom-0 left-0 right-0 z-50 px-4 pb-2">
<div className="flex items-center justify-around px-2 py-2.5 max-w-lg mx-auto bg-surface-1/95 backdrop-blur-xl border border-border/60 rounded-2xl shadow-xl shadow-black/30">
{NAV_ITEMS.map((item) => {
const isActive = location.pathname === item.path
const Icon = item.icon
......@@ -51,7 +51,7 @@ export function BottomNav() {
{isActive && (
<motion.div
className="absolute -bottom-1 w-5 h-1 rounded-full bg-gold"
className="absolute -bottom-0.5 w-5 h-1 rounded-full bg-gold"
layoutId="nav-indicator"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
style={{ boxShadow: '0 0 8px rgba(212, 168, 67, 0.6)' }}
......
......@@ -12,37 +12,39 @@ export function Header() {
const navigate = useNavigate()
return (
<header className="sticky top-0 z-50 flex items-center justify-between px-4 py-3 bg-surface-1/80 backdrop-blur-xl border-b border-border">
<div className="flex items-center gap-2">
<GoldCrown size={28} animate={false} />
<span className="text-lg font-bold text-gold">EL3AB</span>
</div>
<div className="flex items-center gap-4">
{profile && (
<motion.div
className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-surface-2 border border-border-gold"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
<Coins size={14} className="text-gold" />
<span className="text-sm font-semibold text-gold">{profile.coins}</span>
</motion.div>
)}
<header className="sticky top-0 z-50 px-4 pt-3 pb-2">
<div className="flex items-center justify-between px-4 py-2.5 bg-surface-1/90 backdrop-blur-xl border border-border/60 rounded-2xl shadow-lg shadow-black/20">
<div className="flex items-center gap-2">
<GoldCrown size={26} animate={false} />
<span className="text-base font-bold text-gold">EL3AB</span>
</div>
<div className="relative" onClick={() => navigate('/notifications')}>
<AnimatedIcon icon={Bell} size={22} color="var(--color-text-secondary)" onClick={() => {}} />
{unreadCount > 0 && (
<div className="flex items-center gap-3">
{profile && (
<motion.div
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-coral flex items-center justify-center"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-surface-2/80 border border-gold/30"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 500, damping: 20 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
<span className="text-[10px] font-bold text-white">{unreadCount > 9 ? '9+' : unreadCount}</span>
<Coins size={13} className="text-gold" />
<span className="text-xs font-bold text-gold">{profile.coins}</span>
</motion.div>
)}
<div className="relative" onClick={() => navigate('/notifications')}>
<AnimatedIcon icon={Bell} size={20} color="var(--color-text-secondary)" onClick={() => {}} />
{unreadCount > 0 && (
<motion.div
className="absolute -top-1 -right-1 w-4 h-4 rounded-full bg-coral flex items-center justify-center"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 500, damping: 20 }}
>
<span className="text-[10px] font-bold text-white">{unreadCount > 9 ? '9+' : unreadCount}</span>
</motion.div>
)}
</div>
</div>
</div>
</header>
......
......@@ -39,7 +39,7 @@ export function BotSelectPage() {
if (loading) {
return (
<PageTransition className="px-5 py-8 flex flex-col items-center justify-center min-h-[60vh]">
<PageTransition className="px-6 py-8 flex flex-col items-center justify-center min-h-[60vh]">
<div className="w-10 h-10 border-2 border-gold/30 border-t-gold rounded-full animate-spin" />
<p className="mt-4 text-sm text-text-muted">جاري تحميل الروبوتات...</p>
</PageTransition>
......@@ -47,7 +47,7 @@ export function BotSelectPage() {
}
return (
<PageTransition className="px-5 py-6 flex flex-col gap-6 pb-36">
<PageTransition className="px-6 py-7 flex flex-col gap-6 pb-36">
{/* Header */}
<div className="flex items-center gap-4">
<motion.button
......
......@@ -365,7 +365,7 @@ export function GamePage() {
return (
<div className="flex flex-col min-h-dvh bg-background">
{/* Header */}
<div className="flex items-center justify-between px-5 py-3 bg-surface-1 border-b border-border">
<div className="flex items-center justify-between px-6 py-3.5 bg-surface-1 border-b border-border">
<motion.button
onClick={() => navigate(-1)}
className="flex items-center gap-0.5 text-text-muted p-2"
......
......@@ -3,7 +3,6 @@ import { Play, TrendingUp, Swords, Flame, Grid3X3, Bot, Users, Lightbulb, Crown
import { useNavigate } from 'react-router-dom'
import { useAuthStore } from '../stores/authStore'
import { PageTransition } from '../components/layout/PageTransition'
import { Card } from '../components/ui/Card'
const dailyTips = [
'تحكم بالمركز في بداية اللعبة - الاحصنة والفيلة تكون اقوى من المركز',
......@@ -21,197 +20,145 @@ export function HomePage() {
const todayTip = dailyTips[new Date().getDay()]
return (
<PageTransition className="px-5 py-6 flex flex-col gap-7">
{/* Greeting */}
<PageTransition className="px-6 py-7 flex flex-col gap-8">
{profile && (
<motion.div
className="flex items-center gap-4"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-gold/30 to-purple/30 border-2 border-gold/40 flex items-center justify-center shadow-lg shadow-gold/10">
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-gold/20 to-purple/20 border-2 border-gold/30 flex items-center justify-center">
<span className="text-xl font-bold text-gold">
{profile.display_name?.charAt(0) || 'L'}
</span>
</div>
<div className="flex flex-col gap-0.5">
<h2 className="text-xl font-bold leading-tight">اهلا، {profile.display_name}</h2>
<p className="text-sm text-text-muted">المستوى {profile.level}</p>
<div>
<h2 className="text-xl font-bold">اهلا، {profile.display_name}</h2>
<p className="text-xs text-text-muted mt-0.5">المستوى {profile.level}</p>
</div>
</motion.div>
)}
{/* Play Button */}
<motion.button
onClick={() => navigate('/play')}
className="relative w-full py-10 rounded-3xl bg-gradient-to-bl from-gold via-gold-light to-gold overflow-hidden shadow-xl shadow-gold/25"
whileTap={{ scale: 0.97 }}
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
className="relative w-full py-9 rounded-[20px] bg-gradient-to-bl from-gold via-gold-light to-gold overflow-hidden shadow-2xl shadow-gold/30"
whileTap={{ scale: 0.96 }}
>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/15 to-transparent"
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent"
animate={{ x: ['-200%', '200%'] }}
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
transition={{ duration: 2.5, repeat: Infinity, ease: 'linear' }}
/>
<div className="relative flex flex-col items-center gap-3">
<motion.div
animate={{ scale: [1, 1.08, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
>
<Play size={44} className="text-background" fill="currentColor" />
<Play size={42} className="text-background" fill="currentColor" />
</motion.div>
<span className="text-2xl font-black text-background tracking-wide">العب الان</span>
<span className="text-2xl font-black text-background">العب الان</span>
</div>
</motion.button>
{/* Stats */}
{profile && (
<div className="grid grid-cols-3 gap-3">
<StatCard
icon={<Swords size={22} className="text-cyan" />}
value={profile.total_games_played}
label="مباراة"
delay={0.2}
accentColor="cyan"
/>
<StatCard
icon={<TrendingUp size={22} className="text-gold" />}
value={profile.elo_blitz}
label="تقييم"
delay={0.3}
accentColor="gold"
/>
<StatCard
icon={<Flame size={22} className="text-coral" />}
value={profile.win_streak}
label="سلسلة فوز"
delay={0.4}
accentColor="coral"
/>
<StatCard icon={<Swords size={20} className="text-cyan" />} value={profile.total_games_played} label="مباراة" delay={0.2} accent="cyan" />
<StatCard icon={<TrendingUp size={20} className="text-gold" />} value={profile.elo_blitz} label="تقييم" delay={0.3} accent="gold" />
<StatCard icon={<Flame size={20} className="text-coral" />} value={profile.win_streak} label="سلسلة فوز" delay={0.4} accent="coral" />
</div>
)}
{/* Quick Actions */}
<div className="grid grid-cols-2 gap-3">
<motion.button
onClick={() => navigate('/bot-select')}
className="flex items-center gap-3 p-5 rounded-2xl bg-surface-1 border border-border/80 text-start"
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.3, type: 'spring', stiffness: 400, damping: 25 }}
className="flex flex-col items-center gap-3 p-6 rounded-2xl bg-surface-1 border border-border/60"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
whileTap={{ scale: 0.96 }}
>
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-purple/30 to-cyan/20 flex items-center justify-center">
<Bot size={22} className="text-purple" />
<div className="w-12 h-12 rounded-2xl bg-purple/10 border border-purple/20 flex items-center justify-center">
<Bot size={24} className="text-purple" />
</div>
<div className="flex flex-col gap-0.5">
<p className="text-sm font-bold leading-tight">العب ضد روبوت</p>
<p className="text-[10px] text-text-muted">تدريب وتحسين</p>
<div className="text-center">
<p className="text-sm font-bold">العب ضد روبوت</p>
<p className="text-[10px] text-text-muted mt-0.5">تدريب وتحسين</p>
</div>
</motion.button>
<motion.button
onClick={() => navigate('/friends')}
className="flex items-center gap-3 p-5 rounded-2xl bg-surface-1 border border-border/80 text-start"
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.4, type: 'spring', stiffness: 400, damping: 25 }}
className="flex flex-col items-center gap-3 p-6 rounded-2xl bg-surface-1 border border-border/60"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
whileTap={{ scale: 0.96 }}
>
<div className="w-11 h-11 rounded-xl bg-gradient-to-br from-gold/30 to-coral/20 flex items-center justify-center">
<Users size={22} className="text-gold" />
<div className="w-12 h-12 rounded-2xl bg-gold/10 border border-gold/20 flex items-center justify-center">
<Users size={24} className="text-gold" />
</div>
<div className="flex flex-col gap-0.5">
<p className="text-sm font-bold leading-tight">تحدى صديق</p>
<p className="text-[10px] text-text-muted">ارسل دعوة</p>
<div className="text-center">
<p className="text-sm font-bold">تحدى صديق</p>
<p className="text-[10px] text-text-muted mt-0.5">ارسل دعوة</p>
</div>
</motion.button>
</div>
{/* Recent Matches */}
<div>
<h3 className="text-base font-bold mb-4 text-text-secondary">اخر المباريات</h3>
<Card className="flex flex-col items-center justify-center py-10 gap-4">
<h3 className="text-sm font-bold mb-4 text-text-muted">اخر المباريات</h3>
<div className="flex flex-col items-center py-10 gap-4 rounded-2xl bg-surface-1 border border-border/60">
<motion.div
className="w-16 h-16 rounded-2xl bg-surface-3/80 flex items-center justify-center"
className="w-14 h-14 rounded-2xl bg-surface-3/60 flex items-center justify-center"
animate={{ rotate: [0, 3, -3, 0] }}
transition={{ duration: 5, repeat: Infinity, ease: 'easeInOut' }}
transition={{ duration: 5, repeat: Infinity }}
>
<Grid3X3 size={32} className="text-text-muted/60" />
<Grid3X3 size={28} className="text-text-muted/40" />
</motion.div>
<div className="text-center space-y-1.5">
<p className="text-sm font-semibold">لا توجد مباريات بعد</p>
<div className="text-center space-y-1">
<p className="text-sm font-semibold text-text-secondary">لا توجد مباريات بعد</p>
<p className="text-xs text-text-muted">العب اول مباراة وابدا رحلتك</p>
</div>
<motion.button
onClick={() => navigate('/play')}
className="mt-1 px-6 py-2.5 rounded-xl bg-gold/10 border border-gold/30 text-gold text-sm font-bold"
className="px-6 py-2.5 rounded-xl bg-gold/10 border border-gold/30 text-gold text-sm font-bold"
whileTap={{ scale: 0.95 }}
>
ابدا الان
</motion.button>
</Card>
</div>
</div>
{/* Daily Tip */}
<motion.div
className="p-5 rounded-2xl bg-surface-1 border border-border/80 relative overflow-hidden"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, type: 'spring', stiffness: 400, damping: 25 }}
>
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl from-gold/8 to-transparent rounded-bl-full" />
<div className="p-5 rounded-2xl bg-surface-1 border border-border/60 relative overflow-hidden">
<div className="absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl from-gold/6 to-transparent rounded-bl-full" />
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-gold/20 to-gold/5 flex items-center justify-center flex-shrink-0 mt-0.5">
<div className="w-10 h-10 rounded-xl bg-gold/10 flex items-center justify-center flex-shrink-0">
<Lightbulb size={20} className="text-gold" />
</div>
<div className="flex-1">
<div className="flex-1 pt-0.5">
<div className="flex items-center gap-2 mb-2">
<h4 className="text-sm font-bold text-gold">نصيحة اليوم</h4>
<Crown size={12} className="text-gold/40" />
<h4 className="text-xs font-bold text-gold">نصيحة اليوم</h4>
<Crown size={11} className="text-gold/40" />
</div>
<p className="text-[13px] text-text-secondary leading-relaxed">{todayTip}</p>
<p className="text-[12px] text-text-secondary leading-[1.7]">{todayTip}</p>
</div>
</div>
</motion.div>
{/* Bottom spacing for nav */}
<div className="h-4" />
</div>
</PageTransition>
)
}
function StatCard({
icon,
value,
label,
delay,
accentColor,
}: {
icon: React.ReactNode
value: number
label: string
delay: number
accentColor: string
}) {
const colorMap: Record<string, string> = {
cyan: 'from-cyan/50 to-cyan/10',
gold: 'from-gold/50 to-gold/10',
coral: 'from-coral/50 to-coral/10',
}
function StatCard({ icon, value, label, delay, accent }: { icon: React.ReactNode; value: number; label: string; delay: number; accent: string }) {
const borderMap: Record<string, string> = { cyan: 'border-cyan/20', gold: 'border-gold/20', coral: 'border-coral/20' }
return (
<motion.div
className="relative flex flex-col items-center gap-2 p-4 rounded-xl bg-surface-1 border border-border/80 overflow-hidden"
className={`flex flex-col items-center gap-2 p-4 rounded-2xl bg-surface-1 border ${borderMap[accent] || 'border-border/60'}`}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay, type: 'spring', stiffness: 400, damping: 25 }}
transition={{ delay }}
>
<div className={`absolute top-0 right-0 bottom-0 w-1 bg-gradient-to-b ${colorMap[accentColor] || colorMap.gold} rounded-l-full`} />
{icon}
<span className="text-2xl font-black">{value}</span>
<span className="text-[11px] text-text-muted font-medium">{label}</span>
<span className="text-xl font-black">{value}</span>
<span className="text-[10px] text-text-muted font-medium">{label}</span>
</motion.div>
)
}
......@@ -28,7 +28,7 @@ export function PlayPage() {
const otherGames = GAMES.filter((g) => g.key !== 'chess')
return (
<PageTransition className="px-5 py-6 flex flex-col gap-7">
<PageTransition className="px-6 py-7 flex flex-col gap-7">
<h1 className="text-xl font-bold">اختر اللعبة</h1>
{/* Chess Hero Card */}
......
......@@ -26,7 +26,7 @@ export function ProfilePage() {
: 0
return (
<PageTransition className="px-5 py-6 flex flex-col gap-6">
<PageTransition className="px-6 py-7 flex flex-col gap-6">
{/* Avatar + Name */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment