feat: create initial todo app with local storage and UI components

This commit is contained in:
Greg 2025-05-13 23:12:57 +02:00
parent f9785f88d0
commit 644b091ea2
5 changed files with 356 additions and 0 deletions

View File

@ -0,0 +1,12 @@
# Coding pattern preferences
Always prefer simple solutions
Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality
You are careful to only make changes that are requested or you are confident are well understood and related to the change being requested
When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don't have duplicate logic.
Keep the codebase very clean and organized
Avoid writing scripts in files if possible, especially if the script is likely only to be run once
Avoid having files over 200300 lines of code. Refactor at that point.
Mocking data is only needed for tests, never mock data for dev or prod
Never add stubbing or fake data patterns to code that affects the dev or prod environments
Never overwrite my .env file without first asking and confirming

31
Input/Todo app.md Normal file
View File

@ -0,0 +1,31 @@
The Todo App is a web application that runs locally and is used to track Todo's. It shouldn't run in the cloud. I should use the local browser storage.
If feasible it should be password protected, but it still should run locally.
It should contain the following fields:
- A unique identifier that is autoincremented and can't be changed
- The creation date (that date should be filled in automatically, but the user should be able to change it)
- The next date on which the user plans to work on the said task
- The due date for the task
- A status field:
- Busy
- Done
- W4A
- (Blank)
- Urgent
- The options are numbers from 1 to 7
- Importance
- The options are numbers from 1 to 7
- Prio
- Result of the multiplication of Urgent and Importance
- Time estimation
- Expressed in minutes
- Actual time spent on the task
- Category
- The user should be able to fill in categories, but as the user types proposal of the existing categories are made
- Task description
- Text field
- Coms filed
- Data type = url
- Link field
- Data type = url
- Comments
- Link with other tasks

193
app.js Normal file
View File

@ -0,0 +1,193 @@
// --- Basic Todo Model ---
const STORAGE_KEY = 'todos-v1';
const CATEGORY_KEY = 'todo-categories';
let todos = [];
let categories = [];
function loadTodos() {
todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
categories = JSON.parse(localStorage.getItem(CATEGORY_KEY) || '[]');
}
function saveTodos() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
localStorage.setItem(CATEGORY_KEY, JSON.stringify(categories));
}
function getNextId() {
return todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1;
}
function renderForm() {
const form = document.getElementById('todo-form');
form.innerHTML = `
<div class="form-section">
<label for="task">Task Description</label>
<input type="text" id="task" placeholder="Task Description" required>
</div>
<div class="form-section grid-3">
<div>
<label for="creation-date">Creation Date</label>
<input type="date" id="creation-date" title="Creation Date">
</div>
<div>
<label for="next-date">Next Planned Work Date</label>
<input type="date" id="next-date" title="Next Planned Work Date">
</div>
<div>
<label for="due-date">Due Date</label>
<input type="date" id="due-date" title="Due Date">
</div>
</div>
<div class="form-section grid-3">
<div>
<label for="status">Status</label>
<select id="status">
<option value="">(Blank)</option>
<option value="Busy">Busy</option>
<option value="Done">Done</option>
<option value="W4A">W4A</option>
</select>
</div>
<div>
<label for="urgent">Urgent (1-7)</label>
<input type="number" id="urgent" min="1" max="7" placeholder="Urgent (1-7)">
</div>
<div>
<label for="importance">Importance (1-7)</label>
<input type="number" id="importance" min="1" max="7" placeholder="Importance (1-7)">
</div>
</div>
<div class="form-section grid-2">
<div>
<label for="time-estimation">Time Estimation (min)</label>
<input type="number" id="time-estimation" min="0" placeholder="Time Est. (min)">
</div>
<div>
<label for="actual-time">Actual Time Spent (min)</label>
<input type="number" id="actual-time" min="0" placeholder="Actual Time (min)">
</div>
</div>
<div class="form-section grid-2">
<div>
<label for="category">Category</label>
<input type="text" id="category" placeholder="Category" list="category-list">
<datalist id="category-list"></datalist>
</div>
<div>
<label for="linked-tasks">Link with other tasks (IDs, comma separated)</label>
<input type="text" id="linked-tasks" placeholder="Link with other tasks (IDs, comma separated)">
</div>
</div>
<div class="form-section grid-2">
<div>
<label for="coms">Coms URL</label>
<input type="url" id="coms" placeholder="Coms URL">
</div>
<div>
<label for="link">Link URL</label>
<input type="url" id="link" placeholder="Link URL">
</div>
</div>
<div class="form-section">
<label for="comments">Comments</label>
<textarea id="comments" placeholder="Comments"></textarea>
</div>
<button type="submit" class="add-todo-btn">Add Todo</button>
`;
// Autocomplete for categories
const datalist = document.getElementById('category-list');
datalist.innerHTML = categories.map(cat => `<option value="${cat}">`).join('');
}
function renderTodos() {
const list = document.getElementById('todos-list');
if (!todos.length) {
list.innerHTML = '<em>No todos yet.</em>';
return;
}
list.innerHTML = todos.map(todo => `
<div class="todo-item">
<div class="field"><b>ID:</b> ${todo.id}</div>
<div class="field"><b>Created:</b> ${todo.creationDate}</div>
<div class="field"><b>Next:</b> ${todo.nextDate || ''}</div>
<div class="field"><b>Due:</b> ${todo.dueDate || ''}</div>
<div class="field status-${todo.status || 'blank'}"><b>Status:</b> ${todo.status || ''}</div>
<div class="field urgent"><b>Urgent:</b> ${todo.urgent}</div>
<div class="field importance"><b>Importance:</b> ${todo.importance}</div>
<div class="field prio"><b>Prio:</b> ${todo.prio}</div>
<div class="field"><b>Est.:</b> ${todo.timeEstimation}m</div>
<div class="field"><b>Spent:</b> ${todo.actualTime}m</div>
<div class="field"><b>Category:</b> ${todo.category || ''}</div>
<div class="field"><b>Task:</b> ${todo.task}</div>
<div class="field"><b>Coms:</b> ${todo.coms ? `<a href="${todo.coms}" target="_blank">Link</a>` : ''}</div>
<div class="field"><b>Link:</b> ${todo.link ? `<a href="${todo.link}" target="_blank">Link</a>` : ''}</div>
<div class="field"><b>Comments:</b> ${todo.comments || ''}</div>
<div class="field"><b>Linked:</b> ${todo.linkedTasks || ''}</div>
</div>
`).join('');
}
function addTodo(e) {
e.preventDefault();
const task = document.getElementById('task').value.trim();
const creationDate = document.getElementById('creation-date').value || new Date().toISOString().slice(0,10);
const nextDate = document.getElementById('next-date').value;
const dueDate = document.getElementById('due-date').value;
const status = document.getElementById('status').value;
const urgent = parseInt(document.getElementById('urgent').value) || 1;
const importance = parseInt(document.getElementById('importance').value) || 1;
const prio = urgent * importance;
const timeEstimation = parseInt(document.getElementById('time-estimation').value) || 0;
const actualTime = parseInt(document.getElementById('actual-time').value) || 0;
let category = document.getElementById('category').value.trim();
if (category && !categories.includes(category)) {
categories.push(category);
}
const coms = document.getElementById('coms').value.trim();
const link = document.getElementById('link').value.trim();
const comments = document.getElementById('comments').value.trim();
const linkedTasks = document.getElementById('linked-tasks').value.trim();
const todo = {
id: getNextId(),
creationDate,
nextDate,
dueDate,
status,
urgent,
importance,
prio,
timeEstimation,
actualTime,
category,
task,
coms,
link,
comments,
linkedTasks
};
todos.push(todo);
saveTodos();
renderForm();
renderTodos();
document.getElementById('todo-form').reset();
}
function init() {
loadTodos();
renderForm();
renderTodos();
document.getElementById('todo-form').addEventListener('submit', addTodo);
}
document.addEventListener('DOMContentLoaded', init);
// Password protection placeholder (to be implemented if needed)
// If you want to enable password, set a flag in localStorage and show/hide #password-section accordingly.

25
index.html Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo App (Local)</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app-container">
<h1>Todo App</h1>
<div id="password-section" style="display:none;">
<input type="password" id="password-input" placeholder="Enter password">
<button id="password-submit">Unlock</button>
</div>
<div id="todo-section">
<form id="todo-form">
<!-- Fields will be rendered by JS -->
</form>
<div id="todos-list"></div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

95
style.css Normal file
View File

@ -0,0 +1,95 @@
body {
font-family: Arial, sans-serif;
background: #f8f9fa;
margin: 0;
padding: 0;
}
#app-container {
max-width: 700px;
margin: 40px auto;
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
h1 {
text-align: center;
}
form {
display: flex;
flex-direction: column;
gap: 18px;
margin-bottom: 24px;
}
.form-section {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 0;
}
.form-section.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
}
.form-section.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 18px;
}
form label {
font-weight: 500;
margin-bottom: 3px;
}
form input, form select, form textarea {
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
width: 100%;
box-sizing: border-box;
}
.add-todo-btn {
margin-top: 10px;
padding: 12px 0;
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
font-size: 1.1rem;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.add-todo-btn:hover {
background: #1742a7;
}
@media (max-width: 700px) {
.form-section.grid-2, .form-section.grid-3 {
grid-template-columns: 1fr;
}
}
#todos-list {
margin-top: 24px;
}
.todo-item {
background: #f1f3f4;
border-radius: 4px;
padding: 12px;
margin-bottom: 12px;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.todo-item .field {
min-width: 120px;
}
.status-Busy { color: #d97706; }
.status-Done { color: #16a34a; }
.status-W4A { color: #2563eb; }
.status-blank { color: #6b7280; }
.urgent, .importance, .prio {
font-weight: bold;
}