mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
fix(medications): split the card next/last dose lines into label-left, value-right rows
The next-intake and last-intake lines on the medication cards ran the label and the time together on one run of text, reading as crammed. Lay each line out as a two-column row — bold label pinned left, value flush right — so the two rows read as distinct, aligned columns. The label truncates first on a narrow viewport; the time never truncates. The GLP-1 card routes through the same shared slot and now overrides only the left-column label with its appointment-phrased wording, so the value column carries the time and the optional rotation nudge. Its value strings drop the now-redundant leading label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
c59290ddc6
commit
83cf4eff2e
10 files changed
+114
-75
No files matched your search
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Dosen pro Pen / Ampulle",
|
||||
"glp1DosesPerUnitPlaceholder": "z. B. 4 für einen Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Optional — Mounjaro KwikPen = 4, Ozempic 0,25/0,5 mg = 4, Wegovy Einmal-Pen = 1. Leer lassen, wenn keine Dosis-pro-Pen-Verfolgung gewünscht ist.",
|
||||
"glp1LastInjection": "Letzter Termin: {label}",
|
||||
"glp1LastInjectionWithSite": "Letzter Termin: {label} · {site}",
|
||||
"glp1NextInjectionDays": "Nächster Termin: {label} (in {days} Tagen)",
|
||||
"glp1NextInjectionToday": "Nächster Termin: heute",
|
||||
"glp1NextInjectionTomorrow": "Nächster Termin: morgen",
|
||||
"glp1NextLabel": "Nächster Termin:",
|
||||
"glp1LastLabel": "Letzter Termin:",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (in {days} Tagen)",
|
||||
"glp1NextInjectionToday": "heute",
|
||||
"glp1NextInjectionTomorrow": "morgen",
|
||||
"glp1RotationLast": "Letzte Stelle: {site}",
|
||||
"glp1RotationSuggested": "Empfohlen: {site}",
|
||||
"glp1LogSideEffect": "Nebenwirkung loggen",
|
||||
|
||||
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Doses per pen / vial",
|
||||
"glp1DosesPerUnitPlaceholder": "e.g. 4 for a Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Optional — Mounjaro KwikPen = 4, Ozempic 0.25/0.5 mg = 4, Wegovy single-dose pen = 1. Leave blank if you don't track per-pen dosing.",
|
||||
"glp1LastInjection": "Last: {label}",
|
||||
"glp1LastInjectionWithSite": "Last: {label} · {site}",
|
||||
"glp1NextInjectionDays": "Next: {label} (in {days} days)",
|
||||
"glp1NextInjectionToday": "Next: today",
|
||||
"glp1NextInjectionTomorrow": "Next: tomorrow",
|
||||
"glp1NextLabel": "Next:",
|
||||
"glp1LastLabel": "Last:",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (in {days} days)",
|
||||
"glp1NextInjectionToday": "today",
|
||||
"glp1NextInjectionTomorrow": "tomorrow",
|
||||
"glp1RotationLast": "Last site: {site}",
|
||||
"glp1RotationSuggested": "Recommended next: {site}",
|
||||
"glp1LogSideEffect": "Log side effect",
|
||||
|
||||
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Dosis por pluma / vial",
|
||||
"glp1DosesPerUnitPlaceholder": "p. ej. 4 para una pluma Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Opcional — Mounjaro KwikPen = 4, Ozempic 0,25/0,5 mg = 4, Wegovy pluma de un solo uso = 1. Déjalo vacío si no quieres llevar el conteo de dosis por pluma.",
|
||||
"glp1LastInjection": "Última cita: {label}",
|
||||
"glp1LastInjectionWithSite": "Última cita: {label} · {site}",
|
||||
"glp1NextInjectionDays": "Siguiente: {label} (en {days} días)",
|
||||
"glp1NextInjectionToday": "Próxima cita: hoy",
|
||||
"glp1NextInjectionTomorrow": "Próxima cita: mañana",
|
||||
"glp1NextLabel": "Próxima cita:",
|
||||
"glp1LastLabel": "Última cita:",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (en {days} días)",
|
||||
"glp1NextInjectionToday": "hoy",
|
||||
"glp1NextInjectionTomorrow": "mañana",
|
||||
"glp1RotationLast": "Último sitio: {site}",
|
||||
"glp1RotationSuggested": "Sugerido: {site}",
|
||||
"glp1LogSideEffect": "Registrar efecto secundario",
|
||||
|
||||
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Doses par stylo / flacon",
|
||||
"glp1DosesPerUnitPlaceholder": "p. ex. 4 pour un stylo Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Optionnel — Mounjaro KwikPen = 4, Ozempic 0,25/0,5 mg = 4, Wegovy stylo à usage unique = 1. Laisse vide si tu ne veux pas suivre la dose par stylo.",
|
||||
"glp1LastInjection": "Dernier rendez-vous : {label}",
|
||||
"glp1LastInjectionWithSite": "Dernier rendez-vous : {label} · {site}",
|
||||
"glp1NextInjectionDays": "Suivant : {label} (dans {days} jours)",
|
||||
"glp1NextInjectionToday": "Prochain rendez-vous : aujourd’hui",
|
||||
"glp1NextInjectionTomorrow": "Prochain rendez-vous : demain",
|
||||
"glp1NextLabel": "Prochain rendez-vous :",
|
||||
"glp1LastLabel": "Dernier rendez-vous :",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (dans {days} jours)",
|
||||
"glp1NextInjectionToday": "aujourd’hui",
|
||||
"glp1NextInjectionTomorrow": "demain",
|
||||
"glp1RotationLast": "Dernier site : {site}",
|
||||
"glp1RotationSuggested": "Conseillé : {site}",
|
||||
"glp1LogSideEffect": "Consigner un effet secondaire",
|
||||
|
||||
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Dosi per penna / fiala",
|
||||
"glp1DosesPerUnitPlaceholder": "es. 4 per una penna Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Opzionale — Mounjaro KwikPen = 4, Ozempic 0,25/0,5 mg = 4, Wegovy penna monouso = 1. Lascia vuoto se non vuoi monitorare le dosi per penna.",
|
||||
"glp1LastInjection": "Ultimo appuntamento: {label}",
|
||||
"glp1LastInjectionWithSite": "Ultimo appuntamento: {label} · {site}",
|
||||
"glp1NextInjectionDays": "Prossimo: {label} (tra {days} giorni)",
|
||||
"glp1NextInjectionToday": "Prossimo appuntamento: oggi",
|
||||
"glp1NextInjectionTomorrow": "Prossimo appuntamento: domani",
|
||||
"glp1NextLabel": "Prossimo appuntamento:",
|
||||
"glp1LastLabel": "Ultimo appuntamento:",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (tra {days} giorni)",
|
||||
"glp1NextInjectionToday": "oggi",
|
||||
"glp1NextInjectionTomorrow": "domani",
|
||||
"glp1RotationLast": "Ultima sede: {site}",
|
||||
"glp1RotationSuggested": "Suggerita: {site}",
|
||||
"glp1LogSideEffect": "Registra effetto collaterale",
|
||||
|
||||
+7
-5
@@ -702,11 +702,13 @@
|
||||
"glp1DosesPerUnit": "Dawki na pen / fiolkę",
|
||||
"glp1DosesPerUnitPlaceholder": "np. 4 dla pena Mounjaro KwikPen",
|
||||
"glp1DosesPerUnitHelp": "Opcjonalne — Mounjaro KwikPen = 4, Ozempic 0,25/0,5 mg = 4, Wegovy pen jednorazowy = 1. Zostaw puste, jeśli nie chcesz śledzić dawek na pen.",
|
||||
"glp1LastInjection": "Ostatni termin: {label}",
|
||||
"glp1LastInjectionWithSite": "Ostatni termin: {label} · {site}",
|
||||
"glp1NextInjectionDays": "Następnie: {label} (za {days} dni)",
|
||||
"glp1NextInjectionToday": "Następny termin: dziś",
|
||||
"glp1NextInjectionTomorrow": "Następny termin: jutro",
|
||||
"glp1NextLabel": "Następny termin:",
|
||||
"glp1LastLabel": "Ostatni termin:",
|
||||
"glp1LastInjection": "{label}",
|
||||
"glp1LastInjectionWithSite": "{label} · {site}",
|
||||
"glp1NextInjectionDays": "{label} (za {days} dni)",
|
||||
"glp1NextInjectionToday": "dziś",
|
||||
"glp1NextInjectionTomorrow": "jutro",
|
||||
"glp1RotationLast": "Ostatnie miejsce: {site}",
|
||||
"glp1RotationSuggested": "Sugerowane: {site}",
|
||||
"glp1LogSideEffect": "Zapisz działanie niepożądane",
|
||||
|
||||
@@ -320,13 +320,13 @@ describe("<Glp1MedicationCard> — GLP-1 variant rendering", () => {
|
||||
client,
|
||||
);
|
||||
|
||||
// The with-site copy reads "Last: <label> · <site>"; both halves
|
||||
// must show up in the SSR string.
|
||||
// The last-line renders the "Last:" label in its own left column and
|
||||
// the value (label · site) flush right; both halves show in the SSR.
|
||||
expect(html).toContain("Last:");
|
||||
expect(html).toContain("Abdomen, lower left");
|
||||
// The next-injection helper produces one of three strings — today,
|
||||
// tomorrow, or "in N days". All three contain the localised "Next:"
|
||||
// prefix from `glp1NextInjection*` keys.
|
||||
// The next-line carries the "Next:" label in its left column,
|
||||
// independent of which of the three value variants (today / tomorrow /
|
||||
// "in N days") the helper produced.
|
||||
expect(html).toMatch(/Next:/);
|
||||
});
|
||||
|
||||
|
||||
@@ -448,11 +448,19 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
|
||||
expect(html).not.toContain("text-foreground/85");
|
||||
// No literal em-dash placeholder for an absent next-injection.
|
||||
expect(html).not.toContain(">—<");
|
||||
// Both lines present (next + last), so two muted <p> in the slot.
|
||||
// Both lines present (next + last). Each line is a two-column
|
||||
// label-left / value-right row (`flex … justify-between`) so the
|
||||
// label and the time read as distinct columns rather than crammed
|
||||
// onto one run of text. Two such rows in the slot.
|
||||
const slotStart = html.indexOf("min-h-[2.75rem]");
|
||||
const slot = html.slice(slotStart, slotStart + 800);
|
||||
const lines = slot.match(/<p class="text-muted-foreground">/g) ?? [];
|
||||
expect(lines.length).toBe(2);
|
||||
const slot = html.slice(slotStart, slotStart + 1200);
|
||||
const rows =
|
||||
slot.match(
|
||||
/<div class="text-muted-foreground flex items-baseline justify-between gap-3">/g,
|
||||
) ?? [];
|
||||
expect(rows.length).toBe(2);
|
||||
// The value is pinned flush-right in its own column.
|
||||
expect(slot).toContain('<span class="text-right">');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -21,10 +21,17 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
* whose last-dose line is absent the same vertical footprint as a sibling
|
||||
* that renders both lines, so the dose rows line up across the grid.
|
||||
*
|
||||
* Each card still builds the line *content* itself (the generic card emits a
|
||||
* day-label + window-range; the GLP-1 card emits a weekly-cadence prose
|
||||
* string), because the two cadences read differently — but the wrapper,
|
||||
* order, colour, and gating are now guaranteed identical.
|
||||
* Each card still builds the value *content* itself (the generic card emits a
|
||||
* day-label + window-range; the GLP-1 card emits a weekly-cadence string),
|
||||
* because the two cadences read differently — but the wrapper, order, colour,
|
||||
* label/value layout, and gating are now guaranteed identical. The GLP-1 card
|
||||
* overrides the left-column label with its appointment-phrased wording.
|
||||
*
|
||||
* Layout: when `labelled`, each line is a `flex justify-between` row with the
|
||||
* bold label pinned left and the value flush right (`text-right`), so the
|
||||
* label and the time read as two distinct columns rather than crammed onto
|
||||
* one run of text. The label truncates first on a narrow viewport; the value
|
||||
* (the time / cadence) never truncates.
|
||||
*/
|
||||
interface MedicationNextLastSlotProps {
|
||||
/**
|
||||
@@ -36,45 +43,56 @@ interface MedicationNextLastSlotProps {
|
||||
/** The last-intake line content, or null when the med has never been taken. */
|
||||
last: React.ReactNode | null;
|
||||
/**
|
||||
* When true (the generic card) the part prepends the bold
|
||||
* "Next intake" / "Last intake" labels. The GLP-1 card passes its own
|
||||
* site-aware prose (whose i18n strings already read "Next injection …" /
|
||||
* "Last injection …"), so it opts out and supplies the full line content.
|
||||
* Either way the wrapper, order, colour, spacing, gating and reserved
|
||||
* min-height are identical.
|
||||
* When true the part renders each line as a two-column row: a bold label
|
||||
* pinned left and the value flush right. The generic card uses the default
|
||||
* oral "Next intake" / "Last intake" labels; the GLP-1 card overrides them
|
||||
* with its appointment-phrased labels (see `nextLabel` / `lastLabel`).
|
||||
* When false the line renders as a single full-width run of text.
|
||||
*/
|
||||
labelled?: boolean;
|
||||
/**
|
||||
* Override for the left-column label on the next-line. Defaults to the
|
||||
* oral `medications.nextIntake` string. The GLP-1 card passes its own
|
||||
* "next appointment" wording so the value column carries only the time.
|
||||
*/
|
||||
nextLabel?: React.ReactNode;
|
||||
/** Override for the left-column label on the last-line. */
|
||||
lastLabel?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MedicationNextLastSlot({
|
||||
next,
|
||||
last,
|
||||
labelled = true,
|
||||
nextLabel,
|
||||
lastLabel,
|
||||
}: MedicationNextLastSlotProps) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="min-h-[2.75rem] space-y-3.5 text-sm">
|
||||
{next && (
|
||||
<p className="text-muted-foreground">
|
||||
{labelled && (
|
||||
<>
|
||||
<span className="font-medium">{t("medications.nextIntake")}</span>{" "}
|
||||
</>
|
||||
)}
|
||||
{next}
|
||||
</p>
|
||||
)}
|
||||
{last && (
|
||||
<p className="text-muted-foreground">
|
||||
{labelled && (
|
||||
<>
|
||||
<span className="font-medium">{t("medications.lastIntake")}</span>{" "}
|
||||
</>
|
||||
)}
|
||||
{last}
|
||||
</p>
|
||||
)}
|
||||
{next &&
|
||||
(labelled ? (
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{nextLabel ?? t("medications.nextIntake")}
|
||||
</span>
|
||||
<span className="text-right">{next}</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">{next}</p>
|
||||
))}
|
||||
{last &&
|
||||
(labelled ? (
|
||||
<div className="text-muted-foreground flex items-baseline justify-between gap-3">
|
||||
<span className="min-w-0 flex-shrink truncate font-medium">
|
||||
{lastLabel ?? t("medications.lastIntake")}
|
||||
</span>
|
||||
<span className="text-right">{last}</span>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">{last}</p>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -410,13 +410,14 @@ export function Glp1MedicationCard({
|
||||
|
||||
{/* Injection state — next + last, rendered through the SAME shared
|
||||
<MedicationNextLastSlot> the generic card uses, so the order
|
||||
(next-then-last), colour, spacing, gating and reserved
|
||||
min-height are identical across the two card types. The GLP-1
|
||||
phrasing carries its own "Next / Last injection" wording (and
|
||||
the rotation nudge), so it opts out of the part's bold label
|
||||
prefix and supplies the full line content. */}
|
||||
(next-then-last), colour, spacing, gating, reserved min-height
|
||||
and the label-left / value-right two-column layout are identical
|
||||
across the two card types. The GLP-1 card overrides only the
|
||||
left-column label with its appointment-phrased wording; the value
|
||||
column carries the time (plus the optional rotation nudge). */}
|
||||
<MedicationNextLastSlot
|
||||
labelled={false}
|
||||
nextLabel={t("medications.glp1NextLabel")}
|
||||
lastLabel={t("medications.glp1LastLabel")}
|
||||
next={
|
||||
next && currentWindowStatus.status !== "in_window" ? (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user