From 0390d124cbcac773f97be0d0f13186e03388e300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 25 Sep 2026 07:05:03 +0200 Subject: [PATCH] fix(cycle): mark intercourse and other logged entries on the calendar An intercourse entry left no trace on the month grid and the legend had no entry for it. The day now carries a diamond marker, and anything else logged that day with no marker of its own (a test, contraception, spotting, a temperature, a mucus or cervix reading, a note) shows a hollow ring, so a day with one of those logged no longer looks empty. Both are named in the legend and in the cell's accessible label. --- messages/de.json | 4 +- messages/en.json | 4 +- messages/es.json | 4 +- messages/fr.json | 4 +- messages/it.json | 4 +- messages/ko.json | 4 +- messages/pl.json | 4 +- .../cycle/__tests__/bbt-chart.test.tsx | 9 ++ .../cycle/__tests__/cycle-calendar.test.tsx | 99 +++++++++++++ .../cycle-view-sheet-context.test.tsx | 9 ++ .../cycle/__tests__/log-day-sheet.test.ts | 9 ++ src/components/cycle/cycle-calendar.tsx | 132 ++++++++++++++++-- src/components/cycle/phase-tokens.ts | 6 + src/components/cycle/types.ts | 13 ++ 14 files changed, 285 insertions(+), 20 deletions(-) diff --git a/messages/de.json b/messages/de.json index 2629c846f..8557410f9 100644 --- a/messages/de.json +++ b/messages/de.json @@ -8231,7 +8231,9 @@ "flowSpotting": "Schmierblutung", "flowLight": "leicht", "flowMedium": "mittel", - "flowHeavy": "stark" + "flowHeavy": "stark", + "legendIntercourse": "Geschlechtsverkehr", + "legendOtherEntries": "Weitere Einträge" }, "predictions": { "title": "Was als Nächstes kommt", diff --git a/messages/en.json b/messages/en.json index b2d727fc7..0a02c12f6 100644 --- a/messages/en.json +++ b/messages/en.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "light", "flowMedium": "medium", - "flowHeavy": "heavy" + "flowHeavy": "heavy", + "legendIntercourse": "Intercourse", + "legendOtherEntries": "Other entries" }, "predictions": { "title": "What comes next", diff --git a/messages/es.json b/messages/es.json index 638d44389..853bb3f69 100644 --- a/messages/es.json +++ b/messages/es.json @@ -8231,7 +8231,9 @@ "flowSpotting": "manchado", "flowLight": "ligero", "flowMedium": "medio", - "flowHeavy": "abundante" + "flowHeavy": "abundante", + "legendIntercourse": "Relaciones", + "legendOtherEntries": "Otras entradas" }, "predictions": { "title": "Qué viene después", diff --git a/messages/fr.json b/messages/fr.json index a91aaba73..7942c43e9 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "léger", "flowMedium": "moyen", - "flowHeavy": "abondant" + "flowHeavy": "abondant", + "legendIntercourse": "Rapports", + "legendOtherEntries": "Autres saisies" }, "predictions": { "title": "Ce qui arrive ensuite", diff --git a/messages/it.json b/messages/it.json index ba55d1bd9..bb9c4302e 100644 --- a/messages/it.json +++ b/messages/it.json @@ -8231,7 +8231,9 @@ "flowSpotting": "spotting", "flowLight": "leggero", "flowMedium": "medio", - "flowHeavy": "abbondante" + "flowHeavy": "abbondante", + "legendIntercourse": "Rapporti", + "legendOtherEntries": "Altre voci" }, "predictions": { "title": "Cosa arriva dopo", diff --git a/messages/ko.json b/messages/ko.json index 78925959c..e629f3528 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -8231,7 +8231,9 @@ "flowSpotting": "점상 출혈", "flowLight": "적음", "flowMedium": "보통", - "flowHeavy": "많음" + "flowHeavy": "많음", + "legendIntercourse": "성관계", + "legendOtherEntries": "기타 기록" }, "predictions": { "title": "다음에 올 일", diff --git a/messages/pl.json b/messages/pl.json index 89b15b449..536400683 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -8231,7 +8231,9 @@ "flowSpotting": "plamienie", "flowLight": "lekkie", "flowMedium": "średnie", - "flowHeavy": "obfite" + "flowHeavy": "obfite", + "legendIntercourse": "Współżycie", + "legendOtherEntries": "Inne wpisy" }, "predictions": { "title": "Co dalej", diff --git a/src/components/cycle/__tests__/bbt-chart.test.tsx b/src/components/cycle/__tests__/bbt-chart.test.tsx index a07a707f2..e2adcb629 100644 --- a/src/components/cycle/__tests__/bbt-chart.test.tsx +++ b/src/components/cycle/__tests__/bbt-chart.test.tsx @@ -28,6 +28,15 @@ function day(date: string, overrides: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...overrides, }; } diff --git a/src/components/cycle/__tests__/cycle-calendar.test.tsx b/src/components/cycle/__tests__/cycle-calendar.test.tsx index 800802a61..1d8032356 100644 --- a/src/components/cycle/__tests__/cycle-calendar.test.tsx +++ b/src/components/cycle/__tests__/cycle-calendar.test.tsx @@ -28,9 +28,27 @@ function dayBase(date: string): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, }; } +/** The opening tag of one day's cell, by its date in the aria-label. */ +function cellTag(html: string, date: string): string { + const match = html.match( + new RegExp(`<(?:button|div)[^>]*aria-label="${date}[^"]*"[^>]*>`), + ); + if (!match) throw new Error(`no cell for ${date}`); + return match[0]; +} + describe("", () => { const today = "2026-06-15"; @@ -130,4 +148,85 @@ describe("", () => { ); expect(html).toContain('data-fertile="true"'); }); + + describe("per-day entries beyond period and symptoms (#1032)", () => { + it("marks an intercourse day with its own marker and names it", () => { + const days = [{ ...dayBase("2026-06-05"), sexualActivity: true }]; + const html = render( + {}} />, + ); + const cell = cellTag(html, "2026-06-05"); + expect(cell).toContain('data-intercourse="true"'); + expect(cell).toContain("Intercourse"); + // Only that day. + expect(cellTag(html, "2026-06-06")).not.toContain("data-intercourse"); + expect(html).toContain('data-slot="cycle-calendar-intercourse"'); + }); + + it("the legend names intercourse and the other-entries marker", () => { + const html = render( + {}} />, + ); + expect(html).toContain('data-legend="cycle.calendar.legendIntercourse"'); + expect(html).toContain('data-legend="cycle.calendar.legendOtherEntries"'); + expect(html).toContain("Intercourse"); + expect(html).toContain("Other entries"); + }); + + it.each([ + [ + "a pregnancy test", + { pregnancyTest: "POSITIVE" }, + "Pregnancy test (Positive)", + ], + [ + "a progesterone test", + { progesteroneTest: "NEGATIVE" }, + "Progesterone test (Negative)", + ], + ["contraception", { contraceptive: "ORAL" }, "Contraceptive (Pill)"], + [ + "spotting", + { intermenstrualBleeding: true }, + "Spotting between periods", + ], + ["a note", { hasNote: true }, "Note"], + ["a temperature", { basalBodyTempC: 36.6 }, "Basal body temperature"], + [ + "an ovulation test", + { ovulationTest: "POSITIVE_LH_SURGE" }, + "Ovulation test (LH surge)", + ], + [ + "a mucus reading", + { cervicalMucus: "CREAMY" }, + "Cervical mucus (Creamy)", + ], + ["a cervix sign", { cervixPosition: "HIGH" }, "Cervix"], + ] as const)( + "a day with only %s logged no longer looks empty", + (_label, over, aria) => { + const days = [{ ...dayBase("2026-06-08"), ...over } as CalendarDay]; + const html = render( + {}} />, + ); + const cell = cellTag(html, "2026-06-08"); + expect(cell).toContain('data-other-entries="true"'); + expect(cell).toContain(aria); + }, + ); + + it("an empty day carries neither marker", () => { + const html = render( + {}} + />, + ); + const cell = cellTag(html, "2026-06-08"); + expect(cell).not.toContain("data-other-entries"); + expect(cell).not.toContain("data-intercourse"); + }); + }); }); diff --git a/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx b/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx index e32bb1c3a..f727d776e 100644 --- a/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx +++ b/src/components/cycle/__tests__/cycle-view-sheet-context.test.tsx @@ -45,6 +45,15 @@ function gridDay(date: string, over: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...over, }; } diff --git a/src/components/cycle/__tests__/log-day-sheet.test.ts b/src/components/cycle/__tests__/log-day-sheet.test.ts index 9f0a1e56a..78e280933 100644 --- a/src/components/cycle/__tests__/log-day-sheet.test.ts +++ b/src/components/cycle/__tests__/log-day-sheet.test.ts @@ -281,6 +281,15 @@ function gridDay(date: string, over: Partial = {}): CalendarDay { basalBodyTempC: null, ovulationTest: null, cervicalMucus: null, + cervixPosition: null, + cervixFirmness: null, + cervixOpening: null, + intermenstrualBleeding: false, + sexualActivity: false, + pregnancyTest: null, + progesteroneTest: null, + contraceptive: null, + hasNote: false, ...over, }; } diff --git a/src/components/cycle/cycle-calendar.tsx b/src/components/cycle/cycle-calendar.tsx index c38cd30d8..126b41d1a 100644 --- a/src/components/cycle/cycle-calendar.tsx +++ b/src/components/cycle/cycle-calendar.tsx @@ -12,6 +12,7 @@ import type { CalendarDay } from "./types"; import { FERTILE_HUE, FLOW_HUE, + INTERCOURSE_HUE, OVULATION_HUE, flowOpacity, } from "./phase-tokens"; @@ -25,7 +26,11 @@ import { * single dated dot); the fertile window (already goal-gated server-side — the * API nulls it unless TRYING_TO_CONCEIVE) renders as a green ring; the * predicted-ovulation day gets an amber marker; per-day symptom presence - * shows a small dot. Selecting a day calls `onSelectDay` (the log-day sheet). + * shows a small dot; intercourse shows a diamond; anything else logged that + * day (a test, contraception, spotting, a temperature, a mucus or cervix + * reading, a note) shows a hollow ring, so a day with only one of those + * logged no longer looks like a day with nothing logged. Selecting a day + * calls `onSelectDay` (the log-day sheet). * * a11y: each day is a real `