Initial commit: GoalsBreakDown web app

- Flask backend with TinyDB database
- Multi-user auth with bcrypt password hashing
- Goal CRUD with activation/deactivation and per-user limits
- Task CRUD with status tracking (todo/doing/pending/done)
- Focus rule: one doing task per goal
- Time picker-style scroll view with drag-and-drop reordering
- Admin panel for user management
- uv environment management
This commit is contained in:
Yuyao Huang
2026-05-08 12:41:19 +08:00
commit f3bffa40cd
20 changed files with 2374 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
{% extends "base.html" %}
{% block title %}Admin - GoalsBreakDown{% endblock %}
{% block extra_css %}
<style>
.admin-page {
padding: 1rem;
}
.admin-header {
margin-bottom: 2rem;
}
.admin-header h1 {
color: #2c3e50;
}
.users-table {
width: 100%;
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.users-table table {
width: 100%;
border-collapse: collapse;
}
.users-table th,
.users-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}
.users-table th {
background: #f8f9fa;
font-weight: 600;
color: #2c3e50;
}
.users-table tr:last-child td {
border-bottom: none;
}
.users-table input[type="number"] {
width: 80px;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.users-table select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.save-btn {
padding: 0.5rem 1rem;
background-color: #27ae60;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.save-btn:hover {
background-color: #229954;
}
</style>
{% endblock %}
{% block content %}
<div class="admin-page">
<div class="admin-header">
<h1>Admin Panel</h1>
</div>
<div class="users-table">
<table>
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Role</th>
<th>Max Goals</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let users = [];
async function loadUsers() {
try {
users = await get("/api/admin/users");
renderUsers();
} catch (error) {
console.error("Failed to load users:", error);
}
}
function renderUsers() {
const tbody = document.getElementById("users-tbody");
tbody.innerHTML = users.map(user => `
<tr>
<td>${user.user_id}</td>
<td>${escapeHtml(user.username)}</td>
<td>
<select onchange="updateUserRole(${user.user_id}, this.value)">
<option value="user" ${user.role === 'user' ? 'selected' : ''}>User</option>
<option value="admin" ${user.role === 'admin' ? 'selected' : ''}>Admin</option>
</select>
</td>
<td>
<input type="number" min="1" value="${user.max_goals}"
id="max-goals-${user.user_id}">
</td>
<td>
<button class="save-btn" onclick="saveUser(${user.user_id})">Save</button>
</td>
</tr>
`).join("");
}
async function saveUser(userId) {
const maxGoals = parseInt(document.getElementById(`max-goals-${userId}`).value);
try {
await put(`/api/admin/users/${userId}`, { max_goals });
await loadUsers();
} catch (error) {
alert(error.message);
}
}
async function updateUserRole(userId, role) {
try {
await put(`/api/admin/users/${userId}`, { role });
await loadUsers();
} catch (error) {
alert(error.message);
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
document.addEventListener("DOMContentLoaded", () => {
loadUsers();
});
</script>
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}GoalsBreakDown{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
{% block extra_css %}{% endblock %}
</head>
<body>
<nav class="navbar">
<div class="nav-brand">GoalsBreakDown</div>
<div class="nav-links">
<a href="/goals" class="nav-link">Goals</a>
<a href="/tasks" class="nav-link">Tasks</a>
<a href="/admin" class="nav-link" id="admin-link" style="display: none;">Admin</a>
<span id="user-info" class="nav-user"></span>
<button id="logout-btn" class="nav-btn">Logout</button>
</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<script src="{{ url_for('static', filename='js/api.js') }}"></script>
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
{% block extra_js %}{% endblock %}
</body>
</html>
+56
View File
@@ -0,0 +1,56 @@
{% extends "base.html" %}
{% block title %}Goals - GoalsBreakDown{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/goals.css') }}">
{% endblock %}
{% block content %}
<div class="goals-page">
<div class="goals-header">
<h1>My Goals</h1>
<button id="create-goal-btn" class="btn-success">+ Create Goal</button>
</div>
<div id="goals-list" class="goals-list"></div>
</div>
<div id="goal-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Create Goal</h2>
<button class="modal-close" id="modal-close">&times;</button>
</div>
<form id="goal-form">
<input type="hidden" id="goal-id">
<div class="form-group">
<label for="goal-title">Goal Title</label>
<input type="text" id="goal-title" required>
</div>
<div id="goal-error" class="error-message"></div>
<div class="modal-actions">
<button type="submit" class="btn-primary">Save</button>
<button type="button" class="btn-secondary" id="modal-cancel">Cancel</button>
</div>
</form>
</div>
</div>
<div id="delete-confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Confirm Delete</h2>
<button class="modal-close" id="delete-modal-close">&times;</button>
</div>
<p>Are you sure you want to delete this goal? All associated tasks will also be deleted.</p>
<div class="modal-actions">
<button class="btn-danger" id="confirm-delete">Delete</button>
<button class="btn-secondary" id="cancel-delete">Cancel</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="{{ url_for('static', filename='js/goals.js') }}"></script>
{% endblock %}
+29
View File
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - GoalsBreakDown</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="auth-page">
<div class="auth-container">
<h1>Login</h1>
<form id="login-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div id="error-message" class="error-message"></div>
<button type="submit" class="btn-primary">Login</button>
</form>
<p class="auth-link">Don't have an account? <a href="/register">Register</a></p>
</div>
<script src="{{ url_for('static', filename='js/api.js') }}"></script>
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - GoalsBreakDown</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
</head>
<body class="auth-page">
<div class="auth-container">
<h1>Register</h1>
<form id="register-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="6">
</div>
<div class="form-group">
<label for="confirm-password">Confirm Password</label>
<input type="password" id="confirm-password" name="confirm-password" required minlength="6">
</div>
<div id="error-message" class="error-message"></div>
<button type="submit" class="btn-primary">Register</button>
</form>
<p class="auth-link">Already have an account? <a href="/login">Login</a></p>
</div>
<script src="{{ url_for('static', filename='js/api.js') }}"></script>
<script src="{{ url_for('static', filename='js/auth.js') }}"></script>
</body>
</html>
+93
View File
@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}Tasks - GoalsBreakDown{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/tasks.css') }}">
{% endblock %}
{% block content %}
<div class="tasks-page">
<div class="tasks-header">
<h1>Tasks</h1>
<div class="goal-selector-container">
<label for="goal-selector">Select Goal:</label>
<select id="goal-selector"></select>
</div>
</div>
<div class="tasks-container">
<div id="scroll-view" class="scroll-view">
<div id="tasks-list" class="tasks-list"></div>
</div>
<div id="finished-section" class="finished-section">
<h2>Completed Tasks</h2>
<div id="finished-list" class="finished-list"></div>
</div>
</div>
<button id="create-task-btn" class="btn-success">+ Create Task</button>
</div>
<div id="task-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="task-modal-title">Create Task</h2>
<button class="modal-close" id="task-modal-close">&times;</button>
</div>
<form id="task-form">
<input type="hidden" id="task-id">
<div class="form-group">
<label for="task-title">Task Title</label>
<input type="text" id="task-title" required>
</div>
<div class="form-group">
<label for="task-desc">Description</label>
<textarea id="task-desc" rows="3"></textarea>
</div>
<div id="task-error" class="error-message"></div>
<div class="modal-actions">
<button type="submit" class="btn-primary">Save</button>
<button type="button" class="btn-secondary" id="task-modal-cancel">Cancel</button>
</div>
</form>
</div>
</div>
<div id="side-panel" class="side-panel">
<div class="side-panel-header">
<h2>Edit Task</h2>
<button class="side-panel-close" id="side-panel-close">&times;</button>
</div>
<div class="side-panel-content">
<div class="form-group">
<label for="edit-task-title">Title</label>
<input type="text" id="edit-task-title">
</div>
<div class="form-group">
<label for="edit-task-desc">Description</label>
<textarea id="edit-task-desc" rows="3"></textarea>
</div>
<div class="form-group">
<label for="edit-task-status">Status</label>
<select id="edit-task-status">
<option value="todo">To Do</option>
<option value="doing">Doing</option>
<option value="pending">Pending</option>
<option value="done">Done</option>
</select>
</div>
<div id="side-panel-error" class="error-message"></div>
<div class="side-panel-actions">
<button class="btn-success" id="save-task-btn">Save</button>
<button class="btn-danger" id="delete-task-btn">Delete</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script src="{{ url_for('static', filename='js/tasks.js') }}"></script>
{% endblock %}