Add meal tracker app: Flask + SQLite with auth, dashboard, 31 passing tests, Docker Compose
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}Meal Tracker{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar">
|
||||
<span class="brand">🍽 Meal Tracker</span>
|
||||
{% if session.user_id %}
|
||||
<div class="nav-right">
|
||||
<span class="nav-user">{{ session.username }}</span>
|
||||
<a class="btn btn-sm" href="{{ url_for('auth.logout') }}">Logout</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</nav>
|
||||
|
||||
<main class="container">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div class="flashes">
|
||||
{% for category, msg in messages %}
|
||||
<div class="flash flash-{{ category }}">{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dashboard — Meal Tracker{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="dashboard-header">
|
||||
<h2>
|
||||
{% if history_mode %}History — {{ viewing_date.isoformat() }}{% else %}Today's Meals{% endif %}
|
||||
</h2>
|
||||
<div class="date-nav">
|
||||
<a class="btn btn-sm" href="?date={{ prev_date.isoformat() }}">◀ Prev</a>
|
||||
<span class="date-display">{{ viewing_date.isoformat() }}</span>
|
||||
<a class="btn btn-sm" href="?date={{ next_date.isoformat() }}">Next ▶</a>
|
||||
{% if viewing_date != today %}
|
||||
<a class="btn btn-sm" href="{{ url_for('meals.dashboard') }}">Today</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if not dashboard.users %}
|
||||
<p class="empty-state">No registered users yet. Ask household members to register!</p>
|
||||
{% else %}
|
||||
<div class="table-wrapper">
|
||||
<table class="meal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
{% for period in dashboard.periods %}
|
||||
<th class="meal-col">{{ period.meal_type|capitalize }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in dashboard.users %}
|
||||
<tr>
|
||||
<td class="user-cell">{{ user.username }}</td>
|
||||
{% for period in dashboard.periods %}
|
||||
{% set resp = user.responses[period.meal_type] %}
|
||||
<td class="response-cell status-{{ resp.status }}{% if resp.changed_at %} changed-mind{% endif %}">
|
||||
{% if viewing_date >= today or session.is_admin %}
|
||||
<form method="POST" action="{{ url_for('meals.respond') }}" class="respond-form">
|
||||
<input type="hidden" name="date" value="{{ viewing_date.isoformat() }}">
|
||||
<input type="hidden" name="meal_type" value="{{ period.meal_type }}">
|
||||
<div class="btn-row">
|
||||
<button name="status" value="yes"
|
||||
class="chip chip-yes {% if resp.status == 'yes' %}active{% endif %}"
|
||||
{% if viewing_date < today and not session.is_admin %}disabled{% endif %}
|
||||
>Home</button>
|
||||
<button name="status" value="no"
|
||||
class="chip chip-no {% if resp.status == 'no' %}active{% endif %}"
|
||||
{% if viewing_date < today and not session.is_admin %}disabled{% endif %}
|
||||
>Out</button>
|
||||
</div>
|
||||
</form>
|
||||
{% if resp.changed_at %}
|
||||
<div class="changed-badge" title="Changed mind at {{ resp.changed_at }}">↻ {{ resp.changed_at[11:16] }}</div>
|
||||
{% endif %}
|
||||
{% elif resp.status == 'not_answered' %}
|
||||
<span class="muted">—</span>
|
||||
{% else %}
|
||||
<span class="badge badge-{{ resp.status }}">{{ 'Home' if resp.status == 'yes' else 'Out' }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Login — Meal Tracker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-card">
|
||||
<h2>Log In</h2>
|
||||
<form method="POST">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Log In</button>
|
||||
</form>
|
||||
<p class="form-footer">
|
||||
Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Register — Meal Tracker{% endblock %}
|
||||
{% block content %}
|
||||
<div class="form-card">
|
||||
<h2>Create Account</h2>
|
||||
<form method="POST">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
|
||||
<label for="confirm">Confirm Password</label>
|
||||
<input type="password" id="confirm" name="confirm" required>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Register</button>
|
||||
</form>
|
||||
<p class="form-footer">
|
||||
Already have an account? <a href="{{ url_for('auth.login') }}">Log in</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user