mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(medications): restore equal-height cards and aligned dose rows
The v1.8.6 compliance rework left the last/next-dose and compliance rows in a plain vertical stack with independently conditional rows. A card missing its last-dose line (never taken) or its streak flame (zero streak) rendered shorter, so the dose rows and action buttons drifted out of alignment across the two-column grid. Pin the layout on both card variants identically: the Card fills the stretched grid cell with h-full, the CardContent becomes a flex column, the next/last-dose pair sits in a reserved min-height slot so a missing line keeps its footprint, and the intake-actions block is pushed to the bottom with mt-auto for a shared action baseline. Generic and GLP-1 cards now align row-for-row regardless of which optional rows render. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit a7e541eeb5123abb63514f56d12b93b0023ce417)
This commit is contained in:
1 parent
5f8e191e2b
commit
4a0ad1fc64
2 files changed
+99
-76
No files matched your search
@@ -5,6 +5,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
import { MedicationCardMenu } from "@/components/medications/medication-card-menu";
|
||||
import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges";
|
||||
@@ -394,7 +395,7 @@ export function Glp1MedicationCard({
|
||||
const categoryLabel = getMedicationCategoryLabel(medication.category, t);
|
||||
|
||||
return (
|
||||
<Card className={medication.active ? "" : "opacity-60"}>
|
||||
<Card className={cn("h-full", medication.active ? "" : "opacity-60")}>
|
||||
<MedicationCardHeader
|
||||
name={medication.name}
|
||||
dose={medication.dose}
|
||||
@@ -405,7 +406,7 @@ export function Glp1MedicationCard({
|
||||
linkLabel={t("medications.openDetailPage")}
|
||||
/>
|
||||
|
||||
<CardContent className="space-y-3.5">
|
||||
<CardContent className="flex flex-1 flex-col space-y-3.5">
|
||||
{/* Take-now / overdue / very-overdue pill, shared with the
|
||||
generic medication card. The GLP-1 card historically
|
||||
omitted this row, which made Mounjaro feel different from
|
||||
@@ -419,8 +420,12 @@ export function Glp1MedicationCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Injection state — last + next */}
|
||||
<div className="space-y-1 text-sm">
|
||||
{/* Injection state — last + next. The reserved min-height keeps a
|
||||
card whose last-injection line is absent the same vertical
|
||||
footprint as one that renders both lines, so the compliance bars
|
||||
and action row below align row-for-row across the grid — matching
|
||||
the generic medication card's reserved last/next slot. */}
|
||||
<div className="min-h-[2.75rem] space-y-1 text-sm">
|
||||
{medication.lastTakenAt && (
|
||||
<p className="text-muted-foreground">
|
||||
{lastSite
|
||||
@@ -493,12 +498,17 @@ export function Glp1MedicationCard({
|
||||
{/* Primary actions row — shared with the generic medication
|
||||
card. The GLP-1-specific side-effect quick-log lives in the
|
||||
header-actions overflow (kebab), not this row, so Mounjaro
|
||||
and Ramipril share the canonical two-button primary row. */}
|
||||
and Ramipril share the canonical two-button primary row.
|
||||
Pinned to the bottom with `mt-auto` so the action baseline is
|
||||
shared across every card in the grid row, identical to the
|
||||
generic medication card. */}
|
||||
{medication.active && (
|
||||
<MedicationIntakeActions
|
||||
intakeLoading={intakeLoading}
|
||||
onRecordIntake={recordIntake}
|
||||
/>
|
||||
<div className="mt-auto pt-3.5">
|
||||
<MedicationIntakeActions
|
||||
intakeLoading={intakeLoading}
|
||||
onRecordIntake={recordIntake}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useReducer } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
|
||||
import { MedicationCardMenu } from "@/components/medications/medication-card-menu";
|
||||
import { MedicationStateBadges } from "@/components/medications/card-parts/medication-state-badges";
|
||||
@@ -299,7 +300,7 @@ export function MedicationCard({
|
||||
);
|
||||
|
||||
return (
|
||||
<Card className={medication.active ? "" : "opacity-60"}>
|
||||
<Card className={cn("h-full", medication.active ? "" : "opacity-60")}>
|
||||
<MedicationCardHeader
|
||||
name={medication.name}
|
||||
dose={medication.dose}
|
||||
@@ -310,7 +311,7 @@ export function MedicationCard({
|
||||
linkLabel={t("medications.openDetailPage")}
|
||||
/>
|
||||
|
||||
<CardContent className="space-y-3.5">
|
||||
<CardContent className="flex flex-1 flex-col space-y-3.5">
|
||||
{/* Status, last & next intake info */}
|
||||
{currentWindowStatus.status && (
|
||||
<MedicationStatusPill
|
||||
@@ -320,72 +321,79 @@ export function MedicationCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{nextSchedule &&
|
||||
currentWindowStatus.status !== "in_window" &&
|
||||
(() => {
|
||||
const s = nextSchedule;
|
||||
{/* Next / last intake — wrapped in a fixed min-height slot so a
|
||||
card missing its last-dose line keeps the same vertical
|
||||
footprint as a sibling that renders both lines. Without the
|
||||
reserved height the compliance bars and action row below would
|
||||
start at a different Y card-to-card across the 2-col grid. */}
|
||||
<div className="min-h-[2.75rem] space-y-3.5">
|
||||
{nextSchedule &&
|
||||
currentWindowStatus.status !== "in_window" &&
|
||||
(() => {
|
||||
const s = nextSchedule;
|
||||
|
||||
// Format day label relative to today
|
||||
let dayLabel = "";
|
||||
if (nextAt) {
|
||||
const nextDate = toBerlinDate(new Date(nextAt));
|
||||
const todayStr = `${nowBerlin.getFullYear()}-${nowBerlin.getMonth()}-${nowBerlin.getDate()}`;
|
||||
const nextStr = `${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`;
|
||||
const tomorrow = new Date(nowBerlin);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = `${tomorrow.getFullYear()}-${tomorrow.getMonth()}-${tomorrow.getDate()}`;
|
||||
// Format day label relative to today
|
||||
let dayLabel = "";
|
||||
if (nextAt) {
|
||||
const nextDate = toBerlinDate(new Date(nextAt));
|
||||
const todayStr = `${nowBerlin.getFullYear()}-${nowBerlin.getMonth()}-${nowBerlin.getDate()}`;
|
||||
const nextStr = `${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`;
|
||||
const tomorrow = new Date(nowBerlin);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = `${tomorrow.getFullYear()}-${tomorrow.getMonth()}-${tomorrow.getDate()}`;
|
||||
|
||||
const diffDays = Math.round(
|
||||
(nextDate.getTime() - nowBerlin.getTime()) /
|
||||
(24 * 60 * 60 * 1000),
|
||||
);
|
||||
const diffDays = Math.round(
|
||||
(nextDate.getTime() - nowBerlin.getTime()) /
|
||||
(24 * 60 * 60 * 1000),
|
||||
);
|
||||
|
||||
if (nextStr === todayStr) {
|
||||
dayLabel = t("medications.today");
|
||||
} else if (nextStr === tomorrowStr) {
|
||||
dayLabel = t("medications.tomorrow");
|
||||
} else if (diffDays <= 5) {
|
||||
const weekdayLabels = [
|
||||
t("medications.weekdaySunday"),
|
||||
t("medications.weekdayMonday"),
|
||||
t("medications.weekdayTuesday"),
|
||||
t("medications.weekdayWednesday"),
|
||||
t("medications.weekdayThursday"),
|
||||
t("medications.weekdayFriday"),
|
||||
t("medications.weekdaySaturday"),
|
||||
];
|
||||
dayLabel = weekdayLabels[nextDate.getDay()];
|
||||
} else {
|
||||
dayLabel = fmt.dateWithWeekday(nextDate);
|
||||
if (nextStr === todayStr) {
|
||||
dayLabel = t("medications.today");
|
||||
} else if (nextStr === tomorrowStr) {
|
||||
dayLabel = t("medications.tomorrow");
|
||||
} else if (diffDays <= 5) {
|
||||
const weekdayLabels = [
|
||||
t("medications.weekdaySunday"),
|
||||
t("medications.weekdayMonday"),
|
||||
t("medications.weekdayTuesday"),
|
||||
t("medications.weekdayWednesday"),
|
||||
t("medications.weekdayThursday"),
|
||||
t("medications.weekdayFriday"),
|
||||
t("medications.weekdaySaturday"),
|
||||
];
|
||||
dayLabel = weekdayLabels[nextDate.getDay()];
|
||||
} else {
|
||||
dayLabel = fmt.dateWithWeekday(nextDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">
|
||||
{t("medications.nextIntake")}
|
||||
</span>{" "}
|
||||
{dayLabel && `${dayLabel}, `}
|
||||
{formatTimeWindowRange(s.windowStart, s.windowEnd, locale)}
|
||||
{s.label && (
|
||||
<span className="hidden sm:inline"> ({s.label})</span>
|
||||
)}
|
||||
{s.dose && (
|
||||
<span className="hidden font-medium text-purple-400 sm:inline">
|
||||
{" "}
|
||||
— {s.dose}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">
|
||||
{t("medications.nextIntake")}
|
||||
</span>{" "}
|
||||
{dayLabel && `${dayLabel}, `}
|
||||
{formatTimeWindowRange(s.windowStart, s.windowEnd, locale)}
|
||||
{s.label && (
|
||||
<span className="hidden sm:inline"> ({s.label})</span>
|
||||
)}
|
||||
{s.dose && (
|
||||
<span className="hidden font-medium text-purple-400 sm:inline">
|
||||
{" "}
|
||||
— {s.dose}
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
})()}
|
||||
|
||||
{medication.lastTakenAt && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">{t("medications.lastIntake")}</span>{" "}
|
||||
{formatLastTakenAt(medication.lastTakenAt)}
|
||||
</p>
|
||||
)}
|
||||
{medication.lastTakenAt && (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<span className="font-medium">{t("medications.lastIntake")}</span>{" "}
|
||||
{formatLastTakenAt(medication.lastTakenAt)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Compliance bars — always two rows. The server scales the two
|
||||
windows to the dosing cadence (7 / 30 days for dense meds,
|
||||
@@ -401,12 +409,17 @@ export function MedicationCard({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Quick actions — primary buttons of the medication card. */}
|
||||
{/* Quick actions — primary buttons of the medication card.
|
||||
Pinned to the bottom with `mt-auto` so the action baseline is
|
||||
shared across every card in the grid row regardless of how many
|
||||
optional rows above it rendered. */}
|
||||
{medication.active && (
|
||||
<MedicationIntakeActions
|
||||
intakeLoading={intakeLoading}
|
||||
onRecordIntake={recordIntake}
|
||||
/>
|
||||
<div className="mt-auto pt-3.5">
|
||||
<MedicationIntakeActions
|
||||
intakeLoading={intakeLoading}
|
||||
onRecordIntake={recordIntake}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
|
||||
Reference in new issue
Block a user