Initial template

This commit is contained in:
ezkeel-admin 2026-06-11 18:49:05 +00:00
commit 3191680f41
5 changed files with 334 additions and 0 deletions

13
Dockerfile Normal file
View file

@ -0,0 +1,13 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY server.js ./
COPY public/ ./public/
EXPOSE 3000
CMD ["npm", "start"]

3
README.md Normal file
View file

@ -0,0 +1,3 @@
# todo-list
Todo list app with Express backend, Postgres database, and single-page HTML frontend

13
package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "todo-list",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"pg": "^8.11.3",
"dotenv": "^16.3.1"
}
}

216
public/index.html Normal file
View file

@ -0,0 +1,216 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo List</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5;
color: #1a1a2e;
display: flex;
justify-content: center;
padding: 40px 16px;
min-height: 100vh;
}
.app {
width: 100%;
max-width: 520px;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 24px;
text-align: center;
}
.input-row {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.input-row input {
flex: 1;
padding: 12px 16px;
font-size: 1rem;
border: 2px solid #d1d5db;
border-radius: 8px;
outline: none;
transition: border-color 0.2s;
}
.input-row input:focus {
border-color: #6366f1;
}
.input-row button {
padding: 12px 20px;
font-size: 1rem;
font-weight: 600;
background: #6366f1;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.input-row button:hover { background: #4f46e5; }
.todo-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.todo-item {
display: flex;
align-items: center;
gap: 12px;
background: white;
padding: 14px 16px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,0.08);
transition: opacity 0.2s;
}
.todo-item.done .todo-text {
text-decoration: line-through;
color: #9ca3af;
}
.todo-item input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: #6366f1;
cursor: pointer;
flex-shrink: 0;
}
.todo-text {
flex: 1;
font-size: 1rem;
word-break: break-word;
}
.todo-date {
font-size: 0.75rem;
color: #9ca3af;
white-space: nowrap;
}
.todo-item .delete-btn {
background: none;
border: none;
color: #ef4444;
font-size: 1.2rem;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
flex-shrink: 0;
}
.todo-item .delete-btn:hover {
background: #fef2f2;
}
.empty {
text-align: center;
color: #9ca3af;
padding: 40px 0;
font-size: 1rem;
}
</style>
</head>
<body>
<div class="app">
<h1>✅ Todo List</h1>
<form class="input-row" id="addForm">
<input type="text" id="todoInput" placeholder="What needs to be done?" autocomplete="off" required>
<button type="submit">Add</button>
</form>
<ul class="todo-list" id="todoList"></ul>
<p class="empty" id="emptyMsg" style="display:none">No todos yet — add one above!</p>
</div>
<script>
const API = '/api/todos';
const todoList = document.getElementById('todoList');
const addForm = document.getElementById('addForm');
const todoInput = document.getElementById('todoInput');
const emptyMsg = document.getElementById('emptyMsg');
async function loadTodos() {
const res = await fetch(API);
const todos = await res.json();
todoList.innerHTML = '';
if (todos.length === 0) {
emptyMsg.style.display = 'block';
} else {
emptyMsg.style.display = 'none';
todos.forEach(t => {
const li = document.createElement('li');
li.className = 'todo-item' + (t.done ? ' done' : '');
li.dataset.id = t.id;
const date = new Date(t.created_at).toLocaleDateString(undefined, {
month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
});
li.innerHTML = `
<input type="checkbox" ${t.done ? 'checked' : ''} />
<span class="todo-text"></span>
<span class="todo-date">${date}</span>
<button class="delete-btn" title="Delete"></button>
`;
li.querySelector('.todo-text').textContent = t.text;
li.querySelector('input[type="checkbox"]').addEventListener('change', () => toggleTodo(t.id));
li.querySelector('.delete-btn').addEventListener('click', () => deleteTodo(t.id));
todoList.appendChild(li);
});
}
}
async function addTodo(text) {
await fetch(API, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
});
loadTodos();
}
async function toggleTodo(id) {
await fetch(`${API}/${id}/toggle`, { method: 'PUT' });
loadTodos();
}
async function deleteTodo(id) {
await fetch(`${API}/${id}`, { method: 'DELETE' });
loadTodos();
}
addForm.addEventListener('submit', (e) => {
e.preventDefault();
const text = todoInput.value.trim();
if (text) {
addTodo(text);
todoInput.value = '';
todoInput.focus();
}
});
loadTodos();
</script>
</body>
</html>

89
server.js Normal file
View file

@ -0,0 +1,89 @@
const express = require('express');
const { Pool } = require('pg');
const path = require('path');
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function initDB() {
const client = await pool.connect();
try {
await client.query(`
CREATE TABLE IF NOT EXISTS todos (
id SERIAL PRIMARY KEY,
text TEXT NOT NULL,
done BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
)
`);
console.log('Database table ready');
} finally {
client.release();
}
}
// GET /api/todos
app.get('/api/todos', async (req, res) => {
try {
const { rows } = await pool.query('SELECT * FROM todos ORDER BY created_at DESC');
res.json(rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to fetch todos' });
}
});
// POST /api/todos
app.post('/api/todos', async (req, res) => {
const { text } = req.body;
if (!text || typeof text !== 'string' || !text.trim()) {
return res.status(400).json({ error: '"text" is required' });
}
try {
const { rows } = await pool.query(
'INSERT INTO todos (text) VALUES ($1) RETURNING *',
[text.trim()]
);
res.status(201).json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to create todo' });
}
});
// PUT /api/todos/:id/toggle
app.put('/api/todos/:id/toggle', async (req, res) => {
try {
const { rows } = await pool.query(
'UPDATE todos SET done = NOT done WHERE id = $1 RETURNING *',
[req.params.id]
);
if (rows.length === 0) return res.status(404).json({ error: 'Todo not found' });
res.json(rows[0]);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to toggle todo' });
}
});
// DELETE /api/todos/:id
app.delete('/api/todos/:id', async (req, res) => {
try {
const { rowCount } = await pool.query('DELETE FROM todos WHERE id = $1', [req.params.id]);
if (rowCount === 0) return res.status(404).json({ error: 'Todo not found' });
res.json({ message: 'Todo deleted' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Failed to delete todo' });
}
});
initDB().then(() => {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
});