Fix concurrent checklist sync and consistent actions
This commit is contained in:
parent
a33f7a297d
commit
8d5a94d7fa
10 changed files with 124 additions and 20 deletions
|
|
@ -1,5 +1,6 @@
|
|||
(() => {
|
||||
const data = window.VALLEY_DATA;
|
||||
const sync = window.ValleySync;
|
||||
const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" };
|
||||
const state = {
|
||||
|
|
@ -89,22 +90,31 @@
|
|||
return data.crops.filter(crop => crop.season === state.season && crop.last === state.day && (!crop.year || crop.year === state.year));
|
||||
}
|
||||
|
||||
function card(kind, label, title, copy) {
|
||||
return `<article class="today-card ${kind}"><div class="card-label"><span></span>${escapeHtml(label)}</div><h3>${escapeHtml(title)}</h3><p>${escapeHtml(copy)}</p></article>`;
|
||||
function dailyId(kind, title) {
|
||||
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 36);
|
||||
return `daily-${state.year}-${state.season}-${state.day}-${kind}-${slug}`;
|
||||
}
|
||||
|
||||
function card(kind, label, title, copy, itemId = null) {
|
||||
const checked = itemId ? Boolean(state.checks[itemId]) : false;
|
||||
const content = `${itemId ? `<input type="checkbox" data-check-id="${itemId}" ${checked ? "checked" : ""} ${state.saving.has(itemId) ? "disabled" : ""}><span class="checkmark" aria-hidden="true"></span>` : ""}<div class="card-label"><span></span>${escapeHtml(label)}</div><h3>${escapeHtml(title)}</h3><p>${escapeHtml(copy)}</p>`;
|
||||
return itemId
|
||||
? `<label class="today-card ${kind} ${checked ? "complete" : ""}">${content}</label>`
|
||||
: `<article class="today-card ${kind}">${content}</article>`;
|
||||
}
|
||||
|
||||
function renderToday() {
|
||||
const cards = [];
|
||||
const birthday = currentBirthday();
|
||||
const event = currentEvent();
|
||||
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect."));
|
||||
if (event) cards.push(card("event", "Event", event[0], event[1]));
|
||||
reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy)));
|
||||
cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose)));
|
||||
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect.", dailyId("birthday", birthday)));
|
||||
if (event) cards.push(card("event", "Event", event[0], event[1], dailyId("event", event[0])));
|
||||
reminders().forEach(([title, copy]) => cards.push(card("reminder", "Reminder", title, copy, dailyId("reminder", title))));
|
||||
cropDeadlineCards().forEach(crop => cards.push(card("deadline", "Plant by today", crop.name, crop.purpose, itemKey(crop))));
|
||||
if (!cards.length) cards.push(card("quiet", "Open day", "Make your own luck", "No fixed birthdays, festivals, or last planting dates today."));
|
||||
$("#today-cards").innerHTML = cards.join("");
|
||||
const noun = cards.length === 1 ? "thing" : "things";
|
||||
$("#today-summary").textContent = `${cards.length} ${noun} on the calendar`;
|
||||
$("#today-summary").textContent = `${cards.length} ${noun} on the calendar · tick cards when done`;
|
||||
}
|
||||
|
||||
function renderCrops() {
|
||||
|
|
@ -187,9 +197,11 @@
|
|||
const response = await fetch("/api/state", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`State request failed (${response.status})`);
|
||||
const snapshot = await response.json();
|
||||
if (snapshot.revision !== state.revision) {
|
||||
state.checks = snapshot.checks || {};
|
||||
state.revision = snapshot.revision;
|
||||
const merged = sync.mergeSnapshot(state.checks, state.saving, state.revision, snapshot);
|
||||
if (merged.applied) {
|
||||
state.checks = merged.checks;
|
||||
state.revision = merged.revision;
|
||||
renderToday();
|
||||
renderCrops();
|
||||
renderBundles();
|
||||
}
|
||||
|
|
@ -204,6 +216,7 @@
|
|||
const previous = Boolean(state.checks[itemId]);
|
||||
state.checks[itemId] = checked;
|
||||
state.saving.add(itemId);
|
||||
renderToday();
|
||||
renderCrops();
|
||||
renderBundles();
|
||||
setSyncStatus("loading", "Saving…");
|
||||
|
|
@ -215,15 +228,19 @@
|
|||
});
|
||||
if (!response.ok) throw new Error(`Save failed (${response.status})`);
|
||||
const result = await response.json();
|
||||
state.revision = Math.max(state.revision, result.revision);
|
||||
const confirmed = sync.confirmWrite(state.checks, state.revision, result);
|
||||
state.checks = confirmed.checks;
|
||||
state.revision = confirmed.revision;
|
||||
setSyncStatus("ready", "Shared progress synced");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
state.checks[itemId] = previous;
|
||||
setSyncStatus("error", "Couldn’t save");
|
||||
toast("That change wasn’t saved. Your previous shared state is restored.");
|
||||
void loadState({ quiet: true });
|
||||
} finally {
|
||||
state.saving.delete(itemId);
|
||||
renderToday();
|
||||
renderCrops();
|
||||
renderBundles();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue