redesign: THERMAL warm brutalist dashboard UI
Complete visual redesign of all dashboard components with a warm brutalist command terminal aesthetic. Features editorial section numbering, IBM Plex typography, sharp zero-radius cards with colored accent strips, film grain overlay, and data-glow effects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f6a42c2dd2
commit
e94a7706ab
12 changed files with 641 additions and 548 deletions
|
|
@ -14,135 +14,228 @@ export default function Dashboard() {
|
|||
const { data, loading, error, connected, refresh } = useDashboard();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen text-white">
|
||||
<div className="min-h-screen text-base-800">
|
||||
{/* Error banner */}
|
||||
{error && (
|
||||
<div className="fixed top-0 inset-x-0 z-50 flex items-center justify-center gap-2 bg-red-500/15 border-b border-red-500/20 backdrop-blur-sm px-4 py-2 animate-slide-up">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<p className="text-xs text-red-300">{error}</p>
|
||||
<button onClick={refresh} className="ml-3 text-xs text-red-300 hover:text-white underline underline-offset-2 transition-colors">
|
||||
<div className="fixed top-0 inset-x-0 z-50 flex items-center justify-center gap-2 bg-cherry/10 border-b border-cherry/20 px-4 py-2 animate-fade-in">
|
||||
<AlertTriangle className="w-4 h-4 text-cherry flex-shrink-0" />
|
||||
<p className="text-xs text-cherry font-mono">{error}</p>
|
||||
<button
|
||||
onClick={refresh}
|
||||
className="ml-3 text-xs text-cherry hover:text-base-900 underline underline-offset-2 transition-colors font-mono"
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<header className="sticky top-0 z-40 border-b border-white/[0.04] bg-slate-950/80 backdrop-blur-lg">
|
||||
<div className="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-base sm:text-lg font-bold tracking-tight text-white">Daily Briefing</h1>
|
||||
{/* Header */}
|
||||
<header className="sticky top-0 z-40 border-b border-base-300 bg-base-50/95 backdrop-blur-sm">
|
||||
<div className="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<h1 className="font-serif italic text-lg sm:text-xl text-base-900 tracking-tight">
|
||||
Daily Briefing
|
||||
</h1>
|
||||
<LiveIndicator connected={connected} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={refresh}
|
||||
disabled={loading}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-lg bg-white/5 border border-white/[0.06] hover:bg-white/10 transition-colors disabled:opacity-40"
|
||||
className="flex items-center justify-center w-8 h-8 bg-base-100 border border-base-300 hover:border-base-500 transition-colors disabled:opacity-40"
|
||||
title="Daten aktualisieren"
|
||||
>
|
||||
<RefreshCw className={`w-3.5 h-3.5 text-slate-400 ${loading ? "animate-spin" : ""}`} />
|
||||
<RefreshCw
|
||||
className={`w-3.5 h-3.5 text-base-600 ${loading ? "animate-spin" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
<Link
|
||||
to="/admin"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-lg bg-white/5 border border-white/[0.06] hover:bg-white/10 transition-colors"
|
||||
className="flex items-center justify-center w-8 h-8 bg-base-100 border border-base-300 hover:border-base-500 transition-colors"
|
||||
title="Admin Panel"
|
||||
>
|
||||
<Settings className="w-3.5 h-3.5 text-slate-400" />
|
||||
<Settings className="w-3.5 h-3.5 text-base-600" />
|
||||
</Link>
|
||||
<Clock />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 py-6 space-y-6">
|
||||
<main className="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{loading && !data ? (
|
||||
<LoadingSkeleton />
|
||||
) : data ? (
|
||||
<>
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<WeatherCard data={data.weather.primary} accent="cyan" />
|
||||
<WeatherCard data={data.weather.secondary} accent="amber" />
|
||||
<div className="md:col-span-2">
|
||||
<HourlyForecast slots={data.weather.hourly} />
|
||||
<div className="space-y-10">
|
||||
{/* Section 01 — Wetter */}
|
||||
<section>
|
||||
<div className="section-label">
|
||||
<span className="section-number">01</span>
|
||||
<h2 className="section-title">Wetter</h2>
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-px bg-base-300">
|
||||
<WeatherCard data={data.weather.primary} accent="cyan" />
|
||||
<WeatherCard data={data.weather.secondary} accent="amber" />
|
||||
<div className="md:col-span-2">
|
||||
<HourlyForecast slots={data.weather.hourly} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{data.servers.servers.map((srv) => (
|
||||
<ServerCard key={srv.name} server={srv} />
|
||||
))}
|
||||
<HomeAssistant data={data.ha} />
|
||||
<TasksCard data={data.tasks} />
|
||||
{/* Section 02 — Infrastruktur */}
|
||||
<section>
|
||||
<div className="section-label">
|
||||
<span className="section-number">02</span>
|
||||
<h2 className="section-title">Infrastruktur</h2>
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-px bg-base-300">
|
||||
{data.servers.servers.map((srv) => (
|
||||
<ServerCard key={srv.name} server={srv} />
|
||||
))}
|
||||
<HomeAssistant data={data.ha} />
|
||||
<TasksCard data={data.tasks} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{(data.mqtt?.connected || (data.mqtt?.entities?.length ?? 0) > 0) && (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<div className="md:col-span-2 xl:col-span-4">
|
||||
<MqttCard data={data.mqtt} />
|
||||
{/* Section 03 — MQTT (conditional) */}
|
||||
{(data.mqtt?.connected ||
|
||||
(data.mqtt?.entities?.length ?? 0) > 0) && (
|
||||
<section>
|
||||
<div className="section-label">
|
||||
<span className="section-number">03</span>
|
||||
<h2 className="section-title">MQTT</h2>
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
|
||||
<MqttCard data={data.mqtt} />
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Section 04 — Nachrichten */}
|
||||
<section>
|
||||
<NewsGrid data={data.news} />
|
||||
</section>
|
||||
|
||||
<footer className="text-center pb-4">
|
||||
<p className="text-[10px] text-slate-700">
|
||||
{/* Footer */}
|
||||
<footer className="flex items-center justify-between py-4 border-t border-base-300">
|
||||
<p className="text-[10px] font-mono text-base-500">
|
||||
Letzte Aktualisierung:{" "}
|
||||
{new Date(data.timestamp).toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||
})}
|
||||
<span className="text-base-700">
|
||||
{new Date(data.timestamp).toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-[10px] font-mono text-base-500">
|
||||
Daily Briefing v2
|
||||
</p>
|
||||
</footer>
|
||||
</>
|
||||
</div>
|
||||
) : null}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Live indicator ─────────────────────────────────────── */
|
||||
|
||||
function LiveIndicator({ connected }: { connected: boolean }) {
|
||||
return (
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-white/5 border border-white/[0.06]">
|
||||
<div className="flex items-center gap-2 px-2.5 py-1 border border-base-300">
|
||||
<div className="relative">
|
||||
<div className={`w-1.5 h-1.5 rounded-full ${connected ? "bg-emerald-400" : "bg-slate-600"}`} />
|
||||
{connected && <div className="absolute inset-0 w-1.5 h-1.5 rounded-full bg-emerald-400 animate-ping opacity-50" />}
|
||||
<div
|
||||
className={`w-1.5 h-1.5 ${connected ? "bg-mint" : "bg-base-500"}`}
|
||||
style={{ borderRadius: 0 }}
|
||||
/>
|
||||
{connected && (
|
||||
<div
|
||||
className="absolute inset-0 w-1.5 h-1.5 bg-mint animate-ping opacity-40"
|
||||
style={{ borderRadius: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<span className={`text-[10px] font-medium ${connected ? "text-emerald-400" : "text-slate-600"}`}>
|
||||
<span
|
||||
className={`text-[10px] font-mono font-medium ${
|
||||
connected ? "text-mint" : "text-base-500"
|
||||
}`}
|
||||
>
|
||||
{connected ? "Live" : "Offline"}
|
||||
</span>
|
||||
{connected ? <Wifi className="w-3 h-3 text-emerald-400/50" /> : <WifiOff className="w-3 h-3 text-slate-600" />}
|
||||
{connected ? (
|
||||
<Wifi className="w-3 h-3 text-mint/50" />
|
||||
) : (
|
||||
<WifiOff className="w-3 h-3 text-base-500" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Loading skeleton ───────────────────────────────────── */
|
||||
|
||||
function LoadingSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
<SkeletonCard className="h-52" />
|
||||
<SkeletonCard className="h-52" />
|
||||
<SkeletonCard className="h-24 md:col-span-2" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
|
||||
{Array.from({ length: 4 }).map((_, i) => <SkeletonCard key={i} className="h-72" />)}
|
||||
</div>
|
||||
<div className="space-y-10">
|
||||
{/* Section 01 skeleton */}
|
||||
<div>
|
||||
<div className="h-6 w-32 rounded bg-white/5 mb-4" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => <SkeletonCard key={i} className="h-28" />)}
|
||||
<div className="section-label">
|
||||
<span className="section-number">01</span>
|
||||
<div className="h-3 w-16 bg-base-200" />
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-px bg-base-300">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<SkeletonCard key={`w-${i}`} className="h-52" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 02 skeleton */}
|
||||
<div>
|
||||
<div className="section-label">
|
||||
<span className="section-number">02</span>
|
||||
<div className="h-3 w-24 bg-base-200" />
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-px bg-base-300">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<SkeletonCard key={`s-${i}`} className="h-72" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 04 skeleton */}
|
||||
<div>
|
||||
<div className="section-label">
|
||||
<span className="section-number">04</span>
|
||||
<div className="h-3 w-28 bg-base-200" />
|
||||
<span className="section-rule" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-px bg-base-300">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<SkeletonCard key={`n-${i}`} className="h-28" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scan loader */}
|
||||
<div className="scan-loader" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonCard({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<div className={`glass-card ${className}`}>
|
||||
<div className="p-5 space-y-3">
|
||||
<div className="h-3 w-1/3 rounded bg-white/5" />
|
||||
<div className="h-2 w-2/3 rounded bg-white/[0.03]" />
|
||||
<div className="h-2 w-1/2 rounded bg-white/[0.03]" />
|
||||
<div className={`bg-base-50 ${className}`}>
|
||||
<div className="p-5 space-y-3 animate-pulse">
|
||||
<div className="h-3 w-1/3 bg-base-200" />
|
||||
<div className="h-2 w-2/3 bg-base-200/60" />
|
||||
<div className="h-2 w-1/2 bg-base-200/60" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue