Compare commits
9 Commits
05e4a505b3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| abbefd0e80 | |||
| 7995daa6e5 | |||
| 14baafe1aa | |||
| acf5d4bc5c | |||
| cc5b064e81 | |||
| 1e19e5e2dd | |||
| 13ba04ae48 | |||
| a26c92ff2b | |||
| 04891bf449 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,6 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
step_competition.db
|
# step_competition.db
|
||||||
*.db
|
# *.db
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
date,monster_name,monster_description,step_goal,monster_icon
|
date,monster_name,monster_description,step_goal,monster_icon
|
||||||
2025-10-17,Test Monster,Can you catch this spooky creature?,30000,👻
|
|
||||||
2025-10-20,Dracula,The Count demands your steps!,60000,🧛
|
2025-10-20,Dracula,The Count demands your steps!,60000,🧛
|
||||||
2025-10-21,Freddy Krueger,Don't fall asleep on your steps!,48000,🔪
|
2025-10-21,Freddy Krueger,Don't fall asleep on your steps!,48000,🔪
|
||||||
2025-10-22,Mummy,Unwrap your potential!,48000,🧟
|
2025-10-22,Mummy,Unwrap your potential!,48000,🧟
|
||||||
2025-10-23,Aliens,Take steps to another world!,72000,👽
|
2025-10-23,Xenomorph,The perfect organism hunts in the shadows!,72000,👽
|
||||||
2025-10-24,Predator,Hunt down those steps!,66000,👹
|
2025-10-24,Predator,Hunt down those steps!,66000,👹
|
||||||
|
2025-10-27,Blob,This oozing creature is slow and easy to catch!,36000,🟢
|
||||||
|
2025-10-28,Godzilla,Each massive stride covers miles!,72000,🦖
|
||||||
|
2025-10-29,Werewolf,Fast and agile under the full moon!,78000,🐺
|
||||||
|
2025-10-30,Frankenstein's Monster,A lumbering creation seeks companionship!,48000,⚡
|
||||||
|
2025-10-31,Dr. Jekyll/Mr. Hyde,Two personalities - one fast chase!,66000,🧪
|
||||||
|
|||||||
|
143
public/app.js
143
public/app.js
@@ -1,10 +1,39 @@
|
|||||||
const API_URL = window.location.origin;
|
const API_URL = window.location.origin;
|
||||||
|
|
||||||
|
// Humorous step tips
|
||||||
|
const STEP_TIPS = [
|
||||||
|
"Never underestimate a 5-minute dance break. Your step count (and mood) will thank you.",
|
||||||
|
"Don't let the high-steppers get you down. Focus on crushing whoever's ranked right above you.",
|
||||||
|
"Pacing while on phone calls counts. Your colleagues will understand the extra enthusiasm.",
|
||||||
|
"Pro tip: Park in the farthest spot. It's not laziness, it's strategy.",
|
||||||
|
"Taking the stairs? That's just vertical stepping with extra credit.",
|
||||||
|
"Your fitness tracker thinks you're training for a marathon. Let's not disappoint it.",
|
||||||
|
"Remember: Every journey to the fridge and back is an opportunity for greatness.",
|
||||||
|
"Walking meetings are just step competitions in disguise. You're welcome.",
|
||||||
|
"Forgot something upstairs? That's not forgetfulness, that's bonus cardio.",
|
||||||
|
"Why run when you can aggressively walk? Same steps, less judgment.",
|
||||||
|
"Your dog wants a walk. Your step count wants a walk. It's basically a win-win-woof.",
|
||||||
|
"A monster a day keeps the last place away.",
|
||||||
|
"Reading this tip instead of walking? Bold strategy. Let's see how it plays out.",
|
||||||
|
"Pro tip about tips: They're less effective if you're sitting down while reading them.",
|
||||||
|
"Winners log their steps before noon. Legends log them before breakfast. -Mike (probably)",
|
||||||
|
"Infrastructure as a Service is down? Legs as a Service is always running.",
|
||||||
|
"Strapping the tracker to a ceiling fan was attempted in 2019. It didn't work then either.",
|
||||||
|
"Following your dog around the house may not be the most efficient way to get steps, but it's definitely the most confusing for the dog.",
|
||||||
|
"Dancing in the school pickup line may not be the most efficient way to get steps, but it's definitely the most embarrassing for your kids.",
|
||||||
|
"4 out of 5 dentists agree: this has nothing to do with teeth, but walking is still good.",
|
||||||
|
"Rumor has it the winner of the last competition was just trying to learn the latest Katseye choreo.",
|
||||||
|
"Alexa can't count your steps - make your light switch your new favorite app.",
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
// State
|
// State
|
||||||
let teams = [];
|
let teams = [];
|
||||||
let participants = [];
|
let participants = [];
|
||||||
let charts = {};
|
let charts = {};
|
||||||
let selectedParticipant = null;
|
let selectedParticipant = null;
|
||||||
|
let currentTipIndex = 0;
|
||||||
|
let tipRotationInterval = null;
|
||||||
|
|
||||||
// Initialize app
|
// Initialize app
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
@@ -13,8 +42,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
setDefaultDate();
|
setDefaultDate();
|
||||||
handleRouting();
|
handleRouting();
|
||||||
|
checkAdminMode();
|
||||||
|
initializeTipRotation();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Admin mode
|
||||||
|
function checkAdminMode() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.get('admin') === 'true') {
|
||||||
|
document.querySelectorAll('.import-section').forEach(el => {
|
||||||
|
el.style.display = 'block';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Routing
|
// Routing
|
||||||
function handleRouting() {
|
function handleRouting() {
|
||||||
// Handle initial load and hash changes
|
// Handle initial load and hash changes
|
||||||
@@ -63,15 +104,17 @@ function initializeTabs() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default date to today
|
// Set default date to yesterday (since we enter steps the day after)
|
||||||
function setDefaultDate() {
|
function setDefaultDate() {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const yesterdayStr = yesterday.toISOString().split('T')[0];
|
||||||
const dateInput = document.getElementById('step-date');
|
const dateInput = document.getElementById('step-date');
|
||||||
|
|
||||||
dateInput.value = today;
|
dateInput.value = yesterdayStr;
|
||||||
// Set max date to October 31, 2025
|
// Set max date to October 31, 2025
|
||||||
dateInput.max = '2025-10-31';
|
dateInput.max = '2025-10-31';
|
||||||
dateInput.min = '2025-10-15';
|
dateInput.min = '2025-10-20';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
@@ -288,10 +331,9 @@ async function submitSteps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate date is not after October 31, 2025
|
// Validate date is not after October 31, 2025
|
||||||
const contestEndDate = new Date('2025-10-31');
|
const contestEndDate = '2025-10-31';
|
||||||
const selectedDate = new Date(date + 'T00:00:00');
|
|
||||||
|
|
||||||
if (selectedDate > contestEndDate) {
|
if (date > contestEndDate) {
|
||||||
showMessage(messageDiv, 'Contest ended on October 31, 2025. Cannot enter steps after this date.', 'error');
|
showMessage(messageDiv, 'Contest ended on October 31, 2025. Cannot enter steps after this date.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -823,6 +865,7 @@ function renderMonsterTimeline(timeline) {
|
|||||||
// Monster CSV Import
|
// Monster CSV Import
|
||||||
async function importMonsters() {
|
async function importMonsters() {
|
||||||
const fileInput = document.getElementById('monster-csv-file');
|
const fileInput = document.getElementById('monster-csv-file');
|
||||||
|
const overwriteCheckbox = document.getElementById('monster-overwrite');
|
||||||
const messageDiv = document.getElementById('monster-import-message');
|
const messageDiv = document.getElementById('monster-import-message');
|
||||||
|
|
||||||
if (!fileInput.files || !fileInput.files[0]) {
|
if (!fileInput.files || !fileInput.files[0]) {
|
||||||
@@ -830,6 +873,20 @@ async function importMonsters() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const overwrite = overwriteCheckbox.checked;
|
||||||
|
console.log('Overwrite checkbox checked:', overwrite);
|
||||||
|
|
||||||
|
// Confirm if overwrite is selected
|
||||||
|
if (overwrite) {
|
||||||
|
const confirmed = confirm(
|
||||||
|
'WARNING: This will DELETE all existing monsters and monster catches!\n\n' +
|
||||||
|
'Are you sure you want to proceed?'
|
||||||
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const file = fileInput.files[0];
|
const file = fileInput.files[0];
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
@@ -846,18 +903,22 @@ async function importMonsters() {
|
|||||||
return { date, monster_name, monster_description, step_goal, monster_icon };
|
return { date, monster_name, monster_description, step_goal, monster_icon };
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await apiCall('/api/monsters/import', 'POST', { data });
|
const result = await apiCall('/api/monsters/import', 'POST', { data, overwrite });
|
||||||
|
|
||||||
showMessage(
|
let successMessage;
|
||||||
messageDiv,
|
if (overwrite) {
|
||||||
`Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`,
|
successMessage = `Success! Deleted ${result.monstersDeleted} old monsters, created ${result.monstersCreated} new monsters`;
|
||||||
'success'
|
} else {
|
||||||
);
|
successMessage = `Success! Created ${result.monstersCreated} monsters, updated ${result.monstersUpdated}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showMessage(messageDiv, successMessage, 'success');
|
||||||
|
|
||||||
// Reload monsters
|
// Reload monsters
|
||||||
await loadDailyMonsters();
|
await loadDailyMonsters();
|
||||||
|
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
|
overwriteCheckbox.checked = false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showMessage(messageDiv, `Error: ${error.message}`, 'error');
|
showMessage(messageDiv, `Error: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
@@ -886,3 +947,59 @@ function formatDate(dateStr) {
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tip rotation functionality
|
||||||
|
function shuffleTips(array) {
|
||||||
|
// Fisher-Yates shuffle algorithm for true randomization
|
||||||
|
const shuffled = [...array];
|
||||||
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
|
}
|
||||||
|
return shuffled;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeTipRotation() {
|
||||||
|
// Clear any existing interval to prevent duplicates
|
||||||
|
if (tipRotationInterval) {
|
||||||
|
clearInterval(tipRotationInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shuffle the tips array for randomized order
|
||||||
|
window.shuffledTips = shuffleTips(STEP_TIPS);
|
||||||
|
currentTipIndex = 0;
|
||||||
|
|
||||||
|
// Display first tip
|
||||||
|
displayTip();
|
||||||
|
|
||||||
|
// Rotate every 20 seconds
|
||||||
|
tipRotationInterval = setInterval(rotateTip, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayTip() {
|
||||||
|
const tipTextElement = document.getElementById('tip-text');
|
||||||
|
if (!tipTextElement || !window.shuffledTips) return;
|
||||||
|
|
||||||
|
tipTextElement.textContent = window.shuffledTips[currentTipIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rotateTip() {
|
||||||
|
const tipTextElement = document.getElementById('tip-text');
|
||||||
|
if (!tipTextElement || !window.shuffledTips) return;
|
||||||
|
|
||||||
|
// Fade out
|
||||||
|
tipTextElement.classList.add('fade-out');
|
||||||
|
|
||||||
|
// After fade out, change text and fade in
|
||||||
|
setTimeout(() => {
|
||||||
|
currentTipIndex = (currentTipIndex + 1) % window.shuffledTips.length;
|
||||||
|
|
||||||
|
// If we've completed a full cycle, re-shuffle for next cycle
|
||||||
|
if (currentTipIndex === 0) {
|
||||||
|
window.shuffledTips = shuffleTips(STEP_TIPS);
|
||||||
|
}
|
||||||
|
|
||||||
|
tipTextElement.textContent = window.shuffledTips[currentTipIndex];
|
||||||
|
tipTextElement.classList.remove('fade-out');
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<!-- Tip Banner -->
|
||||||
|
<div id="tip-banner" class="tip-banner">
|
||||||
|
<span class="tip-icon">💡</span>
|
||||||
|
<span id="tip-text" class="tip-text"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Log Your Steps Tab -->
|
<!-- Log Your Steps Tab -->
|
||||||
<div id="log-tab" class="tab-content active">
|
<div id="log-tab" class="tab-content active">
|
||||||
<div class="two-column-layout">
|
<div class="two-column-layout">
|
||||||
@@ -96,16 +102,19 @@
|
|||||||
<strong>⏰ Important:</strong> The <strong>"Active Challenge"</strong> shows yesterday's monster.
|
<strong>⏰ Important:</strong> The <strong>"Active Challenge"</strong> shows yesterday's monster.
|
||||||
Log your steps from yesterday to see if your team caught it!
|
Log your steps from yesterday to see if your team caught it!
|
||||||
</p>
|
</p>
|
||||||
<p style="margin: 8px 0; line-height: 1.6; font-size: 14px; color: #aaa;">
|
|
||||||
💡 You can enter steps for any past date, and catches will be updated automatically.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Monster CSV Import Section -->
|
<!-- Monster CSV Import Section -->
|
||||||
<div class="import-section">
|
<div class="import-section" style="display: none;">
|
||||||
<h3>Admin: Import Monsters</h3>
|
<h3>Admin: Import Monsters</h3>
|
||||||
<p class="help-text">Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon</p>
|
<p class="help-text">Upload CSV with columns: date, monster_name, monster_description, step_goal, monster_icon</p>
|
||||||
<input type="file" id="monster-csv-file" accept=".csv">
|
<input type="file" id="monster-csv-file" accept=".csv">
|
||||||
|
<div style="margin: 10px 0;">
|
||||||
|
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||||
|
<input type="checkbox" id="monster-overwrite" style="cursor: pointer;">
|
||||||
|
<span style="color: #ff7700;">Overwrite all existing monsters (deletes all current monsters and catches)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<button id="import-monsters" class="btn-secondary">Import Monsters</button>
|
<button id="import-monsters" class="btn-secondary">Import Monsters</button>
|
||||||
<div id="monster-import-message" class="message"></div>
|
<div id="monster-import-message" class="message"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,6 +39,40 @@ header h1 {
|
|||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tip Banner */
|
||||||
|
.tip-banner {
|
||||||
|
background: linear-gradient(135deg, #8a2be2 0%, #ff7700 100%);
|
||||||
|
padding: 15px 30px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
border-bottom: 2px solid #ff7700;
|
||||||
|
box-shadow: 0 4px 15px rgba(138, 43, 226, 0.3);
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text.fade-out {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.header-logo {
|
.header-logo {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -720,6 +754,19 @@ nav {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tip-banner {
|
||||||
|
padding: 12px 20px;
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-grid {
|
.stats-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,18 +55,25 @@ function initializeDatabase() {
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Monster catches table
|
// Monster catches view - derived from actual step data
|
||||||
|
// Drop the old table/view if it exists and recreate as a view
|
||||||
|
db.run(`DROP VIEW IF EXISTS monster_catches`);
|
||||||
|
db.run(`DROP TABLE IF EXISTS monster_catches`);
|
||||||
db.run(`
|
db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS monster_catches (
|
CREATE VIEW IF NOT EXISTS monster_catches AS
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
SELECT
|
||||||
team_id INTEGER NOT NULL,
|
dm.id as monster_id,
|
||||||
monster_id INTEGER NOT NULL,
|
dm.date,
|
||||||
caught_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
t.id as team_id,
|
||||||
final_steps INTEGER NOT NULL,
|
t.name as team_name,
|
||||||
FOREIGN KEY (team_id) REFERENCES teams(id),
|
COALESCE(SUM(ds.steps), 0) as final_steps,
|
||||||
FOREIGN KEY (monster_id) REFERENCES daily_monsters(id),
|
dm.step_goal
|
||||||
UNIQUE(team_id, monster_id)
|
FROM daily_monsters dm
|
||||||
)
|
CROSS JOIN 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 = dm.date
|
||||||
|
GROUP BY dm.id, dm.date, t.id, t.name, dm.step_goal
|
||||||
|
HAVING final_steps >= dm.step_goal
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Create indexes for better query performance
|
// Create indexes for better query performance
|
||||||
@@ -74,8 +81,6 @@ function initializeDatabase() {
|
|||||||
db.run(`CREATE INDEX IF NOT EXISTS idx_daily_steps_participant ON daily_steps(participant_id)`);
|
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_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_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');
|
console.log('Database initialized successfully');
|
||||||
});
|
});
|
||||||
|
|||||||
118
src/server.js
118
src/server.js
@@ -352,54 +352,17 @@ app.get('/api/monsters/timeline', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
|
const monsters = await allQuery('SELECT * FROM daily_monsters ORDER BY date');
|
||||||
|
|
||||||
// For each monster, check if teams caught it and update catches table
|
// For each monster, get catches from the view
|
||||||
const timeline = await Promise.all(monsters.map(async (monster) => {
|
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(`
|
const catches = await allQuery(`
|
||||||
SELECT
|
SELECT
|
||||||
mc.*,
|
monster_id,
|
||||||
t.name as team_name
|
team_id,
|
||||||
FROM monster_catches mc
|
team_name,
|
||||||
JOIN teams t ON mc.team_id = t.id
|
final_steps
|
||||||
WHERE mc.monster_id = ?
|
FROM monster_catches
|
||||||
ORDER BY mc.caught_at
|
WHERE monster_id = ?
|
||||||
|
ORDER BY final_steps DESC
|
||||||
`, [monster.id]);
|
`, [monster.id]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -430,7 +393,7 @@ app.get('/api/monsters/:date', async (req, res) => {
|
|||||||
// Add/import monsters (bulk CSV import)
|
// Add/import monsters (bulk CSV import)
|
||||||
app.post('/api/monsters/import', async (req, res) => {
|
app.post('/api/monsters/import', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { data } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
|
const { data, overwrite } = req.body; // Expected format: [{date, monster_name, monster_description, step_goal, monster_icon}]
|
||||||
|
|
||||||
if (!data || !Array.isArray(data)) {
|
if (!data || !Array.isArray(data)) {
|
||||||
return res.status(400).json({ error: 'Invalid data format' });
|
return res.status(400).json({ error: 'Invalid data format' });
|
||||||
@@ -438,8 +401,16 @@ app.post('/api/monsters/import', async (req, res) => {
|
|||||||
|
|
||||||
let monstersCreated = 0;
|
let monstersCreated = 0;
|
||||||
let monstersUpdated = 0;
|
let monstersUpdated = 0;
|
||||||
|
let monstersDeleted = 0;
|
||||||
let errors = [];
|
let errors = [];
|
||||||
|
|
||||||
|
// If overwrite is true, delete all existing monsters first
|
||||||
|
// Note: monster_catches is now a view, so no need to delete from it
|
||||||
|
if (overwrite === true) {
|
||||||
|
const deleteMonstersResult = await runQuery('DELETE FROM daily_monsters');
|
||||||
|
monstersDeleted = deleteMonstersResult.changes || 0;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
const row = data[i];
|
const row = data[i];
|
||||||
|
|
||||||
@@ -449,6 +420,14 @@ app.post('/api/monsters/import', async (req, res) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (overwrite) {
|
||||||
|
// When overwriting, always insert (since we deleted everything)
|
||||||
|
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++;
|
||||||
|
} else {
|
||||||
// Check if monster already exists for this date
|
// Check if monster already exists for this date
|
||||||
const existing = await getQuery(
|
const existing = await getQuery(
|
||||||
'SELECT id FROM daily_monsters WHERE date = ?',
|
'SELECT id FROM daily_monsters WHERE date = ?',
|
||||||
@@ -470,17 +449,25 @@ app.post('/api/monsters/import', async (req, res) => {
|
|||||||
);
|
);
|
||||||
monstersCreated++;
|
monstersCreated++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errors.push(`Row ${i + 1}: ${error.message}`);
|
errors.push(`Row ${i + 1}: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
const result = {
|
||||||
success: true,
|
success: true,
|
||||||
monstersCreated,
|
monstersCreated,
|
||||||
monstersUpdated,
|
|
||||||
errors: errors.length > 0 ? errors : null
|
errors: errors.length > 0 ? errors : null
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (overwrite) {
|
||||||
|
result.monstersDeleted = monstersDeleted;
|
||||||
|
} else {
|
||||||
|
result.monstersUpdated = monstersUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
@@ -500,12 +487,13 @@ app.get('/api/monsters/:date/catches', async (req, res) => {
|
|||||||
|
|
||||||
const catches = await allQuery(`
|
const catches = await allQuery(`
|
||||||
SELECT
|
SELECT
|
||||||
mc.*,
|
monster_id,
|
||||||
t.name as team_name
|
team_id,
|
||||||
FROM monster_catches mc
|
team_name,
|
||||||
JOIN teams t ON mc.team_id = t.id
|
final_steps
|
||||||
WHERE mc.monster_id = ?
|
FROM monster_catches
|
||||||
ORDER BY mc.caught_at
|
WHERE monster_id = ?
|
||||||
|
ORDER BY final_steps DESC
|
||||||
`, [monster.id]);
|
`, [monster.id]);
|
||||||
|
|
||||||
res.json(catches);
|
res.json(catches);
|
||||||
@@ -534,6 +522,7 @@ app.get('/api/monsters/status/today', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get team progress for yesterday's date
|
// Get team progress for yesterday's date
|
||||||
|
// The view automatically calculates which teams caught it
|
||||||
const progress = await allQuery(`
|
const progress = await allQuery(`
|
||||||
SELECT
|
SELECT
|
||||||
t.id as team_id,
|
t.id as team_id,
|
||||||
@@ -547,25 +536,6 @@ app.get('/api/monsters/status/today', async (req, res) => {
|
|||||||
ORDER BY total_steps DESC
|
ORDER BY total_steps DESC
|
||||||
`, [monster.step_goal, yesterday]);
|
`, [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 });
|
res.json({ monster, progress });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
BIN
step_competition.db
Normal file
BIN
step_competition.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user