258 lines
No EOL
7 KiB
JavaScript
258 lines
No EOL
7 KiB
JavaScript
const express = require('express');
|
|
const http = require('http');
|
|
const socketIo = require('socket.io');
|
|
const Database = require('better-sqlite3');
|
|
const QRCode = require('qrcode');
|
|
const path = require('path');
|
|
const cors = require('cors');
|
|
|
|
const app = express();
|
|
const server = http.createServer(app);
|
|
const io = socketIo(server, {
|
|
cors: {
|
|
origin: "*",
|
|
methods: ["GET", "POST"]
|
|
}
|
|
});
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
|
|
// Middleware
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
app.use(express.static('public'));
|
|
|
|
// Initialize better-sqlite3 database
|
|
const db = new Database('./data/responses.db');
|
|
|
|
// Configure database for better performance
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('synchronous = NORMAL');
|
|
db.pragma('cache_size = 1000000');
|
|
db.pragma('temp_store = memory');
|
|
|
|
// Initialize database tables
|
|
try {
|
|
// Create responses table
|
|
db.exec(`CREATE TABLE IF NOT EXISTS responses (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
participant_name TEXT NOT NULL,
|
|
response TEXT NOT NULL,
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`);
|
|
|
|
// Create options table for dynamic options
|
|
db.exec(`CREATE TABLE IF NOT EXISTS options (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
option_text TEXT UNIQUE NOT NULL,
|
|
is_predefined BOOLEAN DEFAULT FALSE,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`);
|
|
|
|
// Insert predefined options
|
|
const predefinedOptions = ['Landvetter', 'Backamo', 'Borås', 'Trollhättan'];
|
|
const insertOption = db.prepare("INSERT OR IGNORE INTO options (option_text, is_predefined) VALUES (?, 1)");
|
|
|
|
predefinedOptions.forEach(option => {
|
|
insertOption.run(option);
|
|
});
|
|
} catch (error) {
|
|
console.error('Database initialization error:', error);
|
|
}
|
|
|
|
// Prepared statements for better performance
|
|
const getOptionsStmt = db.prepare("SELECT option_text FROM options ORDER BY is_predefined DESC, created_at ASC");
|
|
const getResponseCountsStmt = db.prepare(`
|
|
SELECT response, COUNT(*) as count
|
|
FROM responses
|
|
GROUP BY response
|
|
ORDER BY response ASC
|
|
`);
|
|
const getAllResponsesStmt = db.prepare(`
|
|
SELECT participant_name, response, timestamp
|
|
FROM responses
|
|
ORDER BY timestamp DESC
|
|
`);
|
|
const insertResponseStmt = db.prepare("INSERT INTO responses (participant_name, response) VALUES (?, ?)");
|
|
const insertOptionStmt = db.prepare("INSERT OR IGNORE INTO options (option_text) VALUES (?)");
|
|
|
|
// Get all available options
|
|
function getOptions() {
|
|
try {
|
|
return getOptionsStmt.all().map(row => row.option_text);
|
|
} catch (error) {
|
|
console.error('Error getting options:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get response counts
|
|
function getResponseCounts() {
|
|
try {
|
|
return getResponseCountsStmt.all();
|
|
} catch (error) {
|
|
console.error('Error getting response counts:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get all responses with participant names
|
|
function getAllResponses() {
|
|
try {
|
|
return getAllResponsesStmt.all();
|
|
} catch (error) {
|
|
console.error('Error getting all responses:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Routes
|
|
app.get('/', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'index.html'));
|
|
});
|
|
|
|
app.get('/admin', (req, res) => {
|
|
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
|
|
});
|
|
|
|
app.get('/api/options', (req, res) => {
|
|
try {
|
|
const options = getOptions();
|
|
res.json(options);
|
|
} catch (error) {
|
|
console.error('API error - options:', error);
|
|
res.status(500).json({ error: 'Failed to fetch options' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/results', (req, res) => {
|
|
try {
|
|
const results = getResponseCounts();
|
|
res.json(results);
|
|
} catch (error) {
|
|
console.error('API error - results:', error);
|
|
res.status(500).json({ error: 'Failed to fetch results' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/all-responses', (req, res) => {
|
|
try {
|
|
const responses = getAllResponses();
|
|
res.json(responses);
|
|
} catch (error) {
|
|
console.error('API error - all responses:', error);
|
|
res.status(500).json({ error: 'Failed to fetch responses' });
|
|
}
|
|
});
|
|
|
|
app.post('/api/submit', (req, res) => {
|
|
const { responses, participantName } = req.body;
|
|
|
|
if (!responses || !Array.isArray(responses) || responses.length === 0) {
|
|
return res.status(400).json({ error: 'Invalid responses' });
|
|
}
|
|
|
|
if (!participantName || typeof participantName !== 'string' || participantName.trim().length === 0) {
|
|
return res.status(400).json({ error: 'Participant name is required' });
|
|
}
|
|
|
|
const trimmedName = participantName.trim();
|
|
|
|
try {
|
|
// Use a transaction for better performance and data integrity
|
|
const insertTransaction = db.transaction((responses, participantName) => {
|
|
// Insert all responses
|
|
responses.forEach(response => {
|
|
insertResponseStmt.run(participantName, response);
|
|
// Add new custom options to options table
|
|
insertOptionStmt.run(response);
|
|
});
|
|
});
|
|
|
|
insertTransaction(responses, trimmedName);
|
|
|
|
// Emit updated data to all clients
|
|
const options = getOptions();
|
|
io.emit('optionsUpdated', options);
|
|
|
|
const results = getResponseCounts();
|
|
io.emit('resultsUpdated', results);
|
|
|
|
const allResponses = getAllResponses();
|
|
io.emit('allResponsesUpdated', allResponses);
|
|
|
|
res.json({ success: true });
|
|
} catch (error) {
|
|
console.error('Error submitting response:', error);
|
|
res.status(500).json({ error: 'Failed to submit response' });
|
|
}
|
|
});
|
|
|
|
app.get('/api/qr', async (req, res) => {
|
|
try {
|
|
const url = `https://aeroklubben.hostux.fr`;
|
|
const qrCode = await QRCode.toDataURL(url);
|
|
res.json({ qrCode });
|
|
} catch (err) {
|
|
res.status(500).json({ error: 'Failed to generate QR code' });
|
|
}
|
|
});
|
|
|
|
// Socket.IO connection handling
|
|
io.on('connection', (socket) => {
|
|
console.log('Client connected');
|
|
|
|
try {
|
|
// Send current options and results to new client
|
|
const options = getOptions();
|
|
socket.emit('optionsUpdated', options);
|
|
|
|
const results = getResponseCounts();
|
|
socket.emit('resultsUpdated', results);
|
|
|
|
const allResponses = getAllResponses();
|
|
socket.emit('allResponsesUpdated', allResponses);
|
|
} catch (error) {
|
|
console.error('Error sending initial data to client:', error);
|
|
}
|
|
|
|
socket.on('disconnect', () => {
|
|
console.log('Client disconnected');
|
|
});
|
|
});
|
|
|
|
// Start server
|
|
server.listen(PORT, () => {
|
|
console.log(`Server running on port ${PORT}`);
|
|
console.log(`Form: https://aeroklubben.hostux.fr`);
|
|
console.log(`Admin: https://aeroklubben.hostux.fr/admin`);
|
|
});
|
|
|
|
// Graceful shutdown
|
|
process.on('SIGINT', () => {
|
|
console.log('Shutting down gracefully...');
|
|
try {
|
|
db.close();
|
|
console.log('Database connection closed.');
|
|
} catch (error) {
|
|
console.error('Error closing database:', error);
|
|
}
|
|
server.close(() => {
|
|
console.log('Server closed.');
|
|
process.exit(0);
|
|
});
|
|
});
|
|
|
|
process.on('SIGTERM', () => {
|
|
console.log('Received SIGTERM, shutting down gracefully...');
|
|
try {
|
|
db.close();
|
|
console.log('Database connection closed.');
|
|
} catch (error) {
|
|
console.error('Error closing database:', error);
|
|
}
|
|
server.close(() => {
|
|
console.log('Server closed.');
|
|
process.exit(0);
|
|
});
|
|
}); |