Problem
I keep trying to use various todo lists. But I keep coming back to the same situation; not having an active todo list. This can come in two forms:
I don't have a habit to open bespoke software.
I've tried using Excel, notes and calendars as they're close to my current work flows. But I tend to always forget about them. This is because even though I check my emails, my calendar and my notes are on different windows that I just slowly forget to check.I frequently move away from the 'todo page' in my pad of paper
I have a pad of paper that I see every day. I can't forget about the pad of paper because it's constantly in my face. However as I normally jot down ideas on the paper I sometimes go to the next page, and it's like the 'todo page' never existed.
I have a local website that serves links to frequently used pages.
Like Firefox's new page tab, but better™.
For example when I want to double check itertools
docs I just click on the Python new tab and there's the link.
This solves both problems:
- I'm in control of the page, and my todo list will always be front and center so I can't forget about it.
- My new tab page won't ever change from my local website. Even if I navigate away my new tab will always stay the same.
Environment
My local website is just a single page and I'm making it fairly modular. To achieve this I'm having a 'compiler server' that builds the HTML from various other servers. As such I'm keeping my assets really basic: a single CSS file and a single JS file.
Both scripts are split into two sections:
The beginning of a very basic component library. I've used a couple of other component libraries and they've normally had a prefix to avoid collisions so I've done the same.
The bespoke code to build the todo list information and bind to the REST database server.
Note: the connection to the REST database hasn't been built yet, which allows for a better stack snippet.
Features
Menu (in the bottom left)
↻
Refresh the list with the REST database server. (currently a static JSON string)
This is ran on load so the list is populated on start.+
Add new task. This will change the view port to a form to add a new task. Clicking again will close the form.
List (in the top left)
A list of tasks. You can click on these to open up the task. Clicking again will close the task.
This is scrollable when 6 or more tasks are entered.
Add a new task to see it yourself :)
View port (on the right)
This has three modes.Default
This is the default mode with nothing displayed. I am planning on putting some information in there that is related to my tasks so I have provided an empty scope.Item
This displays the item with the title in a title bar. And the body in the body.- Clicking the ✔️ or ❌ will allow you to change the status of the task.
- Clicking the × in the top right will go back to the default view.
New task
This is almost identical to the item view, but the title is always "New task" and the body is always a form to fill out.Pressing the "Cancel" button will also change back to the default view.
Review
I would appreciate any comments on my design. I think it looks nice and I've attempted to provide all visual feedback. The feedback is mostly around hovering as I change the mouse pointer and highlight boxes.
I would appreciate most comments on my JavaScript. I've tried to follow what I think is standard JavaScript style. With two main exceptions.
I have used 4 tabs as I don't want to fiddle with my IDE's settings. My code is in a Python project and I don't want the headache.
My code is an old-school god file, rather than modern compiled JS. The simplicity of having two files to maintain greatly outweighs the modern environment I'd set up. Having JS environments for each and every widget with dodgy build processes that automatically move the built code into the relevant server's static directory. Not exactly what I'd call KISS.
Code
const pTodo = {
props: ['todos', 'default'],
emits: ['refresh'],
data: function () {
return {
selected: this.default === undefined ? null : this.default,
};
},
methods: {
setSelected(value) {
this.selected = value === this.selected ? null : value;
}
},
template: `
<div class="p-todo">
<div class="p-aside">
<div class="p-list">
<div
:class="'p-item' + (index === selected ? ' selected' : '')"
@click="setSelected(index)"
v-for="(todo, index) in todos"
>
<slot v-bind:todo="todo" name="list-item"></slot>
</div>
</div>
<div class="p-menu">
<span
@click="setSelected(null); $emit('refresh', null)"
>
↻
</span>
<span
@click="setSelected('new')"
>
+
</span>
</div>
</div>
<div class="p-view">
<slot
v-if="selected === 'new'"
v-bind:setSelected="setSelected"
name="new"
>
</slot>
<slot
v-else-if="selected !== null"
v-bind:todo="todos[selected]"
v-bind:index="selected"
v-bind:setSelected="setSelected"
>
</slot>
<slot
v-else
v-bind:setSelected="setSelected"
name="overview"
>
</slot>
</div>
</div>
`,
};
const pInput = {
props: ['label', 'value'],
emits: ['input'],
template: `
<div class="p-input">
<label>{{ label }}</label>
<input
:value="value"
@input="$emit('input', $event.target.value)"
>
</div>
`,
};
const TODOS = `[
{
"task": "Task 1",
"body": "Body 1",
"completed": true
},
{
"task": "Task 2",
"body": "Body 2",
"completed": false
},
{
"task": "Task 3",
"body": "Body 3",
"completed": false
},
{
"task": "Task 4",
"body": "Body 4",
"completed": false
},
{
"task": "Task 5",
"body": "Body 5",
"completed": false
}
]`;
(function(){
const newTask = {
components: {
'p-input': pInput,
},
data: function () {
return {
task: "",
body: "",
completed: false,
};
},
emits: ['new', 'cancel'],
template: `
<div class="p-form">
<div class="header">
<span>New task</span>
<span class="close" @click="$emit('cancel', null)">×</span>
</div>
<p class="content">
<p-input label="Task" v-model="task"></p-input>
<p-input label="Body" v-model="body"></p-input>
<button
class="p-button--default"
@click="$emit('new', {task: task, body: body, completed: completed})"
>
Submit
</button>
<button
@click="$emit('cancel', null)"
>
Cancel
</button>
</p>
</div>
`,
};
const pane = {
components: {
'p-todo-pane': pTodo,
'new-task': newTask,
},
data: function () {
return {
todos: [],
};
},
created() {
this.refresh();
},
methods: {
refresh() {
this.todos = JSON.parse(TODOS);
},
add(todo, setSelected) {
this.$set(this, 'todos', [...this.todos, todo]);
setSelected(this.todos.length - 1);
},
toggle(todo, index) {
this.$set(this.todos, index, {...todo, completed: !todo.completed});
},
},
template: `
<p-todo-pane
:todos="todos"
@refresh="refresh"
>
<template #list-item="{ todo, setSelected }">
<span>{{ todo.completed ? "✔️" : "❌" }}</span>
<span>{{ todo.task }}</span>
</template>
<template #default="{ todo, setSelected, index }">
<div class="header">
<span
@click="toggle(todo, index)"
>
{{ todo.completed ? "✔️" : "❌" }}
</span>
<span>{{ todo.task }}</span>
<span class="close" @click="setSelected(null)">×</span>
</div>
<p class="content">
{{ todo.body }}
</p>
</template>
<template #new="{ setSelected }">
<new-task
@new="add($event, setSelected)"
@cancel="setSelected(null)"
>
</new-task>
</template>
</p-todo-pane>
`,
};
new Vue({
el: '#todo',
components: {
'todo-pane': pane,
},
data() {
return {};
}
});
})();
html {
background-color: #181a1b;
color: #e8e6e3;
font-family: "DejaVu Serif", "Twemoji Mozilla";
}
@font-face {
font-family: 'Twemoji Mozilla';
/* Changed for the stack snippet */
/* src: url('/static/TwemojiMozilla.ttf') format('truetype'); */
src: url('https://github.com/mozilla/twemoji-colr/releases/download/v0.5.1/TwemojiMozilla.ttf') format('truetype')
}
#todo .p-todo .p-item {
padding: 0.5em;
}
#todo .header {
border-bottom: 1px dotted #333333;
padding-bottom: 0.2em;
}
#todo .header > .close {
float: right;
color: #333333;
cursor: pointer;
}
#todo .content {
margin-bottom: 0;
}
/* Peilonrayz' Components */
.p-todo {
width: 30em;
display: flex;
max-height: 15em;
}
.p-todo > * {
border: 1px solid #111111;
background-color: #222426;
border-right-width: 0;
overflow: hidden;
max-height: inherit;
box-sizing: border-box;
height: 15em;
min-height: 15em;
max-height: 15em;
}
.p-todo > *:first-child {
border-top-left-radius: 0.5em;
border-bottom-left-radius: 0.5em;
}
.p-todo > *:last-child {
border-top-right-radius: 0.5em;
border-bottom-right-radius: 0.5em;
border-right-width: 1px;
}
.p-todo > .p-aside {
flex-grow: 1;
width: 33.33333%;
}
.p-todo > .p-view {
flex-grow: 2;
width: 66.66666%;
padding: 0.5em;
}
.p-todo .p-list {
height: calc(100% - 2em);
overflow-y: scroll;
scrollbar-color: #282a2c #222426;
}
.p-todo .p-list::-webkit-scrollbar
{
width: 12px;
background-color: #222426;
}
.p-todo .p-list::-webkit-scrollbar-thumb
{
background-color: #282a2c;
}
.p-todo .p-menu {
height: 2em;
background-color: #111111;
padding: 0.5em;
display: flex;
width: 100%;
box-sizing: border-box;
justify-content: space-around;
}
.p-todo .p-menu > * {
cursor: pointer;
}
.p-todo .p-list > .p-item {
border-bottom: 1px dotted #333333;
border-top: 1px dotted #333333;
cursor: pointer;
}
.p-todo .p-list > .p-item:first-child {
border-top-width: 0px;
}
.p-todo .p-list > .p-item:last-child {
border-bottom-width: 0px;
}
.p-todo .p-list > .p-item.selected {
background-color: #26282a;
}
.p-todo .p-list > .p-item:not(.selected):hover {
background-color: #242628;
}
.p-input label {
position: absolute;
font-size: 0.8em;
}
.p-input input, .p-input textarea {
background-color : #222426;
border: 0;
border-bottom: 1px dotted #333333;
color: e8e6e3;
padding-top: 1.2em;
margin-bottom: 0.5em;
}
.p-form button {
background-color : #222426;
border: 2px solid #282a2c;
color: e8e6e3;
}
.p-form button:hover {
background-color: #242628;
}
.p-form button.p-button--default {
background-color: #242628;
}
.p-form button.p-button--default:hover {
background-color: #26282a;
}
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<html>
<body>
<div id="todo"><todo-pane></todo-pane></div>
</body>
</html>