diff options
Diffstat (limited to 'kitchen/templates')
| -rw-r--r-- | kitchen/templates/kitchen/base.html | 1 | ||||
| -rw-r--r-- | kitchen/templates/kitchen/pantry.html | 366 | ||||
| -rw-r--r-- | kitchen/templates/kitchen/partials/pantry_suggestions.html | 21 | ||||
| -rw-r--r-- | kitchen/templates/kitchen/partials/pantry_table.html | 143 |
4 files changed, 408 insertions, 123 deletions
diff --git a/kitchen/templates/kitchen/base.html b/kitchen/templates/kitchen/base.html index 6bd33bf..323e985 100644 --- a/kitchen/templates/kitchen/base.html +++ b/kitchen/templates/kitchen/base.html @@ -398,6 +398,7 @@ td, th { padding: 0.35rem 0.5rem; font-size: 0.8rem; } } </style> + {% block extrastyle %}{% endblock %} </head> <body> <nav> diff --git a/kitchen/templates/kitchen/pantry.html b/kitchen/templates/kitchen/pantry.html index fac4d91..1f09db4 100644 --- a/kitchen/templates/kitchen/pantry.html +++ b/kitchen/templates/kitchen/pantry.html @@ -1,43 +1,343 @@ {% extends "kitchen/base.html" %} {% block title %}Pantry β Kitchen{% endblock %} +{% block extrastyle %} +<style> + /* leave room for the fixed bottom add bar */ + .container { padding-bottom: 9.5rem; } + + /* --- summary --- */ + .pantry-summary { + font-family: var(--mono, ui-monospace, Menlo, Consolas, monospace); + font-size: 0.74rem; + letter-spacing: 0.02em; + color: var(--sage); + display: flex; + gap: 1rem; + margin: 0.25rem 0 1rem; + } + .pantry-summary b { font-weight: 700; } + .pantry-summary .s-in b { color: var(--teal-light); } + .pantry-summary .s-low b { color: var(--orange-warm); } + .pantry-summary .s-out b { color: var(--red-bright); } + + /* --- group label --- */ + .pgroup-label { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--teal-light); + padding: 1.1rem 0.15rem 0.5rem; + display: flex; + align-items: center; + gap: 0.6rem; + } + .pgroup-label::after { + content: ""; + flex: 1; + height: 1px; + background: rgba(135, 209, 209, 0.14); + } + .pgroup-label .count { color: var(--sage); } + + /* --- item card: colored rail is the glance signal --- */ + .pitem { + position: relative; + background: #1d1a26; + border: 1px solid rgba(135, 209, 209, 0.14); + border-radius: 10px; + padding: 0.7rem 0.8rem 0.7rem 1rem; + margin-bottom: 0.55rem; + overflow: hidden; + transition: opacity 0.18s ease; + } + .pitem::before { + content: ""; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 5px; + background: var(--sage); + transition: background 0.18s ease; + } + .pitem[data-state="in"]::before { background: var(--teal-light); } + .pitem[data-state="low"]::before { background: var(--orange-warm); } + .pitem[data-state="out"]::before { background: var(--red-bright); } + .pitem[data-state="out"] { opacity: 0.55; } + + .pitem-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + } + .pitem-name { + font-size: 1.02rem; + font-weight: 600; + color: var(--cream); + line-height: 1.2; + } + .pitem-name .staple { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.58rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--yellow); + border: 1px solid rgba(249, 223, 17, 0.4); + border-radius: 3px; + padding: 0.05rem 0.3rem; + margin-left: 0.4rem; + vertical-align: middle; + } + + .pitem-bottom { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + margin-top: 0.6rem; + min-height: 40px; + } + .pitem .meta { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.74rem; + color: var(--sage); + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + } + .pitem .pill { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.66rem; + padding: 0.1rem 0.4rem; + border-radius: 4px; + } + .pill.soon { color: var(--orange-warm); background: rgba(250, 159, 69, 0.16); } + .pill.gone { color: var(--red-bright); background: rgba(240, 17, 17, 0.16); } + .pill.frozen { color: var(--teal-light); background: rgba(135, 209, 209, 0.12); } + .pill.ok { color: var(--sage); background: rgba(101, 141, 137, 0.16); } + + /* --- the hero: segmented state switch --- */ + .segctl { + display: inline-flex; + background: rgba(0, 0, 0, 0.28); + border: 1px solid rgba(135, 209, 209, 0.14); + border-radius: 8px; + padding: 2px; + flex-shrink: 0; + } + .seg { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.04em; + color: var(--sage); + background: none; + border: none; + cursor: pointer; + min-width: 44px; + min-height: 36px; + padding: 0 0.5rem; + border-radius: 6px; + transition: background 0.14s ease, color 0.14s ease; + } + .seg:active { transform: translateY(1px); } + .pitem[data-state="in"] .seg[data-v="in"] { background: var(--teal-light); color: var(--bg-dark); } + .pitem[data-state="low"] .seg[data-v="low"] { background: var(--orange-warm); color: var(--bg-dark); } + .pitem[data-state="out"] .seg[data-v="out"] { background: var(--red-bright); color: var(--cream); } + + /* --- per-item menu (move / expiry / delete) --- */ + .menu { position: relative; } + .menu .kebab { + list-style: none; + cursor: pointer; + color: var(--sage); + font-size: 1.3rem; + line-height: 1; + padding: 0.2rem 0.45rem; + border-radius: 6px; + user-select: none; + } + .menu .kebab::-webkit-details-marker { display: none; } + .menu[open] .kebab { color: var(--cream); background: rgba(255, 255, 255, 0.05); } + .menu-pop { + position: absolute; + right: 0; + top: 2rem; + z-index: 40; + background: #221f2d; + border: 1px solid var(--teal-dark); + border-radius: 10px; + padding: 0.4rem; + min-width: 11rem; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + display: flex; + flex-direction: column; + gap: 0.15rem; + } + .menu-act { + text-align: left; + background: none; + border: none; + color: var(--grey-light); + font-size: 0.85rem; + padding: 0.5rem 0.6rem; + border-radius: 6px; + cursor: pointer; + } + .menu-act:hover { background: rgba(135, 209, 209, 0.1); color: var(--cream); } + .menu-act.danger { color: var(--red-bright); } + .menu-act.danger:hover { background: rgba(240, 17, 17, 0.14); } + .exp-form { + display: flex; + gap: 0.3rem; + padding: 0.35rem 0.6rem 0.2rem; + border-top: 1px solid rgba(135, 209, 209, 0.12); + margin-top: 0.15rem; + } + .exp-form input[type="date"] { font-size: 0.78rem; padding: 0.3rem 0.4rem; flex: 1; } + .exp-form button { font-size: 0.78rem; padding: 0.3rem 0.55rem; } + + /* --- bottom add bar (thumb zone) --- */ + .addbar { + position: fixed; + left: 0; right: 0; bottom: 0; + z-index: 60; + background: linear-gradient(rgba(21, 19, 28, 0), var(--bg-dark) 30%); + padding-top: 1.6rem; + } + .addbar-inner { + max-width: 960px; + margin: 0 auto; + padding: 0 1.5rem 1rem; + } + .chips { + display: flex; + gap: 0.4rem; + overflow-x: auto; + padding-bottom: 0.55rem; + scrollbar-width: none; + } + .chips::-webkit-scrollbar { display: none; } + .chip { + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 0.74rem; + white-space: nowrap; + color: var(--peach); + background: rgba(189, 102, 17, 0.16); + border: 1px solid rgba(250, 159, 69, 0.28); + border-radius: 999px; + padding: 0.34rem 0.7rem; + cursor: pointer; + flex-shrink: 0; + } + .chip:active { transform: translateY(1px); } + .chip .plus { color: var(--orange-warm); font-weight: 700; margin-right: 0.15rem; } + + .suggestions { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.55rem; } + .suggestions:empty { display: none; } + .suggestion { + font-size: 0.85rem; + color: var(--cream); + background: rgba(50, 86, 100, 0.3); + border: 1px solid var(--teal-dark); + border-radius: 8px; + padding: 0.45rem 0.7rem; + cursor: pointer; + } + .suggestion:hover { border-color: var(--yellow); } + .suggestion.new { color: var(--yellow); border-style: dashed; } + + .addrow { display: flex; gap: 0.5rem; } + .addrow select { + min-height: 48px; + border-radius: 10px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--teal-dark); + color: var(--cream); + font-size: 0.9rem; + padding: 0 0.5rem; + } + .addrow input[type="text"] { + flex: 1; + min-height: 48px; + background: rgba(0, 0, 0, 0.3); + border: 1px solid var(--teal-dark); + color: var(--cream); + font-size: 0.95rem; + padding: 0.7rem 0.9rem; + border-radius: 10px; + } + .addrow input[type="text"]::placeholder { color: var(--sage); } + .addrow input[type="text"]:focus { + outline: none; + border-color: var(--yellow); + box-shadow: 0 0 0 2px rgba(249, 223, 17, 0.16); + } + .add-btn { + background: var(--yellow); + color: var(--bg-dark); + border: none; + font-size: 1.5rem; + font-weight: 700; + width: 48px; + min-height: 48px; + border-radius: 10px; + cursor: pointer; + flex-shrink: 0; + line-height: 1; + } + .add-btn:active { transform: translateY(1px); } + + @media (prefers-reduced-motion: reduce) { + .pitem, .pitem::before, .seg { transition: none; } + } +</style> +{% endblock %} + {% block content %} <h1>Pantry</h1> -<!-- Add item form --> -<div class="card" style="margin-bottom: 1.5rem;"> - <form hx-post="{% url 'app-pantry-add' %}" hx-target="#pantry-items" hx-swap="innerHTML" hx-on::after-request="if(event.detail.successful) this.reset()"> - {% csrf_token %} - <div class="form-row"> - <div class="form-group" style="flex: 2;"> - <label for="ingredient_name">Ingredient</label> - <input type="text" id="ingredient_name" name="ingredient_name" placeholder="e.g. chicken thighs" required style="width: 100%;"> - </div> - <div class="form-group"> - <label for="quantity">Qty</label> - <input type="number" id="quantity" name="quantity" step="0.01" placeholder="2" required style="width: 100%;"> - </div> - <div class="form-group"> - <label for="unit">Unit</label> - <input type="text" id="unit" name="unit" placeholder="items" required style="width: 100%;"> - </div> - <div class="form-group"> - <label for="location">Location</label> - <select id="location" name="location" style="width: 100%;"> - <option value="fridge">Fridge</option> - <option value="freezer">Freezer</option> - <option value="cupboard">Cupboard</option> - </select> - </div> - <div class="form-group" style="display: flex; align-items: flex-end;"> - <button type="submit" class="btn btn-primary">Add</button> - </div> - </div> - </form> -</div> - -<!-- Pantry items --> <div id="pantry-items"> {% include "kitchen/partials/pantry_table.html" %} </div> + +<div class="addbar"> + <div class="addbar-inner"> + {% if chips %} + <div class="chips"> + {% for c in chips %} + <button class="chip" type="button" + hx-post="{% url 'app-pantry-add' %}" + hx-vals='{"ingredient_name": "{{ c.name|escapejs }}", "location": "{{ c.location }}"}' + hx-target="#pantry-items" hx-swap="innerHTML"> + <span class="plus">+</span>{{ c.name }} + </button> + {% endfor %} + </div> + {% endif %} + + <div id="add-suggestions" class="suggestions"></div> + + <div class="addrow"> + <select id="add-location" name="location" aria-label="Location"> + <option value="fridge">Fridge</option> + <option value="cupboard">Cupboard</option> + <option value="freezer">Freezer</option> + </select> + <input type="text" id="add-input" name="ingredient_name" placeholder="Add somethingβ¦" + autocomplete="off" + hx-get="{% url 'app-pantry-search' %}" + hx-trigger="keyup changed delay:200ms" + hx-target="#add-suggestions" hx-swap="innerHTML"> + <button class="add-btn" aria-label="Add" + hx-post="{% url 'app-pantry-add' %}" + hx-include="#add-input, #add-location" + hx-target="#pantry-items" hx-swap="innerHTML" + hx-on::after-request="document.getElementById('add-input').value=''; document.getElementById('add-suggestions').innerHTML='';">+</button> + </div> + </div> +</div> {% endblock %} diff --git a/kitchen/templates/kitchen/partials/pantry_suggestions.html b/kitchen/templates/kitchen/partials/pantry_suggestions.html new file mode 100644 index 0000000..4ff2048 --- /dev/null +++ b/kitchen/templates/kitchen/partials/pantry_suggestions.html @@ -0,0 +1,21 @@ +{% for s in suggestions %} +<button type="button" class="suggestion" + hx-post="{% url 'app-pantry-add' %}" + hx-vals='{"ingredient_name": "{{ s.name|escapejs }}"}' + hx-include="#add-location" + hx-target="#pantry-items" hx-swap="innerHTML" + hx-on::after-request="document.getElementById('add-input').value=''; document.getElementById('add-suggestions').innerHTML='';"> + {{ s.name }} +</button> +{% empty %} +{% if q %} +<button type="button" class="suggestion new" + hx-post="{% url 'app-pantry-add' %}" + hx-vals='{"ingredient_name": "{{ q|escapejs }}"}' + hx-include="#add-location" + hx-target="#pantry-items" hx-swap="innerHTML" + hx-on::after-request="document.getElementById('add-input').value=''; document.getElementById('add-suggestions').innerHTML='';"> + + Add β{{ q }}β +</button> +{% endif %} +{% endfor %} diff --git a/kitchen/templates/kitchen/partials/pantry_table.html b/kitchen/templates/kitchen/partials/pantry_table.html index 4a8db02..c9b5674 100644 --- a/kitchen/templates/kitchen/partials/pantry_table.html +++ b/kitchen/templates/kitchen/partials/pantry_table.html @@ -1,97 +1,60 @@ -{% if fridge_items or freezer_items or cupboard_items %} - -{% if fridge_items %} -<h2>π§ Fridge</h2> -<table> - <thead><tr><th>Item</th><th>Qty</th><th>Expiry</th><th></th></tr></thead> - <tbody> - {% for item in fridge_items %} - <tr id="pantry-row-{{ item.id }}"> - <td>{{ item.ingredient.name }}</td> - <td>{{ item.quantity|floatformat:0 }} {{ item.unit }}</td> - <td> - {% if item.is_expired %} - <span class="badge badge-expired">Expired {{ item.expiry_date }}</span> - {% elif item.expiring_soon %} - <span class="badge badge-expiring">{{ item.expiry_date }}</span> - {% elif item.expiry_date %} - <span class="badge badge-ok">{{ item.expiry_date }}</span> - {% else %} - <span style="color: var(--sage);">β</span> - {% endif %} - <button class="btn btn-sm btn-secondary" style="margin-left: 0.25rem; padding: 0.1rem 0.3rem; font-size: 0.7rem;" - hx-post="{% url 'app-pantry-edit-expiry' item.id %}" - hx-target="#pantry-row-{{ item.id }}" - hx-swap="outerHTML" - title="Change expiry">π
</button> - </td> - <td style="text-align: right; white-space: nowrap;"> - <button class="btn btn-sm btn-secondary" style="padding: 0.1rem 0.3rem; font-size: 0.7rem;" - hx-post="{% url 'app-pantry-move' item.id %}" - hx-target="#pantry-items" - hx-vals='{"to": "freezer"}' - title="Move to freezer">βοΈ</button> - <button class="btn btn-danger btn-sm" - hx-delete="{% url 'app-pantry-delete' item.id %}" - hx-target="#pantry-items" - hx-confirm="Remove {{ item.ingredient.name }}?">β</button> - </td> - </tr> - {% endfor %} - </tbody> -</table> -{% endif %} - -{% if freezer_items %} -<h2>βοΈ Freezer</h2> -<table> - <thead><tr><th>Item</th><th>Qty</th><th></th></tr></thead> - <tbody> - {% for item in freezer_items %} - <tr id="pantry-row-{{ item.id }}"> - <td>{{ item.ingredient.name }}</td> - <td>{{ item.quantity|floatformat:0 }} {{ item.unit }}</td> - <td style="text-align: right; white-space: nowrap;"> - <button class="btn btn-sm btn-secondary" style="padding: 0.1rem 0.3rem; font-size: 0.7rem;" - hx-post="{% url 'app-pantry-move' item.id %}" - hx-target="#pantry-items" - hx-vals='{"to": "fridge"}' - title="Defrost β Fridge">π§β</button> - <button class="btn btn-danger btn-sm" - hx-delete="{% url 'app-pantry-delete' item.id %}" - hx-target="#pantry-items" - hx-confirm="Remove {{ item.ingredient.name }}?">β</button> - </td> - </tr> - {% endfor %} - </tbody> -</table> -{% endif %} +<div class="pantry-summary"> + <span class="s-in"><b>{{ n_in }}</b> in stock</span> + <span class="s-low"><b>{{ n_low }}</b> running low</span> + <span class="s-out"><b>{{ n_out }}</b> out</span> +</div> -{% if cupboard_items %} -<h2>ποΈ Cupboard</h2> -<table> - <thead><tr><th>Item</th><th>Qty</th><th></th></tr></thead> - <tbody> - {% for item in cupboard_items %} - <tr id="pantry-row-{{ item.id }}"> - <td>{{ item.ingredient.name }}{% if item.is_staple %} <span style="color: var(--yellow); font-size: 0.7rem;">STAPLE</span>{% endif %}</td> - <td>{{ item.quantity|floatformat:0 }} {{ item.unit }}</td> - <td style="text-align: right;"> - <button class="btn btn-danger btn-sm" - hx-delete="{% url 'app-pantry-delete' item.id %}" - hx-target="#pantry-items" - hx-confirm="Remove {{ item.ingredient.name }}?">β</button> - </td> - </tr> +{% if has_items %} + {% for group in groups %} + {% if group.items %} + <div class="pgroup-label">{{ group.label }} <span class="count">{{ group.items|length }}</span></div> + {% for item in group.items %} + <div class="pitem" data-state="{{ item.state }}"> + <div class="pitem-top"> + <div class="pitem-name">{{ item.ingredient.name }}{% if item.is_staple %}<span class="staple">staple</span>{% endif %}</div> + <details class="menu"> + <summary class="kebab" aria-label="More actions">β―</summary> + <div class="menu-pop"> + {% if item.location != "fridge" %} + <button class="menu-act" hx-post="{% url 'app-pantry-move' item.id %}" hx-vals='{"to": "fridge"}' hx-target="#pantry-items" hx-swap="innerHTML">Move to fridge</button> + {% endif %} + {% if item.location != "cupboard" %} + <button class="menu-act" hx-post="{% url 'app-pantry-move' item.id %}" hx-vals='{"to": "cupboard"}' hx-target="#pantry-items" hx-swap="innerHTML">Move to cupboard</button> + {% endif %} + {% if item.location != "freezer" %} + <button class="menu-act" hx-post="{% url 'app-pantry-move' item.id %}" hx-vals='{"to": "freezer"}' hx-target="#pantry-items" hx-swap="innerHTML">Move to freezer</button> + {% endif %} + {% if item.location != "freezer" %} + <form class="exp-form" hx-post="{% url 'app-pantry-save-expiry' item.id %}" hx-target="#pantry-items" hx-swap="innerHTML"> + <input type="date" name="expiry_date" value="{{ item.expiry_date|date:'Y-m-d' }}"> + <button type="submit" class="menu-act" style="background: var(--teal-dark);">Set</button> + </form> + {% endif %} + <button class="menu-act danger" hx-delete="{% url 'app-pantry-delete' item.id %}" hx-target="#pantry-items" hx-swap="innerHTML" hx-confirm="Remove {{ item.ingredient.name }}?">Delete</button> + </div> + </details> + </div> + <div class="pitem-bottom"> + <div class="meta"> + {% if item.quantity %}<span>{{ item.quantity }} {{ item.unit }}</span>{% endif %} + {% if item.location == "freezer" %}<span class="pill frozen">frozen</span> + {% elif item.is_expired %}<span class="pill gone">expired</span> + {% elif item.expiring_soon %}<span class="pill soon">use in {{ item.days_left }}d</span> + {% elif item.expiry_date %}<span class="pill ok">{{ item.expiry_date|date:'j M' }}</span>{% endif %} + </div> + <div class="segctl"> + <button class="seg" data-v="in" hx-post="{% url 'app-pantry-set-state' item.id %}" hx-vals='{"to": "in"}' hx-target="#pantry-items" hx-swap="innerHTML">In</button> + <button class="seg" data-v="low" hx-post="{% url 'app-pantry-set-state' item.id %}" hx-vals='{"to": "low"}' hx-target="#pantry-items" hx-swap="innerHTML">Low</button> + <button class="seg" data-v="out" hx-post="{% url 'app-pantry-set-state' item.id %}" hx-vals='{"to": "out"}' hx-target="#pantry-items" hx-swap="innerHTML">Out</button> + </div> + </div> + </div> {% endfor %} - </tbody> -</table> -{% endif %} - + {% endif %} + {% endfor %} {% else %} <div class="empty"> <div class="empty-icon">π₯‘</div> - <p>Pantry's empty. Add some items above.</p> + <p>Pantry's empty. Add something below.</p> </div> {% endif %} |
