From aaa3013890faf9c6ea340581781b963cabc583fa Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 5 Oct 2026 19:54:15 +0100 Subject: [PATCH] Add birthday previews and restore checklist taps --- README.md | 4 ++-- flake.nix | 3 +++ package.nix | 2 +- static/app.js | 35 ++++++++++++++++++++++++---------- static/calendar.js | 11 +++++++++++ static/data.js | 43 ++++++++++++++++++++++++++++++++++++++++-- static/index.html | 14 ++++++++------ static/styles.css | 21 +++++++++++++++------ tests/test_app.py | 8 ++++++++ tests/test_calendar.js | 14 ++++++++++++++ tests/test_data.js | 18 ++++++++++++++++++ 11 files changed, 146 insertions(+), 27 deletions(-) create mode 100644 static/calendar.js create mode 100644 tests/test_calendar.js create mode 100644 tests/test_data.js diff --git a/README.md b/README.md index 807782c..d02fe15 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ A small, shared tracker for the first two years of Stardew Valley. It combines: - the standard Community Center bundles; - crop growth times and last safe planting dates; -- birthdays, festivals, seasonal events, and weekly reminders; and +- birthday previews and gift tips, festivals, seasonal events, and weekly reminders; and - one server-side checklist that stays in sync across phones and computers. Every actionable card and checklist row uses the same shared checkbox behavior. Date and season selection remain local to each device, so two players can look at different days without moving each other's calendar. @@ -35,6 +35,6 @@ When proxying through Nginx, protect the complete origin with Authelia and pass ## Data scope -The planner targets Stardew Valley 1.6 and standard bundles, not remixed bundles. Calendar and bundle data is based on the [Stardew Valley Wiki](https://stardewvalleywiki.com/Calendar). +The planner targets Stardew Valley 1.6 and standard bundles, not remixed bundles. Calendar and bundle data is based on the [Stardew Valley Wiki calendar](https://stardewvalleywiki.com/Calendar); birthday gift suggestions use its [gift reference](https://stardewvalleywiki.com/List_of_All_Gifts). Stardew Valley is © ConcernedApe. This is an unofficial fan-made utility and includes no game art or assets. diff --git a/flake.nix b/flake.nix index 9b5a58c..eabbbc4 100644 --- a/flake.nix +++ b/flake.nix @@ -32,8 +32,11 @@ node --check static/data.js node --check static/sync.js node --check static/availability.js + node --check static/calendar.js node tests/test_sync.js node tests/test_availability.js + node tests/test_calendar.js + node tests/test_data.js touch $out ''; }); diff --git a/package.nix b/package.nix index c94477e..f244664 100644 --- a/package.nix +++ b/package.nix @@ -2,7 +2,7 @@ stdenvNoCC.mkDerivation { pname = "stardew-tracker"; - version = "1.2.0"; + version = "1.3.0"; src = ./.; nativeBuildInputs = [ makeWrapper ]; diff --git a/static/app.js b/static/app.js index c401e33..1e437e1 100644 --- a/static/app.js +++ b/static/app.js @@ -2,6 +2,7 @@ const data = window.VALLEY_DATA; const sync = window.ValleySync; const availability = window.ValleyAvailability; + const calendar = window.ValleyCalendar; const weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]; const seasonNames = { spring: "Spring", summer: "Summer", fall: "Fall", winter: "Winter" }; const state = { @@ -59,15 +60,19 @@ $$("[data-season]").forEach(button => button.setAttribute("aria-selected", String(button.dataset.season === state.season))); $("#day-grid").innerHTML = Array.from({ length: 28 }, (_, index) => { const day = index + 1; - return ``; + const birthday = birthdayOn(day); + const label = `${seasonNames[state.season]} ${day}, ${weekday(day)}${birthday ? `, ${birthday}'s birthday` : ""}`; + const marker = birthday ? `` : ""; + return ``; }).join(""); } + function birthdayOn(day) { + return calendar.birthdayOn(data.birthdays, state.season, day, state.year); + } + function currentBirthday() { - const entry = data.birthdays[state.season][state.day]; - if (!entry) return null; - if (entry[1] && entry[1] !== state.year) return null; - return entry[0]; + return birthdayOn(state.day); } function currentEvent() { @@ -96,9 +101,19 @@ return `daily-${state.year}-${state.season}-${state.day}-${kind}-${slug}`; } - function card(kind, label, title, copy, itemId = null) { + function giftGuide(name) { + const profile = data.giftProfiles[name]; + if (!profile) return ""; + const giftList = entries => entries.map(entry => `
  • ${escapeHtml(entry)}
  • `).join(""); + return `
    +
    Loves
      ${giftList(profile.loves)}
    +
    Avoid
      ${giftList(profile.avoid)}
    +
    `; + } + + function card(kind, label, title, copy, itemId = null, extra = "") { const checked = itemId ? Boolean(state.checks[itemId]) : false; - const content = `${itemId ? `` : ""}
    ${escapeHtml(label)}

    ${escapeHtml(title)}

    ${escapeHtml(copy)}

    `; + const content = `${itemId ? `` : ""}
    ${escapeHtml(label)}

    ${escapeHtml(title)}

    ${escapeHtml(copy)}

    ${extra}`; return itemId ? `` : `
    ${content}
    `; @@ -108,7 +123,7 @@ 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.", dailyId("birthday", birthday))); + 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)))); @@ -126,7 +141,7 @@ const overdue = state.day > crop.last && !checked; const today = state.day === crop.last && !checked; return `