From 01c0f792484f8f52606eae0e58abe528acef3086 Mon Sep 17 00:00:00 2001 From: Leo Goetz Date: Thu, 22 Jan 2026 09:10:15 +0100 Subject: feat: completed course, added some types and changed output to dist folder --- backend/package-lock.json | 169 ++++++++++++++++++++++++++++++++++++++ backend/package.json | 13 ++- backend/src/db.js | 72 ---------------- backend/src/db.ts | 67 +++++++++++++++ backend/src/routes/api.js | 12 --- backend/src/routes/api.ts | 10 +++ backend/src/routes/events.js | 97 ---------------------- backend/src/routes/events.ts | 105 +++++++++++++++++++++++ backend/src/routes/users.js | 65 --------------- backend/src/routes/users.ts | 67 +++++++++++++++ backend/src/server.js | 40 --------- backend/src/server.ts | 37 +++++++++ backend/src/types.ts | 24 ++++++ backend/tsconfig.json | 46 +++++++++++ frontend/index.html | 2 +- frontend/package-lock.json | 31 +++++++ frontend/package.json | 3 + frontend/src/components/Events.js | 105 ----------------------- frontend/src/components/Events.ts | 129 +++++++++++++++++++++++++++++ frontend/src/components/Forms.js | 66 --------------- frontend/src/components/Forms.ts | 68 +++++++++++++++ frontend/src/components/Header.js | 33 -------- frontend/src/components/Header.ts | 40 +++++++++ frontend/src/components/Icons.js | 19 ----- frontend/src/components/Icons.ts | 19 +++++ frontend/src/main.js | 21 ++--- frontend/tsconfig.json | 9 ++ package-lock.json | 19 +++++ package.json | 5 ++ 29 files changed, 868 insertions(+), 525 deletions(-) delete mode 100644 backend/src/db.js create mode 100644 backend/src/db.ts delete mode 100644 backend/src/routes/api.js create mode 100644 backend/src/routes/api.ts delete mode 100644 backend/src/routes/events.js create mode 100644 backend/src/routes/events.ts delete mode 100644 backend/src/routes/users.js create mode 100644 backend/src/routes/users.ts delete mode 100644 backend/src/server.js create mode 100644 backend/src/server.ts create mode 100644 backend/src/types.ts create mode 100644 backend/tsconfig.json delete mode 100644 frontend/src/components/Events.js create mode 100644 frontend/src/components/Events.ts delete mode 100644 frontend/src/components/Forms.js create mode 100644 frontend/src/components/Forms.ts delete mode 100644 frontend/src/components/Header.js create mode 100644 frontend/src/components/Header.ts delete mode 100644 frontend/src/components/Icons.js create mode 100644 frontend/src/components/Icons.ts create mode 100644 frontend/tsconfig.json create mode 100644 package-lock.json create mode 100644 package.json 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.js deleted file mode 100644 index f20cac1..0000000 --- a/backend/src/db.js +++ /dev/null @@ -1,72 +0,0 @@ -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'}; - - -const db = new Database('src/sqlite.db', { verbose: console.log }); - -console.log(`Initializing database: ${db.name} `); - - -db.pragma('foreign_keys = ON'); - - -db.exec(` - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - username TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - email TEXT - ); - `); -db.exec(` - CREATE TABLE IF NOT EXISTS events ( - id INTEGER PRIMARY KEY, - title TEXT NOT NULL, - description TEXT, - image_url TEXT, - date DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, - host_id INTEGER REFERENCES users NOT NULL - ); - 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); -}); - - -db.exec(` - CREATE TABLE IF NOT EXISTS rsvps ( - event_id INTEGER REFERENCES events NOT NULL, - name TEXT NOT NULL, - email TEXT NOT NULL, - UNIQUE(event_id, email) ON CONFLICT REPLACE - ); - CREATE INDEX IF NOT EXISTS rsvpevents ON rsvps(event_id); -`); - -const upsertRSVP = db.prepare(` - INSERT INTO rsvps VALUES (@event_id, @name, @email) -`); -RSVPS.map((rsvp) => { - upsertRSVP.run(rsvp); -}); - - - -export default db; \ No newline at end of file diff --git a/backend/src/db.ts b/backend/src/db.ts new file mode 100644 index 0000000..2b124f1 --- /dev/null +++ b/backend/src/db.ts @@ -0,0 +1,67 @@ +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: Database = new DatabaseConstructor("src/sqlite.db", { + verbose: console.log, +}); + +console.log(`Initializing database: ${db.name} `); + +db.pragma("foreign_keys = ON"); + +db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + email TEXT + ); + `); +db.exec(` + CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + description TEXT, + image_url TEXT, + date DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + host_id INTEGER REFERENCES users NOT NULL + ); + 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); +}); + +db.exec(` + CREATE TABLE IF NOT EXISTS rsvps ( + event_id INTEGER REFERENCES events NOT NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + UNIQUE(event_id, email) ON CONFLICT REPLACE + ); + CREATE INDEX IF NOT EXISTS rsvpevents ON rsvps(event_id); +`); + +const upsertRSVP = db.prepare(` + INSERT INTO rsvps VALUES (@event_id, @name, @email) +`); +RSVPS.map((rsvp) => { + upsertRSVP.run(rsvp); +}); + +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.js deleted file mode 100644 index b95c747..0000000 --- a/backend/src/routes/events.js +++ /dev/null @@ -1,97 +0,0 @@ -import db from '../db.js'; -import { Router } from 'express'; -import { getUser } from './users.js'; - -const router = Router(); - -const joinHost = (event) => { - const host = getUser(event.host_id); - return { ...event, host }; -} - -const joinRSVPs = (event) => { - const { id } = event; - const getRSVPs = db.prepare('SELECT * FROM rsvps WHERE event_id = @id'); - const rsvps = getRSVPs.all({ id }); - return { ...event, rsvps }; -} - -const getEvent = (eventId) => { - const byId = db.prepare('SELECT * FROM events WHERE id = @eventId'); - const event = byId.get({ eventId }); - return joinHost(event); -} - -router.get('/', (_req, res) => { - const listEvents = db.prepare(`SELECT * FROM events`); - const events = listEvents.all(); - res.json(events.map(joinHost).map(joinRSVPs)); -}); - -const insertEvent = db.prepare(`INSERT INTO events VALUES (@id, @title, @description, @image_url, @date, @host_id)`); - -router.post('/new', (req, res) => { - const data = req.body; - const { lastInsertRowid: id } = insertEvent.run(data); - const event = getEvent(id); - res.status(201).json(event); -}); - -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' }); - } - res.json(event); -}); - -router.patch('/:id', (req, res) => { - const eventId = parseInt(req.params.id); - const patch = req.body; - - const updateCol = db.prepare(` - UPDATE events SET @col = @val WHERE id = @eventId - `); - const updateEvent = db.transaction((patch) => { - for (const [col, val] of Object.entries(patch)) { - updateCol.run({ col, val, eventId }); - } - }); - - updateEvent(Object.entries(patch)); - const updated = getEvent(eventId); - res.json(updated); -}); - -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' }); - } - deleteEvent.run({ eventId }); - res.json(event); -}); - -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)`); - - let [rsvp] = getRSVP.all({ eventId, email }); - if (rsvp) { - // This email has already RSVPed - res.status(200).json({ rsvp }); - } else { - // New RSVP - insertRSVP.run({ name, email, eventId }); - rsvp = getRSVP.run({ eventId, email }); - res.status(201).json({ rsvp }); - } -}); - -export default router; diff --git a/backend/src/routes/events.ts b/backend/src/routes/events.ts new file mode 100644 index 0000000..df65eb5 --- /dev/null +++ b/backend/src/routes/events.ts @@ -0,0 +1,105 @@ +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: Event) => { + const host = getUser(event.host_id) as User; + return { ...event, host }; +}; + +const joinRSVPs = (event: Event) => { + const { id } = event; + const getRSVPs = db.prepare("SELECT * FROM rsvps WHERE event_id = @id"); + const rsvps = getRSVPs.all({ id }) as Rsvps[]; + return { ...event, rsvps }; +}; + +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) => { + const listEvents = db.prepare(`SELECT * FROM events`); + 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)`, +); + +router.post("/new", (req, res) => { + const data = req.body; + const { lastInsertRowid } = insertEvent.run(data); + const id = lastInsertRowid as number; + const event = getEvent(id); + res.status(201).json(event); +}); + +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" }); + } + res.json(event); +}); + +router.patch("/:id", (req, res) => { + const eventId = parseInt(req.params.id); + const patch = req.body; + + const updateCol = db.prepare(` + UPDATE events SET @col = @val WHERE id = @eventId + `); + const updateEvent = db.transaction((patch) => { + for (const [col, val] of Object.entries(patch)) { + updateCol.run({ col, val, eventId }); + } + }); + + updateEvent(Object.entries(patch)); + const updated = getEvent(eventId); + res.json(updated); +}); + +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" }); + } + deleteEvent.run({ eventId }); + res.json(event); +}); + +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)`, + ); + + let [rsvp] = getRSVP.all({ eventId, email }); + if (rsvp) { + // This email has already RSVPed + res.status(200).json({ rsvp }); + } else { + // New RSVP + insertRSVP.run({ name, email, eventId }); + rsvp = getRSVP.run({ eventId, email }); + res.status(201).json({ rsvp }); + } +}); + +export default router; diff --git a/backend/src/routes/users.js b/backend/src/routes/users.js deleted file mode 100644 index 6d874e9..0000000 --- a/backend/src/routes/users.js +++ /dev/null @@ -1,65 +0,0 @@ -import { Router } from 'express'; -import db from '../db.js'; - -const router = Router(); - -export const getUser = (userId) => { - 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`) - const users = listUsers.all(); - res.json(users); -}); - -router.post('/new', (req, res) => { - const data = req.body; - 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 user = getUser(id); - res.json(user); -}); - -router.get('/:id', (req, res) => { - const id = req.params.id; - const user = getUser(id); - if (!user) { - res.status(404).json({ error: 'User not found' }); - } - res.json(user); -}); - -router.patch('/:id', (req, res) => { - const userId = req.params.id; - const patch = req.body; - - const updateCol = db.prepare(` - UPDATE users SET @col = @val WHERE id = @userId - `); - const updateUser = db.transaction((patch) => { - for (const [col, val] of Object.entries(patch)) { - updateCol.run(col, val, userId); - }; - }); - - updateUser(Object.entries(patch)); - const updated = getUser(userId); - res.json(updated); -}); - -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' }); - } - deleteUser.run({ userId }); - res.json(user); -}); - -export default router; diff --git a/backend/src/routes/users.ts b/backend/src/routes/users.ts new file mode 100644 index 0000000..98ec361 --- /dev/null +++ b/backend/src/routes/users.ts @@ -0,0 +1,67 @@ +import { Router } from "express"; +import db from "../db.js"; +import type { User } from "../types.js"; + +const router = Router(); + +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`); + const users = listUsers.all(); + res.json(users); +}); + +router.post("/new", (req, res) => { + const data = req.body; + const cols = Object.keys(data).join(" , "); + const vals = Object.values(data).join(" , "); + const insertUser = db.prepare(`INSERT INTO users(@cols) VALUES (@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 = Number(req.params.id); + const user = getUser(id); + if (!user) { + res.status(404).json({ error: "User not found" }); + } + res.json(user); +}); + +router.patch("/:id", (req, res) => { + const userId = Number(req.params.id); + const patch = req.body; + + const updateCol = db.prepare(` + UPDATE users SET @col = @val WHERE id = @userId + `); + const updateUser = db.transaction((patch) => { + for (const [col, val] of Object.entries(patch)) { + updateCol.run(col, val, userId); + } + }); + + updateUser(Object.entries(patch)); + const updated = getUser(userId); + res.json(updated); +}); + +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" }); + } + deleteUser.run({ userId }); + res.json(user); +}); + +export default router; 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 @@ - \ No newline at end of file + 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.js deleted file mode 100644 index a84d291..0000000 --- a/frontend/src/components/Events.js +++ /dev/null @@ -1,105 +0,0 @@ -import { Calendar } from './Icons.js'; - -const API_URL = import.meta.env.VITE_API_URL; - -const loadEventsData = async () => { - try { - const response = await fetch(`${API_URL}/events`); - return response.json(); - } catch (e) { - console.error(e); - } -} - - -export const EventModal = (event) => { - const formId = `rsvp-form-${event.ID}`; - const modalId = `modal-event-${event.id}` - return ` -
-
- -

RSVP to ${event.title}

-
-
- - - -
-
- - - - -
-
-
` -} - -export const EventCard = (e) => { - const eventDate = new Date(e.date); - const isPast = eventDate < new Date(); - return ` -
-
- ${e.image_url && `${e.title} thumbnail`} -
-
-

${e.title}

-

${Calendar} ${eventDate.toLocaleDateString()}

-

Host: ${e.host?.name || `User ${e.host_id}`}

- - ${e.description && `

${e.description}

`} -
-
- - ${e.rsvps?.length || 0} ${isPast ? 'went' : 'going'} - - ${!isPast ? ` - `: ''} -
- ${EventModal(e)} -
- ` -} - -export const EventsSection = (title, events) => { - return ` -
-

${title} events

-
- ${events.map((e) => EventCard(e)).join('') || 'No events'} -
-
`; -} - -// 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())); - return ` - ${EventsSection('Upcoming', upcoming)} - ${EventsSection('Past', past)} -`})() \ No newline at end of file diff --git a/frontend/src/components/Events.ts b/frontend/src/components/Events.ts new file mode 100644 index 0000000..a92cf83 --- /dev/null +++ b/frontend/src/components/Events.ts @@ -0,0 +1,129 @@ +import { Calendar } from "./Icons"; + +const API_URL = import.meta.env.VITE_API_URL; + +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 => { + try { + const response = await fetch(`${API_URL}/events`); + return response.json(); + } catch (e) { + console.error(e); + return []; + } +}; + +export const EventModal = (event: Event) => { + const formId = `rsvp-form-${event.id}`; + const modalId = `modal-event-${event.id}`; + return ` +
+
+ +

RSVP to ${event.title}

+
+
+ + + +
+
+ + + + +
+
+
`; +}; + +export const EventCard = (e: Event) => { + const eventDate = new Date(e.date); + const isPast = eventDate < new Date(); + return ` +
+
+ ${e.image_url && `${e.title} thumbnail`} +
+
+

${e.title}

+

${Calendar} ${eventDate.toLocaleDateString()}

+

Host: ${e.host?.name || `User ${e.host_id}`}

+ + ${e.description ? `

${e.description}

` : ""} +
+
+ + ${e.rsvps?.length || 0} ${isPast ? "went" : "going"} + + ${ + !isPast + ? ` + ` + : "" + } +
+ ${EventModal(e)} +
+ `; +}; + +export const EventsSection = (title: string, events: Event[]) => { + return ` +
+

${title} events

+
+ ${events.map((e) => EventCard(e)).join("") || "No events"} +
+
`; +}; + +// 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()); + return ` + ${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 = ` -
-
-

event me

-

All the events you never knew you needed to attend!

-
- - ${ThemeIcon} - -
-`; - -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 = ` +
+
+

event me

+

All the events you never knew you needed to attend!

+
+ + ${ThemeIcon} + +
+`; + +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.js deleted file mode 100644 index c2eb993..0000000 --- a/frontend/src/components/Icons.js +++ /dev/null @@ -1,19 +0,0 @@ -import heart from '../icons/heart.svg?raw'; -import sunMoon from '../icons/sun-moon.svg?raw'; -import typescript from '../icons/typescript.svg?raw'; -import vite from '../icons/vite.svg?raw'; -import pico from '../icons/pico.svg?raw'; -import calendar from '../icons/calendar.svg?raw'; - -// Export the raw SVG strings -export const Heart = heart; -export const Theme = sunMoon; -export const TypeScript = typescript; -export const Vite = vite; -export const Pico = pico; -export const Calendar = calendar; - - - - - diff --git a/frontend/src/components/Icons.ts b/frontend/src/components/Icons.ts new file mode 100644 index 0000000..c2eb993 --- /dev/null +++ b/frontend/src/components/Icons.ts @@ -0,0 +1,19 @@ +import heart from '../icons/heart.svg?raw'; +import sunMoon from '../icons/sun-moon.svg?raw'; +import typescript from '../icons/typescript.svg?raw'; +import vite from '../icons/vite.svg?raw'; +import pico from '../icons/pico.svg?raw'; +import calendar from '../icons/calendar.svg?raw'; + +// Export the raw SVG strings +export const Heart = heart; +export const Theme = sunMoon; +export const TypeScript = typescript; +export const Vite = vite; +export const Pico = pico; +export const Calendar = calendar; + + + + + 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" + } +} -- cgit v1.3