import express from 'express' import cors from 'cors' import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs' import { fileURLToPath } from 'url' import { dirname, join } from 'path' import 'dotenv/config' const __dirname = dirname(fileURLToPath(import.meta.url)) const CHARACTERS = [ { id: 'scarlet', name: 'Miss Scarlett', color: '#c0392b', costumeHint: 'Deep red dress or suit, dark lipstick', icon: '🌹' }, { id: 'mustard', name: 'Colonel Mustard', color: '#d4a017', costumeHint: 'Mustard yellow jacket or military', icon: '🎖️' }, { id: 'white', name: 'Mrs. White', color: '#e8e0d0', costumeHint: 'Off-white apron, black dress', icon: '🕯️' }, { id: 'green', name: 'Mr. Green', color: '#1e6e3b', costumeHint: 'Forest green suit or vest', icon: '📖' }, { id: 'peacock', name: 'Mrs. Peacock', color: '#1a3a6b', costumeHint: 'Royal blue dress, pearls, gloves', icon: '💎' }, { id: 'plum', name: 'Professor Plum', color: '#6b2d6b', costumeHint: 'Purple blazer, glasses', icon: '🔭' }, ] const DEFAULT_ALLOWED_NAMES = 'Jim,Colleen,Rory,Gigi,Meghan,Christine,Bob,Rachel,Chuck' const app = express() app.use(cors()) app.use(express.json()) const DATA_DIR = join(__dirname, 'data') const SUBMISSIONS_PATH = join(DATA_DIR, 'submissions.json') function ensureDataDir() { if (!existsSync(DATA_DIR)) { mkdirSync(DATA_DIR, { recursive: true }) } } function readSubmissions() { ensureDataDir() if (!existsSync(SUBMISSIONS_PATH)) { return [] } try { const data = readFileSync(SUBMISSIONS_PATH, 'utf-8') return JSON.parse(data) } catch { return [] } } function writeSubmissions(submissions) { ensureDataDir() writeFileSync(SUBMISSIONS_PATH, JSON.stringify(submissions, null, 2)) } function getAllowedNames() { const raw = process.env.ALLOWED_NAMES || DEFAULT_ALLOWED_NAMES return raw.split(',').map(s => s.trim()).filter(Boolean) } function isNameAllowed(name) { const allowed = getAllowedNames() const lower = (name || '').trim().toLowerCase() return allowed.some(a => a.toLowerCase() === lower) } function findSubmittedName(name, submissions) { const lower = (name || '').trim().toLowerCase() return submissions.find(s => s.name.toLowerCase() === lower) } const charIds = new Set(CHARACTERS.map(c => c.id)) // API routes app.get('/api/characters', (req, res) => { res.json(CHARACTERS) }) app.get('/api/names', (req, res) => { res.json(getAllowedNames()) }) app.get('/api/submissions', (req, res) => { res.json(readSubmissions()) }) app.post('/api/submissions', (req, res) => { const { name, picks } = req.body const trimmedName = (name || '').trim() if (!trimmedName) { return res.status(400).json({ error: 'Name is required' }) } if (!isNameAllowed(trimmedName)) { return res.status(400).json({ error: "We don't have that name on our guest list." }) } const submissions = readSubmissions() if (findSubmittedName(trimmedName, submissions)) { return res.status(400).json({ error: 'This name has already submitted picks.' }) } if (!Array.isArray(picks) || picks.length !== 3) { return res.status(400).json({ error: 'Exactly 3 picks (1st, 2nd, 3rd choice) are required' }) } if (!picks.every(id => charIds.has(id))) { return res.status(400).json({ error: 'Invalid character IDs in picks' }) } const set = new Set(picks) if (set.size !== 3) { return res.status(400).json({ error: 'Picks must be 3 unique character IDs' }) } submissions.push({ name: trimmedName, picks }) writeSubmissions(submissions) res.json({ success: true }) }) app.post('/api/admin/verify', (req, res) => { const { password } = req.body || {} const expected = process.env.ADMIN_PASSWORD || 'clue2024' if (password === expected) { res.json({ ok: true }) } else { res.status(401).json({ error: 'Incorrect password' }) } }) app.delete('/api/submissions', (req, res) => { const token = req.headers.authorization?.replace(/^Bearer\s+/i, '') || req.headers['x-admin-password'] const expected = process.env.ADMIN_PASSWORD || 'clue2024' if (token !== expected) { return res.status(401).json({ error: 'Unauthorized' }) } writeSubmissions([]) res.json({ success: true }) }) // Production: serve built React app const isProd = process.env.NODE_ENV === 'production' const clientDist = join(__dirname, '..', 'client', 'dist') if (isProd && existsSync(clientDist)) { app.use(express.static(clientDist)) app.get('*', (req, res) => { res.sendFile(join(clientDist, 'index.html')) }) } const PORT = process.env.PORT || 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) })