const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const cors = require('cors'); const path = require('path'); const app = express(); const PORT = 4000; // Middleware app.use(cors()); app.use(express.json()); // SQLite setup const dbPath = path.join(__dirname, 'awards.db'); const db = new sqlite3.Database(dbPath); db.serialize(() => { db.run(`CREATE TABLE IF NOT EXISTS awards ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, address TEXT NOT NULL, submitted_by VARCHAR(255), emoji_tally TEXT DEFAULT '{}', submitted_date DATETIME DEFAULT CURRENT_TIMESTAMP, approved_date DATETIME, lat REAL, lng REAL )`); }); const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); // Basic auth middleware for management endpoints const basicAuth = (req, res, next) => { const auth = req.headers.authorization; if (!auth || !auth.startsWith('Basic ')) { res.set('WWW-Authenticate', 'Basic realm="Management"'); return res.status(401).send('Authentication required.'); } const credentials = Buffer.from(auth.split(' ')[1], 'base64').toString().split(':'); const [user, pass] = credentials; if (user === 'admin' && pass === 'password') return next(); res.set('WWW-Authenticate', 'Basic realm="Management"'); return res.status(401).send('Invalid credentials.'); }; // Get all awards app.get('/awards', (req, res) => { db.all('SELECT * FROM awards ORDER BY submitted_date DESC', [], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); // Parse emoji_tally JSON for each row const awards = rows.map(row => ({ ...row, emoji_tally: row.emoji_tally ? JSON.parse(row.emoji_tally) : {} })); res.json(awards); }); }); // Add a new award app.post('/awards', async (req, res) => { const { category, address, submitted_by } = req.body; if (!category || !address) { return res.status(400).json({ error: 'Category and address are required.' }); } let lat = null, lng = null; let isPB = false; try { const resp = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`, { headers: { 'User-Agent': 'react-awards-map/1.0 (contact@yourdomain.com)' } }); const data = await resp.json(); if (data && data[0]) { lat = parseFloat(data[0].lat); lng = parseFloat(data[0].lon); // Check if address is in Pacific Beach const displayName = data[0].display_name || ''; const addressObj = data[0].address || {}; // Accept if display_name contains 'Pacific Beach' or zip is 92109 if ( displayName.toLowerCase().includes('pacific beach') || addressObj.suburb === 'Pacific Beach' || addressObj.neighbourhood === 'Pacific Beach' || addressObj.city === 'San Diego' && addressObj.postcode === '92109' || addressObj.postcode === '92109' ) { isPB = true; } } } catch (e) { /* ignore geocode errors */ } if (!isPB) { return res.status(400).json({ error: 'Address must be in Pacific Beach (92109, San Diego, CA).' }); } db.run( 'INSERT INTO awards (category, address, submitted_by, emoji_tally, submitted_date, lat, lng) VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, ?, ?)', [category, address, submitted_by || null, '{}', lat, lng], function (err) { if (err) return res.status(500).json({ error: err.message }); db.get('SELECT * FROM awards WHERE id = ?', [this.lastID], (err, row) => { if (err) return res.status(500).json({ error: err.message }); row.emoji_tally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {}; res.status(201).json(row); }); } ); }); // Update emoji tally for an award app.patch('/awards/:id/emojis', (req, res) => { const { id } = req.params; const { emoji, count } = req.body; if (!emoji || typeof count !== 'number') { return res.status(400).json({ error: 'Emoji and count are required.' }); } db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => { if (err || !row) return res.status(404).json({ error: 'Award not found.' }); let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {}; emojiTally[emoji] = count; db.run( 'UPDATE awards SET emoji_tally = ? WHERE id = ?', [JSON.stringify(emojiTally), id], function (err) { if (err) return res.status(500).json({ error: err.message }); res.json({ id, emoji_tally: emojiTally }); } ); }); }); // Remove or decrement an emoji tally for an award app.patch('/awards/:id/emojis/remove', (req, res) => { const { id } = req.params; const { emoji } = req.body; if (!emoji) { return res.status(400).json({ error: 'Emoji is required.' }); } db.get('SELECT * FROM awards WHERE id = ?', [id], (err, row) => { if (err || !row) return res.status(404).json({ error: 'Award not found.' }); let emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {}; if (emojiTally[emoji]) { emojiTally[emoji] = emojiTally[emoji] - 1; if (emojiTally[emoji] <= 0) { delete emojiTally[emoji]; } db.run( 'UPDATE awards SET emoji_tally = ? WHERE id = ?', [JSON.stringify(emojiTally), id], function (err) { if (err) return res.status(500).json({ error: err.message }); res.json({ id, emoji_tally: emojiTally }); } ); } else { res.json({ id, emoji_tally: emojiTally }); } }); }); // Approve an award app.patch('/awards/:id/approve', basicAuth, (req, res) => { const { id } = req.params; db.run( 'UPDATE awards SET approved_date = CURRENT_TIMESTAMP WHERE id = ?', [id], function (err) { if (err) return res.status(500).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'Award not found.' }); res.json({ id, approved_date: new Date().toISOString() }); } ); }); // Get all pending (unapproved) awards app.get('/awards/pending', basicAuth, (req, res) => { db.all('SELECT * FROM awards WHERE approved_date IS NULL ORDER BY submitted_date DESC', [], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); const awards = rows.map(row => ({ ...row, emoji_tally: row.emoji_tally ? JSON.parse(row.emoji_tally) : {} })); res.json(awards); }); }); // Reject (delete) a nomination app.delete('/awards/:id', basicAuth, (req, res) => { const { id } = req.params; db.run('DELETE FROM awards WHERE id = ?', [id], function (err) { if (err) return res.status(500).json({ error: err.message }); if (this.changes === 0) return res.status(404).json({ error: 'Award not found.' }); res.json({ id, deleted: true }); }); }); // Get top 5 awards approved, sorted by emoji tally (descending) app.get('/awards/top', (req, res) => { db.all( `SELECT * FROM awards WHERE approved_date IS NOT NULL`, [], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); // Parse emoji_tally and calculate total emojis for sorting const awards = rows.map(row => { const emojiTally = row.emoji_tally ? JSON.parse(row.emoji_tally) : {}; const emojiTotal = Object.values(emojiTally).reduce((sum, n) => sum + Number(n), 0); return { ...row, emoji_tally: emojiTally, emojiTotal }; }); // Sort by emojiTotal descending, then by most recently approved awards.sort((a, b) => { if (b.emojiTotal !== a.emojiTotal) return b.emojiTotal - a.emojiTotal; return new Date(b.approved_date) - new Date(a.approved_date); }); res.json(awards.slice(0, 5)); } ); }); // Start server app.listen(PORT, () => { console.log(`Backend server running on http://localhost:${PORT}`); });