daily-briefing/web/src/pages/Dashboard.tsx

243 lines
8.7 KiB
TypeScript
Raw Normal View History

import { Link } from "react-router-dom";
import { useDashboard } from "../hooks/useDashboard";
import Clock from "../components/Clock";
import WeatherCard from "../components/WeatherCard";
import HourlyForecast from "../components/HourlyForecast";
import NewsGrid from "../components/NewsGrid";
import ServerCard from "../components/ServerCard";
import HomeAssistant from "../components/HomeAssistant";
import TasksCard from "../components/TasksCard";
import MqttCard from "../components/MqttCard";
import { RefreshCw, Wifi, WifiOff, AlertTriangle, Settings } from "lucide-react";
export default function Dashboard() {
const { data, loading, error, connected, refresh } = useDashboard();
return (
<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-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 */}
<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 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-base-600 ${loading ? "animate-spin" : ""}`}
/>
</button>
<Link
to="/admin"
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-base-600" />
</Link>
<Clock />
</div>
</div>
</header>
<main className="max-w-[1920px] mx-auto px-4 sm:px-6 lg:px-8 py-8">
{loading && !data ? (
<LoadingSkeleton />
) : data ? (
<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 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>
{/* 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 */}
<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:{" "}
<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-2 px-2.5 py-1 border border-base-300">
<div className="relative">
<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-mono font-medium ${
connected ? "text-mint" : "text-base-500"
}`}
>
{connected ? "Live" : "Offline"}
</span>
{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-10">
{/* Section 01 skeleton */}
<div>
<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={`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>
);
}