2026-03-02 01:48:51 +01:00
|
|
|
import { useState, useMemo } from "react";
|
2026-03-02 10:54:28 +01:00
|
|
|
import { ArrowUpRight } from "lucide-react";
|
2026-03-02 01:48:51 +01:00
|
|
|
import type { NewsResponse, NewsArticle } from "../api";
|
|
|
|
|
|
|
|
|
|
interface NewsGridProps {
|
|
|
|
|
data: NewsResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CATEGORIES = [
|
|
|
|
|
{ key: "all", label: "Alle" },
|
|
|
|
|
{ key: "tech", label: "Tech" },
|
|
|
|
|
{ key: "wirtschaft", label: "Wirtschaft" },
|
|
|
|
|
{ key: "politik", label: "Politik" },
|
|
|
|
|
{ key: "allgemein", label: "Allgemein" },
|
|
|
|
|
] as const;
|
|
|
|
|
|
|
|
|
|
const SOURCE_COLORS: Record<string, string> = {
|
2026-03-02 10:54:28 +01:00
|
|
|
heise: "border-gold/40 text-gold bg-gold/5",
|
|
|
|
|
golem: "border-azure/40 text-azure bg-azure/5",
|
|
|
|
|
spiegel: "border-cherry/40 text-cherry bg-cherry/5",
|
|
|
|
|
tagesschau: "border-azure/40 text-azure bg-azure/5",
|
|
|
|
|
zeit: "border-base-400 text-base-700 bg-base-200",
|
|
|
|
|
faz: "border-mint/40 text-mint bg-mint/5",
|
|
|
|
|
welt: "border-iris/40 text-iris bg-iris/5",
|
|
|
|
|
t3n: "border-iris/40 text-iris bg-iris/5",
|
|
|
|
|
default: "border-gold/30 text-gold-muted bg-gold/5",
|
2026-03-02 01:48:51 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function sourceColor(source: string): string {
|
|
|
|
|
const key = source.toLowerCase();
|
|
|
|
|
for (const [prefix, cls] of Object.entries(SOURCE_COLORS)) {
|
|
|
|
|
if (key.includes(prefix)) return cls;
|
|
|
|
|
}
|
|
|
|
|
return SOURCE_COLORS.default;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function relativeTime(isoDate: string): string {
|
|
|
|
|
try {
|
|
|
|
|
const date = new Date(isoDate);
|
|
|
|
|
if (isNaN(date.getTime())) return "";
|
|
|
|
|
const diff = Date.now() - date.getTime();
|
|
|
|
|
const seconds = Math.floor(diff / 1_000);
|
|
|
|
|
const minutes = Math.floor(seconds / 60);
|
|
|
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
|
const days = Math.floor(hours / 24);
|
|
|
|
|
|
|
|
|
|
if (seconds < 60) return "gerade eben";
|
|
|
|
|
if (minutes < 60) return `vor ${minutes} Min.`;
|
|
|
|
|
if (hours < 24) return hours === 1 ? "vor 1 Stunde" : `vor ${hours} Stunden`;
|
|
|
|
|
if (days === 1) return "gestern";
|
|
|
|
|
if (days < 7) return `vor ${days} Tagen`;
|
|
|
|
|
return date.toLocaleDateString("de-DE", { day: "2-digit", month: "short" });
|
|
|
|
|
} catch {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function NewsGrid({ data }: NewsGridProps) {
|
|
|
|
|
const [activeCategory, setActiveCategory] = useState<string>("all");
|
|
|
|
|
|
|
|
|
|
const filteredArticles = useMemo(() => {
|
|
|
|
|
if (!data?.articles) return [];
|
|
|
|
|
if (activeCategory === "all") return data.articles;
|
|
|
|
|
return data.articles.filter(
|
|
|
|
|
(a) => a.category?.toLowerCase() === activeCategory
|
|
|
|
|
);
|
|
|
|
|
}, [data, activeCategory]);
|
|
|
|
|
|
|
|
|
|
if (!data?.articles) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="animate-fade-in">
|
2026-03-02 10:54:28 +01:00
|
|
|
{/* Section header */}
|
|
|
|
|
<div className="section-label">
|
|
|
|
|
<span className="section-number">04</span>
|
|
|
|
|
<h2 className="section-title">Nachrichten</h2>
|
|
|
|
|
<span className="section-rule" />
|
|
|
|
|
<span className="text-xs font-mono text-base-500">{filteredArticles.length}</span>
|
|
|
|
|
</div>
|
2026-03-02 01:48:51 +01:00
|
|
|
|
2026-03-02 10:54:28 +01:00
|
|
|
{/* Category tabs */}
|
|
|
|
|
<div className="flex gap-1 flex-wrap mb-5">
|
|
|
|
|
{CATEGORIES.map((cat) => (
|
|
|
|
|
<button
|
|
|
|
|
key={cat.key}
|
|
|
|
|
onClick={() => setActiveCategory(cat.key)}
|
|
|
|
|
className={`tab-btn ${activeCategory === cat.key ? "active" : ""}`}
|
|
|
|
|
>
|
|
|
|
|
{cat.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-03-02 01:48:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Articles grid */}
|
|
|
|
|
{filteredArticles.length === 0 ? (
|
2026-03-02 10:54:28 +01:00
|
|
|
<div className="deck-card p-8 text-center text-base-500 text-sm">
|
2026-03-02 01:48:51 +01:00
|
|
|
Keine Artikel in dieser Kategorie.
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
2026-03-02 10:54:28 +01:00
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-px bg-base-300">
|
2026-03-02 01:48:51 +01:00
|
|
|
{filteredArticles.map((article) => (
|
|
|
|
|
<ArticleCard key={article.id} article={article} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ArticleCard({ article }: { article: NewsArticle }) {
|
|
|
|
|
return (
|
|
|
|
|
<a
|
|
|
|
|
href={article.url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
2026-03-02 10:54:28 +01:00
|
|
|
className="group block bg-base-50 p-4 hover:bg-base-100 transition-colors relative"
|
2026-03-02 01:48:51 +01:00
|
|
|
>
|
2026-03-02 10:54:28 +01:00
|
|
|
<div className="flex items-center justify-between mb-3">
|
|
|
|
|
<span className={`tag ${sourceColor(article.source)}`}>
|
2026-03-02 01:48:51 +01:00
|
|
|
{article.source}
|
|
|
|
|
</span>
|
2026-03-02 10:54:28 +01:00
|
|
|
<ArrowUpRight className="w-3 h-3 text-base-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
2026-03-02 01:48:51 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-02 10:54:28 +01:00
|
|
|
<h3 className="text-sm font-medium text-base-800 leading-snug line-clamp-2 group-hover:text-base-900 transition-colors">
|
2026-03-02 01:48:51 +01:00
|
|
|
{article.title}
|
|
|
|
|
</h3>
|
|
|
|
|
|
2026-03-02 10:54:28 +01:00
|
|
|
<div className="flex items-center gap-3 mt-3 pt-3 border-t border-base-200">
|
2026-03-02 01:48:51 +01:00
|
|
|
{article.category && (
|
2026-03-02 10:54:28 +01:00
|
|
|
<span className="text-[9px] font-mono text-base-500 uppercase tracking-wider">
|
2026-03-02 01:48:51 +01:00
|
|
|
{article.category}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
2026-03-02 10:54:28 +01:00
|
|
|
<span className="text-[9px] font-mono text-base-500 ml-auto">
|
2026-03-02 01:48:51 +01:00
|
|
|
{relativeTime(article.published_at)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</a>
|
|
|
|
|
);
|
|
|
|
|
}
|