116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
|
|
import { Thermometer, Droplets, Wind, CloudOff } from "lucide-react";
|
||
|
|
import type { WeatherData } from "../api";
|
||
|
|
|
||
|
|
interface WeatherCardProps {
|
||
|
|
data: WeatherData;
|
||
|
|
accent: "cyan" | "amber";
|
||
|
|
}
|
||
|
|
|
||
|
|
const accentMap = {
|
||
|
|
cyan: {
|
||
|
|
gradient: "from-cyan-500/10 to-cyan-900/5",
|
||
|
|
text: "text-cyan-400",
|
||
|
|
border: "border-cyan-500/20",
|
||
|
|
badge: "bg-cyan-500/15 text-cyan-300",
|
||
|
|
statIcon: "text-cyan-400/70",
|
||
|
|
ring: "ring-cyan-500/10",
|
||
|
|
},
|
||
|
|
amber: {
|
||
|
|
gradient: "from-amber-500/10 to-amber-900/5",
|
||
|
|
text: "text-amber-400",
|
||
|
|
border: "border-amber-500/20",
|
||
|
|
badge: "bg-amber-500/15 text-amber-300",
|
||
|
|
statIcon: "text-amber-400/70",
|
||
|
|
ring: "ring-amber-500/10",
|
||
|
|
},
|
||
|
|
} as const;
|
||
|
|
|
||
|
|
export default function WeatherCard({ data, accent }: WeatherCardProps) {
|
||
|
|
const a = accentMap[accent];
|
||
|
|
|
||
|
|
if (data.error) {
|
||
|
|
return (
|
||
|
|
<div className="glass-card p-5 animate-fade-in">
|
||
|
|
<div className="flex items-center gap-3 text-red-400">
|
||
|
|
<CloudOff className="w-5 h-5" />
|
||
|
|
<div>
|
||
|
|
<p className="text-sm font-medium">Wetter nicht verfuegbar</p>
|
||
|
|
<p className="text-xs text-slate-500 mt-0.5">{data.location || "Unbekannt"}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={`glass-card p-5 animate-fade-in bg-gradient-to-br ${a.gradient}`}>
|
||
|
|
{/* Header: location badge */}
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<span className={`badge ${a.badge}`}>{data.location}</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Main row: icon + temp */}
|
||
|
|
<div className="flex items-center justify-between mb-4">
|
||
|
|
<div>
|
||
|
|
<div className="flex items-baseline gap-1">
|
||
|
|
<span
|
||
|
|
className="text-5xl font-extrabold text-white tracking-tighter"
|
||
|
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
||
|
|
>
|
||
|
|
{Math.round(data.temp)}
|
||
|
|
</span>
|
||
|
|
<span className="text-2xl font-light text-slate-400">°</span>
|
||
|
|
</div>
|
||
|
|
<p className="text-sm text-slate-400 mt-1 capitalize">{data.description}</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<span className="text-5xl select-none" role="img" aria-label="weather">
|
||
|
|
{data.icon}
|
||
|
|
</span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stat grid */}
|
||
|
|
<div className="grid grid-cols-3 gap-3">
|
||
|
|
<StatItem
|
||
|
|
icon={<Thermometer className={`w-3.5 h-3.5 ${a.statIcon}`} />}
|
||
|
|
label="Gefuehlt"
|
||
|
|
value={`${Math.round(data.feels_like)}\u00B0`}
|
||
|
|
/>
|
||
|
|
<StatItem
|
||
|
|
icon={<Droplets className={`w-3.5 h-3.5 ${a.statIcon}`} />}
|
||
|
|
label="Feuchte"
|
||
|
|
value={`${data.humidity}%`}
|
||
|
|
/>
|
||
|
|
<StatItem
|
||
|
|
icon={<Wind className={`w-3.5 h-3.5 ${a.statIcon}`} />}
|
||
|
|
label="Wind"
|
||
|
|
value={`${Math.round(data.wind_kmh)} km/h`}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function StatItem({
|
||
|
|
icon,
|
||
|
|
label,
|
||
|
|
value,
|
||
|
|
}: {
|
||
|
|
icon: React.ReactNode;
|
||
|
|
label: string;
|
||
|
|
value: string;
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col items-center gap-1.5 rounded-xl bg-white/[0.03] border border-white/[0.04] px-2 py-2.5">
|
||
|
|
{icon}
|
||
|
|
<span
|
||
|
|
className="text-sm font-semibold text-white"
|
||
|
|
style={{ fontFamily: "'JetBrains Mono', monospace" }}
|
||
|
|
>
|
||
|
|
{value}
|
||
|
|
</span>
|
||
|
|
<span className="text-[10px] uppercase tracking-wider text-slate-500">{label}</span>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|