fix(cycle): route the new save signals through the shared affordances

Two repo-wide guards caught the additions rather than a reviewer, which
is what they are for.

The success toasts reached for sonner directly. The success affordance
belongs to the outcome module, which binds it to the success key and
nothing else, so a surface cannot raise a green toast for a write that
wrote nothing. They go through toastWrittenOutcome now.

The sheet's date header formatted a day key in the browser's zone. A
cycle day key is a calendar day, not an instant, so it is anchored at
noon UTC and rendered in UTC, the same anchor the domain's day
arithmetic uses. Otherwise a reader east or west of the browser zone
could see the sheet name a different day than the one it writes to.
This commit is contained in:
Marc-André Bombeck committed 2026-08-07 16:14:28 +02:00
1 parent 8e3834f38d
commit 85240e96b1
2 files changed
+18 -8

No files matched your search

+12 -3
View File
@@ -479,10 +479,19 @@ function CervixRow<T extends string>({
* deliberately: it is how people recognise a date they are recalling.
*/
export function formatSheetDate(date: string, locale: Locale): string {
const [year, month, day] = date.split("-").map(Number);
return new Date(year, month - 1, day).toLocaleDateString(
// A cycle day key is a calendar day, not an instant, so it is anchored at
// noon UTC and rendered in UTC — the same anchor the domain's day arithmetic
// uses. Formatting it in the browser's zone would let a reader east or west
// of it see the sheet name a different day than the one it writes to.
return new Date(`${date}T12:00:00Z`).toLocaleDateString(
resolveIntlLocale(locale),
{ weekday: "long", day: "numeric", month: "long", year: "numeric" },
{
weekday: "long",
day: "numeric",
month: "long",
year: "numeric",
timeZone: "UTC",
},
);
}
+6 -5
View File
@@ -20,6 +20,7 @@ import {
apiPost,
} from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import { toastWrittenOutcome } from "@/components/outcome/outcome-toast";
import {
cycleDependentKeys,
@@ -247,7 +248,7 @@ export function useLogDay() {
},
onSuccess: () => {
void invalidateKeys(qc, cycleDependentKeys);
toast.success(t("cycle.saveSuccess"));
toastWrittenOutcome("success", t("cycle.saveSuccess"));
},
});
}
@@ -293,7 +294,7 @@ export function usePatchDayLog() {
},
onSuccess: () => {
void invalidateKeys(qc, cycleDependentKeys);
toast.success(t("cycle.saveSuccess"));
toastWrittenOutcome("success", t("cycle.saveSuccess"));
},
});
}
@@ -311,7 +312,7 @@ export function useStartPeriod() {
},
onSuccess: () => {
void invalidateKeys(qc, cycleDependentKeys);
toast.success(t("cycle.periodStartSaved"));
toastWrittenOutcome("success", t("cycle.periodStartSaved"));
},
// v1.16.4 — period boundaries had no failure signal at all (the sheet
// simply stayed open); a toast names the rejection.
@@ -333,7 +334,7 @@ export function useEndPeriod() {
},
onSuccess: () => {
void invalidateKeys(qc, cycleDependentKeys);
toast.success(t("cycle.periodEndSaved"));
toastWrittenOutcome("success", t("cycle.periodEndSaved"));
},
onError: () => toast.error(t("cycle.saveError")),
});
@@ -349,7 +350,7 @@ export function useDeleteDayLog() {
},
onSuccess: () => {
void invalidateKeys(qc, cycleDependentKeys);
toast.success(t("cycle.deleteSuccess"));
toastWrittenOutcome("success", t("cycle.deleteSuccess"));
},
onError: () => toast.error(t("cycle.deleteError")),
});