Initial commit of step-competition project
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
118
src/database.js
Normal file
118
src/database.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'step_competition.db');
|
||||
const db = new sqlite3.Database(dbPath);
|
||||
|
||||
// Initialize database schema
|
||||
function initializeDatabase() {
|
||||
db.serialize(() => {
|
||||
// Teams table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Participants table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS participants (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
email TEXT UNIQUE,
|
||||
team_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Daily steps table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS daily_steps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
participant_id INTEGER NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
steps INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (participant_id) REFERENCES participants(id),
|
||||
UNIQUE(participant_id, date)
|
||||
)
|
||||
`);
|
||||
|
||||
// Daily monsters table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS daily_monsters (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date DATE NOT NULL UNIQUE,
|
||||
monster_name TEXT NOT NULL,
|
||||
monster_description TEXT,
|
||||
step_goal INTEGER NOT NULL,
|
||||
monster_icon TEXT DEFAULT '👹',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Monster catches table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS monster_catches (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
team_id INTEGER NOT NULL,
|
||||
monster_id INTEGER NOT NULL,
|
||||
caught_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
final_steps INTEGER NOT NULL,
|
||||
FOREIGN KEY (team_id) REFERENCES teams(id),
|
||||
FOREIGN KEY (monster_id) REFERENCES daily_monsters(id),
|
||||
UNIQUE(team_id, monster_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// Create indexes for better query performance
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_date ON daily_steps(date)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_participants_team ON participants(team_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_monsters_date ON daily_monsters(date)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_team ON monster_catches(team_id)`);
|
||||
db.run(`CREATE INDEX IF NOT EXISTS idx_monster_catches_monster ON monster_catches(monster_id)`);
|
||||
|
||||
console.log('Database initialized successfully');
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to run queries with promises
|
||||
function runQuery(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function(err) {
|
||||
if (err) reject(err);
|
||||
else resolve({ id: this.lastID, changes: this.changes });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getQuery(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(sql, params, (err, row) => {
|
||||
if (err) reject(err);
|
||||
else resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function allQuery(sql, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(sql, params, (err, rows) => {
|
||||
if (err) reject(err);
|
||||
else resolve(rows);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
initializeDatabase,
|
||||
runQuery,
|
||||
getQuery,
|
||||
allQuery
|
||||
};
|
||||
581
src/server.js
Normal file
581
src/server.js
Normal file
@@ -0,0 +1,581 @@
|
||||
const express = require('express');
|
||||
const bodyParser = require('body-parser');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const { initializeDatabase, runQuery, getQuery, allQuery } = require('./database');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3060;
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
|
||||
// Initialize database
|
||||
initializeDatabase();
|
||||
|
||||
// ===== TEAM ENDPOINTS =====
|
||||
|
||||
// Get all teams
|
||||
app.get('/api/teams', async (req, res) => {
|
||||
try {
|
||||
const teams = await allQuery('SELECT * FROM teams ORDER BY name');
|
||||
res.json(teams);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new team
|
||||
app.post('/api/teams', async (req, res) => {
|
||||
try {
|
||||
const { name } = req.body;
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: 'Team name is required' });
|
||||
}
|
||||
const result = await runQuery('INSERT INTO teams (name) VALUES (?)', [name]);
|
||||
res.json({ id: result.id, name });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== PARTICIPANT ENDPOINTS =====
|
||||
|
||||
// Get all participants
|
||||
app.get('/api/participants', async (req, res) => {
|
||||
try {
|
||||
const participants = await allQuery(`
|
||||
SELECT p.*, t.name as team_name
|
||||
FROM participants p
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
ORDER BY t.name, p.name
|
||||
`);
|
||||
res.json(participants);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get participants by team
|
||||
app.get('/api/teams/:teamId/participants', async (req, res) => {
|
||||
try {
|
||||
const participants = await allQuery(
|
||||
'SELECT * FROM participants WHERE team_id = ? ORDER BY name',
|
||||
[req.params.teamId]
|
||||
);
|
||||
res.json(participants);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Create a new participant
|
||||
app.post('/api/participants', async (req, res) => {
|
||||
try {
|
||||
const { name, email, team_id } = req.body;
|
||||
if (!name || !team_id) {
|
||||
return res.status(400).json({ error: 'Name and team_id are required' });
|
||||
}
|
||||
const result = await runQuery(
|
||||
'INSERT INTO participants (name, email, team_id) VALUES (?, ?, ?)',
|
||||
[name, email, team_id]
|
||||
);
|
||||
res.json({ id: result.id, name, email, team_id });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Bulk import teams and participants from CSV data
|
||||
app.post('/api/import/csv', async (req, res) => {
|
||||
try {
|
||||
const { data } = req.body; // Expected format: [{team_name, participant_name, email}]
|
||||
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'Invalid data format' });
|
||||
}
|
||||
|
||||
const teamMap = new Map();
|
||||
let teamsCreated = 0;
|
||||
let participantsCreated = 0;
|
||||
let errors = [];
|
||||
|
||||
// Process each row
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
|
||||
try {
|
||||
// Get or create team
|
||||
let teamId = teamMap.get(row.team_name);
|
||||
|
||||
if (!teamId) {
|
||||
// Check if team already exists
|
||||
const existingTeam = await getQuery('SELECT id FROM teams WHERE name = ?', [row.team_name]);
|
||||
|
||||
if (existingTeam) {
|
||||
teamId = existingTeam.id;
|
||||
} else {
|
||||
// Create new team
|
||||
const teamResult = await runQuery('INSERT INTO teams (name) VALUES (?)', [row.team_name]);
|
||||
teamId = teamResult.id;
|
||||
teamsCreated++;
|
||||
}
|
||||
|
||||
teamMap.set(row.team_name, teamId);
|
||||
}
|
||||
|
||||
// Create participant (skip if already exists)
|
||||
const existingParticipant = await getQuery(
|
||||
'SELECT id FROM participants WHERE name = ? AND team_id = ?',
|
||||
[row.participant_name, teamId]
|
||||
);
|
||||
|
||||
if (!existingParticipant) {
|
||||
await runQuery(
|
||||
'INSERT INTO participants (name, email, team_id) VALUES (?, ?, ?)',
|
||||
[row.participant_name, row.email || null, teamId]
|
||||
);
|
||||
participantsCreated++;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Row ${i + 1}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
teamsCreated,
|
||||
participantsCreated,
|
||||
errors: errors.length > 0 ? errors : null
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get participant's step history
|
||||
app.get('/api/participants/:participantId/history', async (req, res) => {
|
||||
try {
|
||||
const history = await allQuery(
|
||||
'SELECT date, steps FROM daily_steps WHERE participant_id = ? ORDER BY date DESC',
|
||||
[req.params.participantId]
|
||||
);
|
||||
res.json(history);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== STEP TRACKING ENDPOINTS =====
|
||||
|
||||
// Get steps for a specific participant and date
|
||||
app.get('/api/steps/:participantId/:date', async (req, res) => {
|
||||
try {
|
||||
const step = await getQuery(
|
||||
'SELECT * FROM daily_steps WHERE participant_id = ? AND date = ?',
|
||||
[req.params.participantId, req.params.date]
|
||||
);
|
||||
res.json(step || { steps: 0 });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Submit or update steps for a specific date
|
||||
app.post('/api/steps', async (req, res) => {
|
||||
try {
|
||||
const { participant_id, date, steps } = req.body;
|
||||
if (!participant_id || !date || steps === undefined) {
|
||||
return res.status(400).json({ error: 'participant_id, date, and steps are required' });
|
||||
}
|
||||
|
||||
// Check if entry exists
|
||||
const existing = await getQuery(
|
||||
'SELECT id FROM daily_steps WHERE participant_id = ? AND date = ?',
|
||||
[participant_id, date]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// Update existing entry
|
||||
await runQuery(
|
||||
'UPDATE daily_steps SET steps = ?, updated_at = CURRENT_TIMESTAMP WHERE participant_id = ? AND date = ?',
|
||||
[steps, participant_id, date]
|
||||
);
|
||||
res.json({ message: 'Steps updated', participant_id, date, steps });
|
||||
} else {
|
||||
// Create new entry
|
||||
const result = await runQuery(
|
||||
'INSERT INTO daily_steps (participant_id, date, steps) VALUES (?, ?, ?)',
|
||||
[participant_id, date, steps]
|
||||
);
|
||||
res.json({ id: result.id, participant_id, date, steps });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== LEADERBOARD ENDPOINTS =====
|
||||
|
||||
// Get individual leaderboard (total steps per person)
|
||||
app.get('/api/leaderboard/individual', async (req, res) => {
|
||||
try {
|
||||
const startDate = req.query.start_date;
|
||||
const endDate = req.query.end_date;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
p.id,
|
||||
p.name,
|
||||
t.name as team_name,
|
||||
t.id as team_id,
|
||||
COALESCE(SUM(ds.steps), 0) as total_steps,
|
||||
COUNT(DISTINCT ds.date) as days_logged
|
||||
FROM participants p
|
||||
LEFT JOIN daily_steps ds ON p.id = ds.participant_id
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
if (startDate && endDate) {
|
||||
query += ' WHERE ds.date BETWEEN ? AND ?';
|
||||
params.push(startDate, endDate);
|
||||
}
|
||||
|
||||
query += ' GROUP BY p.id, p.name, t.name, t.id ORDER BY total_steps DESC';
|
||||
|
||||
const leaderboard = await allQuery(query, params);
|
||||
res.json(leaderboard);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get team leaderboard (total steps per team)
|
||||
app.get('/api/leaderboard/team', async (req, res) => {
|
||||
try {
|
||||
const startDate = req.query.start_date;
|
||||
const endDate = req.query.end_date;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
t.id,
|
||||
t.name as team_name,
|
||||
COALESCE(SUM(ds.steps), 0) as total_steps,
|
||||
COUNT(DISTINCT CASE WHEN COALESCE(ds.steps, 0) > 0 THEN p.id END) as active_count,
|
||||
COUNT(DISTINCT p.id) as member_count,
|
||||
COALESCE(AVG(ds.steps), 0) as avg_steps_per_entry
|
||||
FROM teams t
|
||||
LEFT JOIN participants p ON t.id = p.team_id
|
||||
LEFT JOIN daily_steps ds ON p.id = ds.participant_id
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
if (startDate && endDate) {
|
||||
query += ' WHERE ds.date BETWEEN ? AND ?';
|
||||
params.push(startDate, endDate);
|
||||
}
|
||||
|
||||
query += ' GROUP BY t.id, t.name ORDER BY total_steps DESC';
|
||||
|
||||
const leaderboard = await allQuery(query, params);
|
||||
res.json(leaderboard);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get daily progress for all teams
|
||||
app.get('/api/progress/daily', async (req, res) => {
|
||||
try {
|
||||
const startDate = req.query.start_date;
|
||||
const endDate = req.query.end_date;
|
||||
|
||||
let query = `
|
||||
SELECT
|
||||
ds.date,
|
||||
t.id as team_id,
|
||||
t.name as team_name,
|
||||
SUM(ds.steps) as daily_steps
|
||||
FROM daily_steps ds
|
||||
JOIN participants p ON ds.participant_id = p.id
|
||||
JOIN teams t ON p.team_id = t.id
|
||||
`;
|
||||
|
||||
const params = [];
|
||||
if (startDate && endDate) {
|
||||
query += ' WHERE ds.date BETWEEN ? AND ?';
|
||||
params.push(startDate, endDate);
|
||||
}
|
||||
|
||||
query += ' GROUP BY ds.date, t.id, t.name ORDER BY ds.date, t.name';
|
||||
|
||||
const progress = await allQuery(query, params);
|
||||
res.json(progress);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get date range of competition
|
||||
app.get('/api/competition/dates', async (req, res) => {
|
||||
try {
|
||||
const result = await getQuery(`
|
||||
SELECT
|
||||
MIN(date) as start_date,
|
||||
MAX(date) as end_date
|
||||
FROM daily_steps
|
||||
`);
|
||||
res.json(result || { start_date: null, end_date: null });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ===== DAILY MONSTERS ENDPOINTS =====
|
||||
|
||||
// Get all monsters (timeline view)
|
||||
app.get('/api/monsters', async (req, res) => {
|
||||
try {
|
||||
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
|
||||
res.json(monsters);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// IMPORTANT: Timeline route must come BEFORE the :date route to avoid conflicts
|
||||
// Get all monsters with catch status
|
||||
app.get('/api/monsters/timeline', async (req, res) => {
|
||||
try {
|
||||
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
|
||||
|
||||
// For each monster, check if teams caught it and update catches table
|
||||
const timeline = await Promise.all(monsters.map(async (monster) => {
|
||||
// Get team totals for this monster's date
|
||||
const teamTotals = await allQuery(`
|
||||
SELECT
|
||||
t.id as team_id,
|
||||
t.name as team_name,
|
||||
COALESCE(SUM(ds.steps), 0) as total_steps
|
||||
FROM teams t
|
||||
LEFT JOIN participants p ON t.id = p.team_id
|
||||
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ?
|
||||
GROUP BY t.id, t.name
|
||||
`, [monster.date]);
|
||||
|
||||
// Check and record catches for teams that met the goal
|
||||
for (const team of teamTotals) {
|
||||
if (team.total_steps >= monster.step_goal) {
|
||||
// Check if catch already recorded
|
||||
const existingCatch = await getQuery(
|
||||
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
|
||||
[team.team_id, monster.id]
|
||||
);
|
||||
|
||||
if (!existingCatch) {
|
||||
// Record the catch
|
||||
await runQuery(
|
||||
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
|
||||
[team.team_id, monster.id, team.total_steps]
|
||||
);
|
||||
} else {
|
||||
// Update the final_steps if it changed
|
||||
await runQuery(
|
||||
'UPDATE monster_catches SET final_steps = ? WHERE team_id = ? AND monster_id = ?',
|
||||
[team.total_steps, team.team_id, monster.id]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now get the updated catches
|
||||
const catches = await allQuery(`
|
||||
SELECT
|
||||
mc.*,
|
||||
t.name as team_name
|
||||
FROM monster_catches mc
|
||||
JOIN teams t ON mc.team_id = t.id
|
||||
WHERE mc.monster_id = ?
|
||||
ORDER BY mc.caught_at
|
||||
`, [monster.id]);
|
||||
|
||||
return {
|
||||
...monster,
|
||||
catches
|
||||
};
|
||||
}));
|
||||
|
||||
res.json(timeline);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get specific day's monster
|
||||
app.get('/api/monsters/:date', async (req, res) => {
|
||||
try {
|
||||
const monster = await getQuery(
|
||||
'SELECT * FROM daily_monsters WHERE date = ?',
|
||||
[req.params.date]
|
||||
);
|
||||
res.json(monster || null);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Add/import monsters (bulk CSV import)
|
||||
app.post('/api/monsters/import', async (req, res) => {
|
||||
try {
|
||||
const { data } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
|
||||
|
||||
if (!data || !Array.isArray(data)) {
|
||||
return res.status(400).json({ error: 'Invalid data format' });
|
||||
}
|
||||
|
||||
let monstersCreated = 0;
|
||||
let monstersUpdated = 0;
|
||||
let errors = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row = data[i];
|
||||
|
||||
try {
|
||||
if (!row.date || !row.monster_name || !row.step_goal) {
|
||||
errors.push(`Row ${i + 1}: Missing required fields (date, monster_name, step_goal)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if monster already exists for this date
|
||||
const existing = await getQuery(
|
||||
'SELECT id FROM daily_monsters WHERE date = ?',
|
||||
[row.date]
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
// Update existing monster
|
||||
await runQuery(
|
||||
'UPDATE daily_monsters SET monster_name = ?, monster_description = ?, step_goal = ?, monster_icon = ? WHERE date = ?',
|
||||
[row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹', row.date]
|
||||
);
|
||||
monstersUpdated++;
|
||||
} else {
|
||||
// Create new monster
|
||||
await runQuery(
|
||||
'INSERT INTO daily_monsters (date, monster_name, monster_description, step_goal, monster_icon) VALUES (?, ?, ?, ?, ?)',
|
||||
[row.date, row.monster_name, row.monster_description || null, parseInt(row.step_goal), row.monster_icon || '👹']
|
||||
);
|
||||
monstersCreated++;
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(`Row ${i + 1}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
monstersCreated,
|
||||
monstersUpdated,
|
||||
errors: errors.length > 0 ? errors : null
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get which teams caught a specific monster
|
||||
app.get('/api/monsters/:date/catches', async (req, res) => {
|
||||
try {
|
||||
const monster = await getQuery(
|
||||
'SELECT id FROM daily_monsters WHERE date = ?',
|
||||
[req.params.date]
|
||||
);
|
||||
|
||||
if (!monster) {
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
const catches = await allQuery(`
|
||||
SELECT
|
||||
mc.*,
|
||||
t.name as team_name
|
||||
FROM monster_catches mc
|
||||
JOIN teams t ON mc.team_id = t.id
|
||||
WHERE mc.monster_id = ?
|
||||
ORDER BY mc.caught_at
|
||||
`, [monster.id]);
|
||||
|
||||
res.json(catches);
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current day's monster + team progress
|
||||
// Note: Since people enter steps the following day, "today's challenge" is actually yesterday's date
|
||||
app.get('/api/monsters/status/today', async (req, res) => {
|
||||
try {
|
||||
// Get yesterday's date (the challenge people are working on today)
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() - 1);
|
||||
const yesterday = now.toISOString().split('T')[0];
|
||||
|
||||
// Get yesterday's monster (today's challenge)
|
||||
const monster = await getQuery(
|
||||
'SELECT * FROM daily_monsters WHERE date = ?',
|
||||
[yesterday]
|
||||
);
|
||||
|
||||
if (!monster) {
|
||||
return res.json({ monster: null, progress: [] });
|
||||
}
|
||||
|
||||
// Get team progress for yesterday's date
|
||||
const progress = await allQuery(`
|
||||
SELECT
|
||||
t.id as team_id,
|
||||
t.name as team_name,
|
||||
COALESCE(SUM(ds.steps), 0) as total_steps,
|
||||
CASE WHEN COALESCE(SUM(ds.steps), 0) >= ? THEN 1 ELSE 0 END as caught
|
||||
FROM teams t
|
||||
LEFT JOIN participants p ON t.id = p.team_id
|
||||
LEFT JOIN daily_steps ds ON p.id = ds.participant_id AND ds.date = ?
|
||||
GROUP BY t.id, t.name
|
||||
ORDER BY total_steps DESC
|
||||
`, [monster.step_goal, yesterday]);
|
||||
|
||||
// Check and record new catches
|
||||
for (const team of progress) {
|
||||
if (team.caught) {
|
||||
// Check if catch already recorded
|
||||
const existingCatch = await getQuery(
|
||||
'SELECT id FROM monster_catches WHERE team_id = ? AND monster_id = ?',
|
||||
[team.team_id, monster.id]
|
||||
);
|
||||
|
||||
if (!existingCatch) {
|
||||
// Record the catch
|
||||
await runQuery(
|
||||
'INSERT INTO monster_catches (team_id, monster_id, final_steps) VALUES (?, ?, ?)',
|
||||
[team.team_id, monster.id, team.total_steps]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ monster, progress });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Start server on all interfaces (0.0.0.0) to allow Tailscale access
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`Step Competition app running on:`);
|
||||
console.log(` - Local: http://localhost:${PORT}`);
|
||||
console.log(` - Network: http://0.0.0.0:${PORT}`);
|
||||
console.log(` - Tailscale: Access via your Tailscale IP on port ${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user