Add new pages and improve blog content display functionality
Implement new routes for articles, categories, and individual articles. Update the UI to display articles with improved content rendering, including safe HTML and media embeds. Refactor storage to use a database and add image upload capabilities. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 413891e8-d784-4bea-b9f5-91a5a68316b4 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: b96b221e-0ed6-418f-80df-e4670bf5ba4b 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:
@@ -0,0 +1,215 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
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 { 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]);
|
||||
|
||||
if (articles.length === 0) return null;
|
||||
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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"
|
||||
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"
|
||||
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">
|
||||
<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">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm line-clamp-3 mb-3">
|
||||
{article.excerpt}
|
||||
</p>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<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" />
|
||||
{article.views.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
||||
Weiterlesen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
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">
|
||||
Beliebte Artikel
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
{articles.map((article, index) => (
|
||||
<Link key={article.id} href={`/article/${article.slug}`}>
|
||||
<div
|
||||
className="flex gap-3 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">
|
||||
{article.title}
|
||||
</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{format(new Date(article.publishedAt), "d. MMMM yyyy", { locale: de })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
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="h-3 w-2/3" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const { data: articles, isLoading } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles"],
|
||||
});
|
||||
|
||||
const { data: featured } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles/featured"],
|
||||
});
|
||||
|
||||
const { data: popular } = useQuery<Article[]>({
|
||||
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} />
|
||||
)}
|
||||
|
||||
<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">
|
||||
Alle Artikel
|
||||
</h2>
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<ArticleCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{(articles || []).map((article) => (
|
||||
<ArticleCard key={article.id} article={article} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-1">
|
||||
{popular && popular.length > 0 && (
|
||||
<PopularSidebar articles={popular} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user