diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx
index abcfff6a4..79d9a5f83 100644
--- a/src/app/achievements/page.tsx
+++ b/src/app/achievements/page.tsx
@@ -23,6 +23,7 @@ import {
} from "lucide-react";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
+import { EmptyState } from "@/components/ui/empty-state";
import { useAuth } from "@/hooks/use-auth";
import { useTranslations } from "@/lib/i18n/context";
import { formatDate } from "@/lib/format";
@@ -336,11 +337,14 @@ export default function AchievementsPage() {
{grouped.length === 0 ? (
-
-
- {t("achievements.noneUnlockedYet")}
-
-
+ // v1.4.15 phase-C5: upgrade to the EmptyState primitive so the
+ // surface matches the rest of the app. Trophy icon makes the
+ // intent obvious before the user reads the copy. Reuses the
+ // existing `noneUnlockedYet` string to avoid C4 i18n churn.
+ }
+ title={t("achievements.noneUnlockedYet")}
+ />
) : (
{grouped.map(({ category, items }) => {
diff --git a/src/app/measurements/page.tsx b/src/app/measurements/page.tsx
index de99d744c..9901029ff 100644
--- a/src/app/measurements/page.tsx
+++ b/src/app/measurements/page.tsx
@@ -67,7 +67,7 @@ export default function MeasurementsPage() {
-
+ setDialogOpen(true)} />
);
}
diff --git a/src/app/medications/page.tsx b/src/app/medications/page.tsx
index 517880637..7433ddc58 100644
--- a/src/app/medications/page.tsx
+++ b/src/app/medications/page.tsx
@@ -9,6 +9,7 @@ import { parseScheduleRecurrence } from "@/lib/medication-schedule";
import { MedicationForm } from "@/components/medications/medication-form";
import { MedicationCard } from "@/components/medications/medication-card";
import { Button } from "@/components/ui/button";
+import { EmptyState } from "@/components/ui/empty-state";
import { Switch } from "@/components/ui/switch";
import {
Dialog,
@@ -173,16 +174,21 @@ export default function MedicationsPage() {
) : !medications?.length ? (
-
-
-
-
{t("medications.noMedicationsYet")}
-
-
+ // v1.4.15 phase-C5: refactor the inline icon+text+button block
+ // to the shared EmptyState primitive so the empty path matches
+ // every other list page in the app (role=status, dashed
+ // border, consistent icon-bubble + spacing).
+ }
+ title={t("medications.emptyTitle")}
+ description={t("medications.emptyDescription")}
+ action={
+
+
{t("medications.firstMedication")}
-
-
+ }
+ />
) : (
{/* Active medications */}
diff --git a/src/app/mood/page.tsx b/src/app/mood/page.tsx
index 86bb6ccf8..03cebed31 100644
--- a/src/app/mood/page.tsx
+++ b/src/app/mood/page.tsx
@@ -67,7 +67,7 @@ export default function MoodPage() {
-
+ setDialogOpen(true)} />
);
}
diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx
index 2cba40824..db494cd92 100644
--- a/src/components/measurements/measurement-list.tsx
+++ b/src/components/measurements/measurement-list.tsx
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
+import { EmptyState } from "@/components/ui/empty-state";
import {
Select,
SelectContent,
@@ -45,8 +46,10 @@ import {
TableRow,
} from "@/components/ui/table";
import {
+ Activity,
Loader2,
Pencil,
+ Plus,
Trash2,
ChevronLeft,
ChevronRight,
@@ -77,6 +80,13 @@ interface Measurement {
interface MeasurementListProps {
onEdit?: (m: Measurement) => void;
+ /**
+ * v1.4.15 phase-C5: optional callback wired by the parent page so
+ * the empty-state's "Add your first measurement" CTA opens the same
+ * dialog the header button does. When undefined we hide the button
+ * (fallback to the header CTA) instead of crashing.
+ */
+ onAddFirst?: () => void;
}
const PAGE_SIZE = 25;
@@ -94,7 +104,7 @@ function toDateTimeLocalValue(isoString: string): string {
return local.toISOString().slice(0, 16);
}
-export function MeasurementList({ onEdit }: MeasurementListProps) {
+export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
const { t } = useTranslations();
const fmt = useFormatters();
const { isAuthenticated } = useAuth();
@@ -301,9 +311,40 @@ export function MeasurementList({ onEdit }: MeasurementListProps) {
) : !data?.measurements?.length ? (
-
- {t("measurements.noMeasurements")}
-
+ // v1.4.15 phase-C5: replaces the bare-text empty rectangle.
+ // Filter-aware copy distinguishes brand-new accounts ("no
+ // measurements yet") from filtered-out lists ("no measurements
+ // match this filter") and exposes the right CTA for each
+ // case.
+ }
+ title={
+ typeFilter === "ALL"
+ ? t("measurements.emptyTitle")
+ : t("measurements.emptyFilteredTitle")
+ }
+ description={
+ typeFilter === "ALL"
+ ? t("measurements.emptyDescription")
+ : t("measurements.emptyFilteredDescription")
+ }
+ action={
+ typeFilter !== "ALL" ? (
+ setTypeFilter("ALL")}
+ >
+ {t("measurements.emptyResetFilter")}
+
+ ) : onAddFirst ? (
+
+
+ {t("measurements.emptyAddFirst")}
+
+ ) : undefined
+ }
+ />
) : (
<>
{/* Desktop table */}
diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx
index 7ea435d95..ced9dbde7 100644
--- a/src/components/mood/mood-list.tsx
+++ b/src/components/mood/mood-list.tsx
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useAuth } from "@/hooks/use-auth";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
+import { EmptyState } from "@/components/ui/empty-state";
import {
Select,
SelectContent,
@@ -47,6 +48,8 @@ import {
import {
Loader2,
Pencil,
+ Plus,
+ Smile,
Trash2,
ChevronLeft,
ChevronRight,
@@ -103,7 +106,17 @@ function toDateTimeLocalValue(isoString: string): string {
return local.toISOString().slice(0, 16);
}
-export function MoodList() {
+interface MoodListProps {
+ /**
+ * v1.4.15 phase-C5: optional callback wired by the parent page so
+ * the empty-state's "Log your first mood" CTA opens the same dialog
+ * the header button does. When undefined, the CTA hides instead of
+ * rendering a no-op button.
+ */
+ onAddFirst?: () => void;
+}
+
+export function MoodList({ onAddFirst }: MoodListProps = {}) {
const { t } = useTranslations();
const fmt = useFormatters();
const { isAuthenticated } = useAuth();
@@ -296,9 +309,38 @@ export function MoodList() {
) : !data?.entries?.length ? (
-
- {t("mood.noEntries")}
-
+ // v1.4.15 phase-C5: replace bare-text empty rectangle with
+ // EmptyState. Filter-aware copy splits "no mood entries yet"
+ // (brand-new account) from "no entries match this filter".
+ }
+ title={
+ moodFilter === "ALL"
+ ? t("mood.emptyTitle")
+ : t("mood.emptyFilteredTitle")
+ }
+ description={
+ moodFilter === "ALL"
+ ? t("mood.emptyDescription")
+ : t("mood.emptyFilteredDescription")
+ }
+ action={
+ moodFilter !== "ALL" ? (
+ setMoodFilter("ALL")}
+ >
+ {t("mood.emptyResetFilter")}
+
+ ) : onAddFirst ? (
+
+
+ {t("mood.emptyAddFirst")}
+
+ ) : undefined
+ }
+ />
) : (
<>
{/* Desktop table */}