Time Management Web App: Notes on Tasks

Shawn
5 min readOct 17, 2022

--

There’s not a lot of details that can be gathered from just a task’s name. Therefore it makes sense to have notes on them. In this section we add this feature on the workspace page.

API and Socket Setup

Back in the first article we setup the Sequelize model for the task notes. Now we add the functions object and the API. For the functions we’re just doing CRUD operations. Observe how we don’t have any need to get a single note, so we only have a function and route to fetch all notes for a single task.

const TaskNoteFunctions = {
async getByTask(task) {
return TaskNote.findAll({where: {task}});
},
async create(data) {
return TaskNote.create({
task: data.task,
time: data.time,
content: data.content
});
},
async update(id, data) {
const note = await TaskNote.findByPk(id);
note.content = data.content;
return note.save();
},
async del(id) {
const note = await TaskNote.destroy({where: {id}});
return id;
}
}
...app.get('/api/task/:id/notes', async (req, res) => {
const result = await TaskNoteFunctions.getByTask(+req.params.id);
res.send({result});
});
app.post('/api/task/:id/notes', async (req, res) => {
const result = await TaskNoteFunctions.create({
id: req.params.id,
...req.body
});
res.send({result});
});
app.put('/api/task/:id/notes', async (req, res) => {
const {id, ...data} = req.body;
const result = await TaskNoteFunctions.update(id, data);
res.send({result});
});
app.delete('/api/task/:id/notes', async (req, res) => {
const {id} = req.body;
const result = await TaskNoteFunctions.del(id);
res.send({result});
});

The :id parameter in the routes refers to the task id. For the GET and POST routes, this makes sense. But it may seem odd for the PUT and DELETE routes. After all, by this point we have the id for the task note. We even include it in the request body. Ultimately, the decision to format the PUT and DELETE routes this way, is just to keep the API consistent across the methods.

The API will be used not only by the frontend, but also by the web sockets. When the workspace page gets a task from the web socket, we also want to send the task’s notes. So the Socket object needs a reference to the TaskNoteFunctions object.

const socket = new Socket(TaskNoteFunctions);

Then in the Socket object itself:

class Socket {
constructor(taskNoteFns) {
this.taskNoteFns = taskNoteFns;
...
}
async sendTask(task) {
const taskNotes = await this.taskNoteFns.getByTask(task.id);
this.wss.clients.forEach(client => {
if (client.readyState === OPEN) {
const data = {
type: 'task_start',
task,
taskNotes
};
client.send(JSON.stringify(data));
...

Managing Notes on the Frontend

Task notes will be presented in a table. It’ll be pretty similar to how we’re displaying and handling tasks, so we’re going to breeze through this. The components for managing task notes will go into their own file, aptly named task_notes.js . Like before, we start with the API object:

const url = 'http://localhost:3001/api/task';
const headers = {
'Content-Type': 'application/json'
};
const API = {
async create(task, content) {
const res = await fetch(`${url}/${task}/notes`, {
method: 'POST',
headers,
body: JSON.stringify({
task,
content,
time: new Date()
})
});
const {result} = await res.json();
return result;
},
async update(task, body) {
const res = await fetch(`${url}/${task}/notes`, {
method: 'PUT',
headers,
body: JSON.stringify(body)
});
const {result} = await res.json();
return result;
},
async del(task, id) {
const res = await fetch(`${url}/${task}/notes`, {
method: 'DELETE',
headers,
body: JSON.stringify({id})
});
const {result} = await res.json();
return result;
}
};

Nothing too interesting here. The create function only takes the note content as opposed to an entire object. The timestamp is provided in the frontend, and because of how the database is set up, the completed property will be false by default.

Now here’s the structure for the TaskNotes component:

import {useState} from 'react';...const TaskNotes = props => {
const [notes, setNotes] = useState(props.notes);
const handleSave = note => {
setNotes([...notes, note]);
}
const handleUpdate = note => {
const oldNotes = [...notes];
const find = oldNotes.find(n => n.id == note.id);
if (find) {
find.content = note.content;
}
setNotes(oldNotes);
}
const handleDelete = id => {
const oldNotes = [...notes];
const index = oldNotes.findIndex(n => n.id == id);
if (index > -1) {
oldNotes.splice(index, 1);
}
setNotes(oldNotes);
}
return <table border='1'>
<thead>
<tr>
<th>Date/Time</th>
<th>Content</th>
<th>Update</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{notes.map(n => <NoteRow
task={props.task}
key={n.id}
note={n}
handleUpdate={handleUpdate}
handleDelete={handleDelete}
/>)}
<NewNote task={props.task} handleSave={handleSave} />
</tbody>
</table>
}
export default TaskNotes;

The component for adding a new note:

const NewNote = props => {
const [content, setContent] = useState('');
const handleChange = e => setContent(e.target.value);
const save = async e => {
const result = await API.create(props.task, content);
props.handleSave(result);
setContent('');
}

return <tr>
<td></td>
<td><textarea cols={40} rows={10} value={content} onChange={handleChange} /></td>
<td><button onClick={save}>Save</button></td>
<td></td>
</tr>
}

And the component for displaying a note:

const NoteRow = props => {
const [content, setContent] = useState(props.note.content);
const handleChange = e => setContent(e.target.value);
const update = async e => {
const {task} = props;
const result = await API.update(task, {id: props.note.id, content});
props.handleUpdate(result);
}
const del = async e => {
if (window.confirm('Delete note?')) {
const {task} = props;
const result = await API.del(task, props.note.id);
props.handleDelete(props.note.id);
}
}
return <tr>
<td>{(new Date(props.note.time)).toLocaleString()}</td>
<td><textarea cols={40} rows={10} value={content} onChange={handleChange} /></td>
<td><button onClick={update}>Update</button></td>
<td><button onClick={del}>Delete</button></td>
</tr>
}

In order to see the notes in action, we add the TaskNotes component to the Workspace. The Workspace component also needs to get the notes out of the socket data and pass it to the <TaskNotes> component:

 import TaskNotes from './task_notes'; ...    const [task, setTask] = useState({});
const [notes, setNotes] = useState([]);
... useEffect(() => {
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = e => {
try {
const data = JSON.parse(e.data);
setTask(data.task);
setNotes(data.taskNotes);
} catch (e) {
console.log(e);
}
}
}, []);
... </table>
<TaskNotes task={task.id} notes={notes} />
</>

And now when the task appears on the workspace page, you should see this:

And here’s what it looks like with a couple notes added:

You can test the page works by deleting the first note and seeing, after refresh, that the second note is the only one that appears.

Putting It Together

Now we have notes for our tasks!

In the next section we’ll make the app a little easier to use. We’ll do this by 1) providing a basic navigation at the top of the page and 2) getting a list of all tasks scheduled to happen today, and listing them on the home page. Until then, here’s what the code should look like at this point. (You may notice the file names in the Gist start with client_ or server_. Those should be client/ and server/, but Gists don’t allow slashes in the names.)

Time Management Web App Series

--

--