summaryrefslogtreecommitdiffstats
path: root/app/templates
diff options
context:
space:
mode:
authorKen D'Ambrosio <ken@claude>2026-05-25 01:02:59 +0000
committerKen D'Ambrosio <ken@claude>2026-05-25 01:02:59 +0000
commitacfd92ed803e75bd02e291556bba48579add784d (patch)
tree383eb38674da810fccdd89a9102ad84a1b9b75c7 /app/templates
parentb5f0c3ee2c3060dd9821d42f4e1bcbb87cbbee10 (diff)
Add per-user meal plans and household sharing
Each user now has their own meal plan and shopping list. Users can form a household (invite by username, owner can remove members) so that shopping list generation combines all household members' plans. DB migration preserves existing data assigned to user id=1. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/templates')
-rw-r--r--app/templates/base.html5
-rw-r--r--app/templates/household.html106
-rw-r--r--app/templates/shopping_list.html11
3 files changed, 121 insertions, 1 deletions
diff --git a/app/templates/base.html b/app/templates/base.html
index b58ef64..6702474 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -40,6 +40,11 @@
<i class="bi bi-cart me-1"></i>Shopping List
</a>
</li>
+ <li class="nav-item">
+ <a class="nav-link {% if request.endpoint == 'household' %}active{% endif %}" href="/household">
+ <i class="bi bi-people me-1"></i>Household
+ </a>
+ </li>
</ul>
<ul class="navbar-nav ms-auto gap-1 align-items-center">
diff --git a/app/templates/household.html b/app/templates/household.html
new file mode 100644
index 0000000..cbb5121
--- /dev/null
+++ b/app/templates/household.html
@@ -0,0 +1,106 @@
+{% extends "base.html" %}
+{% block title %}Household — Menu Planner{% endblock %}
+
+{% block content %}
+<div class="d-flex align-items-center justify-content-between mb-4">
+ <div>
+ <h1 class="h3 fw-bold mb-0"><i class="bi bi-people me-2"></i>Household</h1>
+ {% if household %}<p class="text-muted mb-0">{{ household.name }}</p>{% endif %}
+ </div>
+</div>
+
+{% if not household %}
+<div class="card shadow-sm">
+ <div class="card-body text-center py-5">
+ <i class="bi bi-house-add fs-1 text-muted mb-3 d-block"></i>
+ <h5 class="fw-semibold">You're not in a household yet</h5>
+ <p class="text-muted">Create one and invite family members.<br>
+ Shopping lists will automatically combine everyone's meal plans.</p>
+ <form method="POST" action="/household/create" class="d-inline-flex gap-2 mt-2">
+ <input type="text" name="name" class="form-control form-control-sm"
+ placeholder="e.g. The Smith Family" style="width:230px">
+ <button type="submit" class="btn btn-primary btn-sm">
+ <i class="bi bi-plus-circle me-1"></i>Create Household
+ </button>
+ </form>
+ </div>
+</div>
+
+{% else %}
+
+<!-- Members -->
+<div class="card shadow-sm mb-4">
+ <div class="card-header fw-semibold d-flex align-items-center gap-2">
+ <i class="bi bi-people-fill"></i> Members
+ <span class="badge bg-secondary ms-auto">{{ members|length }}</span>
+ </div>
+ <ul class="list-group list-group-flush">
+ {% for m in members %}
+ <li class="list-group-item d-flex align-items-center justify-content-between">
+ <div class="d-flex align-items-center gap-2">
+ <i class="bi bi-person-circle text-muted fs-5"></i>
+ <span class="fw-semibold">{{ m.username }}</span>
+ {% if m.id == household.owner_id %}
+ <span class="badge bg-warning text-dark">Owner</span>
+ {% endif %}
+ {% if m.is_me %}
+ <span class="text-muted small">(you)</span>
+ {% endif %}
+ </div>
+ {% if is_owner and not m.is_me %}
+ <form method="POST" action="/household/remove"
+ onsubmit="return confirm('Remove {{ m.username }} from the household?')">
+ <input type="hidden" name="username" value="{{ m.username }}">
+ <button type="submit" class="btn btn-sm btn-outline-danger">
+ <i class="bi bi-person-dash me-1"></i>Remove
+ </button>
+ </form>
+ {% endif %}
+ </li>
+ {% endfor %}
+ </ul>
+</div>
+
+{% if is_owner %}
+<!-- Invite -->
+<div class="card shadow-sm mb-4">
+ <div class="card-header fw-semibold">
+ <i class="bi bi-person-plus me-1"></i>Invite Member
+ </div>
+ <div class="card-body">
+ <form method="POST" action="/household/invite" class="d-flex gap-2">
+ <input type="text" name="username" class="form-control" placeholder="Username" required autocomplete="off">
+ <button type="submit" class="btn btn-primary">Add</button>
+ </form>
+ <p class="text-muted small mt-2 mb-0">
+ The user must already have an account. They'll join immediately and their meal plan
+ will be included in your combined shopping list.
+ </p>
+ </div>
+</div>
+{% endif %}
+
+<!-- Leave / dissolve -->
+<div class="card shadow-sm border-danger border-opacity-25">
+ <div class="card-body">
+ {% if is_owner and members|length > 1 %}
+ <p class="text-muted small mb-2">
+ Remove all other members before you can dissolve the household.
+ </p>
+ <button class="btn btn-outline-danger btn-sm" disabled>
+ <i class="bi bi-box-arrow-left me-1"></i>Dissolve Household
+ </button>
+ {% else %}
+ <form method="POST" action="/household/leave"
+ onsubmit="return confirm('{% if is_owner %}Dissolve this household?{% else %}Leave this household?{% endif %}')">
+ <button type="submit" class="btn btn-outline-danger btn-sm">
+ <i class="bi bi-box-arrow-left me-1"></i>
+ {% if is_owner %}Dissolve Household{% else %}Leave Household{% endif %}
+ </button>
+ </form>
+ {% endif %}
+ </div>
+</div>
+
+{% endif %}
+{% endblock %}
diff --git a/app/templates/shopping_list.html b/app/templates/shopping_list.html
index e6cfaaf..8a3a60e 100644
--- a/app/templates/shopping_list.html
+++ b/app/templates/shopping_list.html
@@ -9,6 +9,9 @@
Week of {{ week_start.strftime('%B %d, %Y') }}
{% if total %}&nbsp;·&nbsp; {{ checked }}/{{ total }} checked{% endif %}
</p>
+ {% if member_count and member_count > 1 %}
+ <p class="text-muted small mb-0"><i class="bi bi-people me-1"></i>Combined from {{ member_count }} household members' plans</p>
+ {% endif %}
</div>
<div class="d-flex gap-2 align-items-center">
<a href="/shopping-list?week={{ prev_week }}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-chevron-left"></i></a>
@@ -145,7 +148,13 @@ async function regenerate() {
if (!res) return;
const data = await res.json();
if (data.success) {
- location.reload();
+ if (data.member_count > 1) {
+ document.getElementById('toastBody').textContent = `Combined from ${data.member_count} members' plans!`;
+ toast.show();
+ setTimeout(() => location.reload(), 1500);
+ } else {
+ location.reload();
+ }
} else {
document.getElementById('toastBody').textContent = data.error || 'No meals planned for this week.';
document.getElementById('toast').className = 'toast align-items-center text-bg-danger border-0';