summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--backend/package-lock.json169
-rw-r--r--backend/package.json13
-rw-r--r--backend/src/db.ts (renamed from backend/src/db.js)31
-rw-r--r--backend/src/routes/api.js12
-rw-r--r--backend/src/routes/api.ts10
-rw-r--r--backend/src/routes/events.ts (renamed from backend/src/routes/events.js)62
-rw-r--r--backend/src/routes/users.ts (renamed from backend/src/routes/users.js)42
-rw-r--r--backend/src/server.js40
-rw-r--r--backend/src/server.ts37
-rw-r--r--backend/src/types.ts24
-rw-r--r--backend/tsconfig.json46
-rw-r--r--frontend/index.html2
-rw-r--r--frontend/package-lock.json31
-rw-r--r--frontend/package.json3
-rw-r--r--frontend/src/components/Events.ts (renamed from frontend/src/components/Events.js)72
-rw-r--r--frontend/src/components/Forms.js66
-rw-r--r--frontend/src/components/Forms.ts68
-rw-r--r--frontend/src/components/Header.js33
-rw-r--r--frontend/src/components/Header.ts40
-rw-r--r--frontend/src/components/Icons.ts (renamed from frontend/src/components/Icons.js)0
-rw-r--r--frontend/src/main.js21
-rw-r--r--frontend/tsconfig.json9
-rw-r--r--package-lock.json19
-rw-r--r--package.json5
24 files changed, 599 insertions, 256 deletions
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 47e566a..e2ddc64 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -14,6 +14,154 @@
"express": "^4.21.2",
"helmet": "^8.1.0",
"morgan": "^1.10.1"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/cookie-parser": "^1.4.10",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "@types/morgan": "^1.9.10",
+ "@types/node": "^25.0.9",
+ "typescript": "^5.9.3"
+ }
+ },
+ "node_modules/@types/better-sqlite3": {
+ "version": "7.6.13",
+ "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/body-parser": {
+ "version": "1.19.6",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
+ "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/connect": {
+ "version": "3.4.38",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
+ "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/cookie-parser": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz",
+ "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/express": "*"
+ }
+ },
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/express": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
+ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "^5.0.0",
+ "@types/serve-static": "^2"
+ }
+ },
+ "node_modules/@types/express-serve-static-core": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz",
+ "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "@types/qs": "*",
+ "@types/range-parser": "*",
+ "@types/send": "*"
+ }
+ },
+ "node_modules/@types/http-errors": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
+ "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/morgan": {
+ "version": "1.9.10",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz",
+ "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.0.9",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.9.tgz",
+ "integrity": "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/qs": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
+ "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/range-parser": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
+ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
+ "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*"
}
},
"node_modules/accepts": {
@@ -1324,6 +1472,27 @@
"node": ">= 0.6"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index b6c45cd..77bb09b 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -4,8 +4,8 @@
"private": true,
"type": "module",
"scripts": {
- "start": "NODE_ENV=production node src/server.js",
- "dev": "node --watch src/server.js"
+ "start": "NODE_ENV=production node dist/server.js",
+ "dev": "node --watch dist/server.js"
},
"dependencies": {
"better-sqlite3": "^11.10.0",
@@ -14,5 +14,14 @@
"express": "^4.21.2",
"helmet": "^8.1.0",
"morgan": "^1.10.1"
+ },
+ "devDependencies": {
+ "@types/better-sqlite3": "^7.6.13",
+ "@types/cookie-parser": "^1.4.10",
+ "@types/cors": "^2.8.19",
+ "@types/express": "^5.0.6",
+ "@types/morgan": "^1.9.10",
+ "@types/node": "^25.0.9",
+ "typescript": "^5.9.3"
}
}
diff --git a/backend/src/db.js b/backend/src/db.ts
index f20cac1..2b124f1 100644
--- a/backend/src/db.js
+++ b/backend/src/db.ts
@@ -1,16 +1,15 @@
-import Database from 'better-sqlite3';
-import EVENTS from './data/events.json' with {type: 'json'};
-import USERS from './data/users.json' with {type: 'json'};
-import RSVPS from './data/rsvps.json' with {type: 'json'};
+import DatabaseConstructor, { type Database } from "better-sqlite3";
+import EVENTS from "./data/events.json" with { type: "json" };
+import USERS from "./data/users.json" with { type: "json" };
+import RSVPS from "./data/rsvps.json" with { type: "json" };
-
-const db = new Database('src/sqlite.db', { verbose: console.log });
+const db: Database = new DatabaseConstructor("src/sqlite.db", {
+ verbose: console.log,
+});
console.log(`Initializing database: ${db.name} `);
-
-db.pragma('foreign_keys = ON');
-
+db.pragma("foreign_keys = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
@@ -32,24 +31,22 @@ db.exec(`
CREATE INDEX IF NOT EXISTS eventhosts ON events(host_id);
`);
-
const upsertUser = db.prepare(`
INSERT INTO users VALUES (@id, @username, @name, @email)
ON CONFLICT(id) DO NOTHING
- `)
+ `);
USERS.map((user) => upsertUser.run(user));
const upsertEvent = db.prepare(`
INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)
ON CONFLICT(id) DO NOTHING
- `)
+ `);
EVENTS.map((event) => {
- upsertEvent.run(event);
+ upsertEvent.run(event);
});
-
db.exec(`
CREATE TABLE IF NOT EXISTS rsvps (
event_id INTEGER REFERENCES events NOT NULL,
@@ -64,9 +61,7 @@ const upsertRSVP = db.prepare(`
INSERT INTO rsvps VALUES (@event_id, @name, @email)
`);
RSVPS.map((rsvp) => {
- upsertRSVP.run(rsvp);
+ upsertRSVP.run(rsvp);
});
-
-
-export default db; \ No newline at end of file
+export default db;
diff --git a/backend/src/routes/api.js b/backend/src/routes/api.js
deleted file mode 100644
index 2eb07d1..0000000
--- a/backend/src/routes/api.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Router } from 'express';
-import usersRouter from './users.js';
-import eventsRouter from './events.js';
-
-
-const router = Router();
-
-router.use('/users', usersRouter);
-router.use('/events', eventsRouter);
-
-
-export default router;
diff --git a/backend/src/routes/api.ts b/backend/src/routes/api.ts
new file mode 100644
index 0000000..92eae46
--- /dev/null
+++ b/backend/src/routes/api.ts
@@ -0,0 +1,10 @@
+import { Router } from "express";
+import usersRouter from "./users.js";
+import eventsRouter from "./events.js";
+
+const router = Router();
+
+router.use("/users", usersRouter);
+router.use("/events", eventsRouter);
+
+export default router;
diff --git a/backend/src/routes/events.js b/backend/src/routes/events.ts
index b95c747..df65eb5 100644
--- a/backend/src/routes/events.js
+++ b/backend/src/routes/events.ts
@@ -1,52 +1,56 @@
-import db from '../db.js';
-import { Router } from 'express';
-import { getUser } from './users.js';
+import db from "../db.js";
+import { Router } from "express";
+import { getUser } from "./users.js";
+import { type Event, type Id, type Rsvps, type User } from "../types.js";
const router = Router();
-const joinHost = (event) => {
- const host = getUser(event.host_id);
+const joinHost = (event: Event) => {
+ const host = getUser(event.host_id) as User;
return { ...event, host };
-}
+};
-const joinRSVPs = (event) => {
+const joinRSVPs = (event: Event) => {
const { id } = event;
- const getRSVPs = db.prepare('SELECT * FROM rsvps WHERE event_id = @id');
- const rsvps = getRSVPs.all({ id });
+ const getRSVPs = db.prepare("SELECT * FROM rsvps WHERE event_id = @id");
+ const rsvps = getRSVPs.all({ id }) as Rsvps[];
return { ...event, rsvps };
-}
+};
-const getEvent = (eventId) => {
- const byId = db.prepare('SELECT * FROM events WHERE id = @eventId');
- const event = byId.get({ eventId });
+const getEvent = (eventId: Id) => {
+ const byId = db.prepare("SELECT * FROM events WHERE id = @eventId");
+ const event = byId.get({ eventId }) as Event;
return joinHost(event);
-}
+};
-router.get('/', (_req, res) => {
+router.get("/", (_req, res) => {
const listEvents = db.prepare(`SELECT * FROM events`);
- const events = listEvents.all();
+ const events = listEvents.all() as Event[];
res.json(events.map(joinHost).map(joinRSVPs));
});
-const insertEvent = db.prepare(`INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)`);
+const insertEvent = db.prepare(
+ `INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)`,
+);
-router.post('/new', (req, res) => {
+router.post("/new", (req, res) => {
const data = req.body;
- const { lastInsertRowid: id } = insertEvent.run(data);
+ const { lastInsertRowid } = insertEvent.run(data);
+ const id = lastInsertRowid as number;
const event = getEvent(id);
res.status(201).json(event);
});
-router.get('/:id', (req, res) => {
+router.get("/:id", (req, res) => {
const id = parseInt(req.params.id);
const event = getEvent(id);
if (!event) {
- return res.status(404).json({ error: 'Event not found' });
+ return res.status(404).json({ error: "Event not found" });
}
res.json(event);
});
-router.patch('/:id', (req, res) => {
+router.patch("/:id", (req, res) => {
const eventId = parseInt(req.params.id);
const patch = req.body;
@@ -64,23 +68,27 @@ router.patch('/:id', (req, res) => {
res.json(updated);
});
-router.delete('/:id', (req, res) => {
+router.delete("/:id", (req, res) => {
const deleteEvent = db.prepare(`DELETE FROM events WHERE id = @eventId`);
const eventId = parseInt(req.params.id);
const event = getEvent(eventId);
if (!event) {
- return res.status(404).json({ error: 'Event not found' });
+ return res.status(404).json({ error: "Event not found" });
}
deleteEvent.run({ eventId });
res.json(event);
});
-router.post('/:id/rsvp', (req, res) => {
+router.post("/:id/rsvp", (req, res) => {
const eventId = parseInt(req.params.id);
const { name, email } = req.body;
- const getRSVP = db.prepare(`SELECT * FROM rsvps WHERE (event_id = ${eventId} AND email = '${email}')`);
- const insertRSVP = db.prepare(`INSERT INTO rsvps VALUES (@eventId, @name, @email)`);
+ const getRSVP = db.prepare(
+ `SELECT * FROM rsvps WHERE (event_id = ${eventId} AND email = '${email}')`,
+ );
+ const insertRSVP = db.prepare(
+ `INSERT INTO rsvps VALUES (@eventId, @name, @email)`,
+ );
let [rsvp] = getRSVP.all({ eventId, email });
if (rsvp) {
diff --git a/backend/src/routes/users.js b/backend/src/routes/users.ts
index 6d874e9..98ec361 100644
--- a/backend/src/routes/users.js
+++ b/backend/src/routes/users.ts
@@ -1,40 +1,42 @@
-import { Router } from 'express';
-import db from '../db.js';
+import { Router } from "express";
+import db from "../db.js";
+import type { User } from "../types.js";
const router = Router();
-export const getUser = (userId) => {
- const byId = db.prepare('SELECT * FROM users WHERE id = @userId');
+export const getUser = (userId: User["id"]) => {
+ const byId = db.prepare("SELECT * FROM users WHERE id = @userId");
return byId.get({ userId });
-}
+};
-router.get('/', (_req, res) => {
- const listUsers = db.prepare(`SELECT * FROM users`)
+router.get("/", (_req, res) => {
+ const listUsers = db.prepare(`SELECT * FROM users`);
const users = listUsers.all();
res.json(users);
});
-router.post('/new', (req, res) => {
+router.post("/new", (req, res) => {
const data = req.body;
- const cols = Object.keys(data).join(' , ');
- const vals = Object.values(data).join(' , ');
+ const cols = Object.keys(data).join(" , ");
+ const vals = Object.values(data).join(" , ");
const insertUser = db.prepare(`INSERT INTO users(@cols) VALUES (@vals)`);
- const { lastInsertRowid: id } = insertUser.run({ cols, vals });
+ const { lastInsertRowid } = insertUser.run({ cols, vals });
+ const id = lastInsertRowid as number;
const user = getUser(id);
res.json(user);
});
-router.get('/:id', (req, res) => {
- const id = req.params.id;
+router.get("/:id", (req, res) => {
+ const id = Number(req.params.id);
const user = getUser(id);
if (!user) {
- res.status(404).json({ error: 'User not found' });
+ res.status(404).json({ error: "User not found" });
}
res.json(user);
});
-router.patch('/:id', (req, res) => {
- const userId = req.params.id;
+router.patch("/:id", (req, res) => {
+ const userId = Number(req.params.id);
const patch = req.body;
const updateCol = db.prepare(`
@@ -43,7 +45,7 @@ router.patch('/:id', (req, res) => {
const updateUser = db.transaction((patch) => {
for (const [col, val] of Object.entries(patch)) {
updateCol.run(col, val, userId);
- };
+ }
});
updateUser(Object.entries(patch));
@@ -51,12 +53,12 @@ router.patch('/:id', (req, res) => {
res.json(updated);
});
-router.delete('/:id', (req, res) => {
- const deleteUser = db.prepare(`DELETE FROM users WHERE id = @userId`)
+router.delete("/:id", (req, res) => {
+ const deleteUser = db.prepare(`DELETE FROM users WHERE id = @userId`);
const userId = parseInt(req.params.id);
const user = getUser(userId);
if (!user) {
- res.status(404).json({ error: 'User not found' });
+ res.status(404).json({ error: "User not found" });
}
deleteUser.run({ userId });
res.json(user);
diff --git a/backend/src/server.js b/backend/src/server.js
deleted file mode 100644
index 8b579b3..0000000
--- a/backend/src/server.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import express from 'express';
-import cookieParser from 'cookie-parser';
-import logger from 'morgan';
-import cors from 'cors';
-import helmet from 'helmet';
-
-import apiRouter from './routes/api.js';
-
-
-const PORT = process.env.PORT || 3000;
-const NODE_ENV = process.env.NODE_ENV || 'development';
-
-const app = express();
-
-
-app.use(logger('dev'));
-app.use(express.json());
-app.use(express.urlencoded({ extended: false }));
-app.use(cookieParser());
-app.use(helmet());
-app.use(cors({
- origin: 'http://localhost:5173'
-}));
-
-app.use('/api', apiRouter);
-
-app.use((_req, res) => {
- res.status(404).json({
- error: 'Not Found',
- endpoints: [
- '/api/events',
- '/api/users'
- ]
- });
-});
-
-app.listen(PORT, () => {
- console.log(`\nServer listening at http://localhost:${PORT}`);
- console.log(`Server running in ${NODE_ENV} mode\n`);
-}); \ No newline at end of file
diff --git a/backend/src/server.ts b/backend/src/server.ts
new file mode 100644
index 0000000..8582347
--- /dev/null
+++ b/backend/src/server.ts
@@ -0,0 +1,37 @@
+import express from "express";
+import cookieParser from "cookie-parser";
+import logger from "morgan";
+import cors from "cors";
+import helmet from "helmet";
+
+import apiRouter from "./routes/api.js";
+
+const PORT = process.env.PORT || 3000;
+const NODE_ENV = process.env.NODE_ENV || "development";
+
+const app = express();
+
+app.use(logger("dev"));
+app.use(express.json());
+app.use(express.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(helmet());
+app.use(
+ cors({
+ origin: "http://localhost:5173",
+ }),
+);
+
+app.use("/api", apiRouter);
+
+app.use((_req, res) => {
+ res.status(404).json({
+ error: "Not Found",
+ endpoints: ["/api/events", "/api/users"],
+ });
+});
+
+app.listen(PORT, () => {
+ console.log(`\nServer listening at http://localhost:${PORT}`);
+ console.log(`Server running in ${NODE_ENV} mode\n`);
+});
diff --git a/backend/src/types.ts b/backend/src/types.ts
new file mode 100644
index 0000000..5c8112c
--- /dev/null
+++ b/backend/src/types.ts
@@ -0,0 +1,24 @@
+export type Id = number;
+
+export interface Event {
+ id: Id;
+ title: string;
+ description?: string;
+ date: Date;
+ host_id: number;
+ image_url?: string;
+ host: User;
+ rsvps: Rsvps[];
+}
+
+export interface User {
+ id: number;
+ name: string;
+ email: string;
+}
+
+export interface Rsvps {
+ id: number;
+ name: string;
+ email: string;
+}
diff --git a/backend/tsconfig.json b/backend/tsconfig.json
new file mode 100644
index 0000000..0373555
--- /dev/null
+++ b/backend/tsconfig.json
@@ -0,0 +1,46 @@
+{
+ "extends": "@tsconfig/node-lts/tsconfig.json",
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "compilerOptions": {
+ // File Layout
+ "rootDir": "./src",
+ "outDir": "./dist",
+
+ // Environment Settings
+ // See also https://aka.ms/tsconfig/module
+ "module": "nodenext",
+ "target": "esnext",
+ "types": [
+ "node"
+ ],
+ // For nodejs:
+ // "lib": ["esnext"],
+ // and npm install -D @types/node
+
+ // Other Outputs
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true,
+
+ // Stricter Typechecking Options
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+
+ // Style Options
+ // "noImplicitReturns": true,
+ // "noImplicitOverride": true,
+ // "noUnusedLocals": true,
+ // "noUnusedParameters": true,
+ // "noFallthroughCasesInSwitch": true,
+ // "noPropertyAccessFromIndexSignature": true,
+
+ // Recommended Options
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true,
+ }
+}
diff --git a/frontend/index.html b/frontend/index.html
index 1b3628c..291902c 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -19,4 +19,4 @@
<script type="module" src="/src/main.js"></script>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index a846104..dfa9ebf 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,9 @@
"name": "event-me-client",
"version": "0.0.0",
"devDependencies": {
+ "@tsconfig/recommended": "^1.0.13",
+ "@tsconfig/vite-react": "^7.0.2",
+ "typescript": "^5.9.3",
"vite": "^7.1.2",
"vitest": "^3.2.4"
}
@@ -741,6 +744,20 @@
"win32"
]
},
+ "node_modules/@tsconfig/recommended": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@tsconfig/recommended/-/recommended-1.0.13.tgz",
+ "integrity": "sha512-sySRuBfMKyKO/j2ZAhR8kSembhjuPEV4Ra3AHtmWLq51+iGaudr45crPSzNC5b7/Ctrh9dfUpBuTlYrH6rM58Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tsconfig/vite-react": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@tsconfig/vite-react/-/vite-react-7.0.2.tgz",
+ "integrity": "sha512-lEj4y5SPRcH+bjw0tyuxrEnPqQUwfQzBKgd1YamD9xyet9zLwh2gwy5F8w/Nxg5DjdgYVjjKo5aLJUf0BTDz4w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/chai": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
@@ -1318,6 +1335,20 @@
"node": ">=14.0.0"
}
},
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
"node_modules/vite": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index bde2683..66fa712 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,9 @@
"test": "vitest"
},
"devDependencies": {
+ "@tsconfig/recommended": "^1.0.13",
+ "@tsconfig/vite-react": "^7.0.2",
+ "typescript": "^5.9.3",
"vite": "^7.1.2",
"vitest": "^3.2.4"
}
diff --git a/frontend/src/components/Events.js b/frontend/src/components/Events.ts
index a84d291..a92cf83 100644
--- a/frontend/src/components/Events.js
+++ b/frontend/src/components/Events.ts
@@ -1,20 +1,39 @@
-import { Calendar } from './Icons.js';
+import { Calendar } from "./Icons";
const API_URL = import.meta.env.VITE_API_URL;
-const loadEventsData = async () => {
+interface Event {
+ id: number;
+ title: string;
+ description?: string;
+ date: Date;
+ host_id: number;
+ image_url?: string;
+ host: {
+ id: number;
+ name: string;
+ email: string;
+ };
+ rsvps: {
+ id: number;
+ name: string;
+ email: string;
+ }[];
+}
+
+const loadEventsData = async (): Promise<Event[]> => {
try {
const response = await fetch(`${API_URL}/events`);
return response.json();
} catch (e) {
console.error(e);
+ return [];
}
-}
-
+};
-export const EventModal = (event) => {
- const formId = `rsvp-form-${event.ID}`;
- const modalId = `modal-event-${event.id}`
+export const EventModal = (event: Event) => {
+ const formId = `rsvp-form-${event.id}`;
+ const modalId = `modal-event-${event.id}`;
return `<dialog id="${modalId}">
<article>
<header>
@@ -49,10 +68,10 @@ export const EventModal = (event) => {
</footer>
</article>
- </dialog>`
-}
+ </dialog>`;
+};
-export const EventCard = (e) => {
+export const EventCard = (e: Event) => {
const eventDate = new Date(e.date);
const isPast = eventDate < new Date();
return `
@@ -65,41 +84,46 @@ export const EventCard = (e) => {
<p>${Calendar} ${eventDate.toLocaleDateString()}</p>
<p>Host: ${e.host?.name || `User ${e.host_id}`}</p>
- ${e.description && `<p>${e.description}</p>`}
+ ${e.description ? `<p>${e.description}</p>` : ""}
</main>
<footer>
<span>
- ${e.rsvps?.length || 0} ${isPast ? 'went' : 'going'}
+ ${e.rsvps?.length || 0} ${isPast ? "went" : "going"}
</span>
- ${!isPast ? `
+ ${
+ !isPast
+ ? `
<button role="button" data-target="modal-event-${e.id}" class="toggle-modal"
title="RSVP to ${e.title}"
>
RSVP
- </button>`: ''}
+ </button>`
+ : ""
+ }
</footer>
${EventModal(e)}
</article>
- `
-}
+ `;
+};
-export const EventsSection = (title, events) => {
+export const EventsSection = (title: string, events: Event[]) => {
return `
<section class='events'>
<h2>${title} events </h2>
<div role = "group">
- ${events.map((e) => EventCard(e)).join('') || 'No events'}
+ ${events.map((e) => EventCard(e)).join("") || "No events"}
</div>
</section>`;
-}
+};
// IIFE to asynchronously load the Event data before exporting the component
// https://developer.mozilla.org/en-US/docs/Glossary/IIFE
export const Events = await (async () => {
const all = await loadEventsData();
- const past = all.filter((e) => (new Date(e.date) < new Date()));
- const upcoming = all.filter((e) => (new Date(e.date) > new Date()));
+ const past = all.filter((e) => new Date(e.date) < new Date());
+ const upcoming = all.filter((e) => new Date(e.date) > new Date());
return `
- ${EventsSection('Upcoming', upcoming)}
- ${EventsSection('Past', past)}
-`})() \ No newline at end of file
+ ${EventsSection("Upcoming", upcoming)}
+ ${EventsSection("Past", past)}
+`;
+})();
diff --git a/frontend/src/components/Forms.js b/frontend/src/components/Forms.js
deleted file mode 100644
index 8e9160f..0000000
--- a/frontend/src/components/Forms.js
+++ /dev/null
@@ -1,66 +0,0 @@
-const API_URL = import.meta.env.VITE_API_URL;
-
-export const setupForm = (form) => {
- if (!form) return;
-
- const eventId = form.id.replace('rsvp-form-', '');
- const url = `${API_URL}/events/${eventId}/rsvp`;
- const btn = document.getElementById(`submit-${form.id}`);
-
- async function sendData(form) {
- const formData = new FormData(form);
- const encoded = new URLSearchParams();
- for (let [key, value] of formData.entries) {
- encoded.append(key, value);
- }
- try {
- const response = await fetch(url, {
- method: "POST",
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: encoded,
- });
- if (response.status === 201) {
- form.dataset.submitted = 'success';
- if (btn) {
- btn.textContent = "You're going!";
- btn.setAttribute('disabled', '');
- }
- } else if (response.status === 200) {
- form.dataset.submitted = 'duplicate';
- if (btn) {
- btn.textContent = "You're already going!";
- btn.setAttribute('disabled', '');
- }
- }
- } catch (e) {
- form.dataset.submitted = 'error';
- console.error(e);
- }
- }
-
- form.addEventListener("submit", (event) => {
- event.preventDefault();
- sendData(form);
- });
-
- if (btn) {
- const emailInput = form.querySelector('input.rsvp-email');
- emailInput.addEventListener('input', () => {
- if (btn.textContent !== 'Submit RSVP') {
- btn.removeAttribute('disabled');
- btn.textContent = 'Submit RSVP';
- }
- });
-
- }
-};
-
-export const setupForms = () => {
- const rsvpForms = document.querySelectorAll('form');
- if (!rsvpForms) return;
- for (let form of rsvpForms) {
- setupForm(form);
- }
-}; \ No newline at end of file
diff --git a/frontend/src/components/Forms.ts b/frontend/src/components/Forms.ts
new file mode 100644
index 0000000..ff35065
--- /dev/null
+++ b/frontend/src/components/Forms.ts
@@ -0,0 +1,68 @@
+const API_URL = import.meta.env.VITE_API_URL;
+
+export const setupForm = (form: HTMLFormElement) => {
+ if (!form) return;
+
+ const eventId = form.id.replace("rsvp-form-", "");
+ const url = `${API_URL}/events/${eventId}/rsvp`;
+ const btn = document.getElementById(`submit-${form.id}`);
+
+ async function sendData(form: HTMLFormElement) {
+ const formData = new FormData(form);
+ const encoded = new URLSearchParams();
+ for (let [key, value] of formData.entries()) {
+ encoded.append(key, value.toString());
+ }
+ try {
+ const response = await fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: encoded,
+ });
+ if (response.status === 201) {
+ form.dataset.submitted = "success";
+ if (btn) {
+ btn.textContent = "You're going!";
+ btn.setAttribute("disabled", "");
+ }
+ } else if (response.status === 200) {
+ form.dataset.submitted = "duplicate";
+ if (btn) {
+ btn.textContent = "You're already going!";
+ btn.setAttribute("disabled", "");
+ }
+ }
+ } catch (e) {
+ form.dataset.submitted = "error";
+ console.error(e);
+ }
+ }
+
+ form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ sendData(form);
+ });
+
+ if (btn) {
+ const emailInput = form.querySelector("input.rsvp-email");
+ if (emailInput) {
+ emailInput.addEventListener("input", () => {
+ if (btn.textContent !== "Submit RSVP") {
+ btn.removeAttribute("disabled");
+ btn.textContent = "Submit RSVP";
+ }
+ });
+ }
+ }
+};
+
+export const setupForms = () => {
+ const rsvpForms = document.querySelectorAll("form");
+ if (!rsvpForms) return;
+ for (let form of rsvpForms) {
+ setupForm(form);
+ }
+};
+
diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js
deleted file mode 100644
index f28f8e5..0000000
--- a/frontend/src/components/Header.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Theme as ThemeIcon } from './Icons';
-
-const themeToggleId = 'theme';
-
-const Header = `
-<header>
- <hgroup>
- <h1 class=".parkinsans">event me</h1>
- <p>All the events you never knew you needed to attend!</p>
- </hgroup>
- <a href="#" role="toggle" id="${themeToggleId}" title="Toggle color scheme" >
- ${ThemeIcon}
- </a>
-</header>
-`;
-
-const toggleDarkMode = () => {
- const doc = document.documentElement;
- const currentTheme = doc.getAttribute('data-theme');
- if (currentTheme === 'dark') {
- doc.setAttribute('data-theme', 'lite');
- } else if (currentTheme === 'light') {
- doc.setAttribute('data-theme', 'dark');
- }
-}
-export function setupThemeToggle() {
- const themeToggle = document.getElementById(themeToggleId);
- themeToggle.addEventListener('click', toggleDarkMode);
-
-}
-
-
-export default Header
diff --git a/frontend/src/components/Header.ts b/frontend/src/components/Header.ts
new file mode 100644
index 0000000..11da46f
--- /dev/null
+++ b/frontend/src/components/Header.ts
@@ -0,0 +1,40 @@
+import { Theme as ThemeIcon } from "./Icons";
+
+const themeToggleId = "theme";
+
+const Header = `
+<header>
+ <hgroup>
+ <h1 class=".parkinsans">event me</h1>
+ <p>All the events you never knew you needed to attend!</p>
+ </hgroup>
+ <a href="#" role="toggle" id="${themeToggleId}" title="Toggle color scheme" >
+ ${ThemeIcon}
+ </a>
+</header>
+`;
+
+type Theme = "dark" | "light";
+
+const toggleDarkMode = () => {
+ const doc = document.documentElement;
+ const currentTheme = doc.getAttribute("data-theme");
+ let newTheme: Theme;
+ if (currentTheme === "dark") {
+ newTheme = "light";
+ doc.setAttribute("data-theme", newTheme);
+ } else if (currentTheme === "light") {
+ newTheme = "dark";
+ doc.setAttribute("data-theme", newTheme);
+ }
+};
+export function setupThemeToggle() {
+ const themeToggle = document.getElementById(themeToggleId);
+ if (!themeToggle) {
+ console.error("Missing Toggle");
+ return;
+ }
+ themeToggle.addEventListener("click", toggleDarkMode);
+}
+
+export default Header;
diff --git a/frontend/src/components/Icons.js b/frontend/src/components/Icons.ts
index c2eb993..c2eb993 100644
--- a/frontend/src/components/Icons.js
+++ b/frontend/src/components/Icons.ts
diff --git a/frontend/src/main.js b/frontend/src/main.js
index cf91fa0..7e9d7c6 100644
--- a/frontend/src/main.js
+++ b/frontend/src/main.js
@@ -1,26 +1,21 @@
-import './style.css'
-import Header, { setupThemeToggle } from './components/Header';
-import Main from './components/Main';
-import Footer from './components/Footer';
-import { setupModals } from './components/Modal';
-import { setupForms } from './components/Forms';
-
+import "./style.css";
+import Header, { setupThemeToggle } from "./components/Header";
+import Main from "./components/Main";
+import Footer from "./components/Footer";
+import { setupModals } from "./components/Modal";
+import { setupForms } from "./components/Forms";
// Quick and dirty - not for production!
const render = (html) => {
- const app = document.querySelector('#app');
+ const app = document.querySelector("#app");
app.innerHTML = html;
setupThemeToggle();
setupModals();
setupForms();
-}
-
+};
render(`
${Header}
${Main}
${Footer}
`);
-
-
-
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..3090677
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": [ "@tsconfig/recommended/tsconfig.json", "@tsconfig/vite-react/tsconfig.json" ],
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "compilerOptions": {
+ "types": [
+ "vite/client",
+ ]
+ }
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..54f88e2
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,19 @@
+{
+ "name": "event-me",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "@tsconfig/node-lts": "^24.0.0"
+ }
+ },
+ "node_modules/@tsconfig/node-lts": {
+ "version": "24.0.0",
+ "resolved": "https://registry.npmjs.org/@tsconfig/node-lts/-/node-lts-24.0.0.tgz",
+ "integrity": "sha512-8mSTqWwCd6aQpvxSrpQlMoA9RiUZSs7bYhL5qsLXIIaN9HQaINeoydrRu/Y7/fws4bvfuyhs0BRnW9/NI8tySg==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..384f2db
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "@tsconfig/node-lts": "^24.0.0"
+ }
+}