Revise how recent articles are displayed on the homepage

Update the featured articles section to a more visually appealing layout, featuring a large hero article alongside two smaller articles.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9da52f08-4599-494b-8f01-1b9c67fcabeb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/413891e8-d784-4bea-b9f5-91a5a68316b4/cftwqyT
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
sebastjanartic 2026-02-28 16:41:02 +00:00
parent 7333cd0c52
commit eb49f9a48d

View File

@ -3,63 +3,85 @@ import { Link } from "wouter";
import { type Article } from "@shared/schema";
import { format } from "date-fns";
import { de } from "date-fns/locale";
import { Eye, MessageSquare, ChevronRight, ChevronLeft, Clock } from "lucide-react";
import { Eye, Clock } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import Header from "@/components/header";
import Footer from "@/components/footer";
function FeaturedCarousel({ articles }: { articles: Article[] }) {
const [current, setCurrent] = useState(0);
useEffect(() => {
if (articles.length === 0) return;
const timer = setInterval(() => {
setCurrent((prev) => (prev + 1) % articles.length);
}, 5000);
return () => clearInterval(timer);
}, [articles.length]);
function FeaturedSection({ articles }: { articles: Article[] }) {
if (articles.length === 0) return null;
const hero = articles[0];
const side = articles.slice(1, 3);
return (
<section className="mb-10" data-testid="featured-carousel">
<h2 className="text-primary font-semibold text-lg mb-4" data-testid="text-featured-heading">Neueste Artikel</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{articles.map((article, i) => (
<Link key={article.id} href={`/article/${article.slug}`}>
<div
className={`relative group cursor-pointer rounded-md overflow-hidden ${i === 0 ? "md:row-span-1" : ""}`}
data-testid={`card-featured-${article.id}`}
>
<div className="aspect-video relative">
<img
src={article.coverImage || "/images/article-1.png"}
alt={article.title}
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
<Badge
className="absolute top-3 left-3 text-xs no-default-active-elevate"
variant={article.category === "Star-News" ? "default" : "default"}
>
{article.category}
</Badge>
<div className="absolute bottom-0 left-0 right-0 p-4">
<span className="text-white/70 text-xs">
{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}
</span>
<h3 className="text-white font-semibold text-sm md:text-base mt-1 line-clamp-2">
{article.title}
</h3>
</div>
<section className="mb-12" data-testid="featured-carousel">
<h2 className="text-primary font-semibold text-xl mb-5" data-testid="text-featured-heading">Neueste Artikel</h2>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-5">
<Link href={`/article/${hero.slug}`} className="lg:col-span-3">
<div
className="relative group cursor-pointer rounded-md overflow-hidden h-full"
data-testid={`card-featured-${hero.id}`}
>
<div className="relative h-full min-h-[320px] md:min-h-[420px]">
<img
src={hero.coverImage || "/images/article-1.png"}
alt={hero.title}
className="w-full h-full object-cover absolute inset-0 transition-transform duration-700 group-hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
<Badge className="absolute top-4 left-4 no-default-active-elevate">
{hero.category}
</Badge>
<div className="absolute bottom-0 left-0 right-0 p-5 md:p-7">
<span className="text-white/70 text-sm">
{format(new Date(hero.publishedAt), "d. MMMM yyyy", { locale: de })}
</span>
<h3 className="text-white font-bold text-xl md:text-2xl mt-2 leading-tight line-clamp-3">
{hero.title}
</h3>
<p className="text-white/60 text-sm mt-2 line-clamp-2 max-w-lg">
{hero.excerpt}
</p>
</div>
</div>
</Link>
))}
</div>
</Link>
<div className="lg:col-span-2 grid grid-cols-1 gap-5">
{side.map((article) => (
<Link key={article.id} href={`/article/${article.slug}`}>
<div
className="relative group cursor-pointer rounded-md overflow-hidden"
data-testid={`card-featured-${article.id}`}
>
<div className="relative min-h-[200px]">
<img
src={article.coverImage || "/images/article-1.png"}
alt={article.title}
className="w-full h-full object-cover absolute inset-0 transition-transform duration-700 group-hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent" />
<Badge className="absolute top-4 left-4 no-default-active-elevate">
{article.category}
</Badge>
<div className="absolute bottom-0 left-0 right-0 p-4 md:p-5">
<span className="text-white/70 text-xs">
{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}
</span>
<h3 className="text-white font-semibold text-base mt-1 leading-snug line-clamp-2">
{article.title}
</h3>
</div>
</div>
</div>
</Link>
))}
</div>
</div>
</section>
);
@ -69,33 +91,33 @@ function ArticleCard({ article }: { article: Article }) {
return (
<Link href={`/article/${article.slug}`}>
<article
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300"
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full flex flex-col"
data-testid={`card-article-${article.id}`}
>
<div className="relative overflow-hidden rounded-t-md">
<img
src={article.coverImage || "/images/article-1.png"}
alt={article.title}
className="w-full aspect-video object-cover transition-transform duration-500 group-hover:scale-105"
className="w-full aspect-[16/10] object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
<Badge className="absolute top-3 left-3 text-xs no-default-active-elevate">
{article.category}
</Badge>
</div>
<div className="p-4">
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-2">
<div className="p-5 flex flex-col flex-1">
<div className="flex items-center gap-2 text-muted-foreground text-xs mb-3">
<span>{article.author}</span>
<span>·</span>
<span>{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}</span>
</div>
<h3 className="font-semibold text-card-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors">
<h3 className="font-semibold text-card-foreground mb-2 line-clamp-2 group-hover:text-primary transition-colors text-base leading-snug">
{article.title}
</h3>
<p className="text-muted-foreground text-sm line-clamp-3 mb-3">
<p className="text-muted-foreground text-sm line-clamp-3 mb-4 flex-1">
{article.excerpt}
</p>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center justify-between gap-2 mt-auto">
<div className="flex items-center gap-3 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Eye className="w-3.5 h-3.5" />
@ -114,25 +136,25 @@ function ArticleCard({ article }: { article: Article }) {
function PopularSidebar({ articles }: { articles: Article[] }) {
return (
<aside className="bg-card rounded-md border border-card-border p-5" data-testid="sidebar-popular">
<h3 className="font-semibold text-card-foreground mb-4 text-base flex items-center justify-between gap-1">
<aside className="bg-card rounded-md border border-card-border p-5 sticky top-20" data-testid="sidebar-popular">
<h3 className="font-semibold text-card-foreground mb-5 text-base">
Beliebte Artikel
</h3>
<div className="space-y-4">
<div className="space-y-5">
{articles.map((article, index) => (
<Link key={article.id} href={`/article/${article.slug}`}>
<div
className="flex gap-3 cursor-pointer group"
className="flex gap-4 cursor-pointer group"
data-testid={`card-popular-${article.id}`}
>
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-primary flex items-center justify-center text-primary-foreground font-bold text-sm">
{index + 1}
</div>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium text-card-foreground line-clamp-2 group-hover:text-primary transition-colors">
<h4 className="text-sm font-medium text-card-foreground line-clamp-2 group-hover:text-primary transition-colors leading-snug">
{article.title}
</h4>
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1.5">
<Clock className="w-3 h-3" />
{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}
</div>
@ -148,8 +170,8 @@ function PopularSidebar({ articles }: { articles: Article[] }) {
function ArticleCardSkeleton() {
return (
<div className="bg-card rounded-md border border-card-border">
<Skeleton className="w-full aspect-video rounded-t-md" />
<div className="p-4 space-y-3">
<Skeleton className="w-full aspect-[16/10] rounded-t-md" />
<div className="p-5 space-y-3">
<Skeleton className="h-3 w-2/3" />
<Skeleton className="h-5 w-full" />
<Skeleton className="h-4 w-full" />
@ -172,19 +194,17 @@ export default function Home() {
queryKey: ["/api/articles/popular"],
});
const nonFeaturedArticles = articles?.filter((a) => !a.featured) || [];
return (
<div className="min-h-screen bg-background">
<Header />
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{featured && featured.length > 0 && (
<FeaturedCarousel articles={featured} />
<FeaturedSection articles={featured} />
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
<h2 className="text-primary font-semibold text-lg mb-4" data-testid="text-articles-heading">
<h2 className="text-primary font-semibold text-xl mb-5" data-testid="text-articles-heading">
Alle Artikel
</h2>
{isLoading ? (