Files
SkinbaseNova/resources/js/components/profile/CreatorJourneySection.jsx
2026-04-18 17:02:56 +02:00

580 lines
29 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
function formatDate(value) {
if (!value) return null
try {
return new Intl.DateTimeFormat('en', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(value))
} catch {
return null
}
}
function formatYear(value) {
if (!value) return null
try {
return new Intl.DateTimeFormat('en', { year: 'numeric' }).format(new Date(value))
} catch {
return null
}
}
function iconForType(type) {
switch (type) {
case 'first_upload': return 'fa-solid fa-seedling'
case 'first_featured_artwork': return 'fa-solid fa-star'
case 'first_group_release': return 'fa-solid fa-people-group'
case 'biggest_download_spike': return 'fa-solid fa-bolt'
case 'best_performing_work': return 'fa-solid fa-trophy'
case 'most_productive_year': return 'fa-solid fa-calendar-check'
case 'yearly_recap': return 'fa-solid fa-chart-column'
case 'comeback_minor': return 'fa-solid fa-rotate-right'
case 'comeback_major': return 'fa-solid fa-person-walking-arrow-right'
case 'comeback_legendary': return 'fa-solid fa-fire-flame-curved'
case 'upload_streak_3':
case 'upload_streak_6':
case 'upload_streak_12': return 'fa-solid fa-fire'
case 'active_year_streak_3':
case 'active_year_streak_5': return 'fa-solid fa-calendar-days'
case 'before_now': return 'fa-solid fa-arrows-rotate'
case 'era_started': return 'fa-solid fa-flag'
default: return 'fa-solid fa-sparkles'
}
}
function colorForType(type) {
switch (type) {
case 'first_featured_artwork': return { icon: 'text-amber-200', bg: 'bg-amber-400/12', border: 'border-amber-300/20', accent: 'from-amber-400/60' }
case 'best_performing_work': return { icon: 'text-amber-200', bg: 'bg-amber-400/12', border: 'border-amber-300/20', accent: 'from-amber-400/60' }
case 'biggest_download_spike': return { icon: 'text-sky-200', bg: 'bg-sky-400/12', border: 'border-sky-300/20', accent: 'from-sky-400/60' }
case 'first_upload': return { icon: 'text-emerald-200', bg: 'bg-emerald-400/12', border: 'border-emerald-300/20', accent: 'from-emerald-400/60' }
case 'first_group_release': return { icon: 'text-violet-200', bg: 'bg-violet-400/12', border: 'border-violet-300/20', accent: 'from-violet-400/60' }
case 'comeback_minor':
case 'comeback_major':
case 'comeback_legendary': return { icon: 'text-orange-200', bg: 'bg-orange-400/12', border: 'border-orange-300/20', accent: 'from-orange-400/60' }
case 'most_productive_year': return { icon: 'text-teal-200', bg: 'bg-teal-400/12', border: 'border-teal-300/20', accent: 'from-teal-400/60' }
default: return { icon: 'text-sky-200', bg: 'bg-sky-400/12', border: 'border-sky-300/20', accent: 'from-sky-400/60' }
}
}
function milestoneHref(item) {
return item?.artwork?.url || item?.release?.url || null
}
function StatPill({ label, value }) {
if (value === null || value === undefined || value === '') return null
return (
<div className="rounded-2xl border border-white/10 bg-white/[0.04] px-3.5 py-2.5">
<div className="text-[10px] font-semibold uppercase tracking-[0.24em] text-sky-200/60">{label}</div>
<div className="mt-1 text-sm font-semibold text-white">{value}</div>
</div>
)
}
function EmptyJourneyState({ username, memberSinceYear, yearsOnSkinbase }) {
return (
<div className="rounded-[28px] border border-dashed border-white/12 bg-white/[0.03] px-6 py-12">
<div className="flex flex-wrap items-center gap-3">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.05] text-slate-200">
<i className="fa-solid fa-route text-lg" />
</div>
<div>
<div className="text-lg font-semibold text-white">Creator Journey is just getting started</div>
<div className="mt-1 text-sm text-slate-400">
Public milestones will appear here as @{username} builds more history on Skinbase.
</div>
</div>
</div>
<div className="mt-5 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<StatPill label="Member since" value={memberSinceYear || 'Unknown'} />
<StatPill label="Years on Skinbase" value={yearsOnSkinbase ?? 0} />
<StatPill label="Milestones saved" value="0" />
</div>
</div>
)
}
// ── v2: Era strip ─────────────────────────────────────────────────────────────
const ERA_COLORS = {
early_years: { bg: 'bg-slate-800/60', border: 'border-slate-600/40', text: 'text-slate-300', icon: 'fa-solid fa-seedling', dot: 'bg-slate-400' },
breakthrough: { bg: 'bg-amber-900/30', border: 'border-amber-600/30', text: 'text-amber-200', icon: 'fa-solid fa-star', dot: 'bg-amber-400' },
experimental: { bg: 'bg-violet-900/30', border: 'border-violet-600/30', text: 'text-violet-200', icon: 'fa-solid fa-flask', dot: 'bg-violet-400' },
comeback: { bg: 'bg-emerald-900/30', border: 'border-emerald-600/30', text: 'text-emerald-200', icon: 'fa-solid fa-rotate-right', dot: 'bg-emerald-400' },
current: { bg: 'bg-sky-900/30', border: 'border-sky-600/30', text: 'text-sky-200', icon: 'fa-solid fa-bolt', dot: 'bg-sky-400' },
}
function EraStrip({ eras }) {
if (!eras?.length) return null
return (
<div className="mt-7">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400 mb-3">Creator Eras</div>
<div className="flex flex-wrap gap-3">
{eras.map((era, i) => {
const style = ERA_COLORS[era.type] ?? ERA_COLORS.current
return (
<div
key={i}
className={`flex items-start gap-3 rounded-2xl border ${style.border} ${style.bg} px-4 py-3 min-w-[180px] max-w-xs flex-1`}
>
<div className={`mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-xl bg-white/5 ${style.text}`}>
<i className={style.icon} />
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className={`text-sm font-semibold ${style.text}`}>{era.title}</span>
{era.is_current && (
<span className="rounded-full bg-white/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-slate-300">
Now
</span>
)}
</div>
<div className="mt-0.5 text-[11px] text-slate-500">
{formatYear(era.starts_at)}
{era.ends_at ? ` ${formatYear(era.ends_at)}` : era.is_current ? ' present' : ''}
</div>
{era.description && (
<p className="mt-1.5 text-[11px] leading-relaxed text-slate-400 line-clamp-2">{era.description}</p>
)}
{(era.stats?.uploads_count ?? 0) > 0 && (
<div className="mt-2 text-[10px] text-slate-500">
{era.stats.uploads_count} upload{era.stats.uploads_count !== 1 ? 's' : ''}
</div>
)}
</div>
</div>
)
})}
</div>
</div>
)
}
// ── v2: Streaks ──────────────────────────────────────────────────────────────
function StreakBadge({ label, value, active = false }) {
if (!value) return null
return (
<div className={`flex items-center gap-3 rounded-2xl border px-4 py-3 ${active ? 'border-orange-500/30 bg-orange-900/20' : 'border-white/10 bg-white/[0.03]'}`}>
<div className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-xl ${active ? 'bg-orange-500/15 text-orange-300' : 'bg-white/5 text-slate-400'}`}>
<i className={`fa-solid fa-fire${active ? '' : ''}`} />
</div>
<div>
<div className={`text-base font-bold tabular-nums ${active ? 'text-orange-200' : 'text-white'}`}>{value}</div>
<div className="text-[11px] text-slate-500">{label}</div>
</div>
</div>
)
}
function StreaksSection({ streaks }) {
if (!streaks) return null
const { current_monthly_upload_streak, best_monthly_upload_streak, current_active_year_streak, best_active_year_streak } = streaks
const hasAny = current_monthly_upload_streak > 0 || best_monthly_upload_streak > 0 || best_active_year_streak > 0
if (!hasAny) return null
return (
<div className="mt-7">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400 mb-3">Creative Streaks</div>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{current_monthly_upload_streak > 0 && (
<StreakBadge label="Current monthly streak" value={`${current_monthly_upload_streak}mo`} active />
)}
{best_monthly_upload_streak > 0 && (
<StreakBadge label="Best monthly streak" value={`${best_monthly_upload_streak}mo`} />
)}
{current_active_year_streak > 0 && (
<StreakBadge label="Active year streak" value={`${current_active_year_streak}yr`} active={current_active_year_streak >= 3} />
)}
{best_active_year_streak > 0 && (
<StreakBadge label="Best year streak" value={`${best_active_year_streak}yr`} />
)}
</div>
</div>
)
}
// ── Yearly Productivity Chart ────────────────────────────────────────────────
const STATUS_BAR_COLOR = {
breakout: { bar: 'bg-emerald-400', label: 'bg-emerald-400/12 text-emerald-200 border-emerald-400/20' },
steady: { bar: 'bg-sky-400', label: 'bg-sky-400/12 text-sky-200 border-sky-400/20' },
experimental: { bar: 'bg-violet-400', label: 'bg-violet-400/12 text-violet-200 border-violet-400/20' },
comeback: { bar: 'bg-amber-400', label: 'bg-amber-400/12 text-amber-200 border-amber-400/20' },
quiet: { bar: 'bg-slate-500', label: 'bg-slate-700/60 text-slate-400 border-slate-600/30' },
}
function YearlyProductivityChart({ recaps }) {
if (!recaps?.length) return null
// Sort oldest → newest for the chart
const sorted = [...recaps]
.filter((r) => r.metrics?.year && r.metrics?.uploads_count != null)
.sort((a, b) => (a.metrics.year ?? 0) - (b.metrics.year ?? 0))
if (!sorted.length) return null
const maxUploads = Math.max(...sorted.map((r) => r.metrics.uploads_count), 1)
return (
<div className="mt-7 rounded-[28px] border border-white/10 bg-white/[0.03] p-5 sm:p-6">
<div className="flex flex-wrap items-end justify-between gap-3">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400">Productivity</div>
<div className="mt-1 text-lg font-semibold text-white">Year-by-year upload activity</div>
</div>
<div className="flex flex-wrap items-center gap-3 text-[11px] text-slate-500">
{Object.entries(STATUS_BAR_COLOR)
.filter(([key]) => sorted.some((r) => (r.metrics?.year_status ?? 'steady') === key))
.map(([key, val]) => (
<span key={key} className="flex items-center gap-1.5 capitalize">
<span className={`inline-block h-2 w-2 rounded-full ${val.bar}`} />
{key}
</span>
))}
</div>
</div>
<div className="mt-5 space-y-2">
{sorted.map((item) => {
const uploads = item.metrics.uploads_count
const pct = Math.max(uploads / maxUploads, uploads > 0 ? 0.018 : 0)
const status = item.metrics?.year_status ?? 'steady'
const colors = STATUS_BAR_COLOR[status] ?? STATUS_BAR_COLOR.steady
const isBest = uploads === maxUploads
return (
<div key={item.metrics.year} className="group grid grid-cols-[3.5rem_minmax(0,1fr)_4rem] items-center gap-3">
{/* Year label */}
<div className={`text-right text-[13px] font-semibold tabular-nums ${isBest ? 'text-white' : 'text-slate-400'}`}>
{item.metrics.year}
</div>
{/* Bar */}
<div className="relative h-7 overflow-hidden rounded-full bg-white/[0.04]">
<div
className={`absolute inset-y-0 left-0 rounded-full ${colors.bar} opacity-80 transition-all duration-500 group-hover:opacity-100`}
style={{ width: `${(pct * 100).toFixed(1)}%` }}
/>
{/* Tooltip on hover */}
<div className="absolute inset-y-0 left-0 flex w-full items-center px-3 opacity-0 transition-opacity group-hover:opacity-100">
<span className="text-[11px] font-semibold text-white drop-shadow-sm">
{uploads} upload{uploads !== 1 ? 's' : ''}
{(item.metrics?.views ?? 0) > 0 ? ` · ${Number(item.metrics.views).toLocaleString()} views` : ''}
{(item.metrics?.downloads ?? 0) > 0 ? ` · ${Number(item.metrics.downloads).toLocaleString()} dl` : ''}
</span>
</div>
</div>
{/* Upload count + badge */}
<div className="flex items-center gap-1.5">
<span className={`text-[13px] font-bold tabular-nums ${isBest ? 'text-white' : 'text-slate-300'}`}>{uploads}</span>
{isBest && <span className="rounded-full border border-amber-400/25 bg-amber-400/10 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider text-amber-300">best</span>}
{(item.metrics?.featured_count ?? 0) > 0 && !isBest && (
<span className="rounded-full border border-sky-400/20 bg-sky-400/10 px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wider text-sky-300">
<i className="fa-solid fa-star text-[8px]" /> {item.metrics.featured_count}
</span>
)}
</div>
</div>
)
})}
</div>
{/* Summary footer */}
<div className="mt-5 flex flex-wrap gap-4 border-t border-white/5 pt-4 text-[12px] text-slate-400">
<span><span className="font-semibold text-white">{sorted.length}</span> active years</span>
<span><span className="font-semibold text-white">{sorted.reduce((s, r) => s + r.metrics.uploads_count, 0).toLocaleString()}</span> total uploads</span>
<span><span className="font-semibold text-white">{Number(sorted.reduce((s, r) => s + (r.metrics.views ?? 0), 0)).toLocaleString()}</span> total views</span>
</div>
</div>
)
}
// ── v2: Growth & Evolution ───────────────────────────────────────────────────
const RELATION_LABELS = {
remake_of: 'Remake',
remaster_of: 'Remaster',
revision_of: 'Revision',
inspired_by: 'Inspired by own work',
variation_of: 'Variation',
}
function EvolutionSection({ evolution }) {
if (!evolution?.length) return null
return (
<div className="mt-7 rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400">Growth &amp; Evolution</div>
<div className="mt-1 text-lg font-semibold text-white">Then &amp; Now</div>
<div className="mt-5 space-y-5">
{evolution.map((item) => (
<div key={item.id} className="grid gap-3 sm:grid-cols-[1fr_auto_1fr]">
{/* Original */}
<a
href={item.target_artwork?.url}
className="group flex items-start gap-3 rounded-2xl border border-white/8 bg-white/[0.03] p-3 transition-colors hover:border-white/20 hover:bg-white/[0.06]"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white/5 text-slate-500 group-hover:text-slate-300">
<i className="fa-solid fa-image" />
</div>
<div className="min-w-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.2em] text-slate-600">Original</div>
<div className="mt-0.5 truncate text-sm font-medium text-slate-300 group-hover:text-white">{item.target_artwork?.title}</div>
<div className="text-[10px] text-slate-600">{formatYear(item.target_artwork?.published_at)}</div>
</div>
</a>
{/* Arrow + relation type */}
<div className="flex flex-col items-center justify-center gap-1 py-2 text-slate-600">
<i className="fa-solid fa-arrow-right-long" />
<span className="text-[10px] font-semibold uppercase tracking-wider text-slate-600">
{RELATION_LABELS[item.relation_type] ?? item.relation_type}
</span>
{item.years_between > 0 && (
<span className="text-[10px] text-slate-700">{item.years_between}yr later</span>
)}
</div>
{/* New version */}
<a
href={item.source_artwork?.url}
className="group flex items-start gap-3 rounded-2xl border border-emerald-700/30 bg-emerald-900/10 p-3 transition-colors hover:border-emerald-600/50 hover:bg-emerald-900/20"
>
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-emerald-500/10 text-emerald-400">
<i className="fa-solid fa-wand-magic-sparkles" />
</div>
<div className="min-w-0">
<div className="text-[10px] font-semibold uppercase tracking-[0.2em] text-emerald-600">New version</div>
<div className="mt-0.5 truncate text-sm font-medium text-emerald-200 group-hover:text-white">{item.source_artwork?.title}</div>
<div className="text-[10px] text-emerald-800">{formatYear(item.source_artwork?.published_at)}</div>
</div>
</a>
</div>
))}
</div>
</div>
)
}
export default function CreatorJourneySection({ journey, username }) {
const summary = journey?.summary ?? {}
const highlights = Array.isArray(journey?.highlights) ? journey.highlights : []
const timeline = Array.isArray(journey?.timeline) ? journey.timeline.slice(0, 6) : []
const recaps = Array.isArray(journey?.yearly_recaps) ? journey.yearly_recaps.slice(0, 3) : []
const allRecaps = Array.isArray(journey?.yearly_recaps) ? journey.yearly_recaps : []
const eras = Array.isArray(journey?.eras) ? journey.eras : []
const evolution = Array.isArray(journey?.evolution) ? journey.evolution : []
const streaks = journey?.streaks ?? null
const latestMilestone = summary.latest_milestone ?? null
const available = !!summary.available
if (!available) {
return (
<section className="rounded-[34px] border border-white/10 bg-[linear-gradient(145deg,rgba(17,24,39,0.96),rgba(15,23,42,0.9))] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.28)] sm:p-7">
<div className="mb-5 flex flex-wrap items-center justify-between gap-4">
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.3em] text-sky-200/70">Creator Journey</div>
<h2 className="mt-2 text-2xl font-semibold text-white">A profile built as a story, not only a feed</h2>
</div>
</div>
<EmptyJourneyState
username={username}
memberSinceYear={summary.member_since_year}
yearsOnSkinbase={summary.years_on_skinbase}
/>
</section>
)
}
return (
<section className="rounded-[34px] border border-white/10 bg-[linear-gradient(145deg,rgba(15,23,42,0.98),rgba(8,15,28,0.92))] p-6 shadow-[0_24px_80px_rgba(2,6,23,0.32)] sm:p-7">
<div className="flex flex-wrap items-start justify-between gap-5">
<div className="max-w-2xl">
<div className="text-[11px] font-semibold uppercase tracking-[0.3em] text-sky-200/70">Creator Journey</div>
<h2 className="mt-2 text-2xl font-semibold text-white">A profile shaped by milestones, turning points, and yearly chapters.</h2>
{latestMilestone && (
<p className="mt-3 max-w-xl text-sm leading-relaxed text-slate-300">
Latest moment: <span className="font-semibold text-white">{latestMilestone.title}</span>
{latestMilestone.headline ? ` - ${latestMilestone.headline}` : ''}
</p>
)}
</div>
<div className="grid min-w-[18rem] gap-3 sm:grid-cols-3">
<StatPill label="Member since" value={summary.member_since_year} />
<StatPill label="Years on Skinbase" value={summary.years_on_skinbase ?? 0} />
<StatPill label="Milestones" value={summary.milestone_count ?? 0} />
</div>
</div>
{/* ── v2: Era strip ── */}
<EraStrip eras={eras} />
{highlights.length > 0 && (
<div className="mt-7 grid gap-4 xl:grid-cols-2">
{highlights.map((item) => {
const href = milestoneHref(item)
return (
<article
key={item.id}
className="relative overflow-hidden rounded-[28px] border border-white/10 bg-[linear-gradient(180deg,rgba(255,255,255,0.06),rgba(255,255,255,0.02))] p-5 transition-colors hover:bg-[linear-gradient(180deg,rgba(255,255,255,0.09),rgba(255,255,255,0.03))]"
>
{(() => { const c = colorForType(item.type); return <div className={`absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r ${c.accent} via-transparent to-transparent`} /> })()
}
<div className="flex items-start justify-between gap-4">
<div className="flex min-w-0 items-start gap-3">
<div className={`flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl ${colorForType(item.type).bg} ${colorForType(item.type).icon}`}>
<i className={iconForType(item.type)} />
</div>
<div className="min-w-0">
<div className="text-xs font-semibold uppercase tracking-[0.22em] text-slate-400">{item.title}</div>
<div className="mt-1 text-lg font-semibold text-white">{item.headline || item.value}</div>
</div>
</div>
{item.value && (
<div className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1 text-xs font-medium text-slate-200">
{item.value}
</div>
)}
</div>
{item.summary && (
<p className="mt-4 text-sm leading-relaxed text-slate-300">{item.summary}</p>
)}
{href && (
<a
href={href}
className="mt-4 inline-flex items-center gap-2 text-sm font-medium text-sky-200 transition-colors hover:text-white"
>
Open source moment
<i className="fa-solid fa-arrow-up-right-from-square text-xs" />
</a>
)}
</article>
)
})}
</div>
)}
<div className="mt-7 grid gap-6">
<div className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400">Timeline</div>
<div className="mt-1 text-lg font-semibold text-white">Important creator milestones</div>
</div>
</div>
<div className="mt-5 space-y-4">
{timeline.map((item, index) => {
const href = milestoneHref(item)
return (
<div key={item.id} className="grid grid-cols-[2.5rem_minmax(0,1fr)] gap-3">
<div className="flex flex-col items-center">
<div className={`flex h-10 w-10 items-center justify-center rounded-2xl border ${colorForType(item.type).border} ${colorForType(item.type).bg} ${colorForType(item.type).icon}`}>
<i className={iconForType(item.type)} />
</div>
{index < timeline.length - 1 && <div className="mt-2 h-full w-px bg-white/10" />}
</div>
<div className="pb-4">
<div className="flex flex-wrap items-center gap-2">
<div className="text-base font-semibold text-white">{item.title}</div>
{formatDate(item.occurred_at) && (
<div className="text-xs uppercase tracking-[0.2em] text-slate-500">{formatDate(item.occurred_at)}</div>
)}
</div>
{item.headline && <div className="mt-1 text-sm font-medium text-sky-100">{item.headline}</div>}
{item.summary && <div className="mt-1 text-sm leading-relaxed text-slate-400">{item.summary}</div>}
{href && (
<a href={href} className="mt-2 inline-flex items-center gap-2 text-sm text-slate-200 transition-colors hover:text-white">
View linked work
<i className="fa-solid fa-arrow-right text-xs" />
</a>
)}
</div>
</div>
)
})}
</div>
</div>
<div className="rounded-[28px] border border-white/10 bg-white/[0.03] p-5">
<div>
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-slate-400">Yearly recap</div>
<div className="mt-1 text-lg font-semibold text-white">Recent chapters</div>
</div>
<div className="mt-5 space-y-3">
{recaps.map((item) => {
const status = item.metrics?.year_status
const statusColors = {
breakout: 'bg-emerald-400/12 text-emerald-200',
steady: 'bg-sky-400/12 text-sky-200',
experimental: 'bg-violet-400/12 text-violet-200',
comeback: 'bg-amber-400/12 text-amber-200',
quiet: 'bg-slate-700 text-slate-400',
}
return (
<article key={item.id} className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="flex items-center gap-2">
<div className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">{item.value}</div>
{status && (
<span className={`rounded-full px-2 py-0.5 text-[10px] font-semibold capitalize ${statusColors[status] ?? statusColors.steady}`}>
{status}
</span>
)}
</div>
<div className="mt-1 text-lg font-semibold text-white">{item.headline}</div>
</div>
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-amber-400/12 text-amber-200">
<i className="fa-solid fa-chart-column" />
</div>
</div>
<p className="mt-3 text-sm leading-relaxed text-slate-300">{item.summary}</p>
<div className="mt-4 grid gap-2 sm:grid-cols-2">
<StatPill label="Views" value={(item.metrics?.views ?? 0).toLocaleString()} />
<StatPill label="Downloads" value={(item.metrics?.downloads ?? 0).toLocaleString()} />
{(item.metrics?.featured_count ?? 0) > 0 && (
<StatPill label="Featured" value={item.metrics.featured_count} />
)}
{item.metrics?.top_category && (
<StatPill label="Top category" value={item.metrics.top_category} />
)}
</div>
</article>
)
})}
</div>
</div>
</div>
{/* ── v2: Streaks ── */}
<StreaksSection streaks={streaks} />
{/* ── Yearly productivity chart ── */}
<YearlyProductivityChart recaps={allRecaps} />
{/* ── v2: Growth & Evolution ── */}
<EvolutionSection evolution={evolution} />
</section>
)
}