Highlight seasonal Community Center priorities

This commit is contained in:
Codex 2026-10-05 19:10:19 +01:00
parent 8d5a94d7fa
commit d2c7f004ea
8 changed files with 199 additions and 16 deletions

View file

@ -1,6 +1,7 @@
(() => {
const data = window.VALLEY_DATA;
const sync = window.ValleySync;
const availability = window.ValleyAvailability;
const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" };
const state = {
@ -34,7 +35,7 @@
renderDate();
renderToday();
renderCrops();
if (state.filter === "season") renderBundles();
renderBundles();
}
function moveDay(delta) {
@ -149,19 +150,60 @@
}, totals), { done: 0, required: 0 });
}
function entryStatus(entry, group) {
const progress = groupProgress(group);
return availability.classify(entry, {
checked: Boolean(state.checks[entry.id]),
chooseGroupComplete: Boolean(group.choose && progress.complete),
season: state.season
});
}
function allEntryStatuses() {
return data.bundles.flatMap(room => room.groups.flatMap(group =>
group.items.map(entry => ({ entry, group, status: entryStatus(entry, group) }))
));
}
function renderPrioritySummary() {
const entries = allEntryStatuses();
const count = filter => entries.filter(({ status }) => availability.matchesFilter(status, filter)).length;
$("#needed-now-count").textContent = count("now");
$("#exclusive-count").textContent = count("exclusive");
$("#weather-count").textContent = count("weather");
$("#needed-now-detail").textContent = `Available in ${seasonNames[state.season]}`;
$("#exclusive-detail").textContent = `Only in ${seasonNames[state.season]}`;
}
function seasonList(seasons) {
return seasons.map(season => seasonNames[season]).join(" + ");
}
function statusBadges(status) {
if (status.checked) return "";
const badges = [];
if (!status.needed) badges.push(`<span class="status-badge optional">Bundle ready</span>`);
else if (status.seasonExclusive) badges.push(`<span class="status-badge exclusive">Only ${seasonNames[state.season]}</span>`);
else if (status.inSeason) badges.push(`<span class="status-badge in-season">In season</span>`);
else if (status.seasonal) badges.push(`<span class="status-badge later">${seasonList(status.seasons)}</span>`);
if (status.weather) badges.push(`<span class="status-badge weather ${status.weather.toLowerCase()}">${status.weather === "Rain" ? "Rain needed" : "Sunny only"}</span>`);
return badges.length ? `<span class="item-badges">${badges.join("")}</span>` : "";
}
function renderBundles() {
const html = data.bundles.map(room => {
const groups = room.groups.map(group => {
const progress = groupProgress(group);
let items = group.items;
if (state.filter === "season") items = items.filter(entry => !entry.seasons.length || entry.seasons.includes(state.season));
if (state.filter === "open") items = items.filter(entry => !state.checks[entry.id]);
if (state.filter !== "all") items = items.filter(entry => availability.matchesFilter(entryStatus(entry, group), state.filter));
if (!items.length) return "";
return `<article class="bundle-card ${progress.complete ? "complete" : ""}">
<header><div><h4>${escapeHtml(group.name)}</h4>${group.choose ? `<small>Choose ${group.required}</small>` : ""}</div><span>${Math.min(progress.done, progress.required)}/${progress.required}</span></header>
<div class="bundle-items">${items.map(entry => {
const checked = Boolean(state.checks[entry.id]);
return `<label class="bundle-item ${checked ? "complete" : ""}"><input type="checkbox" data-check-id="${entry.id}" ${checked ? "checked" : ""} ${state.saving.has(entry.id) ? "disabled" : ""}><span class="checkmark" aria-hidden="true"></span><span><strong>${escapeHtml(entry.label)}</strong><small>${escapeHtml(entry.meta)}</small></span></label>`;
const status = entryStatus(entry, group);
const classes = [checked ? "complete" : "", status.needed ? "needed" : "optional", status.needed && status.availableNow ? "available-now" : "", status.seasonExclusive ? "season-exclusive" : "", status.weather ? "weather-specific" : ""].filter(Boolean).join(" ");
return `<label class="bundle-item ${classes}"><input type="checkbox" data-check-id="${entry.id}" ${checked ? "checked" : ""} ${state.saving.has(entry.id) ? "disabled" : ""}><span class="checkmark" aria-hidden="true"></span><span class="item-copy"><strong>${escapeHtml(entry.label)}</strong><small>${escapeHtml(entry.meta)}</small>${statusBadges(status)}</span></label>`;
}).join("")}</div>
</article>`;
}).filter(Boolean).join("");
@ -170,7 +212,16 @@
const roomDone = roomGroups.filter(progress => progress.complete).length;
return `<section class="room"><div class="room-title"><span>${room.icon}</span><div><h3>${escapeHtml(room.room)}</h3><p>${roomDone} of ${room.groups.length} bundles ready</p></div></div><div class="room-grid">${groups}</div></section>`;
}).filter(Boolean).join("");
$("#bundle-list").innerHTML = html || `<div class="empty-state"><span>✓</span><h3>Everything in this view is done</h3><p>Switch filters to review the whole Community Center.</p></div>`;
const emptyMessages = {
now: ["Nothing needed right now", `No unfinished bundle items are available from normal sources in ${seasonNames[state.season]}.`],
exclusive: ["No season-only items left", `You have covered every unfinished item exclusive to ${seasonNames[state.season]}.`],
weather: ["No weather windows left", `No unfinished fish in ${seasonNames[state.season]} require rain or sun.`],
open: ["Community Center covered", "No required bundle items remain."],
all: ["Everything in this view is done", "Switch filters to review the whole Community Center."]
};
const empty = emptyMessages[state.filter] || emptyMessages.all;
$("#bundle-list").innerHTML = html || `<div class="empty-state"><span>✓</span><h3>${empty[0]}</h3><p>${empty[1]}</p></div>`;
renderPrioritySummary();
const totals = bundleTotals();
const percent = totals.required ? Math.round(totals.done / totals.required * 100) : 0;
$("#progress-label").textContent = `${totals.done} of ${totals.required} essentials`;
@ -282,7 +333,7 @@
const filterButton = event.target.closest("[data-filter]");
if (filterButton) {
state.filter = filterButton.dataset.filter;
$$("[data-filter]").forEach(button => button.setAttribute("aria-pressed", String(button === filterButton)));
$$("[data-filter]").forEach(button => button.setAttribute("aria-pressed", String(button.dataset.filter === state.filter)));
renderBundles();
}
const navLink = event.target.closest(".section-nav a");