diff --git a/makefile b/makefile index 4608ffa..eea3411 100644 --- a/makefile +++ b/makefile @@ -22,3 +22,14 @@ test: check .PHONY: format format: init @pnpm format + +# This command assumes that a `sync` will result in two pushes: +# 1) pushing the code +# 2) pushing the tags +# As a result, the first code push should kick off a workflow run before the +# `gh run watch` command is issued. However, there is some risk that this +# sequence could run too quickly before the workflow run is available. +.PHONY: sync +sync: + @git-town sync + @gh run watch diff --git a/src/experiments/todo/implementations/html-and-expressjs/app.js b/src/experiments/todo/implementations/html-and-expressjs/app.js index 815db5f..55c9cf9 100644 --- a/src/experiments/todo/implementations/html-and-expressjs/app.js +++ b/src/experiments/todo/implementations/html-and-expressjs/app.js @@ -8,6 +8,7 @@ var session = require('express-session') var indexRouter = require('./routes/index') var createTodoRouter = require('./routes/createTodo') var toggleCompletedRouter = require('./routes/toggleCompleted') +var saveAllRouter = require('./routes/saveAll') var app = express() @@ -38,6 +39,7 @@ app.use(express.static(path.join(__dirname, 'public'))) app.use('/', indexRouter) app.use('/todos/create', createTodoRouter) app.use('/todos/toggle-completed', toggleCompletedRouter) +app.use('/todos/save-all', saveAllRouter) // catch 404 and forward to error handler app.use(function (req, res, next) { diff --git a/src/experiments/todo/implementations/html-and-expressjs/routes/index.js b/src/experiments/todo/implementations/html-and-expressjs/routes/index.js index 6382b5e..4693916 100644 --- a/src/experiments/todo/implementations/html-and-expressjs/routes/index.js +++ b/src/experiments/todo/implementations/html-and-expressjs/routes/index.js @@ -1,19 +1,26 @@ -var express = require('express') -var router = express.Router() +const express = require('express') -const TODO_STATUS = { - none: 'none', - completed: 'completed', -} +const { TODO_STATUS } = require('./todos') + +const router = express.Router() + +const createViewModel = (serverTodos, errorMessage) => { + const areAllCompleted = + serverTodos.filter((todo) => todo.status != TODO_STATUS.completed) + .length == 0 -const mapServerTodosToClient = (serverTodos) => { - return serverTodos.map((serverTodo) => { - return { - id: serverTodo.id, - text: serverTodo.text, - isCompleted: serverTodo.status == TODO_STATUS.completed, - } - }) + return { + title: 'Web 1.0: Our Todos', + areAllCompleted, + todos: serverTodos.map((serverTodo) => { + return { + id: serverTodo.id, + text: serverTodo.text, + isCompleted: serverTodo.status == TODO_STATUS.completed, + } + }), + error: errorMessage || null, + } } /* GET home page. */ @@ -22,11 +29,9 @@ router.get('/', function (req, res, next) { req.session.todos = [] } - res.render('index', { - title: 'Our Todos', - todos: mapServerTodosToClient(req.session.todos), - error: req.query.e || null, - }) + const viewModel = createViewModel(req.session.todos, req.query.e) + + res.render('index', viewModel) }) module.exports = router diff --git a/src/experiments/todo/implementations/html-and-expressjs/routes/saveAll.js b/src/experiments/todo/implementations/html-and-expressjs/routes/saveAll.js new file mode 100644 index 0000000..caeab5d --- /dev/null +++ b/src/experiments/todo/implementations/html-and-expressjs/routes/saveAll.js @@ -0,0 +1,53 @@ +const express = require('express') + +const { TODO_STATUS } = require('./todos') + +const router = express.Router() + +router.post('/', function (req, res, next) { + if (!req.session.todos) { + req.session.todos = [] + } + + const originalTodos = req.session.todos + + // start with every todo marked as "none" + const updatedTodos = originalTodos.map((todo) => { + todo.status = TODO_STATUS.none + return todo + }) + + const completedTodoIds = req.body['completed-todo-id'] + ? Array.isArray(req.body['completed-todo-id']) + ? req.body['completed-todo-id'] + : [req.body['completed-todo-id']] + : [] + + for (const completedTodoId of completedTodoIds) { + const todosRequiringCompletion = updatedTodos.filter( + (todo) => todo.id == completedTodoId + ) + + if (todosRequiringCompletion.length > 1) { + res.status(403) + res.send( + `Unexpected duplicate todos discovered with id: ${completedTodoId}` + ) + return + } + + if (todosRequiringCompletion.length === 0) { + res.status(403) + res.send(`Unexpected todo id: ${completedTodoId}`) + return + } + + todosRequiringCompletion[0].status = TODO_STATUS.completed + } + + req.session.todos = updatedTodos + + res.redirect('/') +}) + +module.exports = router diff --git a/src/experiments/todo/implementations/html-and-expressjs/routes/todos.js b/src/experiments/todo/implementations/html-and-expressjs/routes/todos.js new file mode 100644 index 0000000..4da4cb7 --- /dev/null +++ b/src/experiments/todo/implementations/html-and-expressjs/routes/todos.js @@ -0,0 +1,8 @@ +const TODO_STATUS = { + none: 'none', + completed: 'completed', +} + +module.exports = { + TODO_STATUS, +} diff --git a/src/experiments/todo/implementations/html-and-expressjs/views/index.pug b/src/experiments/todo/implementations/html-and-expressjs/views/index.pug index 9ca83ad..4a80be6 100644 --- a/src/experiments/todo/implementations/html-and-expressjs/views/index.pug +++ b/src/experiments/todo/implementations/html-and-expressjs/views/index.pug @@ -5,19 +5,25 @@ block content h1=title if todos.length > 0 form(method="POST",action="/todos/toggle-completed") - button(type="submit",id="toggle-completed",name="toggle-completed") Toggle completed + if areAllCompleted + button(type="submit",id="toggle-completed",name="toggle-completed",aria-label="Toggle all items as complete or incomplete") ∧ + else + button(type="submit",id="toggle-completed",name="toggle-completed",aria-label="Toggle all items as complete or incomplete") ∨ form(method="POST",action="/todos/create") input(name="new-todo",placeholder="What needs to be done?",value="",autofocus,aria-label="New todo") + button(type="submit") Submit if error div(data-testid="error",class="error")= error main if todos.length > 0 - section(data-testid="main") - ul - each todo in todos - li(data-id=todo.id) - label(for="todo-"+todo.id) - input(type="checkbox",id="todo-"+todo.id,name="todo-"+todo.id,checked=todo.isCompleted) - span #{todo.text} + form(method="POST",action="/todos/save-all") + section(data-testid="main") + ul + each todo in todos + li(data-id=todo.id) + label(for="todo-"+todo.id) + input(type="checkbox",id="todo-"+todo.id,name="completed-todo-id",checked=todo.isCompleted,value=todo.id) + span #{todo.text} + button(type="submit") Save if todos.length > 0 footer(data-testid="footer") Some footer content