feat: Add CalendarSelector component and install new dependencies.

This commit is contained in:
2026-01-06 16:14:22 +00:00
commit 4ab350105d
3592 changed files with 470732 additions and 0 deletions

18
server/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
FROM node:20-alpine
WORKDIR /app
# Copy root package files
COPY package.json package-lock.json* ./
# Install production dependencies
RUN npm ci --omit=dev
# Copy server source code
COPY server ./server
# Expose port
EXPOSE 3000
# Start server
CMD ["node", "server/index.js"]

52
server/db.js Normal file
View File

@@ -0,0 +1,52 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.resolve(__dirname, 'eventy.db');
const db = new Database(dbPath);
// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL');
function initDb() {
const schema = `
CREATE TABLE IF NOT EXISTS events (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS participants (
id TEXT PRIMARY KEY,
event_id TEXT NOT NULL,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS votes (
id TEXT PRIMARY KEY,
participant_id TEXT NOT NULL,
date TEXT NOT NULL,
status TEXT CHECK(status IN ('available', 'unavailable', 'maybe')) NOT NULL,
FOREIGN KEY (participant_id) REFERENCES participants(id) ON DELETE CASCADE,
UNIQUE(participant_id, date)
);
CREATE TABLE IF NOT EXISTS event_dates (
event_id TEXT NOT NULL,
date TEXT NOT NULL,
PRIMARY KEY (event_id, date),
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
);
`;
db.exec(schema);
console.log('Database initialized');
}
module.exports = {
db,
initDb
};

BIN
server/eventy.db Normal file

Binary file not shown.

BIN
server/eventy.db-shm Normal file

Binary file not shown.

BIN
server/eventy.db-wal Normal file

Binary file not shown.

81
server/index.js Normal file
View File

@@ -0,0 +1,81 @@
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
const { initDb } = require('./db');
const routes = require('./routes');
const fs = require('fs');
initDb();
app.use('/api', routes);
// Serve static files from the React app build directory
app.use(express.static(path.join(__dirname, '../client/dist')));
// Handle social previews for shared event links
app.get('/event/:id', (req, res) => {
const { db } = require('./db');
const eventId = req.params.id;
try {
const event = db.prepare('SELECT * FROM events WHERE id = ?').get(eventId);
const filePath = path.join(__dirname, '../client/dist/index.html');
// In dev, we might not have dist, or we might want to fallback to source index.html for testing logic
// But source index.html hasn't been built, so it won't resolve assets.
// For this specific feature to work "perfectly", we rely on the built assets or a dev-mode workaround.
// We'll proceed assuming a build exists or gracefully fail to just serving the file without injection or 404.
if (!event || !fs.existsSync(filePath)) {
// Fallback or 404. If in dev mode and accessing port 3000, we might just redirect to vite port 5173?
// No, that doesn't help bots.
// Let's just try to read it.
if (fs.existsSync(filePath)) {
return res.sendFile(filePath);
} else {
return res.send('Frontend not built. Run "npm run build" in client/ to enable social previews on this port.');
}
}
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) return res.status(500).send('Error reading index.html');
const title = `Invited to: ${event.name}`;
const desc = `${event.start_date} - ${event.end_date}. Let us know when you're free!`;
// Inject meta tags into the <head>
const result = data
.replace('<title>Vite + React</title>', `<title>${title}</title>`)
.replace('</head>', `
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${desc}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${req.protocol}://${req.get('host')}${req.originalUrl}" />
</head>
`);
res.send(result);
});
} catch (err) {
console.error(err);
res.redirect('/'); // Fallback to home
}
});
// The "catchall" handler: for any request that doesn't match one above, send back React's index.html file.
app.use((req, res) => {
res.sendFile(path.join(__dirname, '../client/dist/index.html'));
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});

111
server/routes.js Normal file
View File

@@ -0,0 +1,111 @@
const express = require('express');
const router = express.Router();
const { db } = require('./db');
const crypto = require('crypto');
// Health Check
router.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Create Event
router.post('/events', (req, res) => {
const { name, description, startDate, endDate } = req.body;
if (!name || !startDate || !endDate) {
return res.status(400).json({ error: 'Missing required fields' });
}
const id = crypto.randomUUID();
try {
const stmt = db.prepare('INSERT INTO events (id, name, description, start_date, end_date) VALUES (?, ?, ?, ?, ?)');
stmt.run(id, name, description || '', startDate, endDate);
res.json({
id,
message: 'Event created successfully'
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create event' });
}
});
// Get Event Details
router.get('/events/:id', (req, res) => {
try {
const stmt = db.prepare('SELECT * FROM events WHERE id = ?');
const event = stmt.get(req.params.id);
if (!event) {
return res.status(404).json({ error: 'Event not found' });
}
res.json(event);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to retrieve event' });
}
});
// Submit Response (Vote)
router.post('/events/:id/respond', (req, res) => {
const eventId = req.params.id;
const { name, votes } = req.body; // votes: [{ date: '2023-01-01', status: 'available' }]
if (!name || !Array.isArray(votes)) {
return res.status(400).json({ error: 'Invalid input' });
}
const participantId = crypto.randomUUID();
const insertParticipant = db.prepare('INSERT INTO participants (id, event_id, name) VALUES (?, ?, ?)');
const insertVote = db.prepare('INSERT INTO votes (id, participant_id, date, status) VALUES (?, ?, ?, ?)');
const transaction = db.transaction(() => {
insertParticipant.run(participantId, eventId, name);
for (const vote of votes) {
insertVote.run(crypto.randomUUID(), participantId, vote.date, vote.status);
}
});
try {
transaction();
res.json({ success: true, participantId });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to submit response' });
}
});
// Get Analytics
router.get('/events/:id/analytics', (req, res) => {
const eventId = req.params.id;
try {
// Check if event exists
const event = db.prepare('SELECT * FROM events WHERE id = ?').get(eventId);
if (!event) return res.status(404).json({ error: 'Event not found' });
// Get all participants
const participants = db.prepare('SELECT * FROM participants WHERE event_id = ?').all(eventId);
// Get all votes for this event (via participants)
const votes = db.prepare(`
SELECT v.participant_id, v.date, v.status
FROM votes v
JOIN participants p ON v.participant_id = p.id
WHERE p.event_id = ?
`).all(eventId);
res.json({
event,
participants,
votes
});
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to retrieve analytics' });
}
});
module.exports = router;