Refactor dashboard and upload flows

Remove dead admin UI code, redesign dashboard followers/following and upload experiences, and add schema audit tooling with repair migrations for forum and upload drift.
This commit is contained in:
2026-03-21 11:02:22 +01:00
parent 29c3ff8572
commit 979e011257
55 changed files with 2576 additions and 1923 deletions

View File

@@ -359,26 +359,60 @@ function SuggestionChip({ href, label, icon, highlight = false, onNavigate }) {
)
}
function OverviewMetric({ label, value, href, icon, accent = 'sky', onNavigate }) {
function OverviewMetric({ label, value, href, icon, accent = 'sky', caption = null, onNavigate }) {
const accents = {
sky: 'text-sky-200 border-sky-300/20 bg-sky-400/10',
amber: 'text-amber-200 border-amber-300/20 bg-amber-400/10',
emerald: 'text-emerald-200 border-emerald-300/20 bg-emerald-400/10',
rose: 'text-rose-200 border-rose-300/20 bg-rose-400/10',
slate: 'text-slate-200 border-white/10 bg-white/5',
sky: {
icon: 'text-sky-100 border-sky-300/20 bg-sky-400/12',
card: 'hover:border-sky-300/35 hover:bg-[#102033]',
glow: 'from-sky-400/18 via-sky-400/8 to-transparent',
caption: 'text-sky-100/75',
},
amber: {
icon: 'text-amber-100 border-amber-300/20 bg-amber-400/12',
card: 'hover:border-amber-300/35 hover:bg-[#1a2130]',
glow: 'from-amber-400/18 via-amber-400/8 to-transparent',
caption: 'text-amber-100/75',
},
emerald: {
icon: 'text-emerald-100 border-emerald-300/20 bg-emerald-400/12',
card: 'hover:border-emerald-300/35 hover:bg-[#0f2130]',
glow: 'from-emerald-400/18 via-emerald-400/8 to-transparent',
caption: 'text-emerald-100/75',
},
rose: {
icon: 'text-rose-100 border-rose-300/20 bg-rose-400/12',
card: 'hover:border-rose-300/35 hover:bg-[#1d1d31]',
glow: 'from-rose-400/18 via-rose-400/8 to-transparent',
caption: 'text-rose-100/75',
},
slate: {
icon: 'text-slate-100 border-white/10 bg-white/5',
card: 'hover:border-white/20 hover:bg-[#102033]',
glow: 'from-white/10 via-white/5 to-transparent',
caption: 'text-slate-300/80',
},
}
const tone = accents[accent] || accents.slate
return (
<a
href={href}
onClick={() => onNavigate?.(href, label)}
className="group rounded-2xl border border-white/10 bg-[#0b1826]/85 p-4 shadow-lg shadow-black/20 transition hover:-translate-y-0.5 hover:border-sky-300/35 hover:bg-[#102033]"
className={[
'group relative overflow-hidden rounded-2xl border border-white/10 bg-[#0b1826]/85 p-4 shadow-lg shadow-black/20 transition hover:-translate-y-0.5',
tone.card,
].join(' ')}
>
<div className={`pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b ${tone.glow}`} />
<div className="flex items-center justify-between gap-3">
<span className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl border ${accents[accent] || accents.slate}`}>
<span className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl border ${tone.icon}`}>
<i className={icon} aria-hidden="true" />
</span>
<span className="text-2xl font-semibold text-white">{value}</span>
<div className="text-right">
<span className="block text-2xl font-semibold text-white">{value}</span>
{caption ? <span className={`mt-1 block text-[11px] uppercase tracking-[0.16em] ${tone.caption}`}>{caption}</span> : null}
</div>
</div>
<p className="mt-4 text-xs uppercase tracking-[0.18em] text-slate-400">{label}</p>
</a>
@@ -386,15 +420,64 @@ function OverviewMetric({ label, value, href, icon, accent = 'sky', onNavigate }
}
function SectionLinkCard({ item, onNavigate, onTogglePin, isPinned = false }) {
const accents = {
sky: {
icon: 'border-sky-300/20 bg-sky-400/12 text-sky-100',
badge: 'border-sky-300/20 bg-sky-400/10 text-sky-100',
preview: 'text-sky-200/80',
open: 'text-sky-100',
hover: 'hover:border-sky-300/35 hover:bg-[#102033]',
glow: 'from-sky-400/16 via-sky-400/8 to-transparent',
},
emerald: {
icon: 'border-emerald-300/20 bg-emerald-400/12 text-emerald-100',
badge: 'border-emerald-300/20 bg-emerald-400/10 text-emerald-100',
preview: 'text-emerald-200/80',
open: 'text-emerald-100',
hover: 'hover:border-emerald-300/35 hover:bg-[#102033]',
glow: 'from-emerald-400/16 via-emerald-400/8 to-transparent',
},
amber: {
icon: 'border-amber-300/20 bg-amber-400/12 text-amber-100',
badge: 'border-amber-300/20 bg-amber-400/10 text-amber-100',
preview: 'text-amber-200/80',
open: 'text-amber-100',
hover: 'hover:border-amber-300/35 hover:bg-[#1a2130]',
glow: 'from-amber-400/16 via-amber-400/8 to-transparent',
},
rose: {
icon: 'border-rose-300/20 bg-rose-400/12 text-rose-100',
badge: 'border-rose-300/20 bg-rose-400/10 text-rose-100',
preview: 'text-rose-200/80',
open: 'text-rose-100',
hover: 'hover:border-rose-300/35 hover:bg-[#1d1d31]',
glow: 'from-rose-400/16 via-rose-400/8 to-transparent',
},
slate: {
icon: 'border-white/10 bg-white/5 text-sky-200',
badge: 'border-white/10 bg-white/5 text-slate-200',
preview: 'text-sky-200/80',
open: 'text-sky-100',
hover: 'hover:border-white/20 hover:bg-[#102033]',
glow: 'from-white/10 via-white/5 to-transparent',
},
}
const tone = accents[item.accent] || accents.slate
return (
<article className="group rounded-2xl border border-white/10 bg-[#0b1826]/80 p-4 shadow-lg shadow-black/20 transition hover:-translate-y-0.5 hover:border-sky-300/35 hover:bg-[#102033]">
<article className={[
'group relative overflow-hidden rounded-2xl border border-white/10 bg-[#0b1826]/80 p-4 shadow-lg shadow-black/20 transition hover:-translate-y-0.5',
tone.hover,
].join(' ')}>
<div className={`pointer-events-none absolute inset-x-0 top-0 h-24 bg-gradient-to-b ${tone.glow}`} />
<div className="flex items-start justify-between gap-3">
<span className="inline-flex h-11 w-11 items-center justify-center rounded-2xl border border-white/10 bg-white/5 text-lg text-sky-200">
<span className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl border text-lg ${tone.icon}`}>
<i className={item.icon} aria-hidden="true" />
</span>
<div className="flex items-center gap-2">
{item.badge ? (
<span className="rounded-full border border-sky-300/20 bg-sky-400/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] text-sky-100">
<span className={`rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em] ${tone.badge}`}>
{item.badge}
</span>
) : null}
@@ -418,12 +501,12 @@ function SectionLinkCard({ item, onNavigate, onTogglePin, isPinned = false }) {
<div className="mt-4">
<h3 className="text-base font-semibold text-white transition group-hover:text-sky-100">{item.label}</h3>
<p className="mt-2 text-sm leading-6 text-slate-300">{item.description}</p>
{item.preview ? <p className="mt-3 text-xs font-semibold uppercase tracking-[0.14em] text-sky-200/80">{item.preview}</p> : null}
{item.preview ? <p className={`mt-3 text-xs font-semibold uppercase tracking-[0.14em] ${tone.preview}`}>{item.preview}</p> : null}
</div>
<div className="mt-4 flex items-center justify-between gap-3 text-xs uppercase tracking-[0.18em] text-slate-400">
<span>{item.meta}</span>
<a href={item.href} onClick={() => onNavigate?.(item.href, item.label)} className="text-sky-200 transition group-hover:translate-x-0.5">
<a href={item.href} onClick={() => onNavigate?.(item.href, item.label)} className={`${tone.open} transition group-hover:translate-x-0.5`}>
Open
</a>
</div>
@@ -849,8 +932,8 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
},
{
eyebrow: 'Community',
title: 'Followers, following, and saved work',
description: 'Move between your people-focused spaces without digging through the navigation.',
title: 'Audience, network, and saved work',
description: 'Stay close to the people around your account, from new followers to the creators shaping your feed.',
items: [
{
label: 'Followers',
@@ -860,6 +943,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
meta: 'Dashboard Followers',
badge: overviewStats.followers > 0 ? String(overviewStats.followers) : null,
preview: previewLabelForRoute('/dashboard/followers', overviewStats),
accent: 'sky',
},
{
label: 'Following',
@@ -869,6 +953,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
meta: 'Dashboard Following',
badge: overviewStats.following > 0 ? String(overviewStats.following) : null,
preview: previewLabelForRoute('/dashboard/following', overviewStats),
accent: 'emerald',
},
{
label: 'Favorites',
@@ -878,6 +963,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
meta: 'Dashboard Favorites',
badge: overviewStats.favorites > 0 ? String(overviewStats.favorites) : null,
preview: previewLabelForRoute('/dashboard/favorites', overviewStats),
accent: 'rose',
},
],
},
@@ -933,6 +1019,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
href: '/dashboard/notifications',
icon: 'fa-solid fa-bell',
accent: overviewStats.notifications > 0 ? 'amber' : 'slate',
caption: overviewStats.notifications > 0 ? 'Needs review' : 'All clear',
},
{
label: 'Followers',
@@ -940,13 +1027,15 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
href: '/dashboard/followers',
icon: 'fa-solid fa-user-group',
accent: 'sky',
caption: overviewStats.followers > 0 ? 'Audience' : 'Build audience',
},
{
label: 'Following',
value: overviewStats.following,
href: '/dashboard/following',
icon: 'fa-solid fa-users',
accent: 'slate',
accent: 'emerald',
caption: overviewStats.following > 0 ? 'Network' : 'Find creators',
},
{
label: 'Saved favorites',
@@ -954,6 +1043,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
href: '/dashboard/favorites',
icon: 'fa-solid fa-bookmark',
accent: 'rose',
caption: overviewStats.favorites > 0 ? 'Inspiration' : 'Nothing saved',
},
{
label: 'Artworks',
@@ -961,6 +1051,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
href: '/dashboard/artworks',
icon: 'fa-solid fa-layer-group',
accent: 'emerald',
caption: overviewStats.artworks > 0 ? 'Portfolio' : 'Start uploading',
},
{
label: 'Stories',
@@ -968,6 +1059,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
href: isCreator ? '/creator/stories' : '/creator/stories/create',
icon: 'fa-solid fa-pen-nib',
accent: 'amber',
caption: overviewStats.stories > 0 ? 'Creator voice' : 'Tell your story',
},
]
const suggestions = [
@@ -1069,7 +1161,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
<h1 className="mt-3 max-w-3xl text-3xl font-semibold tracking-tight text-white sm:text-4xl">
Welcome back, {username}
</h1>
<div className="mt-4 flex items-center gap-2">
<div className="mt-4 flex flex-wrap items-center gap-2">
<LevelBadge level={level} rank={rank} />
</div>
<p className="mt-4 max-w-2xl text-sm leading-7 text-slate-300 sm:text-base">
@@ -1090,7 +1182,7 @@ export default function DashboardPage({ username, isCreator, level, rank, receiv
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<HeroStat label="Level" value={`Lv. ${level}`} tone="sky" />
<HeroStat label="Rank" value={rank} tone="amber" />
<HeroStat