diff --git a/src/app/achievements/page.tsx b/src/app/achievements/page.tsx
index 17f4bf79a..abcfff6a4 100644
--- a/src/app/achievements/page.tsx
+++ b/src/app/achievements/page.tsx
@@ -167,11 +167,7 @@ function AchievementCard({ achievement, t }: AchievementCardProps) {
{t("achievements.criterionHint", {
- current: formatMetric(
- achievement.format,
- achievement.current,
- t,
- ),
+ current: formatMetric(achievement.format, achievement.current, t),
target: formatMetric(achievement.format, achievement.target, t),
})}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 9e65c9a82..148ef5ee3 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -42,6 +42,7 @@ import { TrendCard } from "@/components/charts/trend-card";
import { TrendHint } from "@/components/charts/trend-hint";
import { summaryToTrend7Delta } from "@/lib/analytics/trend-delta";
import { GettingStartedChecklist } from "@/components/onboarding/getting-started-checklist";
+import { RecentAchievementsCard } from "@/components/gamification/recent-achievements-card";
const HealthChart = dynamic(
() =>
@@ -263,6 +264,11 @@ export default function DashboardPage() {
const showSleepChart = isChartVisible("sleep") && hasSleep;
const showStepsChart = isChartVisible("steps") && hasSteps;
const showMedicationsCard = isChartVisible("medications");
+ // v1.4.15 phase-B4 — recent unlocks dashboard surface. The card itself
+ // self-handles the empty state (CTA → /achievements), so we only need
+ // the layout-toggle gate here. No data-floor check (the empty card is
+ // intentional — Marc wants the user to discover the feature).
+ const showAchievementsCard = isChartVisible("achievements");
// Glucose widget — visible iff layout enables it AND at least one reading exists.
// Glucose has no separate chart slot today, so the tile flag is the
@@ -915,6 +921,17 @@ export default function DashboardPage() {
node:
,
});
}
+ if (showAchievementsCard) {
+ // v1.4.15 phase-B4 — slotted at the user's chosen position via
+ // the layout `order`. Default order from
+ // `DEFAULT_DASHBOARD_LAYOUT` puts it last (below the chart row)
+ // which matches Marc's brief "below the chart row".
+ charts.push({
+ id: "achievements",
+ order: widgetOrder("achievements"),
+ node:
,
+ });
+ }
charts.sort((a, b) => a.order - b.order);
diff --git a/src/components/settings/dashboard-layout-section.tsx b/src/components/settings/dashboard-layout-section.tsx
index afd7e4e27..18cb37799 100644
--- a/src/components/settings/dashboard-layout-section.tsx
+++ b/src/components/settings/dashboard-layout-section.tsx
@@ -33,6 +33,7 @@ const WIDGET_LABEL_KEYS: Record
= {
boneMass: "measurements.typeBoneMass",
bpInTarget: "dashboard.bpInTarget",
oxygenSaturation: "measurements.typeOxygenSaturation",
+ achievements: "achievements.title",
};
export function DashboardLayoutSection({ id }: { id: string }) {
@@ -172,8 +173,12 @@ export function DashboardLayoutSection({ id }: { id: string }) {
- {t("dashboard.layoutTileColumn")}
- {t("dashboard.layoutChartColumn")}
+
+ {t("dashboard.layoutTileColumn")}
+
+
+ {t("dashboard.layoutChartColumn")}
+
{[...layout.widgets]
.sort((a, b) => a.order - b.order)
@@ -215,7 +220,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
+
toggleTile(widget.id, v)}
@@ -224,7 +229,7 @@ export function DashboardLayoutSection({ id }: { id: string }) {
data-slot="widget-tile-switch"
/>
-
+
toggle(widget.id, v)}
diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts
index c409e6ed1..0b18c2030 100644
--- a/src/lib/dashboard-layout.ts
+++ b/src/lib/dashboard-layout.ts
@@ -18,7 +18,8 @@ export type DashboardWidgetId =
| "totalBodyWater"
| "boneMass"
| "bpInTarget"
- | "oxygenSaturation";
+ | "oxygenSaturation"
+ | "achievements";
export interface DashboardWidgetConfig {
id: DashboardWidgetId;
@@ -70,6 +71,13 @@ export const DEFAULT_DASHBOARD_LAYOUT: DashboardLayout = {
{ id: "totalBodyWater", visible: false, tileVisible: false, order: 10 },
{ id: "boneMass", visible: false, tileVisible: false, order: 11 },
{ id: "oxygenSaturation", visible: false, tileVisible: false, order: 12 },
+ // v1.4.15 phase-B4 — recent-unlocks card (dashboard surface for the
+ // gamification feature). Visible by default — the card is small,
+ // self-gates on having any achievements (otherwise it shows a brief
+ // "no achievements yet" + link), and Marc-asked-for-it explicitly.
+ // `tileVisible` is forced false because there is no tile surface
+ // for this widget; only the chart-row card.
+ { id: "achievements", visible: true, tileVisible: false, order: 13 },
],
};