mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
feat(lists): empty states for measurements, mood, medications, achievements (impl)
Follow-up to9a74f8e— that commit's pathspec didn't pick up the implementation files, only the new test. This commit lands the actual EmptyState wiring described in the previous message: - src/components/measurements/measurement-list.tsx — Activity icon, filter-aware empty state, optional onAddFirst CTA prop. - src/components/mood/mood-list.tsx — Smile icon, mirroring filter semantics + optional onAddFirst. - src/app/measurements/page.tsx + src/app/mood/page.tsx — pass onAddFirst so the empty-state CTA opens the same dialog as the header button. - src/app/medications/page.tsx — refactor inline icon+text+button block to the EmptyState primitive (Pill icon). - src/app/achievements/page.tsx — Trophy icon for the no-achievements state, reusing the existing noneUnlockedYet string. Together with9a74f8ethe four feature lists now share a consistent empty-state surface with the rest of the app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9a74f8e8f2
commit
1d65f3b925
6 files changed
+116
-23
No files matched your search
@@ -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) {
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : !data?.measurements?.length ? (
|
||||
<div className="text-muted-foreground flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||
{t("measurements.noMeasurements")}
|
||||
</div>
|
||||
// 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.
|
||||
<EmptyState
|
||||
icon={<Activity className="size-6" />}
|
||||
title={
|
||||
typeFilter === "ALL"
|
||||
? t("measurements.emptyTitle")
|
||||
: t("measurements.emptyFilteredTitle")
|
||||
}
|
||||
description={
|
||||
typeFilter === "ALL"
|
||||
? t("measurements.emptyDescription")
|
||||
: t("measurements.emptyFilteredDescription")
|
||||
}
|
||||
action={
|
||||
typeFilter !== "ALL" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setTypeFilter("ALL")}
|
||||
>
|
||||
{t("measurements.emptyResetFilter")}
|
||||
</Button>
|
||||
) : onAddFirst ? (
|
||||
<Button size="sm" onClick={onAddFirst}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{t("measurements.emptyAddFirst")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Desktop table */}
|
||||
|
||||
@@ -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() {
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin" />
|
||||
</div>
|
||||
) : !data?.entries?.length ? (
|
||||
<div className="text-muted-foreground flex h-32 items-center justify-center rounded-lg border border-dashed">
|
||||
{t("mood.noEntries")}
|
||||
</div>
|
||||
// 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".
|
||||
<EmptyState
|
||||
icon={<Smile className="size-6" />}
|
||||
title={
|
||||
moodFilter === "ALL"
|
||||
? t("mood.emptyTitle")
|
||||
: t("mood.emptyFilteredTitle")
|
||||
}
|
||||
description={
|
||||
moodFilter === "ALL"
|
||||
? t("mood.emptyDescription")
|
||||
: t("mood.emptyFilteredDescription")
|
||||
}
|
||||
action={
|
||||
moodFilter !== "ALL" ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMoodFilter("ALL")}
|
||||
>
|
||||
{t("mood.emptyResetFilter")}
|
||||
</Button>
|
||||
) : onAddFirst ? (
|
||||
<Button size="sm" onClick={onAddFirst}>
|
||||
<Plus className="mr-1 h-4 w-4" />
|
||||
{t("mood.emptyAddFirst")}
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{/* Desktop table */}
|
||||
|
||||
Reference in new issue
Block a user