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:
@@ -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 %}
|
||||
@@ -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>
|
||||
@@ -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">×</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">×</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 %}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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">×</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">×</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 %}
|
||||
Reference in New Issue
Block a user