211 lines
7.7 KiB
HTML
211 lines
7.7 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Entries for {{ page.date_text }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<a id="back-to-calendar" href="/" class="inline-flex items-center gap-2 text-sm font-medium text-slate-600 transition hover:text-slate-900">
|
|
<span>←</span>
|
|
<span>Back to calendar</span>
|
|
</a>
|
|
|
|
<section class="mt-4 rounded-3xl border border-slate-200/80 bg-white/80 p-6 shadow-sm backdrop-blur">
|
|
<h1 class="text-3xl font-bold tracking-tight">{{ page.date_text }}</h1>
|
|
<p class="mt-3 inline-flex items-center rounded-full bg-emerald-100 px-3 py-1 text-sm font-semibold text-emerald-900">
|
|
Daily total: {{ page.daily_total }} cal
|
|
</p>
|
|
|
|
<div class="mt-5 rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-600">Daily weight (lbs)</h2>
|
|
<form id="weight-form" method="post" action="/day/{{ page.date_text }}/weight" data-autosave="true" class="mt-3 flex flex-wrap items-center gap-2">
|
|
<input
|
|
type="number"
|
|
step="0.1"
|
|
min="1"
|
|
name="weight"
|
|
value="{{ page.weight_value }}"
|
|
placeholder="Weight (lbs)"
|
|
required
|
|
class="w-40 rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<span data-status-for="weight-form" class="text-xs text-slate-500">Auto-save enabled</span>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-5 rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-600">Add entry</h2>
|
|
<form method="post" action="/day/{{ page.date_text }}/add" class="mt-3 grid gap-2 sm:grid-cols-4">
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
placeholder="Food name"
|
|
required
|
|
class="sm:col-span-2 w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<input
|
|
type="number"
|
|
name="calories"
|
|
placeholder="Calories each"
|
|
min="0"
|
|
required
|
|
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<input
|
|
type="number"
|
|
name="multiplier"
|
|
placeholder="Count"
|
|
min="1"
|
|
step="1"
|
|
value="1"
|
|
required
|
|
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<div class="sm:col-span-4">
|
|
<button type="submit" class="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-700">Add entry</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="mt-5 overflow-hidden rounded-2xl border border-slate-200">
|
|
<table class="w-full border-collapse">
|
|
<thead class="bg-slate-50 text-left text-xs uppercase tracking-wide text-slate-500">
|
|
<tr><th class="px-5 py-3">Entry</th><th class="px-5 py-3">Calories</th></tr>
|
|
</thead>
|
|
<tbody class="bg-white">
|
|
{% if page.entries.is_empty() %}
|
|
<tr><td colspan="2" class="px-5 py-8 text-center text-slate-500">No entries yet for this day.</td></tr>
|
|
{% else %}
|
|
{% for entry in page.entries %}
|
|
<tr class="border-t border-slate-200 align-top">
|
|
<td class="px-5 py-3">
|
|
<form id="entry-form-{{ entry.id }}" method="post" action="/day/{{ page.date_text }}/entry/{{ entry.id }}/update" data-autosave="true" class="grid gap-2 md:grid-cols-2">
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
value="{{ entry.name }}"
|
|
required
|
|
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<input
|
|
type="number"
|
|
name="calories"
|
|
value="{{ entry.calories }}"
|
|
min="0"
|
|
required
|
|
class="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:border-teal-500 focus:outline-none"
|
|
/>
|
|
<div class="md:col-span-2 text-xs text-slate-500" data-status-for="entry-form-{{ entry.id }}">Auto-save enabled</div>
|
|
</form>
|
|
<form method="post" action="/day/{{ page.date_text }}/entry/{{ entry.id }}/delete" class="mt-2">
|
|
<button type="submit" class="rounded-lg border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs font-semibold text-rose-700 hover:bg-rose-100">Delete</button>
|
|
</form>
|
|
</td>
|
|
<td class="px-5 py-3 font-semibold text-slate-900">{{ entry.calories }} cal</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
const AUTO_SAVE_DELAY_MS = 700;
|
|
const autosaveForms = Array.from(document.querySelectorAll("form[data-autosave='true']"));
|
|
const backLink = document.getElementById("back-to-calendar");
|
|
const pending = new Map(); // form -> timer id
|
|
const dirty = new Set();
|
|
|
|
function formStatus(form) {
|
|
return document.querySelector(`[data-status-for="${form.id}"]`);
|
|
}
|
|
|
|
function setStatus(form, text, cssClass = "text-slate-500") {
|
|
const node = formStatus(form);
|
|
if (!node) return;
|
|
node.className = `text-xs ${cssClass}`;
|
|
node.textContent = text;
|
|
}
|
|
|
|
function serializeForm(form) {
|
|
return new URLSearchParams(new FormData(form));
|
|
}
|
|
|
|
async function saveForm(form) {
|
|
const body = serializeForm(form);
|
|
setStatus(form, "Saving...", "text-amber-700");
|
|
try {
|
|
const response = await fetch(form.action, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
body,
|
|
credentials: "same-origin"
|
|
});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
dirty.delete(form);
|
|
setStatus(form, "Saved", "text-emerald-700");
|
|
} catch (error) {
|
|
setStatus(form, "Save failed", "text-rose-700");
|
|
}
|
|
}
|
|
|
|
function queueAutosave(form) {
|
|
dirty.add(form);
|
|
setStatus(form, "Editing...", "text-slate-500");
|
|
if (pending.has(form)) {
|
|
clearTimeout(pending.get(form));
|
|
}
|
|
const timer = setTimeout(async () => {
|
|
pending.delete(form);
|
|
await saveForm(form);
|
|
}, AUTO_SAVE_DELAY_MS);
|
|
pending.set(form, timer);
|
|
}
|
|
|
|
async function flushAllAutosaves() {
|
|
for (const [form, timer] of pending.entries()) {
|
|
clearTimeout(timer);
|
|
pending.delete(form);
|
|
}
|
|
for (const form of Array.from(dirty)) {
|
|
await saveForm(form);
|
|
}
|
|
}
|
|
|
|
autosaveForms.forEach((form) => {
|
|
form.addEventListener("input", () => queueAutosave(form));
|
|
form.addEventListener("change", () => queueAutosave(form));
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
if (pending.has(form)) {
|
|
clearTimeout(pending.get(form));
|
|
pending.delete(form);
|
|
}
|
|
dirty.add(form);
|
|
await saveForm(form);
|
|
});
|
|
});
|
|
|
|
backLink.addEventListener("click", async (event) => {
|
|
event.preventDefault();
|
|
await flushAllAutosaves();
|
|
window.location.href = backLink.getAttribute("href");
|
|
});
|
|
|
|
document.addEventListener("keydown", async (event) => {
|
|
if (event.key !== "Escape") return;
|
|
event.preventDefault();
|
|
await flushAllAutosaves();
|
|
window.location.href = "/";
|
|
});
|
|
|
|
window.addEventListener("beforeunload", () => {
|
|
// Best-effort last-moment save for browser back/close/refresh.
|
|
for (const form of Array.from(dirty)) {
|
|
navigator.sendBeacon(form.action, serializeForm(form));
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|