Files
bestofpb/backend/index.js
Scott Hatlen e3941089e1 initial commit
2025-11-05 09:06:54 -08:00

220 lines
7.7 KiB
JavaScript

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}`);
});