// fuesse.sexy — "Wanted!" (Fuss-Fahndung) // Schlanker Express-Server: liefert das Frontend + die Bildchen aus, stellt die Liste // der verfuegbaren Fuesse/Deko bereit und verwaltet eine persistente Rangliste. import express from 'express'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const PUBLIC_DIR = join(ROOT, 'public'); const LIBRARY_DIR = join(ROOT, 'library'); const DATA_DIR = join(ROOT, 'data'); const LEADERBOARD_FILE = join(DATA_DIR, 'leaderboard.json'); const PORT = process.env.PORT || 3000; const IMAGE_EXT = ['.svg', '.png', '.webp', '.jpg', '.jpeg', '.gif']; // ---- Bildchen aus der Library einlesen ---- function isImage(f) { return IMAGE_EXT.some((ext) => f.toLowerCase().endsWith(ext)); } function prettyName(file) { return file .replace(/\.[^.]+$/, '') .replace(/[-_]+/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); } function listImages(dir, urlPrefix) { if (!existsSync(dir)) return []; return readdirSync(dir) .filter(isImage) .sort() .map((f) => ({ id: f.replace(/\.[^.]+$/, ''), name: prettyName(f), url: `${urlPrefix}/${f}` })); } const feet = listImages(join(LIBRARY_DIR, 'feet'), '/library/feet'); const props = listImages(join(LIBRARY_DIR, 'props'), '/library/props'); if (feet.length < 2) { console.warn(`[server] Achtung: nur ${feet.length} Fuss/Fuesse in library/feet — brauche mind. 2.`); } else { console.log(`[server] ${feet.length} Fuesse, ${props.length} Deko-Bildchen geladen.`); } // ---- Rangliste (persistente JSON-Datei) ---- function readLeaderboard() { try { if (!existsSync(LEADERBOARD_FILE)) return []; const data = JSON.parse(readFileSync(LEADERBOARD_FILE, 'utf8')); return Array.isArray(data) ? data : []; } catch (err) { console.warn('[server] Rangliste unlesbar:', err.message); return []; } } function writeLeaderboard(list) { if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true }); writeFileSync(LEADERBOARD_FILE, JSON.stringify(list, null, 2)); } function topN(list, n) { return [...list].sort((a, b) => b.score - a.score).slice(0, n); } const app = express(); app.use(express.json()); // Statisches Frontend + Bildchen (no-cache -> nie veraltetes JS nach Update) app.use(express.static(PUBLIC_DIR, { setHeaders: (res) => res.setHeader('Cache-Control', 'no-cache') })); app.use('/library', express.static(LIBRARY_DIR)); // Verfuegbare Bildchen app.get('/api/feet', (req, res) => { res.json({ feet, props }); }); // Rangliste: Top 10 app.get('/api/leaderboard', (req, res) => { res.json(topN(readLeaderboard(), 10)); }); // Score eintragen -> aktualisierte Top 10 zurueck app.post('/api/score', (req, res) => { let { name, score } = req.body || {}; score = Number(score); if (!Number.isFinite(score) || score < 0) { return res.status(400).json({ error: 'Ungueltiger Score.' }); } name = String(name || 'Anonym').trim().slice(0, 20) || 'Anonym'; score = Math.min(Math.floor(score), 100000); // grobe Plausibilitaet const list = readLeaderboard(); list.push({ name, score, at: new Date().toISOString() }); const trimmed = topN(list, 100); // Datei klein halten writeLeaderboard(trimmed); res.json(topN(trimmed, 10)); }); app.listen(PORT, () => { console.log(`[server] fuesse.sexy (Wanted!) laeuft auf http://localhost:${PORT}`); });