diff --git a/sample-app/src/app.ts b/sample-app/src/app.ts index b5942f3..cdd26f8 100644 --- a/sample-app/src/app.ts +++ b/sample-app/src/app.ts @@ -1,6 +1,7 @@ import express, { type Express } from 'express'; import { TaskStore } from './tasks/store.js'; import { createTaskRouter } from './tasks/routes.js'; +import { renderTasksPage } from './tasks/view.js'; /** * Build the Express application. A store can be injected to make testing easy. @@ -13,6 +14,10 @@ export function createApp(store: TaskStore = new TaskStore()): Express { res.json({ status: 'ok' }); }); + app.get('/', (_req, res) => { + res.type('html').send(renderTasksPage(store.list())); + }); + app.use('/tasks', createTaskRouter(store)); return app; diff --git a/sample-app/src/server.ts b/sample-app/src/server.ts index 63af235..db81c68 100644 --- a/sample-app/src/server.ts +++ b/sample-app/src/server.ts @@ -1,8 +1,12 @@ import { createApp } from './app.js'; +import { TaskStore } from './tasks/store.js'; const port = Number(process.env.PORT ?? 3000); -createApp().listen(port, () => { +const store = new TaskStore(); +store.seed(['comb my hair', 'brush my teeth', 'pet the octocat', 'plan world domination']); + +createApp(store).listen(port, () => { // eslint-disable-next-line no-console console.log(`Task API listening on http://localhost:${port}`); }); diff --git a/sample-app/src/tasks/store.ts b/sample-app/src/tasks/store.ts index a230b92..c381f04 100644 --- a/sample-app/src/tasks/store.ts +++ b/sample-app/src/tasks/store.ts @@ -38,5 +38,9 @@ export class TaskStore { return task; } + seed(titles: string[]): void { + for (const title of titles) this.create(title); + } + // Exercise 2: add remove(id) so the API can support DELETE /tasks/:id. } diff --git a/sample-app/src/tasks/view.ts b/sample-app/src/tasks/view.ts new file mode 100644 index 0000000..726a5a1 --- /dev/null +++ b/sample-app/src/tasks/view.ts @@ -0,0 +1,124 @@ +import type { Task } from './types.js'; + +function escapeHtml(value: string): string { + return value + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Render a small, modern dark-themed HTML page listing the tasks. + * Kept dependency-free so the demo stays easy to read. + */ +export function renderTasksPage(tasks: Task[]): string { + const total = tasks.length; + const done = tasks.filter((t) => t.completed).length; + + const items = + total === 0 + ? '
  • No tasks yet.
  • ' + : tasks + .map((task) => { + const created = new Date(task.createdAt).toLocaleString(); + return ` +
  • + +
    + ${escapeHtml(task.title)} + created ${escapeHtml(created)} +
    + ${task.completed ? 'Done' : 'Open'} +
  • `; + }) + .join(''); + + return ` + + + + + Task API + + + +
    +
    +

    Tasks

    + ${done}/${total} done +
    + + +
    + +`; +}