Mark crops as planted (on the way)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-05 22:56:06 +01:00
parent 35df8dbd01
commit 07057e6c3d
4 changed files with 46 additions and 18 deletions

View file

@ -19,6 +19,10 @@
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
const escapeHtml = (value) => String(value).replace(/[&<>'"]/g, char => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" }[char]));
const itemKey = crop => crop.sharedId || crop.id;
// Planting is tracked as its own shared flag so "on the way" never counts
// toward bundle completion.
const plantedId = key => `planted-${key}`;
const isPlanted = key => !state.checks[key] && Boolean(state.checks[plantedId(key)]);
function saveDate() {
localStorage.setItem("valley-year", state.year);
@ -126,7 +130,10 @@
if (birthday) cards.push(card("birthday", "Birthday", birthday, "A birthday gift gives 8× the usual friendship effect.", dailyId("birthday", birthday), giftGuide(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))));
cropDeadlineCards().forEach(crop => {
const planted = isPlanted(itemKey(crop));
cards.push(card(planted ? "deadline planted" : "deadline", planted ? "Planted · on the way" : "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";
@ -138,15 +145,21 @@
const rows = crops.map(crop => {
const key = itemKey(crop);
const checked = Boolean(state.checks[key]);
const overdue = state.day > crop.last && !checked;
const today = state.day === crop.last && !checked;
return `<label class="crop-row ${checked ? "complete" : ""} ${overdue ? "overdue" : ""}">
const planted = isPlanted(key);
const overdue = state.day > crop.last && !checked && !planted;
const today = state.day === crop.last && !checked && !planted;
const plantButton = checked ? "" : `<button type="button" class="plant-toggle" data-plant-id="${plantedId(key)}" aria-pressed="${planted}" aria-label="${planted ? "Unmark" : "Mark"} ${escapeHtml(crop.name)} as planted" ${state.saving.has(plantedId(key)) ? "disabled" : ""}><span aria-hidden="true">🌱</span>${planted ? "Planted" : "Plant"}</button>`;
const date = planted
? `On the way<small>planted</small>`
: `${overdue ? "Passed" : `${seasonNames[crop.season]} ${crop.last}`}<small>${overdue ? "for this season" : "plant by"}</small>`;
return `<div class="crop-row ${checked ? "complete" : ""} ${overdue ? "overdue" : ""} ${planted ? "planted" : ""}">
<input type="checkbox" data-check-id="${key}" aria-label="Mark ${escapeHtml(crop.name)} complete" ${checked ? "checked" : ""} ${state.saving.has(key) ? "disabled" : ""}>
<span class="checkmark" aria-hidden="true"></span>
<span class="crop-name"><strong>${escapeHtml(crop.name)}</strong><small>${escapeHtml(crop.purpose)}</small></span>
<span class="plant-slot">${plantButton}</span>
<span class="grow-time">${crop.grow} days<small>to grow</small></span>
<span class="plant-date ${today ? "today" : ""}">${overdue ? "Passed" : `${seasonNames[crop.season]} ${crop.last}`}<small>${overdue ? "for this season" : "plant by"}</small></span>
</label>`;
<span class="plant-date ${today ? "today" : ""}">${date}</span>
</div>`;
});
$("#crop-list").innerHTML = rows.length ? rows.join("") : `<div class="empty-state"><span>❄</span><h3>No mandatory field crops</h3><p>Use Winter for the foraging bundle, mines, fishing, and any unfinished animal or artisan goods.</p></div>`;
}
@ -194,9 +207,10 @@
return seasons.map(season => seasonNames[season]).join(" + ");
}
function statusBadges(status) {
function statusBadges(status, entry) {
if (status.checked) return "";
const badges = [];
if (isPlanted(entry.id)) badges.push(`<span class="status-badge planted">🌱 Planted</span>`);
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>`);
@ -217,8 +231,8 @@
<div class="bundle-items">${items.map(entry => {
const checked = Boolean(state.checks[entry.id]);
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}" aria-label="Mark ${escapeHtml(entry.label)} complete" ${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>`;
const classes = [checked ? "complete" : "", status.needed ? "needed" : "optional", status.needed && status.availableNow ? "available-now" : "", status.seasonExclusive ? "season-exclusive" : "", status.weather ? "weather-specific" : "", isPlanted(entry.id) ? "planted" : ""].filter(Boolean).join(" ");
return `<label class="bundle-item ${classes}"><input type="checkbox" data-check-id="${entry.id}" aria-label="Mark ${escapeHtml(entry.label)} complete" ${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, entry)}</span></label>`;
}).join("")}</div>
</article>`;
}).filter(Boolean).join("");
@ -351,6 +365,8 @@
$$("[data-filter]").forEach(button => button.setAttribute("aria-pressed", String(button.dataset.filter === state.filter)));
renderBundles();
}
const plantButton = event.target.closest("button[data-plant-id]");
if (plantButton && !plantButton.disabled) updateCheck(plantButton.dataset.plantId, !state.checks[plantButton.dataset.plantId]);
const navLink = event.target.closest(".section-nav a");
if (navLink) {
$$(".section-nav a").forEach(link => link.classList.toggle("active", link === navLink));

View file

@ -6,7 +6,7 @@
<meta name="theme-color" content="#132720">
<meta name="description" content="A shared two-year Stardew Valley Community Center planner.">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="stylesheet" href="/styles.css?v=1.3.1">
<link rel="stylesheet" href="/styles.css?v=1.4.0">
<title>Valley Yearbook</title>
</head>
<body>
@ -115,10 +115,10 @@
</footer>
</div>
<div id="toast" class="toast" role="alert" aria-live="assertive"></div>
<script src="/data.js?v=1.3.1"></script>
<script src="/sync.js?v=1.3.1"></script>
<script src="/availability.js?v=1.3.1"></script>
<script src="/calendar.js?v=1.3.1"></script>
<script src="/app.js?v=1.3.1"></script>
<script src="/data.js?v=1.4.0"></script>
<script src="/sync.js?v=1.4.0"></script>
<script src="/availability.js?v=1.4.0"></script>
<script src="/calendar.js?v=1.4.0"></script>
<script src="/app.js?v=1.4.0"></script>
</body>
</html>

View file

@ -114,7 +114,7 @@ label.today-card:hover { border-color: color-mix(in srgb, var(--season) 55%, tra
.today-card.event { --season: #86bcd8; }
.crop-list { border: 1px solid var(--line); background: var(--panel); border-radius: var(--radius); overflow: hidden; }
.crop-row { position: relative; display: grid; grid-template-columns: 24px minmax(0, 1fr) 90px 110px; align-items: center; gap: 14px; padding: 16px 18px; border-bottom: 1px solid var(--line); cursor: pointer; transition: background .15s ease; }
.crop-row { position: relative; display: grid; grid-template-columns: 24px minmax(0, 1fr) auto 90px 110px; align-items: center; gap: 14px; padding: 16px 18px; border-bottom: 1px solid var(--line); cursor: pointer; transition: background .15s ease; }
.crop-row:last-child { border: 0; }
.crop-row:hover { background: rgba(255,255,255,.025); }
.today-card > input, .crop-row input, .bundle-item input { position: absolute; inset: 0; z-index: 3; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
@ -130,6 +130,17 @@ label.today-card:hover { border-color: color-mix(in srgb, var(--season) 55%, tra
.crop-row.overdue .plant-date { color: var(--danger); }
.crop-row.complete { opacity: .52; }
.crop-row.complete .crop-name strong { text-decoration: line-through; }
.plant-toggle { position: relative; z-index: 4; display: inline-flex; align-items: center; gap: 6px; padding: 6px 11px; border: 1px solid var(--line); border-radius: 999px; background: transparent; color: var(--muted); font: inherit; font-size: .78rem; font-weight: 800; cursor: pointer; transition: .15s ease; }
.plant-toggle span { filter: grayscale(1); opacity: .7; transition: .15s ease; }
.plant-toggle:hover { border-color: #79b56b; color: var(--ink); }
.plant-toggle:hover span, .plant-toggle[aria-pressed="true"] span { filter: none; opacity: 1; }
.plant-toggle[aria-pressed="true"] { border-color: #79b56b; background: rgba(121,181,107,.18); color: #a9dfa4; }
.plant-toggle:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
.plant-toggle:disabled { cursor: wait; opacity: .6; }
.crop-row.planted { box-shadow: inset 3px 0 0 #79b56b; }
.crop-row.planted .plant-date { color: #a9dfa4; }
.today-card.deadline.planted { --season: #79b56b; }
.status-badge.planted { color: #a9dfa4; background: rgba(121,181,107,.18); }
.bundle-heading { align-items: center; }
.progress-cluster { width: min(290px, 42%); color: var(--muted); text-align: right; font-size: .84rem; }
@ -210,7 +221,8 @@ footer a { text-underline-offset: 3px; }
.section-heading > p { text-align: left; }
.today-grid, .room-grid { grid-template-columns: 1fr; }
.today-card { min-height: 155px; }
.crop-row { grid-template-columns: 24px 1fr 94px; gap: 11px; }
.crop-row { grid-template-columns: 24px 1fr auto 86px; gap: 10px; }
.plant-toggle { padding: 6px 9px; }
.grow-time { display: none; }
.crop-name small { max-width: 31ch; }
.bundle-heading { align-items: stretch; }