mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(cycle): surface the cervix secondary-symptom choice in the UI
- Advanced cycle settings gains a "secondary fertility sign" selector (cervical mucus default | cervix observation). It sits below the priors in the advanced settings so it never forces itself on a casual user. - The log-day sheet shows the cervix entry — position / firmness / opening chips — only when the user picked cervix observation; otherwise the cervical-mucus picker stays as before. Progressive disclosure: exactly one secondary sign is offered at a time so the sheet never overwhelms. - Web client types, the cyclePrefs patch, and the day-log payload builders carry the new fields; copy added across all six locales.
This commit is contained in:
1 parent
609d248252
commit
e832222402
11 files changed
+325
-15
No files matched your search
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Eiweißartig",
|
||||
"label": "Zervixschleim"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Tief",
|
||||
"HIGH": "Hoch"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Fest",
|
||||
"SOFT": "Weich"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Geschlossen",
|
||||
"OPEN": "Geöffnet"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "Allgemeine Gesundheit",
|
||||
"AVOID_PREGNANCY": "Verhütung",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Fieber, Krankheit, Alkohol oder eine späte Messung — wird von der Temperaturauswertung ausgeschlossen.",
|
||||
"ovulationTest": "Ovulationstest",
|
||||
"mucus": "Zervixschleim",
|
||||
"cervix": "Gebärmutterhals",
|
||||
"cervixPosition": "Position",
|
||||
"cervixFirmness": "Festigkeit",
|
||||
"cervixOpening": "Öffnung",
|
||||
"intercourse": "Geschlechtsverkehr",
|
||||
"protected": "Geschützt",
|
||||
"note": "Notiz",
|
||||
@@ -5915,6 +5931,12 @@
|
||||
"priorsDescription": "Diese Vorgaben helfen deinen Prognosen, bevor du viele Zyklen erfasst hast. Leer lassen, um die Standardwerte zu nutzen.",
|
||||
"rawChartMode": "Roh-Diagramm-Modus",
|
||||
"rawChartModeDescription": "Zeige nur deine erfassten Daten, ohne Prognosen oder Deutung.",
|
||||
"secondarySymptomLabel": "Zweites Fruchtbarkeitszeichen",
|
||||
"secondarySymptomDescription": "Das zweite Zeichen, das für die symptothermale Doppelkontrolle mit deiner Temperatur kombiniert wird. Die meisten beobachten den Zervixschleim; wechsle zur Gebärmutterhals-Beobachtung, wenn du das bevorzugst.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Zervixschleim",
|
||||
"CERVIX": "Gebärmutterhals"
|
||||
},
|
||||
"prediction": "Prognosen",
|
||||
"predictionDescription": "Prognostiziere deine nächste Periode und, wenn relevant, dein fruchtbares Fenster.",
|
||||
"encryption": "Sensible Einträge verschlüsseln",
|
||||
|
||||
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Egg white",
|
||||
"label": "Cervical mucus"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Low",
|
||||
"HIGH": "High"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Firm",
|
||||
"SOFT": "Soft"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Closed",
|
||||
"OPEN": "Open"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "General health",
|
||||
"AVOID_PREGNANCY": "Avoiding pregnancy",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Fever, illness, alcohol or a late measurement — excluded from the temperature evaluation.",
|
||||
"ovulationTest": "Ovulation test",
|
||||
"mucus": "Cervical mucus",
|
||||
"cervix": "Cervix",
|
||||
"cervixPosition": "Position",
|
||||
"cervixFirmness": "Firmness",
|
||||
"cervixOpening": "Opening",
|
||||
"intercourse": "Intercourse",
|
||||
"protected": "Protected",
|
||||
"intermenstrualBleeding": "Spotting between periods",
|
||||
@@ -5920,6 +5936,12 @@
|
||||
"priorsDescription": "These priors help your predictions before you have logged many cycles. Leave blank to use the defaults.",
|
||||
"rawChartMode": "Raw chart mode",
|
||||
"rawChartModeDescription": "Show only your logged data with no predictions or interpretation.",
|
||||
"secondarySymptomLabel": "Secondary fertility sign",
|
||||
"secondarySymptomDescription": "The second sign paired with your temperature for the symptothermal double-check. Most people track cervical mucus; switch to cervix observation if you prefer.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Cervical mucus",
|
||||
"CERVIX": "Cervix observation"
|
||||
},
|
||||
"prediction": "Predictions",
|
||||
"predictionDescription": "Forecast your next period and, when relevant, your fertile window.",
|
||||
"encryption": "Encrypt sensitive entries",
|
||||
|
||||
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Clara de huevo",
|
||||
"label": "Moco cervical"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Bajo",
|
||||
"HIGH": "Alto"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Firme",
|
||||
"SOFT": "Blando"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Cerrado",
|
||||
"OPEN": "Abierto"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "Salud general",
|
||||
"AVOID_PREGNANCY": "Evitar el embarazo",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Fiebre, enfermedad, alcohol o una medición tardía: se excluye de la evaluación de temperatura.",
|
||||
"ovulationTest": "Test de ovulación",
|
||||
"mucus": "Moco cervical",
|
||||
"cervix": "Cuello uterino",
|
||||
"cervixPosition": "Posición",
|
||||
"cervixFirmness": "Firmeza",
|
||||
"cervixOpening": "Apertura",
|
||||
"intercourse": "Relaciones",
|
||||
"protected": "Protegidas",
|
||||
"note": "Nota",
|
||||
@@ -5915,6 +5931,12 @@
|
||||
"priorsDescription": "Estos valores ayudan a tus predicciones antes de registrar muchos ciclos. Déjalos en blanco para usar los predeterminados.",
|
||||
"rawChartMode": "Modo de gráfico en bruto",
|
||||
"rawChartModeDescription": "Muestra solo tus datos registrados, sin predicciones ni interpretación.",
|
||||
"secondarySymptomLabel": "Segundo signo de fertilidad",
|
||||
"secondarySymptomDescription": "El segundo signo que se combina con tu temperatura para la doble comprobación sintotérmica. La mayoría observa el moco cervical; cambia a la observación del cuello uterino si lo prefieres.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Moco cervical",
|
||||
"CERVIX": "Cuello uterino"
|
||||
},
|
||||
"prediction": "Predicciones",
|
||||
"predictionDescription": "Pronostica tu próximo periodo y, cuando corresponda, tu ventana fértil.",
|
||||
"encryption": "Cifrar entradas sensibles",
|
||||
|
||||
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Blanc d'œuf",
|
||||
"label": "Glaire cervicale"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Bas",
|
||||
"HIGH": "Haut"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Ferme",
|
||||
"SOFT": "Souple"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Fermé",
|
||||
"OPEN": "Ouvert"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "Santé générale",
|
||||
"AVOID_PREGNANCY": "Éviter une grossesse",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Fièvre, maladie, alcool ou mesure tardive — exclue de l'évaluation de la température.",
|
||||
"ovulationTest": "Test d'ovulation",
|
||||
"mucus": "Glaire cervicale",
|
||||
"cervix": "Col de l'utérus",
|
||||
"cervixPosition": "Position",
|
||||
"cervixFirmness": "Fermeté",
|
||||
"cervixOpening": "Ouverture",
|
||||
"intercourse": "Rapports",
|
||||
"protected": "Protégés",
|
||||
"note": "Note",
|
||||
@@ -5915,6 +5931,12 @@
|
||||
"priorsDescription": "Ces valeurs aident tes prévisions avant d'avoir enregistré beaucoup de cycles. Laisse vide pour utiliser les valeurs par défaut.",
|
||||
"rawChartMode": "Mode graphique brut",
|
||||
"rawChartModeDescription": "Affiche uniquement tes données enregistrées, sans prévision ni interprétation.",
|
||||
"secondarySymptomLabel": "Deuxième signe de fertilité",
|
||||
"secondarySymptomDescription": "Le deuxième signe associé à ta température pour la double vérification sympto-thermique. La plupart des personnes observent la glaire cervicale ; passe à l'observation du col de l'utérus si tu préfères.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Glaire cervicale",
|
||||
"CERVIX": "Col de l'utérus"
|
||||
},
|
||||
"prediction": "Prévisions",
|
||||
"predictionDescription": "Prévois tes prochaines règles et, le cas échéant, ta fenêtre fertile.",
|
||||
"encryption": "Chiffrer les entrées sensibles",
|
||||
|
||||
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Albume",
|
||||
"label": "Muco cervicale"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Basso",
|
||||
"HIGH": "Alto"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Duro",
|
||||
"SOFT": "Morbido"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Chiuso",
|
||||
"OPEN": "Aperto"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "Salute generale",
|
||||
"AVOID_PREGNANCY": "Evitare la gravidanza",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Febbre, malattia, alcol o una misurazione tardiva: esclusa dalla valutazione della temperatura.",
|
||||
"ovulationTest": "Test di ovulazione",
|
||||
"mucus": "Muco cervicale",
|
||||
"cervix": "Cervice",
|
||||
"cervixPosition": "Posizione",
|
||||
"cervixFirmness": "Consistenza",
|
||||
"cervixOpening": "Apertura",
|
||||
"intercourse": "Rapporti",
|
||||
"protected": "Protetti",
|
||||
"note": "Nota",
|
||||
@@ -5915,6 +5931,12 @@
|
||||
"priorsDescription": "Questi valori aiutano le previsioni prima di aver registrato molti cicli. Lascia vuoto per usare i valori predefiniti.",
|
||||
"rawChartMode": "Modalità grafico grezzo",
|
||||
"rawChartModeDescription": "Mostra solo i tuoi dati registrati, senza previsioni né interpretazioni.",
|
||||
"secondarySymptomLabel": "Secondo segno di fertilità",
|
||||
"secondarySymptomDescription": "Il secondo segno abbinato alla tua temperatura per il doppio controllo sintotermico. La maggior parte osserva il muco cervicale; passa all'osservazione della cervice se preferisci.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Muco cervicale",
|
||||
"CERVIX": "Cervice"
|
||||
},
|
||||
"prediction": "Previsioni",
|
||||
"predictionDescription": "Prevedi il prossimo ciclo e, quando rilevante, la tua finestra fertile.",
|
||||
"encryption": "Cifra le voci sensibili",
|
||||
|
||||
@@ -5797,6 +5797,18 @@
|
||||
"EGG_WHITE": "Jak białko jaja",
|
||||
"label": "Śluz szyjkowy"
|
||||
},
|
||||
"cervixPosition": {
|
||||
"LOW": "Nisko",
|
||||
"HIGH": "Wysoko"
|
||||
},
|
||||
"cervixFirmness": {
|
||||
"FIRM": "Twarda",
|
||||
"SOFT": "Miękka"
|
||||
},
|
||||
"cervixOpening": {
|
||||
"CLOSED": "Zamknięta",
|
||||
"OPEN": "Otwarta"
|
||||
},
|
||||
"goal": {
|
||||
"GENERAL_HEALTH": "Ogólne zdrowie",
|
||||
"AVOID_PREGNANCY": "Unikanie ciąży",
|
||||
@@ -5886,6 +5898,10 @@
|
||||
"temperatureDisturbedHint": "Gorączka, choroba, alkohol lub spóźniony pomiar — wykluczony z oceny temperatury.",
|
||||
"ovulationTest": "Test owulacyjny",
|
||||
"mucus": "Śluz szyjkowy",
|
||||
"cervix": "Szyjka macicy",
|
||||
"cervixPosition": "Położenie",
|
||||
"cervixFirmness": "Twardość",
|
||||
"cervixOpening": "Rozwarcie",
|
||||
"intercourse": "Współżycie",
|
||||
"protected": "Zabezpieczone",
|
||||
"note": "Notatka",
|
||||
@@ -5915,6 +5931,12 @@
|
||||
"priorsDescription": "Te wartości pomagają prognozom, zanim zapiszesz wiele cykli. Zostaw puste, aby użyć wartości domyślnych.",
|
||||
"rawChartMode": "Tryb surowego wykresu",
|
||||
"rawChartModeDescription": "Pokazuj tylko zapisane dane, bez prognoz i interpretacji.",
|
||||
"secondarySymptomLabel": "Drugi objaw płodności",
|
||||
"secondarySymptomDescription": "Drugi objaw łączony z temperaturą w podwójnej kontroli objawowo-termicznej. Większość obserwuje śluz szyjkowy; przełącz na obserwację szyjki macicy, jeśli wolisz.",
|
||||
"secondarySymptom": {
|
||||
"MUCUS": "Śluz szyjkowy",
|
||||
"CERVIX": "Szyjka macicy"
|
||||
},
|
||||
"prediction": "Prognozy",
|
||||
"predictionDescription": "Prognozuj następną miesiączkę oraz, gdy to istotne, okno płodne.",
|
||||
"encryption": "Szyfruj wrażliwe wpisy",
|
||||
|
||||
@@ -14,6 +14,9 @@ function blank(): DayLogFormState {
|
||||
bbtDisturbed: false,
|
||||
opk: null,
|
||||
mucus: null,
|
||||
cervixPosition: null,
|
||||
cervixFirmness: null,
|
||||
cervixOpening: null,
|
||||
intercourse: false,
|
||||
protectedSex: false,
|
||||
pregnancyTest: null,
|
||||
@@ -103,6 +106,24 @@ describe("buildDayLogInput (new row → omit empties)", () => {
|
||||
expect(patch.temperatureExcluded).toBe(true);
|
||||
});
|
||||
|
||||
it("carries the cervix signs through both payload builders", () => {
|
||||
const state = {
|
||||
...blank(),
|
||||
cervixPosition: "HIGH" as const,
|
||||
cervixFirmness: "SOFT" as const,
|
||||
cervixOpening: "OPEN" as const,
|
||||
};
|
||||
const input = buildDayLogInput(state, "2026-06-06");
|
||||
expect(input.cervixPosition).toBe("HIGH");
|
||||
expect(input.cervixFirmness).toBe("SOFT");
|
||||
expect(input.cervixOpening).toBe("OPEN");
|
||||
// A blank edit nulls every cervix sign (clear semantics).
|
||||
const patch = buildDayLogPatch(blank());
|
||||
expect(patch.cervixPosition).toBeNull();
|
||||
expect(patch.cervixFirmness).toBeNull();
|
||||
expect(patch.cervixOpening).toBeNull();
|
||||
});
|
||||
|
||||
it("never marks an absent BBT reading disturbed", () => {
|
||||
// A disturbed flag with no temperature is meaningless — it must reset.
|
||||
const patch = buildDayLogPatch({ ...blank(), bbt: "", bbtDisturbed: true });
|
||||
|
||||
@@ -17,7 +17,11 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { CycleGoal, CycleProfileDTO } from "./types";
|
||||
import type {
|
||||
CycleGoal,
|
||||
CycleProfileDTO,
|
||||
SecondarySymptom,
|
||||
} from "./types";
|
||||
import { useUpdateCyclePrefs } from "./use-cycle";
|
||||
|
||||
/**
|
||||
@@ -43,6 +47,8 @@ const GOALS: CycleGoal[] = [
|
||||
"OFF",
|
||||
];
|
||||
|
||||
const SECONDARY_SYMPTOMS: SecondarySymptom[] = ["MUCUS", "CERVIX"];
|
||||
|
||||
function numOrEmpty(v: number | null): string {
|
||||
return v == null ? "" : String(v);
|
||||
}
|
||||
@@ -60,6 +66,9 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) {
|
||||
profile.sensitiveCategoryEncryption,
|
||||
);
|
||||
const [discreet, setDiscreet] = useState(profile.discreetNotifications);
|
||||
const [secondarySymptom, setSecondarySymptom] = useState<SecondarySymptom>(
|
||||
profile.secondarySymptom,
|
||||
);
|
||||
const [cycleLen, setCycleLen] = useState(
|
||||
numOrEmpty(profile.typicalCycleLength),
|
||||
);
|
||||
@@ -78,6 +87,7 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) {
|
||||
async function handleSave() {
|
||||
await update.mutateAsync({
|
||||
goal,
|
||||
secondarySymptom,
|
||||
rawChartMode,
|
||||
predictionEnabled,
|
||||
sensitiveCategoryEncryption: encryption,
|
||||
@@ -167,6 +177,33 @@ export function CycleSettings({ profile }: { profile: CycleProfileDTO }) {
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Advanced — symptothermal secondary symptom. The default is mucus;
|
||||
the choice never forces itself on a casual user (progressive
|
||||
disclosure: it sits here in the advanced cycle settings). */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cycle-secondary-symptom">
|
||||
{t("cycle.settings.secondarySymptomLabel")}
|
||||
</Label>
|
||||
<NativeSelect
|
||||
id="cycle-secondary-symptom"
|
||||
value={secondarySymptom}
|
||||
onChange={(e) =>
|
||||
setSecondarySymptom(e.target.value as SecondarySymptom)
|
||||
}
|
||||
>
|
||||
{SECONDARY_SYMPTOMS.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{t(`cycle.settings.secondarySymptom.${s}`)}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.settings.secondarySymptomDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Privacy toggles */}
|
||||
<ToggleRow
|
||||
id="cycle-encryption"
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
useCreateCustomSymptom,
|
||||
useCustomSymptoms,
|
||||
useCycleDayLog,
|
||||
useCycleProfile,
|
||||
useDeleteCustomSymptom,
|
||||
useDeleteDayLog,
|
||||
useEndPeriod,
|
||||
@@ -63,6 +64,9 @@ import {
|
||||
} from "./use-cycle";
|
||||
import type {
|
||||
CervicalMucus,
|
||||
CervixFirmness,
|
||||
CervixOpening,
|
||||
CervixPosition,
|
||||
ContraceptiveKind,
|
||||
CycleDayLogInput,
|
||||
CycleSymptomSelection,
|
||||
@@ -98,6 +102,9 @@ const MUCUS_VALUES: CervicalMucus[] = [
|
||||
"WATERY",
|
||||
"EGG_WHITE",
|
||||
];
|
||||
const CERVIX_POSITION_VALUES: CervixPosition[] = ["LOW", "HIGH"];
|
||||
const CERVIX_FIRMNESS_VALUES: CervixFirmness[] = ["FIRM", "SOFT"];
|
||||
const CERVIX_OPENING_VALUES: CervixOpening[] = ["CLOSED", "OPEN"];
|
||||
const TEST_RESULTS: HomeTestResult[] = [
|
||||
"POSITIVE",
|
||||
"NEGATIVE",
|
||||
@@ -158,6 +165,9 @@ export interface DayLogFormState {
|
||||
bbtDisturbed: boolean;
|
||||
opk: OvulationTest | null;
|
||||
mucus: CervicalMucus | null;
|
||||
cervixPosition: CervixPosition | null;
|
||||
cervixFirmness: CervixFirmness | null;
|
||||
cervixOpening: CervixOpening | null;
|
||||
intercourse: boolean;
|
||||
protectedSex: boolean;
|
||||
pregnancyTest: HomeTestResult | null;
|
||||
@@ -195,6 +205,9 @@ export function buildDayLogPatch(s: DayLogFormState): CycleDayLogPatch {
|
||||
temperatureExcluded: resolveBbt(s.bbt) != null ? s.bbtDisturbed : false,
|
||||
ovulationTest: s.opk ?? null,
|
||||
cervicalMucus: s.mucus ?? null,
|
||||
cervixPosition: s.cervixPosition ?? null,
|
||||
cervixFirmness: s.cervixFirmness ?? null,
|
||||
cervixOpening: s.cervixOpening ?? null,
|
||||
sexualActivity: s.intercourse,
|
||||
protectedSex: s.intercourse ? s.protectedSex : null,
|
||||
pregnancyTest: s.pregnancyTest ?? null,
|
||||
@@ -223,6 +236,9 @@ export function buildDayLogInput(
|
||||
: {}),
|
||||
...(s.opk ? { ovulationTest: s.opk } : {}),
|
||||
...(s.mucus ? { cervicalMucus: s.mucus } : {}),
|
||||
...(s.cervixPosition ? { cervixPosition: s.cervixPosition } : {}),
|
||||
...(s.cervixFirmness ? { cervixFirmness: s.cervixFirmness } : {}),
|
||||
...(s.cervixOpening ? { cervixOpening: s.cervixOpening } : {}),
|
||||
sexualActivity: s.intercourse,
|
||||
protectedSex: s.intercourse ? s.protectedSex : null,
|
||||
...(s.pregnancyTest ? { pregnancyTest: s.pregnancyTest } : {}),
|
||||
@@ -274,6 +290,34 @@ function Chip({
|
||||
);
|
||||
}
|
||||
|
||||
/** One labelled row of cervix-sign chips (position / firmness / opening). */
|
||||
function CervixRow<T extends string>({
|
||||
label,
|
||||
values,
|
||||
current,
|
||||
labelFor,
|
||||
onToggle,
|
||||
}: {
|
||||
label: string;
|
||||
values: readonly T[];
|
||||
current: T | null;
|
||||
labelFor: (v: T) => string;
|
||||
onToggle: (v: T) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-1.5">
|
||||
<p className="text-muted-foreground text-xs font-medium">{label}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{values.map((v) => (
|
||||
<Chip key={v} active={current === v} onClick={() => onToggle(v)}>
|
||||
{labelFor(v)}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LogDaySheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -291,6 +335,11 @@ export function LogDaySheet({
|
||||
const dayLog = useCycleDayLog(open ? date : null);
|
||||
// The caller's custom symptoms, merged into the seeded chip grid.
|
||||
const customSymptoms = useCustomSymptoms();
|
||||
// Drives which symptothermal secondary sign the sheet offers — mucus (default)
|
||||
// or the cervix pickers (progressive disclosure: cervix entry only appears
|
||||
// when the user chose CERVIX in the advanced cycle settings).
|
||||
const cycleProfile = useCycleProfile();
|
||||
const showCervix = cycleProfile.data?.secondarySymptom === "CERVIX";
|
||||
|
||||
const [flow, setFlow] = useState<FlowLevel | null>(null);
|
||||
const [intermenstrual, setIntermenstrual] = useState(false);
|
||||
@@ -301,6 +350,13 @@ export function LogDaySheet({
|
||||
const [bbtDisturbed, setBbtDisturbed] = useState(false);
|
||||
const [opk, setOpk] = useState<OvulationTest | null>(null);
|
||||
const [mucus, setMucus] = useState<CervicalMucus | null>(null);
|
||||
const [cervixPosition, setCervixPosition] = useState<CervixPosition | null>(
|
||||
null,
|
||||
);
|
||||
const [cervixFirmness, setCervixFirmness] = useState<CervixFirmness | null>(
|
||||
null,
|
||||
);
|
||||
const [cervixOpening, setCervixOpening] = useState<CervixOpening | null>(null);
|
||||
const [intercourse, setIntercourse] = useState(false);
|
||||
const [protectedSex, setProtectedSex] = useState(false);
|
||||
const [pregnancyTest, setPregnancyTest] = useState<HomeTestResult | null>(
|
||||
@@ -334,6 +390,9 @@ export function LogDaySheet({
|
||||
setBbtDisturbed(dto?.temperatureExcluded ?? false);
|
||||
setOpk((dto?.ovulationTest ?? null) as OvulationTest | null);
|
||||
setMucus((dto?.cervicalMucus ?? null) as CervicalMucus | null);
|
||||
setCervixPosition((dto?.cervixPosition ?? null) as CervixPosition | null);
|
||||
setCervixFirmness((dto?.cervixFirmness ?? null) as CervixFirmness | null);
|
||||
setCervixOpening((dto?.cervixOpening ?? null) as CervixOpening | null);
|
||||
setIntercourse(dto?.sexualActivity ?? false);
|
||||
setProtectedSex(dto?.protectedSex ?? false);
|
||||
setPregnancyTest(dto?.pregnancyTest ?? null);
|
||||
@@ -370,6 +429,9 @@ export function LogDaySheet({
|
||||
bbtDisturbed,
|
||||
opk,
|
||||
mucus,
|
||||
cervixPosition,
|
||||
cervixFirmness,
|
||||
cervixOpening,
|
||||
intercourse,
|
||||
protectedSex,
|
||||
pregnancyTest,
|
||||
@@ -644,20 +706,56 @@ export function LogDaySheet({
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* Cervical mucus */}
|
||||
<Field label={t("cycle.sheet.mucus")}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{MUCUS_VALUES.map((v) => (
|
||||
<Chip
|
||||
key={v}
|
||||
active={mucus === v}
|
||||
onClick={() => setMucus((cur) => (cur === v ? null : v))}
|
||||
>
|
||||
{t(`cycle.mucus.${v}`)}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
{/* Symptothermal secondary symptom: cervical mucus (default) OR the
|
||||
cervix observation, per the user's advanced-settings choice. Only one
|
||||
of the two is offered so the log sheet never overwhelms. */}
|
||||
{showCervix ? (
|
||||
<Field label={t("cycle.sheet.cervix")}>
|
||||
<div className="space-y-3">
|
||||
<CervixRow
|
||||
label={t("cycle.sheet.cervixPosition")}
|
||||
values={CERVIX_POSITION_VALUES}
|
||||
current={cervixPosition}
|
||||
labelFor={(v) => t(`cycle.cervixPosition.${v}`)}
|
||||
onToggle={(v) =>
|
||||
setCervixPosition((cur) => (cur === v ? null : v))
|
||||
}
|
||||
/>
|
||||
<CervixRow
|
||||
label={t("cycle.sheet.cervixFirmness")}
|
||||
values={CERVIX_FIRMNESS_VALUES}
|
||||
current={cervixFirmness}
|
||||
labelFor={(v) => t(`cycle.cervixFirmness.${v}`)}
|
||||
onToggle={(v) =>
|
||||
setCervixFirmness((cur) => (cur === v ? null : v))
|
||||
}
|
||||
/>
|
||||
<CervixRow
|
||||
label={t("cycle.sheet.cervixOpening")}
|
||||
values={CERVIX_OPENING_VALUES}
|
||||
current={cervixOpening}
|
||||
labelFor={(v) => t(`cycle.cervixOpening.${v}`)}
|
||||
onToggle={(v) =>
|
||||
setCervixOpening((cur) => (cur === v ? null : v))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
) : (
|
||||
<Field label={t("cycle.sheet.mucus")}>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{MUCUS_VALUES.map((v) => (
|
||||
<Chip
|
||||
key={v}
|
||||
active={mucus === v}
|
||||
onClick={() => setMucus((cur) => (cur === v ? null : v))}
|
||||
>
|
||||
{t(`cycle.mucus.${v}`)}
|
||||
</Chip>
|
||||
))}
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Intercourse + protection */}
|
||||
<Field label={t("cycle.sheet.intercourse")}>
|
||||
|
||||
@@ -101,9 +101,18 @@ export interface CycleHistoryResponse {
|
||||
};
|
||||
}
|
||||
|
||||
/** Symptothermal secondary symptom — mucus (default) or cervix observation. */
|
||||
export type SecondarySymptom = "MUCUS" | "CERVIX";
|
||||
|
||||
/** The three Sensiplan cervix signs. */
|
||||
export type CervixPosition = "LOW" | "HIGH";
|
||||
export type CervixFirmness = "FIRM" | "SOFT";
|
||||
export type CervixOpening = "CLOSED" | "OPEN";
|
||||
|
||||
export interface CycleProfileDTO {
|
||||
goal: CycleGoal;
|
||||
cycleTrackingEnabled: boolean;
|
||||
secondarySymptom: SecondarySymptom;
|
||||
rawChartMode: boolean;
|
||||
predictionEnabled: boolean;
|
||||
discreetNotifications: boolean;
|
||||
@@ -141,6 +150,9 @@ export interface CycleDayLogInput {
|
||||
temperatureExcluded?: boolean;
|
||||
ovulationTest?: OvulationTest;
|
||||
cervicalMucus?: CervicalMucus;
|
||||
cervixPosition?: CervixPosition | null;
|
||||
cervixFirmness?: CervixFirmness | null;
|
||||
cervixOpening?: CervixOpening | null;
|
||||
sexualActivity?: boolean;
|
||||
protectedSex?: boolean | null;
|
||||
pregnancyTest?: HomeTestResult;
|
||||
@@ -163,6 +175,9 @@ export interface CycleDayLogDTO {
|
||||
temperatureExcluded: boolean;
|
||||
ovulationTest: OvulationTest | null;
|
||||
cervicalMucus: CervicalMucus | null;
|
||||
cervixPosition: CervixPosition | null;
|
||||
cervixFirmness: CervixFirmness | null;
|
||||
cervixOpening: CervixOpening | null;
|
||||
sexualActivity: boolean;
|
||||
protectedSex: boolean | null;
|
||||
pregnancyTest: HomeTestResult | null;
|
||||
|
||||
@@ -29,6 +29,9 @@ import {
|
||||
import type {
|
||||
CalendarResponse,
|
||||
CervicalMucus,
|
||||
CervixFirmness,
|
||||
CervixOpening,
|
||||
CervixPosition,
|
||||
ContraceptiveKind,
|
||||
CycleDayLogDTO,
|
||||
CycleDayLogInput,
|
||||
@@ -240,6 +243,9 @@ export interface CycleDayLogPatch {
|
||||
temperatureExcluded?: boolean;
|
||||
ovulationTest?: OvulationTest | null;
|
||||
cervicalMucus?: CervicalMucus | null;
|
||||
cervixPosition?: CervixPosition | null;
|
||||
cervixFirmness?: CervixFirmness | null;
|
||||
cervixOpening?: CervixOpening | null;
|
||||
sexualActivity?: boolean;
|
||||
protectedSex?: boolean | null;
|
||||
pregnancyTest?: HomeTestResult | null;
|
||||
@@ -317,6 +323,7 @@ export function useDeleteDayLog() {
|
||||
export interface CyclePrefsPatch {
|
||||
enabled?: boolean;
|
||||
goal?: CycleProfileDTO["goal"];
|
||||
secondarySymptom?: CycleProfileDTO["secondarySymptom"];
|
||||
rawChartMode?: boolean;
|
||||
predictionEnabled?: boolean;
|
||||
discreetNotifications?: boolean;
|
||||
|
||||
Reference in new issue
Block a user