diff --git a/apps/manacore/apps/landing/src/content/config.ts b/apps/manacore/apps/landing/src/content/config.ts index 174094d91..f9c24b494 100644 --- a/apps/manacore/apps/landing/src/content/config.ts +++ b/apps/manacore/apps/landing/src/content/config.ts @@ -203,6 +203,15 @@ const manascoreCollection = defineCollection({ security: z.number().min(0).max(100), ux: z.number().min(0).max(100), }), + // Score history for trend visualization + history: z + .array( + z.object({ + date: z.string(), // ISO date string e.g. "2026-03-19" + score: z.number().min(0).max(100), + }) + ) + .optional(), // Readiness level status: z.enum(['prototype', 'alpha', 'beta', 'production', 'mature']), // Stats diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-calendar.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-calendar.md index 6dd45aa66..51c017d18 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-calendar.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-calendar.md @@ -6,6 +6,9 @@ app: 'calendar' author: 'Till Schneider' tags: ['audit', 'calendar', 'production-readiness'] score: 97 +history: + - { date: '2026-03-19', score: 82 } + - { date: '2026-03-24', score: 97 } scores: backend: 95 frontend: 96 diff --git a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-todo.md b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-todo.md index 72b19af93..af1d13587 100644 --- a/apps/manacore/apps/landing/src/content/manascore/2026-03-19-todo.md +++ b/apps/manacore/apps/landing/src/content/manascore/2026-03-19-todo.md @@ -6,6 +6,9 @@ app: 'todo' author: 'Till Schneider' tags: ['audit', 'todo', 'production-readiness'] score: 96 +history: + - { date: '2026-03-19', score: 80 } + - { date: '2026-03-24', score: 96 } scores: backend: 94 frontend: 95 diff --git a/apps/manacore/apps/landing/src/pages/manascore/[slug].astro b/apps/manacore/apps/landing/src/pages/manascore/[slug].astro index a5b0264db..3bb58aedd 100644 --- a/apps/manacore/apps/landing/src/pages/manascore/[slug].astro +++ b/apps/manacore/apps/landing/src/pages/manascore/[slug].astro @@ -101,23 +101,100 @@ function getBarColor(score: number): string {

{audit.data.description}

- {/* Overall Score */} -
-
-
-

Gesamtscore

-

Gewichteter Durchschnitt aus 8 Kategorien

-
-
- - {audit.data.score} - - /100 -
-
-
+ {/* Overall Score + Trend */} + { + (() => { + const history = audit.data.history || [ + { date: audit.data.date.toISOString(), score: audit.data.score }, + ]; + const hasTrend = history.length > 1; + const trendDelta = hasTrend ? history[history.length - 1].score - history[0].score : 0; + const minScore = Math.min(...history.map((h: { score: number }) => h.score)) - 5; + const maxScore = Math.max(...history.map((h: { score: number }) => h.score)) + 5; + const range = maxScore - minScore || 1; + const chartWidth = 200; + const chartHeight = 60; + const points = history.map((h: { score: number }, i: number) => { + const x = + history.length > 1 ? (i / (history.length - 1)) * chartWidth : chartWidth / 2; + const y = chartHeight - ((h.score - minScore) / range) * (chartHeight - 4) - 2; + return { x, y, score: h.score, date: h.date }; + }); + const polyline = points.map((p: { x: number; y: number }) => `${p.x},${p.y}`).join(' '); + const areaPath = `M${points[0].x},${chartHeight} ${points.map((p: { x: number; y: number }) => `L${p.x},${p.y}`).join(' ')} L${points[points.length - 1].x},${chartHeight} Z`; + + return ( +
+
+
+

Gesamtscore

+

+ Gewichteter Durchschnitt aus 8 Kategorien +

+ {hasTrend && ( +

= 0 ? 'text-green-500' : 'text-red-500'}`} + > + {trendDelta > 0 ? '↑' : '↓'} {Math.abs(trendDelta)} Punkte seit erstem + Assessment +

+ )} +
+
+ {hasTrend && ( + + = 0 ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)'} + /> + = 0 ? '#22c55e' : '#ef4444'} + stroke-width="2" + stroke-linecap="round" + stroke-linejoin="round" + /> + {points.map( + (p: { x: number; y: number; score: number; date: string }, i: number) => ( + + = 0 ? '#22c55e' : '#ef4444'} + /> + + {p.score} + + + ) + )} + + )} +
+ + {audit.data.score} + + /100 +
+
+
+
+ ); + })() + } {/* Category Scores */}
a.data.status))]; const status = statusColors[data.status] || statusColors.alpha; const scoreColor = getScoreColor(data.score); + // Build sparkline SVG path from history + const history = data.history || [{ date: data.date.toISOString(), score: data.score }]; + const sparkWidth = 48; + const sparkHeight = 20; + const minScore = Math.min(...history.map((h: { score: number }) => h.score)); + const maxScore = Math.max(...history.map((h: { score: number }) => h.score)); + const range = maxScore - minScore || 1; + const sparkPoints = history + .map((h: { score: number }, i: number) => { + const x = + history.length > 1 ? (i / (history.length - 1)) * sparkWidth : sparkWidth / 2; + const y = sparkHeight - ((h.score - minScore) / range) * (sparkHeight - 2) - 1; + return `${x},${y}`; + }) + .join(' '); + const hasTrend = history.length > 1; + const trendDelta = hasTrend ? history[history.length - 1].score - history[0].score : 0; + return ( a.data.status))]; )}
- {/* Score */} -
+ {/* Score + Trend */} +
{data.score}
- /100 + {hasTrend ? ( +
+ + = 0 ? '#22c55e' : '#ef4444'} + stroke-width="1.5" + stroke-linecap="round" + stroke-linejoin="round" + /> + {history.map((h: { score: number }, i: number) => { + const x = + history.length > 1 + ? (i / (history.length - 1)) * sparkWidth + : sparkWidth / 2; + const y = + sparkHeight - ((h.score - minScore) / range) * (sparkHeight - 2) - 1; + return ( + = 0 ? '#22c55e' : '#ef4444'} + /> + ); + })} + + = 0 ? 'text-green-500' : 'text-red-500'}`} + > + {trendDelta > 0 ? '+' : ''} + {trendDelta} + +
+ ) : ( + /100 + )}