Initial commit: Who Did It Clue character preference collector

Made-with: Cursor
This commit is contained in:
2026-03-04 22:00:59 -05:00
commit 9b71099658
27 changed files with 4048 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
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}`)
})