diff options
| author | pack <pack@packgekko.xyz> | 2026-08-12 17:19:51 +0000 |
|---|---|---|
| committer | pack <pack@packgekko.xyz> | 2026-08-12 17:19:51 +0000 |
| commit | 647f2c663aaf756bee8474e75dff72422ba240c5 (patch) | |
| tree | 7cd5b15024fb7c64cd8dd0bbcc9c7e96466105e1 | |
| parent | 5611527e313c4add16c432e5cf9d55a987558c02 (diff) | |
| download | crud-647f2c663aaf756bee8474e75dff72422ba240c5.tar.gz | |
final commit IIking
forgot to add 'public/' as well.
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | public/app.js | 167 | ||||
| -rw-r--r-- | public/index.html | 50 | ||||
| -rw-r--r-- | public/style.css | 284 |
4 files changed, 503 insertions, 2 deletions
@@ -1,8 +1,8 @@ - + A minimal task management API with user authentication, built with express and better-sqlite3. - + diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..a943f85 --- /dev/null +++ b/public/app.js @@ -0,0 +1,167 @@ +/* Auth. */ +function switchTab(tab) +{ + const loginForm = document.getElementById('login-form'); + const registerForm = document.getElementById('register-form'); + const loginTab = document.getElementById('login-tab'); + const registerTab = document.getElementById('register-tab'); + if (tab === 'login') + { + loginForm.style.display = 'flex'; + registerForm.style.display = 'none'; + loginTab.classList.add('active'); + registerTab.classList.remove('active'); + } + else + { + loginForm.style.display = 'none'; + registerForm.style.display = 'flex'; + loginTab.classList.remove('active'); + registerTab.classList.add('active'); + } + document.getElementById('auth-error').textContent = ''; +} + +async function register(e) +{ + e.preventDefault(); + const username = document.getElementById('reg-username').value; + const password = document.getElementById('reg-password').value; + const res = await fetch('/auth/register', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + if (res.ok) + { + document.getElementById('auth-error').textContent = ''; + showTasks(); + } + else + { + const data = await res.json(); + document.getElementById('auth-error').textContent = data.error || 'Registration failed'; + } +} + +async function login(e) +{ + e.preventDefault(); + const username = document.getElementById('login-username').value; + const password = document.getElementById('login-password').value; + const res = await fetch('/auth/login', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + if (res.ok) + { + document.getElementById('auth-error').textContent = ''; + showTasks(); + } + else + { + const data = await res.json(); + document.getElementById('auth-error').textContent = data.error || 'Login failed'; + } +} + +async function logout() +{ + await fetch('/auth/logout', { method: 'POST' }); + document.getElementById('tasks-section').style.display = 'none'; + document.getElementById('auth-section').style.display = 'block'; +} + +/* Tasks. */ +async function showTasks() +{ + document.getElementById('auth-section').style.display = 'none'; + document.getElementById('tasks-section').style.display = 'block'; + await loadTasks(); +} + +async function loadTasks() +{ + const res = await fetch('/tasks'); + const tasks = await res.json(); + const list = document.getElementById('task-list'); + if (tasks.length === 0) + { + list.innerHTML = '<p class="empty-msg">No tasks yet. Add one above!</p>'; + return; + } + list.innerHTML = tasks.map(task => ` + <li class="task-item"> + <input type="checkbox" ${task.done ? 'checked' : ''} onchange="toggleDone(${task.id}, this.checked)"> + <div class="task-info"> + <div class="task-title ${task.done ? 'done' : ''}">${escapeHtml(task.title)}</div> + ${task.description ? `<div class="task-desc">${escapeHtml(task.description)}</div>` : ''} + </div> + <button class="delete-btn" onclick="deleteTask(${task.id})" title="Delete">×</button> + </li> + `).join(''); +} + +async function createTask(e) +{ + e.preventDefault(); + const title = document.getElementById('task-title').value; + const description = document.getElementById('task-desc').value; + const res = await fetch('/tasks', + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, description }) + }); + if (res.ok) + { + document.getElementById('task-title').value = ''; + document.getElementById('task-desc').value = ''; + await loadTasks(); + } +} + +async function toggleDone(id, done) +{ + const res = await fetch(`/tasks/${id}`, + { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ done: done ? 1 : 0 }) + }); + if (res.ok) + { + await loadTasks(); + } +} + +async function deleteTask(id) +{ + const res = await fetch(`/tasks/${id}`, { method: 'DELETE' }); + if (res.ok) + { + await loadTasks(); + } +} + +/* Helpers. */ +function escapeHtml(str) +{ + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; +} + +/* Init. */ +/* Check if already logged in on page load. */ +(async function init() +{ + const res = await fetch('/auth/check'); + if (res.ok) + { + showTasks(); + } +})(); diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..f9ab8c8 --- /dev/null +++ b/public/index.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<title>​</title> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<meta charset="UTF-8"> +<link rel="stylesheet" href="/style.css"> +</head> +<body> +<div class="container"> +<h1>📝</h1> + +<!--- auth section. ---> +<div id="auth-section"> + <div class="tabs"> + <button class="tab-btn active" id="login-tab" onclick="switchTab('login')">Log in_::</button> + <button class="tab-btn" id="register-tab" onclick="switchTab('register')">Register_::</button> + </div> + <form id="login-form" onsubmit="login(event)"> + <input type="text" id="login-username" placeholder=":user" required> + <input type="password" id="login-password" placeholder=":pass" required> + <button type="submit">Login</button> + </form> + <form id="register-form" style="display:none" onsubmit="register(event)"> + <input type="text" id="reg-username" placeholder=":user" required> + <input type="password" id="reg-password" placeholder=":pass" required> + <button type="submit">Register</button> + </form> + <p id="auth-error" class="error"></p> +</div> + +<!--- tasks section. ---> +<div id="tasks-section" style="display:none"> + <div class="header"> + <h2>Your Tasks</h2> + <button onclick="logout()" class="logout-btn">Logout</button> + </div> + <!--- create task. ---> + <form id="task-form" onsubmit="createTask(event)"> + <input type="text" id="task-title" placeholder="Task title" required> + <input type="text" id="task-desc" placeholder="Description (optional)"> + <button type="submit">Add Task</button> + </form> + <!--- task list. ---> + <ul id="task-list"></ul> +</div> + +<script src="/app.js"></script> +</body> +</html> diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..72d0f9f --- /dev/null +++ b/public/style.css @@ -0,0 +1,284 @@ +* +{ + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body +{ + font-family: 'MS Sans Serif', Tahoma, Geneva, sans-serif; + background: linear-gradient(#cccccc, #ffffff);; + color: #000; + min-height: 100vh; + display: flex; + justify-content: center; + padding: 2rem 1rem; +} + +.container +{ + width: 100%; + max-width: 600px; + background: #c0c0c0; + border-radius: 0; + box-shadow: + inset -1px -1px 0 #000, + inset 1px 1px 0 #fff, + inset -2px -2px 0 #808080, + inset 2px 2px 0 #dfdfdf, + 2px 2px 0 rgba(0, 0, 0, 0.4); + padding: 0; +} + +h1 +{ + text-align: left; + margin: 0; + padding: 0.35rem 0.6rem; + font-size: 0.95rem; + font-weight: bold; + color: #fff; + background: linear-gradient(to bottom, #5a9bd4 0%, #2c689c 48%, #1f5689 52%, #3a7fb8 100%); + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6); + border-bottom: 1px solid #000; +} + +h2 +{ + font-size: 0.95rem; + font-weight: bold; + margin-bottom: 0.6rem; +} + +/* Tabs. */ +.tabs +{ + display: flex; + gap: 2px; + margin-bottom: 0.6rem; + padding: 0.6rem 0.6rem 0; +} + +.tab-btn +{ + flex: 1; + padding: 0.4rem; + border: 1px solid #000; + background: linear-gradient(to bottom, #cfcfcf, #a0a0a0); + color: #000; + border-radius: 0; + cursor: pointer; + font-size: 0.8rem; + font-family: inherit; + box-shadow: + inset -1px -1px 0 #808080, + inset 1px 1px 0 #fff; + transition: none; +} + +.tab-btn.active +{ + background: linear-gradient(to bottom, #7db8e8 0%, #3a7fb8 50%, #2c689c 51%, #5a9bd4 100%); + color: #fff; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); +} + +/* Forms. */ +form +{ + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 0.6rem; + padding: 0 0.6rem; +} + +input[type="text"], +input[type="password"] +{ + padding: 0.3rem; + border: 1px solid #000; + border-radius: 0; + font-size: 0.8rem; + font-family: inherit; + outline: none; + background: #fff; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #fff; + transition: none; +} + +input:focus +{ + border-color: #000; +} + +button[type="submit"] +{ + padding: 0.35rem; + background: linear-gradient(to bottom, #7db8e8 0%, #3a7fb8 50%, #2c689c 51%, #5a9bd4 100%); + color: #fff; + border: 1px solid #000; + border-radius: 0; + font-size: 0.8rem; + font-family: inherit; + cursor: pointer; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + box-shadow: + inset -1px -1px 0 #1f5689, + inset 1px 1px 0 #b0d8f0; + transition: none; +} + +button[type="submit"]:hover +{ + background: linear-gradient(to bottom, #8fc8f0 0%, #4a90d9 50%, #357ab8 51%, #6aabe0 100%); +} + +button[type="submit"]:active +{ + box-shadow: + inset 1px 1px 0 #1f5689, + inset -1px -1px 0 #b0d8f0; +} + +/* Header. */ +.header +{ + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.6rem; + padding: 0 0.6rem; +} + +.logout-btn +{ + padding: 0.3rem 0.8rem; + background: linear-gradient(to bottom, #e88080 0%, #c0392b 50%, #a5281b 51%, #d05040 100%); + color: #fff; + border: 1px solid #000; + border-radius: 0; + cursor: pointer; + font-size: 0.8rem; + font-family: inherit; + text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5); + box-shadow: + inset -1px -1px 0 #802018, + inset 1px 1px 0 #f0b0a0; +} + +.logout-btn:hover +{ + background: linear-gradient(to bottom, #f09090 0%, #d04030 50%, #b53020 51%, #e06050 100%); +} + +.logout-btn:active +{ + box-shadow: + inset 1px 1px 0 #802018, + inset -1px -1px 0 #f0b0a0; +} + +#task-list +{ + list-style: none; + margin-top: 0.6rem; + padding: 0 0.6rem; +} + +.task-item +{ + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.4rem; + border: 1px solid #000; + background: #fff; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #fff; + margin-bottom: 3px; + transition: none; +} + +.task-item:hover +{ + background: #e8eef5; +} + +.task-item input[type="checkbox"] +{ + width: 16px; + height: 16px; + cursor: pointer; +} + +.task-info +{ + flex: 1; +} + +.task-title +{ + font-weight: bold; + font-size: 0.8rem; +} + +.task-title.done +{ + text-decoration: line-through; + color: #808080; +} + +.task-desc +{ + font-size: 0.75rem; + color: #606060; + margin-top: 0.1rem; +} + +.delete-btn +{ + background: #c0c0c0; + border: 1px solid #000; + color: #000; + cursor: pointer; + font-size: 0.9rem; + font-family: inherit; + padding: 0.1rem 0.4rem; + box-shadow: + inset -1px -1px 0 #808080, + inset 1px 1px 0 #fff; +} + +.delete-btn:hover +{ + color: #c0392b; +} + +.delete-btn:active +{ + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #fff; +} + +/* Messages. */ +.error +{ + color: #c0392b; + font-size: 0.8rem; + text-align: center; + padding: 0 0.6rem; +} + +.empty-msg +{ + text-align: center; + color: #606060; + padding: 1.5rem 0; + font-size: 0.8rem; +} |