feat(insights): reorder the mood page to the calendar-first spine

Lift the Stimmungskalender above the line chart and place the better-days
assessment directly under the Ziel card. `MoodInsightsSections` now renders by
region (`heatmap` / `assessment` / `rest`); the page threads them so the spine
reads heading → summary → calendar → chart → target → assessment →
classification + breakdowns. All three regions share the one `moodInsights`
query so the fetch is deduped. Every `data-slot` is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-05 13:46:42 +02:00
1 parent 56934fd658
commit 80c5f14748
2 files changed
+84 -42

No files matched your search

+13 -1
View File
@@ -97,6 +97,14 @@ export default function InsightsStimmungPage() {
explainerMetric="mood"
coachLaunch
>
{/* v1.12.7 — operator spine: heading + summary (the shell above), then
the Stimmungskalender, then the line chart, then the Ziel card, then
the better-days Einschätzung, then the classification + breakdowns.
The heatmap and the assessment are lifted out of `MoodInsightsSections`
into their own regions so they land in this exact order; all three
regions share one `moodInsights` query (TanStack dedups the fetch). */}
<MoodInsightsSections region="heatmap" />
{/* No `<MetricRangeControls>` here: mood is event-driven, not a
MeasurementType series, so the period-over-period range read
(`/api/analytics/range`, keyed on a MeasurementType enum) has
@@ -109,7 +117,11 @@ export default function InsightsStimmungPage() {
<MetricTargetSummary slug="mood" />
<MoodInsightsSections />
{/* The better-days Einschätzung sits directly under the Ziel card,
ahead of the classification tiles and breakdowns. */}
<MoodInsightsSections region="assessment" />
<MoodInsightsSections region="rest" />
{/* v1.12.2 — the assessment is the LAST block on every bespoke
metric-detail page, matching the canonical spine the generic
@@ -167,7 +167,28 @@ function SectionCard({
);
}
export function MoodInsightsSections() {
/**
* v1.12.7 — the mood spine is split into three render regions so the page can
* thread the operator's exact top-to-bottom order without prop-drilling the
* whole aggregate payload:
*
* - "heatmap" — the Stimmungskalender, lifted above the line chart.
* - "assessment" — the better-days Einschätzung, placed right after the
* Ziel card.
* - "rest" — the Einordnung classification tiles, the "what stands
* out" card, and every deep-dive breakdown.
*
* All three regions read the SAME `queryKeys.moodInsights()` query, so
* TanStack Query dedups the fetch (single network read, 60 s staleTime); the
* regions are pure JSX slices of one resolved payload.
*/
export type MoodInsightsRegion = "heatmap" | "assessment" | "rest";
export function MoodInsightsSections({
region = "rest",
}: {
region?: MoodInsightsRegion;
} = {}) {
const { isAuthenticated } = useAuth();
const { t } = useTranslations();
@@ -184,7 +205,12 @@ export function MoodInsightsSections() {
});
if (isLoading) {
return <Skeleton className="h-48 w-full rounded-lg" />;
// Only the "rest" region carries the page-level loading skeleton; the
// heatmap / assessment regions stay invisible while loading so they don't
// stack three skeletons down the page.
return region === "rest" ? (
<Skeleton className="h-48 w-full rounded-lg" />
) : null;
}
if (!data || data.summary.totalEntries === 0) {
@@ -225,13 +251,50 @@ export function MoodInsightsSections() {
const crosstabRows = data.tagMetricCrosstab ?? [];
const hasCrosstab = crosstabRows.length > 0;
// v1.12.7 — the Stimmungskalender is lifted above the line chart on the page,
// so it renders as its own region here.
if (region === "heatmap") {
return (
<SectionCard title={t("insights.mood.heatmapTitle")} icon={CalendarDays}>
<MoodHeatmap cells={heatmapCells} days={data.heatmap.windowDays} stretch />
</SectionCard>
);
}
// v1.12.7 — the better-days Einschätzung is placed right after the Ziel card
// on the page, ahead of the classification tiles, so it renders as its own
// region. It carries the same assessment-card weight the per-metric
// `<InsightStatusCard>` uses on the other subpages (tighter `gap`/`py`, the
// `insight-in` entry, and the `insight-assessment` hook) so it reads as THE
// mood assessment, consistent across the app.
if (region === "assessment") {
if (!hasBetterDays) return null;
return (
<Card
aria-live="polite"
data-slot="insight-assessment"
className="animate-insight-in gap-2 py-4 md:gap-3 md:py-5"
>
<CardHeader className="pb-2">
<TileHeader
icon={Sparkles}
title={t("insights.mood.betterDays.title")}
/>
</CardHeader>
<CardContent>
<MoodBetterDays factors={betterDays} />
</CardContent>
</Card>
);
}
return (
// v1.12.6 — the mood spine is ordered as: Einordnung (the classification
// tiles: in-target share + stability band) FIRST, then the "what goes with
// your better days" assessment read, then the descriptive breakdown
// sections. The classification answers "where do I stand", the better-days
// board answers "what associates with my good days", and the rest is the
// supporting detail.
// v1.12.7 — "rest" region. The Stimmungskalender (now above the chart) and
// the better-days Einschätzung (now right after the Ziel card) render in
// their own regions above; this region carries the Einordnung classification
// tiles (in-target share + stability band) FIRST, then the "what stands
// out" card, then the descriptive breakdown sections. The classification
// answers "where do I stand", and the rest is the supporting detail.
<div className="space-y-4">
{/* Einordnung — the classification tiles. */}
{(hasInTargetTile || hasStabilityTile) && (
@@ -252,45 +315,12 @@ export function MoodInsightsSections() {
</div>
)}
{/* "Was zu deinen besseren Tagen passt" — the better-days assessment,
ranked by effect size. The substance of the Einschätzung sits here,
directly under the classification. v1.12.7 — it now carries the same
assessment-card weight the per-metric `<InsightStatusCard>` uses on
the other subpages (tighter `gap`/`py`, the `insight-in` entry, and
the `insight-assessment` hook) so it reads as THE mood assessment,
consistent across the app. */}
{hasBetterDays && (
<Card
aria-live="polite"
data-slot="insight-assessment"
className="animate-insight-in gap-2 py-4 md:gap-3 md:py-5"
>
<CardHeader className="pb-2">
<TileHeader
icon={Sparkles}
title={t("insights.mood.betterDays.title")}
/>
</CardHeader>
<CardContent>
<MoodBetterDays factors={betterDays} />
</CardContent>
</Card>
)}
{/* v1.12.7 — the single "What stands out" card folds the narrative
one-liners AND the FDR-controlled discovered relations into one tile
(was two separate cards). Self-fetches the discovery surface and
renders nothing when both halves are empty. */}
<MoodWhatStandsOut narratives={narratives} />
<SectionCard title={t("insights.mood.heatmapTitle")} icon={CalendarDays}>
<MoodHeatmap
cells={heatmapCells}
days={data.heatmap.windowDays}
stretch
/>
</SectionCard>
<div className="grid gap-4 lg:grid-cols-2">
<SectionCard
title={t("insights.mood.distributionTitle")}