mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean baseline. Formatting only — no behaviour change. Generator-owned and vendored artifacts stay untouched via .prettierignore.
This commit is contained in:
1 parent
b3fea2ef59
commit
1f4ba5939d
1000 files changed
+12130
-8385
No files matched your search
@@ -11,10 +11,7 @@ function render(node: React.ReactNode) {
|
||||
);
|
||||
}
|
||||
|
||||
function day(
|
||||
date: string,
|
||||
overrides: Partial<CalendarDay> = {},
|
||||
): CalendarDay {
|
||||
function day(date: string, overrides: Partial<CalendarDay> = {}): CalendarDay {
|
||||
return {
|
||||
date,
|
||||
phase: "FOLLICULAR",
|
||||
|
||||
@@ -29,7 +29,10 @@ describe("<FieldInfo>", () => {
|
||||
// Radix renders the tooltip content lazily (portal, on open); the trigger
|
||||
// therefore reports a closed state until interacted with.
|
||||
const html = render(
|
||||
<FieldInfo label="Cervical mucus" detail="Discharge changes across the cycle." />,
|
||||
<FieldInfo
|
||||
label="Cervical mucus"
|
||||
detail="Discharge changes across the cycle."
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('data-state="closed"');
|
||||
});
|
||||
|
||||
@@ -107,7 +107,11 @@ describe("buildDayLogInput (new row → omit empties)", () => {
|
||||
);
|
||||
expect(input.basalBodyTempC).toBe(36.5);
|
||||
expect(input.temperatureExcluded).toBe(true);
|
||||
const patch = buildDayLogPatch({ ...blank(), bbt: "36.5", bbtDisturbed: true });
|
||||
const patch = buildDayLogPatch({
|
||||
...blank(),
|
||||
bbt: "36.5",
|
||||
bbtDisturbed: true,
|
||||
});
|
||||
expect(patch.temperatureExcluded).toBe(true);
|
||||
});
|
||||
|
||||
@@ -145,7 +149,13 @@ describe("buildDayLogInput (new row → omit empties)", () => {
|
||||
describe("section summary-badge counts (v1.17.0)", () => {
|
||||
it("counts each logged symptom", () => {
|
||||
expect(symptomsCount(blank())).toBe(0);
|
||||
const s = { ...blank(), symptoms: new Map([["cramps", 2], ["headache", null]]) };
|
||||
const s = {
|
||||
...blank(),
|
||||
symptoms: new Map([
|
||||
["cramps", 2],
|
||||
["headache", null],
|
||||
]),
|
||||
};
|
||||
expect(symptomsCount(s)).toBe(2);
|
||||
});
|
||||
|
||||
|
||||
@@ -131,9 +131,7 @@ export function CycleHistoryChart({
|
||||
{t("cycle.history.chartTitle")}
|
||||
</CardTitle>
|
||||
{hasData ? (
|
||||
<CardDescription>
|
||||
{t("cycle.history.chartCaption")}
|
||||
</CardDescription>
|
||||
<CardDescription>{t("cycle.history.chartCaption")}</CardDescription>
|
||||
) : null}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -144,7 +142,10 @@ export function CycleHistoryChart({
|
||||
) : (
|
||||
<>
|
||||
{/* Stat chips — avg / variability / regularity classification. */}
|
||||
<ul className="flex flex-wrap gap-2" data-slot="cycle-history-stats">
|
||||
<ul
|
||||
className="flex flex-wrap gap-2"
|
||||
data-slot="cycle-history-stats"
|
||||
>
|
||||
<StatChip
|
||||
hue={REST_HUE}
|
||||
label={t("cycle.history.avgLength")}
|
||||
@@ -187,9 +188,7 @@ export function CycleHistoryChart({
|
||||
})}
|
||||
periodLabel={t("cycle.history.legendPeriodSegment")}
|
||||
ovulationLabel={t("cycle.calendar.legendOvulationConfirmed")}
|
||||
barTitle={(b) =>
|
||||
t("cycle.history.days", { count: b.lengthDays })
|
||||
}
|
||||
barTitle={(b) => t("cycle.history.days", { count: b.lengthDays })}
|
||||
/>
|
||||
|
||||
{/* Inline legend mirroring the bar segments. */}
|
||||
@@ -279,8 +278,7 @@ function BarField({
|
||||
const x = cx - barW / 2;
|
||||
const totalH = scale(b.lengthDays);
|
||||
const topY = baseY - totalH;
|
||||
const periodH =
|
||||
b.periodDays != null ? scale(b.periodDays) : 0;
|
||||
const periodH = b.periodDays != null ? scale(b.periodDays) : 0;
|
||||
const periodTopY = baseY - periodH;
|
||||
const restH = totalH - periodH;
|
||||
const ovuY =
|
||||
@@ -353,9 +351,7 @@ function StatChip({
|
||||
} & React.HTMLAttributes<HTMLLIElement>) {
|
||||
return (
|
||||
<li
|
||||
className={cn(
|
||||
"bg-muted/40 flex items-center gap-2 rounded-md px-3 py-2",
|
||||
)}
|
||||
className={cn("bg-muted/40 flex items-center gap-2 rounded-md px-3 py-2")}
|
||||
{...rest}
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -114,28 +114,28 @@ export function CycleRingTile({ className }: { className?: string }) {
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="text-foreground flex items-center gap-2">
|
||||
<Droplets className="h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate text-base leading-none font-semibold">
|
||||
{t("cycle.wellnessRing.label")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<CycleRing
|
||||
dayOfCycle={wheel.dayOfCycle}
|
||||
cycleLength={wheel.cycleLength}
|
||||
phase={wheel.phase}
|
||||
spans={wheel.spans}
|
||||
animate={play}
|
||||
size={120}
|
||||
/>
|
||||
<span
|
||||
data-slot="wellness-cycle-band-word"
|
||||
className="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
{t(`cycle.phase.${wheel.phase}`)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-foreground flex items-center gap-2">
|
||||
<Droplets className="h-5 w-5 shrink-0" aria-hidden="true" />
|
||||
<span className="truncate text-base leading-none font-semibold">
|
||||
{t("cycle.wellnessRing.label")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<CycleRing
|
||||
dayOfCycle={wheel.dayOfCycle}
|
||||
cycleLength={wheel.cycleLength}
|
||||
phase={wheel.phase}
|
||||
spans={wheel.spans}
|
||||
animate={play}
|
||||
size={120}
|
||||
/>
|
||||
<span
|
||||
data-slot="wellness-cycle-band-word"
|
||||
className="text-muted-foreground text-center text-xs"
|
||||
>
|
||||
{t(`cycle.phase.${wheel.phase}`)}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,11 +17,7 @@ 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,
|
||||
SecondarySymptom,
|
||||
} from "./types";
|
||||
import type { CycleGoal, CycleProfileDTO, SecondarySymptom } from "./types";
|
||||
import { useUpdateCyclePrefs } from "./use-cycle";
|
||||
|
||||
/**
|
||||
|
||||
@@ -171,10 +171,7 @@ export function CycleView() {
|
||||
node never matched, leaving the tile stuck at the rise rule's
|
||||
`opacity: 0` (invisible but still boxed). The `contents` wrapper
|
||||
carries the flag without adding a layout box. */}
|
||||
<div
|
||||
data-revealed={play ? "true" : undefined}
|
||||
className="contents"
|
||||
>
|
||||
<div data-revealed={play ? "true" : undefined} className="contents">
|
||||
<div
|
||||
data-slot="cycle-wheel-tile"
|
||||
style={
|
||||
@@ -187,49 +184,49 @@ export function CycleView() {
|
||||
play && "wellness-tile-rise",
|
||||
)}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex h-[220px] items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : calendarError ? (
|
||||
<div className="flex h-[220px] flex-col items-center justify-center gap-3 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.loadError")}
|
||||
{loading ? (
|
||||
<div className="flex h-[220px] items-center justify-center">
|
||||
<Loader2 className="text-primary h-8 w-8 animate-spin motion-reduce:animate-none" />
|
||||
</div>
|
||||
) : calendarError ? (
|
||||
<div className="flex h-[220px] flex-col items-center justify-center gap-3 text-center">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("cycle.loadError")}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => void calendar.refetch()}
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<CycleRing
|
||||
dayOfCycle={wheel.dayOfCycle}
|
||||
cycleLength={wheel.cycleLength}
|
||||
phase={wheel.phase}
|
||||
spans={wheel.spans}
|
||||
animate={play}
|
||||
/>
|
||||
)}
|
||||
{!calendarError ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("cycle.ring.caption")}
|
||||
</p>
|
||||
) : null}
|
||||
{/* First-period CTA — only when no cycle is active yet. */}
|
||||
{!loading && !calendarError && wheel.dayOfCycle == null ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-1.5"
|
||||
onClick={() => void calendar.refetch()}
|
||||
className="mt-1 w-full"
|
||||
onClick={() => openSheet(today)}
|
||||
>
|
||||
<RefreshCw className="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{t("common.retry")}
|
||||
{t("cycle.ring.firstPeriodCta")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<CycleRing
|
||||
dayOfCycle={wheel.dayOfCycle}
|
||||
cycleLength={wheel.cycleLength}
|
||||
phase={wheel.phase}
|
||||
spans={wheel.spans}
|
||||
animate={play}
|
||||
/>
|
||||
)}
|
||||
{!calendarError ? (
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("cycle.ring.caption")}
|
||||
</p>
|
||||
) : null}
|
||||
{/* First-period CTA — only when no cycle is active yet. */}
|
||||
{!loading && !calendarError && wheel.dayOfCycle == null ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-1 w-full"
|
||||
onClick={() => openSheet(today)}
|
||||
>
|
||||
{t("cycle.ring.firstPeriodCta")}
|
||||
</Button>
|
||||
) : null}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,10 +42,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import {
|
||||
SheetSection,
|
||||
SheetSectionCount,
|
||||
} from "@/components/ui/sheet-section";
|
||||
import { SheetSection, SheetSectionCount } from "@/components/ui/sheet-section";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { CUSTOM_SYMPTOM_ICON_ALLOWLIST } from "@/lib/cycle/custom-symptoms-shared";
|
||||
import { FieldInfo } from "./field-info";
|
||||
@@ -509,7 +506,9 @@ export function LogDaySheet({
|
||||
const [cervixFirmness, setCervixFirmness] = useState<CervixFirmness | null>(
|
||||
null,
|
||||
);
|
||||
const [cervixOpening, setCervixOpening] = useState<CervixOpening | 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>(
|
||||
@@ -884,121 +883,121 @@ export function LogDaySheet({
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.01"
|
||||
min={30}
|
||||
max={45}
|
||||
value={bbt}
|
||||
onChange={(e) => setBbt(e.target.value)}
|
||||
placeholder={t("cycle.sheet.temperaturePlaceholder")}
|
||||
className="w-32"
|
||||
aria-label={t("cycle.sheet.temperature")}
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("cycle.sheet.bbtHint")}
|
||||
</p>
|
||||
{resolveBbt(bbt) != null && (
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm">
|
||||
{t("cycle.sheet.temperatureDisturbed")}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("cycle.sheet.temperatureDisturbedHint")}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={bbtDisturbed}
|
||||
onCheckedChange={setBbtDisturbed}
|
||||
aria-label={t("cycle.sheet.temperatureDisturbed")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{/* Ovulation test */}
|
||||
<Field
|
||||
label={t("cycle.sheet.ovulationTest")}
|
||||
info={
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.ovulationTestLabel")}
|
||||
detail={t("cycle.fieldInfo.ovulationTest")}
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="decimal"
|
||||
step="0.01"
|
||||
min={30}
|
||||
max={45}
|
||||
value={bbt}
|
||||
onChange={(e) => setBbt(e.target.value)}
|
||||
placeholder={t("cycle.sheet.temperaturePlaceholder")}
|
||||
className="w-32"
|
||||
aria-label={t("cycle.sheet.temperature")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{OPK_VALUES.map((v) => (
|
||||
<Chip
|
||||
key={v}
|
||||
active={opk === v}
|
||||
onClick={() => setOpk((cur) => (cur === v ? null : v))}
|
||||
>
|
||||
{t(`cycle.ovulationTest.${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>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("cycle.sheet.bbtHint")}
|
||||
</p>
|
||||
{resolveBbt(bbt) != null && (
|
||||
<div className="mt-3 flex items-start justify-between gap-3">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm">
|
||||
{t("cycle.sheet.temperatureDisturbed")}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("cycle.sheet.temperatureDisturbedHint")}
|
||||
</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={bbtDisturbed}
|
||||
onCheckedChange={setBbtDisturbed}
|
||||
aria-label={t("cycle.sheet.temperatureDisturbed")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
) : (
|
||||
|
||||
{/* Ovulation test */}
|
||||
<Field
|
||||
label={t("cycle.sheet.mucus")}
|
||||
label={t("cycle.sheet.ovulationTest")}
|
||||
info={
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.mucusLabel")}
|
||||
detail={t("cycle.fieldInfo.mucus")}
|
||||
label={t("cycle.fieldInfo.ovulationTestLabel")}
|
||||
detail={t("cycle.fieldInfo.ovulationTest")}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{MUCUS_VALUES.map((v) => (
|
||||
{OPK_VALUES.map((v) => (
|
||||
<Chip
|
||||
key={v}
|
||||
active={mucus === v}
|
||||
onClick={() => setMucus((cur) => (cur === v ? null : v))}
|
||||
active={opk === v}
|
||||
onClick={() => setOpk((cur) => (cur === v ? null : v))}
|
||||
>
|
||||
{t(`cycle.mucus.${v}`)}
|
||||
{t(`cycle.ovulationTest.${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")}
|
||||
info={
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.mucusLabel")}
|
||||
detail={t("cycle.fieldInfo.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>
|
||||
)}
|
||||
</SheetSection>
|
||||
|
||||
{/* Intimacy & contraception */}
|
||||
@@ -1008,48 +1007,48 @@ export function LogDaySheet({
|
||||
>
|
||||
{/* Intercourse + protection */}
|
||||
<Field label={t("cycle.sheet.intercourse")}>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label
|
||||
htmlFor="cycle-intercourse"
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
{t("cycle.sheet.intercourse")}
|
||||
</Label>
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.intercourseLabel")}
|
||||
detail={t("cycle.fieldInfo.intercourse")}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
id="cycle-intercourse"
|
||||
checked={intercourse}
|
||||
onCheckedChange={setIntercourse}
|
||||
/>
|
||||
</div>
|
||||
{intercourse ? (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label
|
||||
htmlFor="cycle-protected"
|
||||
className="text-muted-foreground text-sm font-normal"
|
||||
htmlFor="cycle-intercourse"
|
||||
className="text-sm font-normal"
|
||||
>
|
||||
{t("cycle.sheet.protected")}
|
||||
{t("cycle.sheet.intercourse")}
|
||||
</Label>
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.protectedLabel")}
|
||||
detail={t("cycle.fieldInfo.protected")}
|
||||
label={t("cycle.fieldInfo.intercourseLabel")}
|
||||
detail={t("cycle.fieldInfo.intercourse")}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
id="cycle-protected"
|
||||
checked={protectedSex}
|
||||
onCheckedChange={setProtectedSex}
|
||||
id="cycle-intercourse"
|
||||
checked={intercourse}
|
||||
onCheckedChange={setIntercourse}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{intercourse ? (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label
|
||||
htmlFor="cycle-protected"
|
||||
className="text-muted-foreground text-sm font-normal"
|
||||
>
|
||||
{t("cycle.sheet.protected")}
|
||||
</Label>
|
||||
<FieldInfo
|
||||
label={t("cycle.fieldInfo.protectedLabel")}
|
||||
detail={t("cycle.fieldInfo.protected")}
|
||||
/>
|
||||
</div>
|
||||
<Switch
|
||||
id="cycle-protected"
|
||||
checked={protectedSex}
|
||||
onCheckedChange={setProtectedSex}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{/* Contraceptive */}
|
||||
|
||||
@@ -124,83 +124,83 @@ export function PhaseEducationCard({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{/* Zone 1 — eyebrow + phase name with a hue dot (never colour-only). */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles
|
||||
className="h-4 w-4 shrink-0"
|
||||
style={{ color: hue }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{t("cycle.phaseEducation.title")}
|
||||
</span>
|
||||
</div>
|
||||
{/* Zone 1 — eyebrow + phase name with a hue dot (never colour-only). */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles
|
||||
className="h-4 w-4 shrink-0"
|
||||
style={{ color: hue }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||
{t("cycle.phaseEducation.title")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{stillLearning ? (
|
||||
<p
|
||||
data-slot="cycle-phase-education-learning"
|
||||
className="text-muted-foreground mt-2 text-sm"
|
||||
>
|
||||
{t("cycle.phaseEducation.stillLearning")}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-foreground mt-2 flex items-center gap-2 text-base font-semibold">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: hue }}
|
||||
/>
|
||||
{phaseName}
|
||||
</h3>
|
||||
{stillLearning ? (
|
||||
<p
|
||||
data-slot="cycle-phase-education-learning"
|
||||
className="text-muted-foreground mt-2 text-sm"
|
||||
>
|
||||
{t("cycle.phaseEducation.stillLearning")}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-foreground mt-2 flex items-center gap-2 text-base font-semibold">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="inline-block h-2.5 w-2.5 shrink-0 rounded-full"
|
||||
style={{ backgroundColor: hue }}
|
||||
/>
|
||||
{phaseName}
|
||||
</h3>
|
||||
|
||||
{/* Zone 2 — the curated, descriptive-only "what's happening" line,
|
||||
{/* Zone 2 — the curated, descriptive-only "what's happening" line,
|
||||
followed by a deeper "context" line (what someone might notice +
|
||||
gentle framing of normal variation). Both stay descriptive, never
|
||||
clinical advice / a medical claim. */}
|
||||
<p className="text-muted-foreground mt-1.5 text-sm leading-relaxed">
|
||||
{t(PHASE_WHATS_HAPPENING_KEY[phase as CyclePhase])}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">
|
||||
{t(PHASE_CONTEXT_KEY[phase as CyclePhase])}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-1.5 text-sm leading-relaxed">
|
||||
{t(PHASE_WHATS_HAPPENING_KEY[phase as CyclePhase])}
|
||||
</p>
|
||||
<p className="text-muted-foreground mt-2 text-sm leading-relaxed">
|
||||
{t(PHASE_CONTEXT_KEY[phase as CyclePhase])}
|
||||
</p>
|
||||
|
||||
{/* Zone 3 — the user's OWN clustered symptoms for this phase
|
||||
{/* Zone 3 — the user's OWN clustered symptoms for this phase
|
||||
("you often notice this here"). */}
|
||||
{chips.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-muted-foreground text-xs font-medium">
|
||||
{t("cycle.phaseEducation.commonHere")}
|
||||
</p>
|
||||
<ul
|
||||
data-slot="cycle-phase-education-chips"
|
||||
className="mt-2 flex flex-wrap gap-1.5"
|
||||
>
|
||||
{chips.map((row) => {
|
||||
const meta = byKey.get(row.symptomKey);
|
||||
const Icon = meta?.icon;
|
||||
const name = meta ? t(meta.labelKey) : row.symptomKey;
|
||||
return (
|
||||
<li
|
||||
key={row.symptomKey}
|
||||
className="border-foreground/15 bg-background/55 text-foreground inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium"
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className="h-3 w-3 shrink-0"
|
||||
style={{ color: hue }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
{name}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{chips.length > 0 ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-muted-foreground text-xs font-medium">
|
||||
{t("cycle.phaseEducation.commonHere")}
|
||||
</p>
|
||||
<ul
|
||||
data-slot="cycle-phase-education-chips"
|
||||
className="mt-2 flex flex-wrap gap-1.5"
|
||||
>
|
||||
{chips.map((row) => {
|
||||
const meta = byKey.get(row.symptomKey);
|
||||
const Icon = meta?.icon;
|
||||
const name = meta ? t(meta.labelKey) : row.symptomKey;
|
||||
return (
|
||||
<li
|
||||
key={row.symptomKey}
|
||||
className="border-foreground/15 bg-background/55 text-foreground inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium"
|
||||
>
|
||||
{Icon ? (
|
||||
<Icon
|
||||
className="h-3 w-3 shrink-0"
|
||||
style={{ color: hue }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
{name}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -193,8 +193,7 @@ export function useDeleteCustomSymptom() {
|
||||
export function useCycleProfile() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.cycleProfile(),
|
||||
queryFn: () =>
|
||||
apiGet<CycleProfileDTO>("/api/cycle/profile"),
|
||||
queryFn: () => apiGet<CycleProfileDTO>("/api/cycle/profile"),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
@@ -207,8 +206,7 @@ export function useCycleProfile() {
|
||||
export function useCycleInsights() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.cycleInsights(),
|
||||
queryFn: () =>
|
||||
apiGet<CycleInsightsResponse>("/api/cycle/insights"),
|
||||
queryFn: () => apiGet<CycleInsightsResponse>("/api/cycle/insights"),
|
||||
staleTime: 5 * 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user