CurseTechnique/templates/day.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>&larr;</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 %}