Initial commit

This commit is contained in:
ryang3d
2026-03-30 20:14:14 -07:00
commit 9145687b68
6 changed files with 2009 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Dependencies
node_modules/
# AI assistant configuration
.claude/
# User data and persistence
todos.json
# OS-specific files
.DS_Store
Thumbs.db
# Editor files
*.swp
*.swo
*~
.idea/
.vscode/
*.sublime-*
# Logs (optional)
*.log
npm-debug.log*
+215
View File
@@ -0,0 +1,215 @@
# Todo List Application
A modern, feature-rich todo list application built with Express.js and vanilla JavaScript. This application provides a clean interface for managing your daily tasks with persistent storage and an intuitive drag-and-drop reordering system.
> **Built with AI Assistants**: This entire application was developed as an **experiment** to test the capabilities of locally hosted LLMs running on consumer-grade hardware. Using agentic coding techniques, every line of code was generated through iterative agent interactions without any cloud-based APIs or external AI services. This demonstrates what's possible with AI-assisted software development workflows running entirely on personal machines.
## Features
### Core Functionality
- **Create Todos**: Add new tasks with a simple input field
- **View Todos**: See all your tasks in a clean, organized list
- **Edit Todos**: Click on any todo text to edit it inline
- **Complete Tasks**: Check off items as you finish them
- **Delete Todos**: Remove completed or unwanted tasks
- **Drag & Drop Reordering**: Reorganize your tasks by dragging and dropping them into your preferred order
### User Experience
- **Dark/Light Mode Toggle**: Switch between dark and light themes with a single click
- **Customizable Title**: Click on the "Todo List" title to rename it
- **Visual Indicators**: Clear visual feedback when dragging items to reorder
- **Responsive Design**: Clean, modern interface that adapts to your preferences
### Technical Features
- **Persistent Storage**: All todos are saved to a `todos.json` file, so your data persists between sessions
- **RESTful API**: Full REST API endpoints for external integration
- **CORS Enabled**: Supports cross-origin requests for potential mobile or web app integrations
- **Auto-Initialization**: Automatically creates the storage file if it doesn't exist
## Technologies Used
- **Backend**: Node.js with Express.js v5.2.1
- **Frontend**: Vanilla JavaScript, HTML5, CSS3
- **Storage**: File-based JSON storage
- **Styling**: CSS Variables for theming support
## Prerequisites
Before you begin, ensure you have the following installed:
- **Node.js** (v14 or higher recommended)
- **npm** (comes bundled with Node.js)
## Installation & Setup
### 1. Clone or Download the Project
Make sure you have all the project files in your directory:
```
project/
├── server.js
├── index.html
├── package.json
└── todos.json (created automatically)
```
### 2. Install Dependencies
Navigate to the project directory and run:
```bash
npm install
```
This will install the required dependencies:
- `express` - Web framework for Node.js
- `cors` - Middleware for enabling CORS
### 3. Start the Server
Run the server with:
```bash
node server.js
```
You should see the following message:
```
Todo API server running at http://localhost:3000
```
### 4. Access the Application
Open your web browser and navigate to:
```
http://localhost:3000
```
## Usage Guide
### Adding a New Todo
1. Type your task in the input field labeled "What needs to be done?"
2. Click the "Add" button or press Enter
3. Your new todo will appear in the list
### Marking a Todo as Complete
- Click the checkbox to the left of any todo to toggle its completion status
- Completed items will have a strikethrough text style
### Editing a Todo
1. Click directly on the todo text you want to edit
2. The text will become an editable input field
3. Type your changes and press Enter or click outside to save
### Deleting a Todo
- Click the delete button (🗑️ icon) on the right side of any todo to remove it
### Reordering Todos
1. Click and hold on the drag handle (⋮⋮) on the left side of any todo
2. Drag the todo to your desired position
3. You'll see visual indicators showing where the item will be inserted
4. Release to drop the todo in its new position
### Dark Mode Toggle
- Click the moon/sun icon in the top-right corner to switch between dark and light themes
- Your theme preference is saved automatically
### Changing the Title
- Click on the "Todo List" title at the top of the page
- Type your custom title
- Press Enter or click outside to save
## API Endpoints
The application provides a RESTful API for programmatic access:
### Get All Todos
```
GET /todos
```
Returns all todos as a JSON array.
### Create a Todo
```
POST /todos
Content-Type: application/json
{
"text": "Your todo text here"
}
```
Creates a new todo and returns it with an assigned ID.
### Update a Todo
```
PUT /todos/:id
Content-Type: application/json
{
"text": "Updated text (optional)",
"completed": true (optional)
}
```
Updates the specified todo by ID.
### Delete a Todo
```
DELETE /todos/:id
```
Deletes the todo with the specified ID.
### Reorder Todos
```
POST /reorder
Content-Type: application/json
{
"order": [1, 2, 3]
}
```
Reorders todos based on the array of IDs provided.
## File Structure
```
project/
├── server.js # Express.js backend server
├── index.html # Frontend HTML with embedded CSS and JavaScript
├── package.json # Node.js dependencies
└── todos.json # Persistent storage file (auto-created)
```
## Data Storage
All todo data is stored in a `todos.json` file in the project root directory. This file is:
- Automatically created on first run if it doesn't exist
- Updated after every create, update, delete, or reorder operation
- Persisted even after server restart
## Default Port
The application runs on port `3000` by default. You can change this by modifying the `PORT` constant in `server.js`.
## Troubleshooting
### Common Issues
**Port already in use:**
If port 3000 is already occupied, either:
- Stop the process using port 3000
- Change the `PORT` variable in `server.js` to a different value
**Todos not persisting:**
Check that the `todos.json` file has write permissions in your project directory.
**Dependencies not installed:**
Run `npm install` again to ensure all dependencies are properly installed.
## License
This project is provided as-is for educational and personal use.
## Future Enhancements
Potential features for future versions:
- Category/tags support
- Due dates and reminders
- Priority levels
- Search and filter functionality
- Export/import todos
- Cloud synchronization
+782
View File
@@ -0,0 +1,782 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Todo List</title>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
rel="stylesheet"
/>
<style>
:root {
--bg-color: #f5f7fa;
--primary-color: #0066ff;
--primary-dark: #0052cc;
--text-color: #333;
--border-color: #ddd;
--radius: 8px;
--shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.dark-mode {
--bg-color: #1e1e1e;
--primary-color: #bb86fc;
--primary-dark: #a074f2;
--text-color: #e0e0e0;
--border-color: #444;
--shadow: none;
}
body {
font-family: "Inter", sans-serif;
background: var(--bg-color);
margin: 0;
padding: 2rem;
color: var(--text-color);
}
.container {
position: relative; /* needed for absolute toggle */
max-width: 480px;
margin: 0 auto;
background: #fff;
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 2rem;
width: 100%;
}
.dark-mode .container {
background: #2b2b2b;
}
h1 {
font-weight: 600;
text-align: center;
color: var(--primary-color);
margin-bottom: 1.5rem;
}
.input-group {
display: flex;
gap: 0.5rem;
}
#newTodoInput {
flex: 1;
padding: 0.75rem;
border: 1px solid var(--border-color);
border-radius: var(--radius);
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
#newTodoInput:focus {
border-color: var(--primary-color);
}
#addBtn {
background: var(--primary-color);
color: #fff;
border: none;
border-radius: var(--radius);
padding: 0.75rem 1rem;
cursor: pointer;
font-size: 1rem;
transition: background 0.2s;
}
#addBtn:hover {
background: var(--primary-dark);
}
ul {
list-style: none;
padding: 0;
margin-top: 1.5rem;
}
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem;
border-bottom: 1px solid var(--border-color);
border-radius: var(--radius);
background: #fafafa;
transition: background 0.2s;
gap: 0.5rem;
position: relative; /* for highlight overlay */
}
.dark-mode li {
background: #3a3a3a;
color: var(--text-color);
}
li:hover {
background: #f0f8ff;
}
.dark-mode li:hover {
background: #4a4a4a;
}
li.completed .todo-text {
text-decoration: line-through;
color: #888;
}
.drag-handle {
cursor: move;
user-select: none;
display: inline-block;
width: 1.2em;
text-align: center;
margin-right: 0.5rem;
color: #666;
font-weight: bold;
}
.drag-handle:hover {
opacity: 0.7;
}
.todo-text {
flex: 1;
margin-left: 0.75rem;
word-break: break-word;
}
.editing-input {
flex: 1;
font-size: 1rem;
padding: 0.25rem;
border: 1px solid var(--border-color);
border-radius: var(--radius);
box-sizing: border-box;
}
button.action-btn {
background: none;
border: none;
color: #007bff;
cursor: pointer;
font-size: 0.9rem;
padding: 0.25rem 0.5rem;
border-radius: var(--radius);
transition: background 0.2s;
}
button.action-btn:hover {
background: #e0e0ff;
}
button.delete-btn {
background: #ff6b6b;
color: #fff;
padding: 0.25rem 0.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 0.9rem;
border: none;
transition: background 0.2s;
}
button.delete-btn:hover {
background: #e55a5a;
}
.dragging {
opacity: 0.5;
}
/* Visual cue draws a line either ABOVE or BELOW the target li */
.indicator-above {
border-top: 3px solid var(--primary-color);
} /* line ABOVE the item */
.indicator-below {
border-bottom: 3px solid var(--primary-color);
} /* line BELOW the item */
.theme-toggle {
position: absolute;
top: 1rem;
right: 1rem;
background: transparent;
border: none;
color: var(--text-color);
font-size: 1.5rem;
cursor: pointer;
padding: 0;
width: 2rem;
height: 2rem;
line-height: 1;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.theme-toggle:hover {
background: rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div class="container">
<div style="text-align: center; margin-bottom: 1.5rem">
<h1
id="todoTitle"
style="cursor: pointer; display: inline-block"
>
Todo List
</h1>
</div>
<div class="input-group">
<input
type="text"
id="newTodoInput"
placeholder="What needs to be done?"
/>
<button id="addBtn">Add</button>
</div>
<ul id="todoList"></ul>
<button
id="themeToggle"
class="theme-toggle"
aria-label="Toggle dark mode"
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="theme-icon"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
<script>
const input = document.getElementById("newTodoInput");
const addBtn = document.getElementById("addBtn");
const list = document.getElementById("todoList");
const themeToggle = document.getElementById("themeToggle");
const todoTitle = document.getElementById("todoTitle");
// Load theme preference from localStorage
if (localStorage.getItem("darkMode") === "enabled") {
document.body.classList.add("dark-mode");
// Update icon to sun for dark mode
themeToggle.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="5" fill="currentColor" />
<g fill="currentColor">
<circle cx="12" cy="2" r="1" />
<circle cx="12" cy="22" r="1" />
<circle cx="2" cy="12" r="1" />
<circle cx="22" cy="12" r="1" />
<circle cx="4.22" cy="4.22" r="1" />
<circle cx="19.78" cy="19.78" r="1" />
<circle cx="4.22" cy="19.78" r="1" />
<circle cx="19.78" cy="4.22" r="1" />
</g>
</svg>
`;
}
// Load saved title from localStorage or use default
const savedTitle = localStorage.getItem("todoListTitle");
if (savedTitle) {
todoTitle.textContent = savedTitle;
document.title = savedTitle;
}
// Make title editable by clicking
todoTitle.addEventListener("click", function () {
const currentTitle = todoTitle.textContent;
todoTitle.innerHTML = `<input type="text" id="titleInput" value="${currentTitle}" style="font-size: 1.5rem; font-weight: 600; width: 200px; border: 1px solid var(--border-color); padding: 0.25rem; border-radius: var(--radius); text-align: center;">`;
const titleInput = document.getElementById("titleInput");
titleInput.focus();
titleInput.select();
// Handle saving when pressing Enter or losing focus
titleInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
saveTitle();
}
});
titleInput.addEventListener("blur", saveTitle);
});
function saveTitle() {
const titleInput = document.getElementById("titleInput");
if (titleInput) {
const newTitle = titleInput.value.trim();
if (newTitle) {
todoTitle.textContent = newTitle;
document.title = newTitle; // Update browser tab title
localStorage.setItem("todoListTitle", newTitle); // Save to localStorage
} else {
todoTitle.textContent = "Todo List"; // Reset to default
document.title = "Todo List";
localStorage.removeItem("todoListTitle"); // Remove from localStorage
}
}
}
// Load all todos from localStorage
function loadTodos() {
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
list.innerHTML = "";
todos.forEach((todo) => {
const li = document.createElement("li");
li.dataset.id = todo.id;
li.setAttribute("draggable", "true");
li.className = todo.completed ? "completed" : "";
const dragHandle = document.createElement("span");
dragHandle.className = "drag-handle";
dragHandle.textContent = "≡";
dragHandle.title = "Reorder";
li.prepend(dragHandle);
// Create checkbox for strikethrough
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.style.marginRight = "0.5rem";
checkbox.style.cursor = "pointer";
// Set checkbox state based on todo completion
if (todo.completed) {
checkbox.checked = true;
}
// Add event listener to checkbox
checkbox.addEventListener("change", function (e) {
e.stopPropagation(); // Prevent event from bubbling up to parent
// Find the todo text span
const textSpan = li.querySelector(".todo-text");
if (this.checked) {
textSpan.style.textDecoration = "line-through";
textSpan.style.color = "#888";
} else {
textSpan.style.textDecoration = "none";
textSpan.style.color = "";
}
// Update todo in localStorage
const todos = JSON.parse(
localStorage.getItem("todos") || "[]",
);
const updatedTodos = todos.map((t) =>
t.id === todo.id
? { ...t, completed: this.checked }
: t,
);
// Move checked item to correct position (place at bottom of list)
if (this.checked) {
// Save updated todos to localStorage
localStorage.setItem(
"todos",
JSON.stringify(updatedTodos),
);
// Mark this li as completed
li.classList.add("completed");
// Find the last unchecked item to place completed item after it
let lastUncheckedItem = null;
for (
let i = list.children.length - 1;
i >= 0;
i--
) {
if (
!list.children[i].classList.contains(
"completed",
)
) {
lastUncheckedItem = list.children[i];
break;
}
}
// If there are unchecked items, insert after the last unchecked item
// Otherwise, append to the end of the list
if (lastUncheckedItem) {
list.insertBefore(
li,
lastUncheckedItem.nextSibling,
);
} else {
list.appendChild(li);
}
// Get the new order from DOM and reorder todos
const newOrder = Array.from(list.children).map(
(li) => parseInt(li.dataset.id),
);
const reorderedTodos = newOrder.map((id) =>
updatedTodos.find((t) => t.id === id),
);
localStorage.setItem(
"todos",
JSON.stringify(reorderedTodos),
);
} else {
// Capture the current position of li BEFORE any changes
const currentIndex = Array.from(
list.children,
).indexOf(li);
// Find the first checked item BEFORE removing completed class
const firstCheckedItem = Array.from(
list.children,
).find((sibling) =>
sibling.classList.contains("completed"),
);
// Find the last unchecked item (excluding this li) to place unchecked item after it
let lastUncheckedItem = null;
for (
let i = list.children.length - 1;
i >= 0;
i--
) {
const sibling = list.children[i];
if (
sibling !== li &&
!sibling.classList.contains("completed")
) {
lastUncheckedItem = sibling;
break;
}
}
// Remove completed class when unchecking
li.classList.remove("completed");
// If there are checked items, insert before the first checked item
// If there are unchecked items before this one, insert after the last unchecked item
// Otherwise, append to the end of unchecked items
if (firstCheckedItem) {
const firstCheckedIndex = Array.from(
list.children,
).indexOf(firstCheckedItem);
const lastIndex = Array.from(
list.children,
).indexOf(li);
if (
lastUncheckedItem &&
lastIndex > 0 &&
Array.from(list.children).indexOf(
lastUncheckedItem,
) < lastIndex
) {
list.insertBefore(
li,
lastUncheckedItem.nextSibling,
);
} else {
list.insertBefore(li, firstCheckedItem);
}
} else if (
lastUncheckedItem &&
currentIndex > 0 &&
Array.from(list.children).indexOf(
lastUncheckedItem,
) < currentIndex
) {
list.insertBefore(
li,
lastUncheckedItem.nextSibling,
);
}
// Get the new order from DOM after reordering and save to localStorage
const newOrder = Array.from(list.children).map(
(li) => parseInt(li.dataset.id),
);
const reorderedTodos = newOrder.map((id) =>
updatedTodos.find((t) => t.id === id),
);
localStorage.setItem(
"todos",
JSON.stringify(reorderedTodos),
);
}
});
// Insert checkbox at the beginning of the li
li.insertBefore(checkbox, li.firstChild);
const span = document.createElement("span");
span.className = "todo-text";
span.textContent = todo.text;
span.style.cursor = "pointer"; // Make it clear it's editable
li.appendChild(span);
// Make the entire li clickable for editing
li.addEventListener("click", (e) => {
// Don't trigger edit if clicking on the delete button, edit button, or checkbox
if (
e.target.classList.contains("delete-btn") ||
e.target.classList.contains("action-btn") ||
e.target.type === "checkbox"
) {
return;
}
startEdit(li);
});
const delBtn = document.createElement("button");
delBtn.className = "delete-btn";
delBtn.textContent = "Delete";
delBtn.onclick = () => deleteTodo(todo.id);
li.appendChild(delBtn);
// ---------- Drag & Drop Handlers ----------
li.addEventListener("dragstart", (e) => {
e.dataTransfer.setData("text/plain", todo.id);
e.dataTransfer.effectAllowed = "move";
li.classList.add("dragging");
});
li.addEventListener("dragover", (e) => {
e.preventDefault(); // allow drop
// Remove any existing indicator classes from ALL list items
document
.querySelectorAll(
".indicator-above, .indicator-below",
)
.forEach((el) =>
el.classList.remove(
"indicator-above",
"indicator-below",
),
);
const targetLi = li; // the li we are hovering over
const rect = targetLi.getBoundingClientRect();
const offsetY = e.clientY - rect.top;
const halfway = rect.height / 2;
// If cursor is in the upper half of the item, we will INSERT ABOVE it
const insertAbove = offsetY < halfway;
// Store the decision on the element so we can reuse it later
targetLi.dataset.insertAbove = insertAbove;
// Add the appropriate visual indicator class to THIS li only
if (insertAbove) {
targetLi.classList.add("indicator-above"); // draws a line ABOVE the item
} else {
targetLi.classList.add("indicator-below"); // draws a line BELOW the item
}
});
li.addEventListener("dragleave", (e) => {
// Clean up highlight as soon as the cursor leaves this li
highlightDropTarget(li, false);
});
li.addEventListener("drop", handleDrop);
li.addEventListener("dragend", () => {
li.classList.remove("dragging");
// Always strip any lingering indicator classes
document
.querySelectorAll(
".indicator-above, .indicator-below",
)
.forEach((el) =>
el.classList.remove(
"indicator-above",
"indicator-below",
),
);
// Clean the stored flag
delete targetLi.dataset.insertAbove;
});
// --------------------------- ----------------
list.appendChild(li);
});
}
// Add a new todo
async function addTodo() {
const text = input.value.trim();
if (!text) return;
// Get existing todos from localStorage
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
// Create new todo with unique ID
const newTodo = {
id: Date.now(),
text: text,
completed: false,
};
// Add new todo to the array
todos.push(newTodo);
// Save to localStorage
localStorage.setItem("todos", JSON.stringify(todos));
input.value = "";
loadTodos();
}
// Delete a todo
async function deleteTodo(id) {
if (!confirm("Delete this todo?")) return;
// Get existing todos from localStorage
let todos = JSON.parse(localStorage.getItem("todos") || "[]");
// Filter out the todo with the given ID
todos = todos.filter((t) => t.id !== id);
// Save to localStorage
localStorage.setItem("todos", JSON.stringify(todos));
loadTodos();
}
// Inline edit
function startEdit(li) {
const id = li.dataset.id;
const textSpan = li.querySelector(".todo-text");
if (!textSpan) return;
const editingInput = document.createElement("input");
editingInput.type = "text";
editingInput.value = textSpan.textContent;
editingInput.className = "editing-input";
editingInput.style.width = "100%";
editingInput.setAttribute("autofocus", "");
li.replaceChild(editingInput, textSpan);
editingInput.focus();
function commit() {
const newText = editingInput.value.trim();
if (newText !== textSpan.textContent) {
// Get existing todos from localStorage
const todos = JSON.parse(
localStorage.getItem("todos") || "[]",
);
// Update todo text
const updatedTodos = todos.map((t) =>
t.id === id ? { ...t, text: newText } : t,
);
// Save to localStorage
localStorage.setItem(
"todos",
JSON.stringify(updatedTodos),
);
loadTodos();
} else {
loadTodos();
}
}
editingInput.addEventListener("blur", commit);
editingInput.addEventListener("keypress", (e) => {
if (e.key === "Enter") commit();
});
}
// Highlight the target li with either .indicator-above or .indicator-below
function highlightDropTarget(li, remove) {
if (!remove) {
// Caller will handle removal we just return here for clarity
return;
}
li.classList.remove("indicator-above", "indicator-below");
}
// Handle drop to reorder items
async function handleDrop(event) {
event.preventDefault();
const draggedId = event.dataTransfer.getData("text/plain");
const targetLi = event.currentTarget.closest("li");
if (!targetLi || targetLi.dataset.id === draggedId) return;
// Retrieve the insertion side we stored earlier
const insertAbove = targetLi.dataset.insertAbove === "true";
// Remove dragged element from the DOM
const draggedLi = list.querySelector(
`[data-id="${draggedId}"]`,
);
if (!draggedLi) return;
list.removeChild(draggedLi);
// Insert based on the stored decision
if (insertAbove) {
// Insert *before* the target (so dragged item appears ABOVE it)
list.insertBefore(draggedLi, targetLi);
} else {
// Insert *after* the target (so dragged item appears BELOW it)
// If target is the last child, just append
if (targetLi.nextSibling) {
list.insertBefore(draggedLi, targetLi.nextSibling);
} else {
list.appendChild(draggedLi);
}
}
// Clean up any lingering indicator classes and the stored flag
document
.querySelectorAll(".indicator-above, .indicator-below")
.forEach((el) =>
el.classList.remove(
"indicator-above",
"indicator-below",
),
);
delete targetLi.dataset.insertAbove;
// Persist the new order to localStorage
const todos = JSON.parse(localStorage.getItem("todos") || "[]");
const newOrder = Array.from(list.children).map((li) =>
parseInt(li.dataset.id),
);
const reorderedTodos = newOrder.map((id) =>
todos.find((t) => t.id === id),
);
localStorage.setItem("todos", JSON.stringify(reorderedTodos));
}
// Theme toggle
themeToggle.addEventListener("click", () => {
document.body.classList.toggle("dark-mode");
if (document.body.classList.contains("dark-mode")) {
localStorage.setItem("darkMode", "enabled");
// Update icon to sun for dark mode
themeToggle.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="5" fill="currentColor" />
<g fill="currentColor">
<circle cx="12" cy="2" r="1" />
<circle cx="12" cy="22" r="1" />
<circle cx="2" cy="12" r="1" />
<circle cx="22" cy="12" r="1" />
<circle cx="4.22" cy="4.22" r="1" />
<circle cx="19.78" cy="19.78" r="1" />
<circle cx="4.22" cy="19.78" r="1" />
<circle cx="19.78" cy="4.22" r="1" />
</g>
</svg>
`;
} else {
localStorage.setItem("darkMode", "disabled");
// Update icon to moon for light mode
themeToggle.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="theme-icon" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
}
});
// Kick off
addBtn.onclick = addTodo;
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") addTodo();
});
loadTodos();
</script>
</body>
</html>
+850
View File
@@ -0,0 +1,850 @@
{
"name": "local_coder_test1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1"
}
},
"node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
"negotiator": "^1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/body-parser": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
"content-type": "^1.0.5",
"debug": "^4.4.3",
"http-errors": "^2.0.0",
"iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
"qs": "^6.14.1",
"raw-body": "^3.0.1",
"type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/call-bound": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/content-disposition": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
"integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/content-type": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"node_modules/cors": {
"version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
"content-disposition": "^1.0.0",
"content-type": "^1.0.5",
"cookie": "^0.7.1",
"cookie-signature": "^1.2.1",
"debug": "^4.4.0",
"depd": "^2.0.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"finalhandler": "^2.1.0",
"fresh": "^2.0.0",
"http-errors": "^2.0.0",
"merge-descriptors": "^2.0.0",
"mime-types": "^3.0.0",
"on-finished": "^2.4.1",
"once": "^1.4.0",
"parseurl": "^1.3.3",
"proxy-addr": "^2.0.7",
"qs": "^6.14.0",
"range-parser": "^1.2.1",
"router": "^2.2.0",
"send": "^1.1.0",
"serve-static": "^2.2.0",
"statuses": "^2.0.1",
"type-is": "^2.0.1",
"vary": "^1.1.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/finalhandler": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"on-finished": "^2.4.1",
"parseurl": "^1.3.3",
"statuses": "^2.0.1"
},
"engines": {
"node": ">= 18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
"license": "MIT",
"engines": {
"node": ">= 0.10"
}
},
"node_modules/is-promise": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
"license": "MIT"
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
"license": "MIT",
"dependencies": {
"ee-first": "1.1.1"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
"integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
"ipaddr.js": "1.9.1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/qs": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
"integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/raw-body": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
"iconv-lite": "~0.7.0",
"unpipe": "~1.0.0"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
"depd": "^2.0.0",
"is-promise": "^4.0.0",
"parseurl": "^1.3.3",
"path-to-regexp": "^8.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/safer-buffer": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
"node_modules/send": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.3",
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"etag": "^1.8.1",
"fresh": "^2.0.0",
"http-errors": "^2.0.1",
"mime-types": "^3.0.2",
"ms": "^2.1.3",
"on-finished": "^2.4.1",
"range-parser": "^1.2.1",
"statuses": "^2.0.2"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/serve-static": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
"escape-html": "^1.0.3",
"parseurl": "^1.3.3",
"send": "^1.2.0"
},
"engines": {
"node": ">= 18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/setprototypeof": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
"side-channel-list": "^1.0.0",
"side-channel-map": "^1.0.1",
"side-channel-weakmap": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-map": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/side-channel-weakmap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.5",
"object-inspect": "^1.13.3",
"side-channel-map": "^1.0.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT",
"engines": {
"node": ">=0.6"
}
},
"node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1"
}
}
+132
View File
@@ -0,0 +1,132 @@
const express = require("express");
const cors = require("cors");
const path = require("path");
const fs = require("fs").promises;
const app = express();
const PORT = 3000;
// Middleware
app.use(cors());
app.use(express.json());
// File path for todo persistence
const TODO_FILE = "todos.json";
// Load todos from file
async function loadTodos() {
try {
const data = await fs.readFile(TODO_FILE, "utf8");
const parsed = JSON.parse(data);
if (Array.isArray(parsed)) {
return parsed;
}
return [];
} catch (error) {
// If file doesn't exist or is invalid, return empty array
return [];
}
}
// Save todos to file
async function saveTodos(todos) {
try {
await fs.writeFile(TODO_FILE, JSON.stringify(todos, null, 2));
} catch (error) {
console.error("Failed to save todos:", error);
}
}
// Load todos on startup
let todos = [];
let nextId = 1;
async function initializeTodos() {
todos = await loadTodos();
if (todos.length > 0) {
// Find the highest ID and set nextId accordingly
nextId = Math.max(...todos.map(todo => todo.id)) + 1;
}
}
// Initialize todos on startup
initializeTodos();
// GET /todos - get all todos
app.get("/todos", (req, res) => {
res.json(todos);
});
// POST /todos - create a new todo
app.post("/todos", (req, res) => {
const { text } = req.body;
if (!text) {
return res.status(400).json({ error: "Text is required" });
}
const newTodo = { id: nextId++, text };
todos.push(newTodo);
saveTodos(todos); // Save to file after adding
res.status(201).json(newTodo);
});
// PUT /todos/:id - update a todo
app.put("/todos/:id", (req, res) => {
const id = parseInt(req.params.id);
const todo = todos.find((t) => t.id === id);
if (!todo) return res.status(404).json({ error: "Todo not found" });
const { text, completed } = req.body;
if (text !== undefined) todo.text = text;
if (completed !== undefined) todo.completed = completed;
saveTodos(todos); // Save to file after updating
res.json(todo);
});
// DELETE /todos/:id - delete a todo
app.delete("/todos/:id", (req, res) => {
const id = parseInt(req.params.id);
const index = todos.findIndex((t) => t.id === id);
if (index === -1) return res.status(404).json({ error: "Todo not found" });
const deleted = todos.splice(index, 1)[0];
saveTodos(todos); // Save to file after deleting
res.json(deleted);
});
// Reorder todos (client can send new order of ids)
// Expected body: { order: [1,2,3] } where each number is a todo id
app.post("/reorder", (req, res) => {
const { order } = req.body;
if (!Array.isArray(order)) {
return res.status(400).json({ error: "Order must be an array of ids" });
}
// Build a new array in the requested order
const orderedTodos = [];
for (const id of order) {
const item = todos.find((t) => t.id === id);
if (item) orderedTodos.push(item);
}
// Replace the current todos array with the reordered version
todos.length = 0; // empty the existing array
todos.push(...orderedTodos);
saveTodos(todos); // Save to file after reordering
res.json(orderedTodos);
});
// Serve static files (index.html, etc.) from the same directory
app.use(express.static(path.join(__dirname)));
// Start server
app.listen(PORT, () => {
console.log(`Todo API server running at http://localhost:${PORT}`);
});
// Create todos.json file if it doesn't exist
async function ensureTodoFileExists() {
try {
await fs.access(TODO_FILE);
} catch (error) {
// File doesn't exist, create it with empty array
await saveTodos([]);
}
}
ensureTodoFileExists();