Skip to content

Instantly share code, notes, and snippets.

@shawnco
Created October 26, 2022 21:06
Show Gist options
  • Save shawnco/cab58b64323de88fd1602fb144330dfa to your computer and use it in GitHub Desktop.
Save shawnco/cab58b64323de88fd1602fb144330dfa to your computer and use it in GitHub Desktop.
Time Management Web App section 6
#nav {
list-style-type: none;
margin: 0;
padding: 0 0 10px 0;
}
#nav li {
display: inline;
margin-right: 10px
}
import {Outlet} from 'react-router-dom';
function App() {
return (
<div className="App">
<Outlet />
</div>
);
}
export default App;
import {useState, useEffect} from 'react';
import Datepicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css';
const Timepicker = props => {
const {time} = props;
return <Datepicker
selected={time}
showTimeSelect
showTimeSelectOnly
timeIntervals={15}
timeCaption='Time'
dateFormat='h:mm aa'
onChange={props.handleChange}
/>
}
const FrequencyDropdown = props => {
const {frequency} = props;
const handleChange = e => {
props.handleFrequency(e.target.value);
}
return <select value={frequency} onChange={handleChange}>
<option value='daily'>Daily</option>
<option value='weekly'>Weekly</option>
<option value='monthly'>Monthly</option>
</select>
}
const Days = props => {
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d, i) => ({
id: i,
day: d
}));
const monthdays = [...Array(31)].map((u, i) => ({
id: i + 1,
day: (i + 1).toString()
}));
const {frequency, days} = props;
const handleChange = e => {
props.handleDayChange(+e.target.value);
}
if (frequency === 'daily') {
return null;
} else if (frequency === 'weekly') {
return weekdays.map(w => <div key={w.id}>
<input
type='checkbox'
value={w.id}
checked={days.includes(w.id)}
onChange={handleChange}
/> <label>{w.day}</label>
</div>);
} else if (frequency === 'monthly') {
return monthdays.map(m => <div key={m.id}>
<input
type='checkbox'
value={m.id}
checked={days.includes(m.id)}
onChange={handleChange}
/> <label>{m.day}</label>
</div>);
}
}
const CronDate = props => {
const {cron_string} = props;
const [frequency, setFrequency] = useState('daily');
const [time, setTime] = useState(null);
const [days, setDays] = useState([]);
const handleFrequency = freq => setFrequency(freq);
const handleDayChange = day => {
const oldDays = [...days];
const index = oldDays.findIndex(d => day === d);
if (index > -1) {
oldDays.splice(index, 1);
} else {
oldDays.push(day);
}
setDays(oldDays);
}
const handleChange = date => setTime(date);
const allAsterisk = chars => {
const asterisks = chars.filter(c => c === '*');
return asterisks.length === chars.length;
}
const noneAsterisk = chars => {
const nonAsterisks = chars.filter(c => c !== '*');
return nonAsterisks.length === chars.length;
}
useEffect(() => {
if (cron_string === '') {
const now = new Date();
now.setHours(18);
now.setMinutes(0);
now.setSeconds(0);
now.setMilliseconds(0);
setTime(now);
} else {
const [minute, hour, ...rest] = cron_string.split(' ');
const now = new Date();
now.setMinutes(minute);
now.setHours(hour);
setTime(now);
}
}, []);
useEffect(() => {
const {cron_string} = props;
if (cron_string === '') {
return;
}
const [a, b, c, d, e] = cron_string.split(' ');
let freq;
if (noneAsterisk([a, b]) && allAsterisk([c, d, e])) {
freq = 'daily';
} else if (noneAsterisk([a, b, e]) && allAsterisk([c, d])) {
freq = 'weekly';
} else if (noneAsterisk([a, b, c]) && allAsterisk([d, e])) {
freq = 'monthly';
} else {
console.error(`Invalid format for cron string: ${cron_string}`);
}
if (freq) {
setFrequency(freq);
}
}, []);
useEffect(() => {
if (frequency === 'weekly') {
const weeklyDays = cron_string.split(' ')[4];
if (weeklyDays === '*') {
setDays([0, 1, 2, 3, 4, 5, 6]);
} else if (weeklyDays.indexOf(',') > -1) {
setDays(weeklyDays.split(',').map(d => +d));
} else {
setDays([+weeklyDays]);
}
} else if (frequency === 'monthly') {
const monthlyDays = cron_string.split(' ')[2];
if (monthlyDays === '*') {
setDays([1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31]);
} else if (monthlyDays.indexOf(',') > -1) {
setDays(monthlyDays.split(',').map(d => +d));
} else {
setDays([+monthlyDays]);
}
}
}, [frequency]);
useEffect(() => {
if (time) {
const minute = time.getMinutes();
const hour = time.getHours();
let output = `${minute} ${hour} `;
if (frequency === 'daily') {
output += '* * *';
} else if (frequency === 'weekly') {
output += `* * ${days.join(',')}`;
} else if (frequency === 'monthly') {
output += `${days.join(',')} * *`;
}
props.saveNewCronString(output);
}
}, [frequency, days, time]);
return <>
<td><FrequencyDropdown frequency={frequency} handleFrequency={handleFrequency} /></td>
<td><Days frequency={frequency} days={days} handleDayChange={handleDayChange} /></td>
<td><Timepicker time={time} handleChange={handleChange} /></td>
</>
}
export default CronDate;
import {useEffect, useState} from 'react';
const url = 'http://localhost:3001/api/task/today';
const headers = {
'Content-Type': 'applicaton/json'
};
const API = {
async getToday() {
const res = await fetch(`${url}`, {
method: 'GET',
headers
});
const {result} = await res.json();
return result;
}
}
const TaskRow = props => {
const {task} = props;
return <tr>
<td>{task.name}</td>
<td>{task.cron_string}</td>
</tr>
}
const Home = props => {
const [tasks, setTasks] = useState([]);
useEffect(() => {
async function getTasks() {
const data = await API.getToday();
setTasks(data);
}
getTasks();
}, []);
return <>
<h1>Today's Incomplete Tasks</h1>
<table border='1'>
<thead>
<tr>
<th>Name</th>
<th>Cron String</th>
</tr>
</thead>
<tbody>
{tasks.map(t => <TaskRow key={t.id} task={t} />)}
</tbody>
</table>
</>
}
export default Home;
import React from 'react';
import ReactDOM from 'react-dom/client';
import {BrowserRouter, Routes, Route} from 'react-router-dom';
import App from './App';
import TaskTable from './tasks';
import Workspace from './workspace';
import Home from './home';
import Nav from './nav';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<BrowserRouter>
<Nav />
<Routes>
<Route path='/' element={<App />}>
<Route path='' element={<Home />} />
<Route path='tasks' element={<TaskTable />} />
<Route path='workspace' element={<Workspace />} />
</Route>
</Routes>
</BrowserRouter>
</React.StrictMode>
);
import {useState} from 'react';
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;
}
};
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>
}
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>
}
const TaskNotes = props => {
const [notes, setNotes] = useState(props.notes);
const handleSave = note => {
setNotes([...notes, note])
}
const handleUpdate = note => {
console.log('nooooote',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;
import {useState, useEffect} from 'react';
import {isValidCron} from 'cron-validator';
import CronDate from './cron_date';
const url = 'http://localhost:3001/api/task';
const headers = {
'Content-Type': 'application/json'
};
const API = {
async getAll() {
const res = await fetch(`${url}/all`, {method: 'GET', headers});
const {result} = await res.json();
return result;
},
async get(id) {
const res = await fetch(`${url}/${id}`, {method: 'GET',headers});
const {result} = await res.json();
return result;
},
async create(body) {
const res = await fetch(url, {method: 'POST', headers, body: JSON.stringify(body)});
const {result} = await res.json();
return result;
},
async update(id, body) {
const res = await fetch(`${url}/${id}`, {method: 'PUT',headers, body:JSON.stringify(body)});
const {result} = await res.json();
return result;
},
async del(id) {
const res = await fetch(`${url}/${id}`, {method: 'DELETE', headers});
const {result} = await res.json();
return result;
}
}
const NewTask = props => {
const [task, setTask] = useState({name: '', cron_string: '', completed: false});
const handleChange = e => setTask({...task, [e.target.name]:e.target.value});
const saveNewCronString = cron => setTask({...task, cron_string: cron});
const save = async e => {
const result = await API.create(task);
props.handleSave(result);
setTask({name: '', cron_string: '', completed: false});
}
return <tr>
<td></td>
<td><input type='text' name='name' value={task.name} onChange={handleChange} /></td>
<CronDate cron_string={task.cron_string} saveNewCronString={saveNewCronString} />
<td><button onClick={save} disabled={!task.name || !isValidCron(task.cron_string)}>Save</button></td>
<td></td>
</tr>
}
const TaskRow = props => {
const [task, setTask] = useState(props.task);
const [newCronString, setNewCronString] = useState(props.task.cron_string);
const handleClick = e => setTask({...task, completed: !task.completed});
const handleChange = e => setTask({...task, [e.target.name]: e.target.value});
const saveNewCronString = cron => setNewCronString(cron);
const update = async e => {
const {id, ...rest} = task;
rest.cron_string = newCronString;
const result = await API.update(id, rest);
props.handleUpdate(id, result);
}
const del = async e => {
if(window.confirm(`Delete task: ${task.name}?`)) {
const result = await API.del(task.id);
props.handleDelete(task.id);
}
}
return <tr>
<td><input type='checkbox' name='completed' checked={task.completed} onChange={handleClick} /></td>
<td><input type='text' name='name' value={task.name} onChange={handleChange} /></td>
<CronDate cron_string={task.cron_string} saveNewCronString={saveNewCronString} />
<td><button onClick={update} disabled={!task.name || !isValidCron(task.cron_string)}>Update</button></td>
<td><button onClick={del}>Delete</button></td>
</tr>
}
const TaskTable = props => {
const [tasks, setTasks] = useState([]);
const handleSave = task => {
setTasks([...tasks, task]);
}
const handleUpdate = (id, task) => {
const oldTasks = [...tasks];
const find = oldTasks.find(t => t.id == id);
if (find) {
find.completed = task.completed;
find.name = task.name;
find.cron_string = task.cron_string;
}
setTasks(oldTasks);
}
const handleDelete = id => {
const oldTasks = [...tasks];
const index = oldTasks.findIndex(t => t.id == id);
if (index > -1) {
oldTasks.splice(index, 1);
}
setTasks(oldTasks);
}
useEffect(() => {
async function getTasks() {
const data = await API.getAll();
setTasks(data);
}
getTasks();
}, []);
return <table border='1'>
<thead>
<tr>
<th>Completed</th>
<th>Name</th>
<th>Frequency</th>
<th>Days</th>
<th>Time</th>
<th>Edit</th>
<th>Delete</th>
</tr>
</thead>
<tbody>
{tasks.map(t => <TaskRow key={t.id} task={t} handleUpdate={handleUpdate} handleDelete={handleDelete} />)}
<NewTask handleSave={handleSave} />
</tbody>
</table>
}
export default TaskTable;
import {useState, useEffect} from 'react';
import TaskNotes from './task_notes';
const url = 'http://localhost:3001/api/task';
const headers = {
'Content-Type': 'application/json'
};
const API = {
async update(id, body) {
const res = await fetch(`${url}/${id}`, {method: 'PUT', headers, body: JSON.stringify(body)});
const {result} = await res.json();
return result;
}
}
const Workspace = props => {
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);
}
}
}, []);
const handleClick = async e => {
const {id, completed} = task;
const result = await API.update(id, {completed: !completed});
setTask({...task, completed: !completed});
};
if (task.id) {
return <>
<table border='1'>
<thead>
<tr>
<th>Completed</th>
<th>Name</th>
</tr>
</thead>
<tbody>
<tr>
<td><input type='checkbox' name='completed' checked={task.completed} onChange={handleClick} /></td>
<td>{task.name}</td>
</tr>
</tbody>
</table>
<TaskNotes task={task.id} notes={notes} />
</>
} else {
return <b>Waiting for a task to start</b>
}
}
export default Workspace;
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const {Sequelize} = require('sequelize');
const path = require('path');
const TaskSchedule = require('./task_schedule');
const Socket = require('./socket');
const db = new Sequelize('task_schedule', 'username', 'password', {
host: 'localhost',
dialect: 'mysql',
operatorAliases: false,
pool: { max: 5, min: 0, acquire: 30000, idle: 10000 }
});
const Task = db.define('task', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: Sequelize.STRING,
cron_string: Sequelize.STRING,
completed: Sequelize.BOOLEAN
}, {
freezeTableName: true,
timestamps: false
});
const TaskNote = db.define('task_note', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
task: Sequelize.INTEGER,
time: Sequelize.DATE,
content: Sequelize.TEXT
}, {
freezeTableName: true,
timestamps: false
});
const TaskFunctions = {
async getAll() {
return Task.findAll();
},
async get(id) {
return Task.findByPk(id);
},
async create(data) {
const task = Task.create({
name: data.name,
cron_string: data.cron_string
});
return task;
},
async update(id, data) {
const task = await Task.findByPk(id);
task.name = data.name || task.name;
task.cron_string = data.cron_string || task.cron_string;
task.completed = [true, false].includes(data.completed) ? data.completed : task.completed;
await task.save();
return task;
},
async del(id) {
const task = await Task.destroy({where: {id}});
return id;
},
async getIncomplete() {
return Task.findAll({where: {completed: false}});
},
async getByIds(ids) {
return Task.findAll({where:{id: ids}});
},
}
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;
}
}
const socket = new Socket(TaskNoteFunctions);
const taskSchedule = new TaskSchedule(TaskFunctions, socket);
taskSchedule.rebuildSchedule();
const PORT = 3001;
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname + '/../client')));
app.get('/api/test', (req, res) => {
res.send('Hello world!');
});
app.get('/api/task/today', async (req,res)=> {
const start = new Date();
start.setHours(0);
start.setMinutes(0);
start.setSeconds(0);
start.setMilliseconds(0);
const end = new Date();
end.setHours(23);
end.setMinutes(59);
end.setSeconds(59);
end.setMilliseconds(999);
const ids = taskSchedule.getTasksForRange(start.getTime(), end.getTime());
const result = await TaskFunctions.getByIds(ids);
res.send({result});
});
app.get('/api/task/all', async (req, res) => {
const result = await TaskFunctions.getAll();
res.send({result});
});
app.get('/api/task/:id', async (req, res) => {
const result = await TaskFunctions.get(req.params.id);
res.send({result});
});
app.post('/api/task', async (req,res) => {
const result = await TaskFunctions.create(req.body);
await taskSchedule.rebuildSchedule();
res.send({result});
});
app.put('/api/task/:id', async (req, res) => {
const result = await TaskFunctions.update(req.params.id, req.body);
await taskSchedule.rebuildSchedule();
res.send({result});
});
app.delete('/api/task/:id', async (req,res) => {
const result = await TaskFunctions.del(+req.params.id);
await taskSchedule.rebuildSchedule();
res.send({result});
});
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})
});
app.listen(PORT, () => {
console.log(`App is live on port ${PORT}`);
});
const {WebSocketServer, OPEN} = require('ws');
class Socket {
constructor(taskNoteFns) {
this.taskNoteFns = taskNoteFns;
this.wss = new WebSocketServer({port: 8080});
this.wss.on('connection', ws => console.log('Connected!'));
}
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));
}
});
}
}
module.exports = Socket;
const schedule = require('node-schedule');
class TaskSchedule {
constructor(taskFns, socket) {
this.taskFns = taskFns;
this.socket = socket;
process.on('SIGINT', async () => {
await schedule.gracefulShutdown();
process.exit(0);
});
}
addTask(task) {
const {id, cron_string} = task;
const job = schedule.scheduleJob(`job:${id}`, cron_string, () => {
this.socket.sendTask(task);
});
return job;
}
async populateTasks() {
const tasks = await this.taskFns.getIncomplete();
return tasks.map(t => this.addTask(t));
}
clearTasks() {
Object.keys(schedule.scheduledJobs).map(k => {
const job = schedule.scheduledJobs[k];
job.cancel();
});
}
async rebuildSchedule() {
this.clearTasks();
await this.populateTasks();
}
getTasksForRange(start, end) {
const todayTasks = [];
Object.keys(schedule.scheduledJobs).map(k => {
const job = schedule.scheduledJobs[k];
const next = job.nextInvocation().toDate().getTime();
if (next >= start && next <= end) {
const id = +job.name.split(':')[1];
todayTasks.push(id);
}
});
return todayTasks;
}
}
module.exports = TaskSchedule;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment