Files
jackboxpartypack-gamepicker/backend/routes/games.js
2025-10-30 04:27:43 -04:00

365 lines
9.7 KiB
JavaScript

const express = require('express');
const { authenticateToken } = require('../middleware/auth');
const db = require('../database');
const { stringify } = require('csv-stringify/sync');
const { parse } = require('csv-parse/sync');
const router = express.Router();
// Get all games with optional filters
router.get('/', (req, res) => {
try {
const {
enabled,
minPlayers,
maxPlayers,
playerCount,
drawing,
length,
familyFriendly,
pack
} = req.query;
let query = 'SELECT * FROM games WHERE 1=1';
const params = [];
if (enabled !== undefined) {
query += ' AND enabled = ?';
params.push(enabled === 'true' ? 1 : 0);
}
if (playerCount) {
const count = parseInt(playerCount);
query += ' AND min_players <= ? AND max_players >= ?';
params.push(count, count);
}
if (drawing === 'only') {
query += ' AND game_type = ?';
params.push('Drawing');
} else if (drawing === 'exclude') {
query += ' AND (game_type != ? OR game_type IS NULL)';
params.push('Drawing');
}
if (length) {
if (length === 'short') {
query += ' AND (length_minutes <= 15 OR length_minutes IS NULL)';
} else if (length === 'medium') {
query += ' AND length_minutes > 15 AND length_minutes <= 25';
} else if (length === 'long') {
query += ' AND length_minutes > 25';
}
}
if (familyFriendly !== undefined) {
query += ' AND family_friendly = ?';
params.push(familyFriendly === 'true' ? 1 : 0);
}
if (pack) {
query += ' AND pack_name = ?';
params.push(pack);
}
query += ' ORDER BY pack_name, title';
const stmt = db.prepare(query);
const games = stmt.all(...params);
res.json(games);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get single game by ID
router.get('/:id', (req, res) => {
try {
const game = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
if (!game) {
return res.status(404).json({ error: 'Game not found' });
}
res.json(game);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Create new game (admin only)
router.post('/', authenticateToken, (req, res) => {
try {
const {
pack_name,
title,
min_players,
max_players,
length_minutes,
has_audience,
family_friendly,
game_type,
secondary_type
} = req.body;
if (!pack_name || !title || !min_players || !max_players) {
return res.status(400).json({ error: 'Missing required fields' });
}
const stmt = db.prepare(`
INSERT INTO games (
pack_name, title, min_players, max_players, length_minutes,
has_audience, family_friendly, game_type, secondary_type
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
pack_name,
title,
min_players,
max_players,
length_minutes || null,
has_audience ? 1 : 0,
family_friendly ? 1 : 0,
game_type || null,
secondary_type || null
);
const newGame = db.prepare('SELECT * FROM games WHERE id = ?').get(result.lastInsertRowid);
res.status(201).json(newGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Update game (admin only)
router.put('/:id', authenticateToken, (req, res) => {
try {
const {
pack_name,
title,
min_players,
max_players,
length_minutes,
has_audience,
family_friendly,
game_type,
secondary_type,
enabled
} = req.body;
const stmt = db.prepare(`
UPDATE games SET
pack_name = COALESCE(?, pack_name),
title = COALESCE(?, title),
min_players = COALESCE(?, min_players),
max_players = COALESCE(?, max_players),
length_minutes = ?,
has_audience = COALESCE(?, has_audience),
family_friendly = COALESCE(?, family_friendly),
game_type = ?,
secondary_type = ?,
enabled = COALESCE(?, enabled)
WHERE id = ?
`);
const result = stmt.run(
pack_name || null,
title || null,
min_players || null,
max_players || null,
length_minutes !== undefined ? length_minutes : null,
has_audience !== undefined ? (has_audience ? 1 : 0) : null,
family_friendly !== undefined ? (family_friendly ? 1 : 0) : null,
game_type !== undefined ? game_type : null,
secondary_type !== undefined ? secondary_type : null,
enabled !== undefined ? (enabled ? 1 : 0) : null,
req.params.id
);
if (result.changes === 0) {
return res.status(404).json({ error: 'Game not found' });
}
const updatedGame = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
res.json(updatedGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete game (admin only)
router.delete('/:id', authenticateToken, (req, res) => {
try {
const stmt = db.prepare('DELETE FROM games WHERE id = ?');
const result = stmt.run(req.params.id);
if (result.changes === 0) {
return res.status(404).json({ error: 'Game not found' });
}
res.json({ message: 'Game deleted successfully' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Toggle game enabled status (admin only)
router.patch('/:id/toggle', authenticateToken, (req, res) => {
try {
const game = db.prepare('SELECT enabled FROM games WHERE id = ?').get(req.params.id);
if (!game) {
return res.status(404).json({ error: 'Game not found' });
}
const newStatus = game.enabled === 1 ? 0 : 1;
db.prepare('UPDATE games SET enabled = ? WHERE id = ?').run(newStatus, req.params.id);
const updatedGame = db.prepare('SELECT * FROM games WHERE id = ?').get(req.params.id);
res.json(updatedGame);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Get list of unique pack names
router.get('/meta/packs', (req, res) => {
try {
const packs = db.prepare(`
SELECT
pack_name,
COUNT(*) as game_count,
SUM(enabled) as enabled_count
FROM games
GROUP BY pack_name
ORDER BY pack_name
`).all();
res.json(packs);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Toggle entire pack (admin only)
router.patch('/packs/:name/toggle', authenticateToken, (req, res) => {
try {
const { enabled } = req.body;
if (enabled === undefined) {
return res.status(400).json({ error: 'enabled status required' });
}
const stmt = db.prepare('UPDATE games SET enabled = ? WHERE pack_name = ?');
const result = stmt.run(enabled ? 1 : 0, req.params.name);
res.json({
message: `Pack ${enabled ? 'enabled' : 'disabled'} successfully`,
gamesAffected: result.changes
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Export games to CSV (admin only)
router.get('/export/csv', authenticateToken, (req, res) => {
try {
const games = db.prepare('SELECT * FROM games ORDER BY pack_name, title').all();
const csvData = games.map(game => ({
'Game Pack': game.pack_name,
'Game Title': game.title,
'Min. Players': game.min_players,
'Max. Players': game.max_players,
'Length': game.length_minutes ? `${game.length_minutes} minutes` : '????',
'Audience': game.has_audience ? 'Yes' : 'No',
'Family Friendly?': game.family_friendly ? 'Yes' : 'No',
'Game Type': game.game_type || '',
'Secondary Type': game.secondary_type || ''
}));
const csv = stringify(csvData, { header: true });
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=games-export.csv');
res.send(csv);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Import games from CSV (admin only)
router.post('/import/csv', authenticateToken, (req, res) => {
try {
const { csvData, mode } = req.body; // mode: 'append' or 'replace'
if (!csvData) {
return res.status(400).json({ error: 'CSV data required' });
}
const records = parse(csvData, {
columns: true,
skip_empty_lines: true,
trim: true
});
if (mode === 'replace') {
db.prepare('DELETE FROM games').run();
}
const insert = db.prepare(`
INSERT INTO games (
pack_name, title, min_players, max_players, length_minutes,
has_audience, family_friendly, game_type, secondary_type, enabled
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`);
const insertMany = db.transaction((games) => {
for (const game of games) {
insert.run(
game['Game Pack'],
game['Game Title'],
parseInt(game['Min. Players']) || 1,
parseInt(game['Max. Players']) || 8,
parseLengthMinutes(game['Length']),
parseBoolean(game['Audience']),
parseBoolean(game['Family Friendly?']),
game['Game Type'] || null,
game['Secondary Type'] || null
);
}
});
insertMany(records);
res.json({
message: `Successfully imported ${records.length} games`,
count: records.length,
mode
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
function parseLengthMinutes(lengthStr) {
if (!lengthStr || lengthStr === '????' || lengthStr === '?') {
return null;
}
const match = lengthStr.match(/(\d+)/);
return match ? parseInt(match[1]) : null;
}
function parseBoolean(value) {
if (!value || value === '?' || value === '????') {
return 0;
}
return value.toLowerCase() === 'yes' ? 1 : 0;
}
module.exports = router;