Add pagination to article categories and video pages, and enhance home page with more content and ads
Implement paginated API endpoints for articles and videos, add pagination UI to category and video pages, and inject additional ads and content widgets into the home page. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: c201f10b-bbd3-4402-9212-e0b79bfa670f Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/EtK2Sno Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
365161a381
commit
3d672766b8
@ -15,6 +15,8 @@
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href="https://www.folx.tv/" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||
<link rel="shortcut icon" href="/favicon.png" />
|
||||
<script async src="https://fundingchoicesmessages.google.com/i/pub-4465464714854276?ers=1" nonce=""></script>
|
||||
<script nonce="">(function() {function signalGooglefcPresent() {if (!window.frames['googlefcPresent']) {if (document.body) {const iframe = document.createElement('iframe'); iframe.style = 'width: 0; height: 0; border: none; z-index: -1000; left: -1000px; top: -1000px;'; iframe.style.display = 'none'; iframe.name = 'googlefcPresent'; iframe.id = 'googlefcPresent'; document.body.appendChild(iframe);} else {setTimeout(signalGooglefcPresent, 0);}}}signalGooglefcPresent();})();</script>
|
||||
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4465464714854276" crossorigin="anonymous"></script>
|
||||
|
||||
@ -1,24 +1,115 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams, Link } from "wouter";
|
||||
import { useParams, Link, useLocation, useSearch } from "wouter";
|
||||
import { type Article } from "@shared/schema";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { Eye, ArrowLeft } from "lucide-react";
|
||||
import { Eye, ArrowLeft, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { ArticleCardAd } from "@/components/adsense";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface PaginatedResponse {
|
||||
articles: Article[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
export default function CategoryPage() {
|
||||
const { category } = useParams<{ category: string }>();
|
||||
usePageMeta(`${category} - Volksmusik & Schlager`, `Aktuelle ${category}-Beiträge aus der Volksmusik- und Schlagerszene bei FOLX TV.`);
|
||||
const searchString = useSearch();
|
||||
const [, setLocation] = useLocation();
|
||||
|
||||
const { data: articles, isLoading } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles/category", category],
|
||||
const searchParams = new URLSearchParams(searchString);
|
||||
const currentPage = Math.max(1, parseInt(searchParams.get("page") || "1"));
|
||||
|
||||
usePageMeta(
|
||||
`${category}${currentPage > 1 ? ` – Seite ${currentPage}` : ""} - Volksmusik & Schlager`,
|
||||
`Aktuelle ${category}-Beiträge aus der Volksmusik- und Schlagerszene bei FOLX TV.`
|
||||
);
|
||||
|
||||
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||
queryKey: ["/api/articles/category", category, { page: currentPage }],
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`/api/articles/category/${category}?page=${currentPage}&limit=12`);
|
||||
if (!res.ok) throw new Error("Failed to fetch articles");
|
||||
return res.json();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [currentPage]);
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
if (page === 1) {
|
||||
setLocation(`/category/${category}`);
|
||||
} else {
|
||||
setLocation(`/category/${category}?page=${page}`);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPagination = () => {
|
||||
if (!data || data.totalPages <= 1) return null;
|
||||
|
||||
const pages: (number | string)[] = [];
|
||||
const total = data.totalPages;
|
||||
const current = data.page;
|
||||
|
||||
pages.push(1);
|
||||
if (current > 3) pages.push("...");
|
||||
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
if (current < total - 2) pages.push("...");
|
||||
if (total > 1) pages.push(total);
|
||||
|
||||
return (
|
||||
<nav className="flex items-center justify-center gap-1 mt-10" data-testid="pagination-nav">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(current - 1)}
|
||||
disabled={current <= 1}
|
||||
data-testid="button-prev-page"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
{pages.map((p, i) =>
|
||||
typeof p === "string" ? (
|
||||
<span key={`ellipsis-${i}`} className="px-2 text-muted-foreground">
|
||||
{p}
|
||||
</span>
|
||||
) : (
|
||||
<Button
|
||||
key={p}
|
||||
variant={p === current ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => goToPage(p)}
|
||||
data-testid={`button-page-${p}`}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(current + 1)}
|
||||
disabled={!data.hasMore}
|
||||
data-testid="button-next-page"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
@ -30,9 +121,17 @@ export default function CategoryPage() {
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-category-title">
|
||||
{category}
|
||||
</h1>
|
||||
<div className="flex items-center justify-between gap-2 mb-6 flex-wrap">
|
||||
<h1 className="text-2xl font-bold text-foreground" data-testid="text-category-title">
|
||||
{category}
|
||||
</h1>
|
||||
{data && data.total > 0 && (
|
||||
<span className="text-sm text-muted-foreground" data-testid="text-article-count">
|
||||
{data.total} Beiträge
|
||||
{data.totalPages > 1 && ` · Seite ${data.page} von ${data.totalPages}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
@ -47,57 +146,60 @@ export default function CategoryPage() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : articles && articles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{articles.flatMap((article, index) => {
|
||||
const items = [
|
||||
<Link key={article.id} href={`/article/${article.slug}`}>
|
||||
<article
|
||||
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full"
|
||||
data-testid={`card-article-${article.id}`}
|
||||
>
|
||||
<div className="relative rounded-t-md">
|
||||
<div className="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"
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
loading="lazy"
|
||||
/>
|
||||
) : data && data.articles.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data.articles.flatMap((article, index) => {
|
||||
const items = [
|
||||
<Link key={article.id} href={`/article/${article.slug}`}>
|
||||
<article
|
||||
className="group cursor-pointer bg-card rounded-md border border-card-border transition-all duration-300 h-full"
|
||||
data-testid={`card-article-${article.id}`}
|
||||
>
|
||||
<div className="relative rounded-t-md">
|
||||
<div className="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"
|
||||
style={{ objectPosition: "center 25%" }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</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 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">
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{article.views.toLocaleString()}
|
||||
</span>
|
||||
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
||||
Weiterlesen
|
||||
</Button>
|
||||
</div>
|
||||
</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">
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
{article.views.toLocaleString()}
|
||||
</span>
|
||||
<Button size="sm" data-testid={`button-read-${article.id}`}>
|
||||
Weiterlesen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</Link>,
|
||||
];
|
||||
if (index === 2) {
|
||||
items.push(<ArticleCardAd key="ad-cat-1" />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
</article>
|
||||
</Link>,
|
||||
];
|
||||
if ((index + 1) % 3 === 0) {
|
||||
items.push(<ArticleCardAd key={`ad-cat-${index}`} />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
{renderPagination()}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground text-lg">
|
||||
|
||||
@ -4,6 +4,7 @@ import GalleryPage from "@/components/photo-gallery";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Link } from "wouter";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { InArticleAd } from "@/components/adsense";
|
||||
|
||||
export default function GalleryPageWrapper() {
|
||||
usePageMeta("Fotogalerie - Volksmusik & Schlager Bilder", "Fotos und Bilder von Volksmusik- und Schlager-Stars bei FOLX TV.");
|
||||
@ -20,7 +21,9 @@ export default function GalleryPageWrapper() {
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-gallery-title">
|
||||
Fotogalerie
|
||||
</h1>
|
||||
<InArticleAd />
|
||||
<GalleryPage />
|
||||
<InArticleAd />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
@ -8,7 +8,7 @@ import { Eye, Play, Images } from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import AdSense, { ArticleCardAd, MultiplexAd, SidebarAd } from "@/components/adsense";
|
||||
import AdSense, { ArticleCardAd, InArticleAd, MultiplexAd, SidebarAd } from "@/components/adsense";
|
||||
import ArtistPatternBg from "@/components/artist-pattern-bg";
|
||||
import { PhotoGalleryWidget } from "@/components/photo-gallery";
|
||||
import { HoroscopeWidget } from "@/components/horoscope-widget";
|
||||
@ -516,6 +516,10 @@ export default function Home() {
|
||||
{ id: "recipe", el: <RecipeWidget key="recipe" /> },
|
||||
{ id: "breaking", el: <div key="breaking" className="flex flex-col gap-4"><BreakingNewsWidget /></div> },
|
||||
{ id: "gallery2", el: <PhotoGalleryWidget key="gallery2" reverseOrder={true} /> },
|
||||
{ id: "horoscope2", el: <HoroscopeWidget key="horoscope2" /> },
|
||||
{ id: "news2", el: <div key="news2" className="flex flex-col gap-4"><NewsWidget /></div> },
|
||||
{ id: "recipe2", el: <RecipeWidget key="recipe2" /> },
|
||||
{ id: "breaking2", el: <div key="breaking2" className="flex flex-col gap-4"><BreakingNewsWidget /></div> },
|
||||
], []);
|
||||
|
||||
const gridItems = useMemo(() => {
|
||||
@ -546,7 +550,7 @@ export default function Home() {
|
||||
}
|
||||
|
||||
const totalRows = items.length / 4;
|
||||
const adRows = [1, 3, 5, 7];
|
||||
const adRows = [1, 3, 5, 7, 9, 11, 13];
|
||||
let adCount = 0;
|
||||
for (const row of adRows) {
|
||||
if (row >= totalRows) continue;
|
||||
@ -599,6 +603,16 @@ export default function Home() {
|
||||
return articles.filter((a) => !usedIds.has(a.id)).slice(0, 3);
|
||||
}, [articles, widePickedArticles]);
|
||||
|
||||
const extraBottomArticles = useMemo(() => {
|
||||
if (!articles || articles.length < 15) return [];
|
||||
const usedIds = new Set([
|
||||
...articles.slice(0, 9).map((a) => a.id),
|
||||
...widePickedArticles.map((a) => a.id),
|
||||
...bottomArticles.map((a) => a.id),
|
||||
]);
|
||||
return articles.filter((a) => !usedIds.has(a.id)).slice(0, 6);
|
||||
}, [articles, widePickedArticles, bottomArticles]);
|
||||
|
||||
const bottomSection = useMemo(() => {
|
||||
const items: { type: "widget" | "ad" | "article"; el: JSX.Element }[] = [
|
||||
{ type: "widget", el: <NewsWidget key="bottom-news" /> },
|
||||
@ -611,9 +625,14 @@ export default function Home() {
|
||||
})),
|
||||
{ type: "ad", el: <div key="ad-bottom-1"><ArticleCardAd /></div> },
|
||||
{ type: "widget", el: <PhotoGalleryWidget key="bottom-gallery" /> },
|
||||
{ type: "ad", el: <div key="ad-bottom-2"><ArticleCardAd /></div> },
|
||||
...extraBottomArticles.slice(0, 3).map((a) => ({
|
||||
type: "article" as const,
|
||||
el: <BlogCard key={`bottom-extra-${a.id}`} article={a} focalPoints={focalPoints} />,
|
||||
})),
|
||||
];
|
||||
return items;
|
||||
}, [bottomArticles, focalPoints]);
|
||||
}, [bottomArticles, extraBottomArticles, focalPoints]);
|
||||
|
||||
if (isLoading || !articles) {
|
||||
return (
|
||||
@ -661,6 +680,8 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<InArticleAd />
|
||||
|
||||
{widePickedArticles.length > 0 && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<ArtistPatternBg className="hidden lg:block rounded-lg overflow-hidden bg-card border border-card-border" seed={99}>
|
||||
@ -683,25 +704,30 @@ export default function Home() {
|
||||
)}
|
||||
|
||||
{gridRows.map((row, ri) => (
|
||||
<div key={`row-${ri}`} className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{row.map((item) =>
|
||||
item.type === "widget"
|
||||
? <div key={item.key}>{item.widget!.el}</div>
|
||||
: item.type === "ad"
|
||||
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
|
||||
: item.article
|
||||
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
|
||||
: null
|
||||
)}
|
||||
{ri === gridRows.length - 1 && widePickedArticles.length > 0 && (
|
||||
<div className="sm:col-span-2 lg:col-span-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<WideCardClassic article={widePickedArticles[0]} focalPoints={focalPoints} />
|
||||
{widePickedArticles[1] && <WideCardClassic article={widePickedArticles[1]} focalPoints={focalPoints} />}
|
||||
</div>
|
||||
)}
|
||||
<div key={`row-group-${ri}`}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{row.map((item) =>
|
||||
item.type === "widget"
|
||||
? <div key={item.key}>{item.widget!.el}</div>
|
||||
: item.type === "ad"
|
||||
? <div key={item.key} className="h-full" data-testid={`ad-grid-${item.key}`}><ArticleCardAd /></div>
|
||||
: item.article
|
||||
? <MediumCard key={item.key} article={item.article} focalPoints={focalPoints} />
|
||||
: null
|
||||
)}
|
||||
{ri === gridRows.length - 1 && widePickedArticles.length > 0 && (
|
||||
<div className="sm:col-span-2 lg:col-span-4 grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<WideCardClassic article={widePickedArticles[0]} focalPoints={focalPoints} />
|
||||
{widePickedArticles[1] && <WideCardClassic article={widePickedArticles[1]} focalPoints={focalPoints} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{ri % 3 === 2 && ri < gridRows.length - 1 && <InArticleAd />}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<InArticleAd />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{bottomSection.map((item, i) => (
|
||||
<div key={`bottom-${i}`}>
|
||||
@ -710,6 +736,24 @@ export default function Home() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<InArticleAd />
|
||||
|
||||
{extraBottomArticles.length > 3 && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{extraBottomArticles.slice(3, 6).map((a) => (
|
||||
<BlogCard key={`extra-bottom-${a.id}`} article={a} focalPoints={focalPoints} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MultiplexAd />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<PhotoGalleryWidget key="extra-gallery" />
|
||||
<NewsWidget key="extra-news" />
|
||||
<RecipeWidget key="extra-recipe" />
|
||||
</div>
|
||||
|
||||
<MultiplexAd />
|
||||
|
||||
</main>
|
||||
|
||||
@ -420,6 +420,8 @@ export default function HoroscopePage() {
|
||||
|
||||
<AstroEventsSection />
|
||||
|
||||
<InArticleAd />
|
||||
|
||||
<SignGrid onSelect={handleSelect} selectedIndex={selected} aiHoroscopes={aiHoroscopes} />
|
||||
|
||||
<div ref={detailRef}>
|
||||
|
||||
@ -350,7 +350,7 @@ export default function RecipesPage() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{ri % 2 === 0 && <InArticleAd />}
|
||||
<InArticleAd />
|
||||
</div>
|
||||
))}
|
||||
</main>
|
||||
|
||||
@ -1,14 +1,24 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "wouter";
|
||||
import { Link, useLocation, useSearch } from "wouter";
|
||||
import { type Article } from "@shared/schema";
|
||||
import { format } from "date-fns";
|
||||
import { de } from "date-fns/locale";
|
||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||
import { Play, ArrowLeft } from "lucide-react";
|
||||
import { Play, ArrowLeft, ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import Header from "@/components/header";
|
||||
import Footer from "@/components/footer";
|
||||
import { ArticleCardAd } from "@/components/adsense";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface PaginatedResponse {
|
||||
articles: Article[];
|
||||
total: number;
|
||||
page: number;
|
||||
totalPages: number;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
function VideoCard({ article }: { article: Article }) {
|
||||
const thumbSrc = article.coverImage
|
||||
@ -63,10 +73,48 @@ function VideoCardSkeleton() {
|
||||
|
||||
export default function VideosPage() {
|
||||
usePageMeta("Volksmusik & Schlager Videos", "Musikvideos und Live-Auftritte aus der Volksmusik- und Schlagerszene bei FOLX TV.");
|
||||
const { data: articles, isLoading } = useQuery<Article[]>({
|
||||
queryKey: ["/api/articles/category/Video"],
|
||||
|
||||
const searchString = useSearch();
|
||||
const [, setLocation] = useLocation();
|
||||
const params = new URLSearchParams(searchString);
|
||||
const currentPage = Math.max(1, parseInt(params.get("page") || "1"));
|
||||
const limit = 12;
|
||||
|
||||
const { data, isLoading } = useQuery<PaginatedResponse>({
|
||||
queryKey: [`/api/articles/category/Video?page=${currentPage}&limit=${limit}`],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
}, [currentPage]);
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
if (page === 1) {
|
||||
setLocation("/videos");
|
||||
} else {
|
||||
setLocation(`/videos?page=${page}`);
|
||||
}
|
||||
};
|
||||
|
||||
const totalPages = data?.totalPages || 1;
|
||||
const articles = data?.articles || [];
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages: (number | "...")[] = [];
|
||||
if (totalPages <= 7) {
|
||||
for (let i = 1; i <= totalPages; i++) pages.push(i);
|
||||
} else {
|
||||
pages.push(1);
|
||||
if (currentPage > 3) pages.push("...");
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
for (let i = start; i <= end; i++) pages.push(i);
|
||||
if (currentPage < totalPages - 2) pages.push("...");
|
||||
pages.push(totalPages);
|
||||
}
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
@ -78,24 +126,70 @@ export default function VideosPage() {
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<h1 className="text-2xl font-bold text-foreground mb-6" data-testid="text-videos-title">
|
||||
Videos
|
||||
</h1>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<VideoCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
) : articles && articles.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
|
||||
{articles.flatMap((article, index) => {
|
||||
const items = [
|
||||
<VideoCard key={article.id} article={article} />,
|
||||
];
|
||||
if (index === 2) {
|
||||
items.push(<ArticleCardAd key="ad-video-1" />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
) : articles.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-5">
|
||||
{articles.flatMap((article, index) => {
|
||||
const items: JSX.Element[] = [
|
||||
<VideoCard key={article.id} article={article} />,
|
||||
];
|
||||
if ((index + 1) % 3 === 0) {
|
||||
items.push(<ArticleCardAd key={`ad-video-${index}`} />);
|
||||
}
|
||||
return items;
|
||||
})}
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<nav className="flex items-center justify-center gap-1 mt-10" data-testid="pagination-videos">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage <= 1}
|
||||
data-testid="button-prev-page"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
{getPageNumbers().map((p, i) =>
|
||||
p === "..." ? (
|
||||
<span key={`ellipsis-${i}`} className="px-2 text-muted-foreground">...</span>
|
||||
) : (
|
||||
<Button
|
||||
key={p}
|
||||
variant={p === currentPage ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => goToPage(p as number)}
|
||||
data-testid={`button-page-${p}`}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage >= totalPages}
|
||||
data-testid="button-next-page"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground">Noch keine Videos vorhanden.</p>
|
||||
|
||||
34
replit.md
34
replit.md
@ -85,6 +85,40 @@ The official website for Folx Music Television (folx.tv). Dark-themed bento grid
|
||||
- Dropbox: Gallery image thumbnails (547 images from 16 folders)
|
||||
- Google News RSS: Volksmusik/Schlager news feed
|
||||
|
||||
## Publishing Workflow
|
||||
|
||||
### Adding or Updating Articles
|
||||
1. **Edit seed data**: Update `server/seed.ts` with new article data
|
||||
- Add new articles to the `articlesData` array
|
||||
- Update existing articles as needed
|
||||
- Use unique slugs for SEO-friendly URLs
|
||||
2. **Run seed locally** (for development testing):
|
||||
- Execute: `npm run seed`
|
||||
- This populates the development database with article data
|
||||
3. **Commit changes**: Push code changes to the repository
|
||||
4. **Deploy to production**:
|
||||
- Deployment is configured as "autoscale" type in `.replit`
|
||||
- Build command: `npm run build`
|
||||
- Run command: `node ./dist/index.cjs`
|
||||
- The production database is separate from the development database
|
||||
- **Important**: Seed must run on every deploy to ensure the production database is updated with new/modified articles
|
||||
- To run seed on deploy, add seed execution to the deployment process or manually trigger seed after deployment
|
||||
|
||||
### Database Management
|
||||
- **Development**: Local PostgreSQL database (populated by `npm run seed`)
|
||||
- **Production**: Separate PostgreSQL database on Replit deployment
|
||||
- All article content is hardcoded in `server/seed.ts` for reproducible deployments
|
||||
- The `DATABASE_URL` environment variable automatically points to the correct database (local for dev, production for deploy)
|
||||
|
||||
### Deployment Checklist
|
||||
- [ ] Update `server/seed.ts` with new article data
|
||||
- [ ] Test locally: run `npm run seed` and verify articles appear in dev
|
||||
- [ ] Run `npm run build` to ensure no build errors
|
||||
- [ ] Commit all changes
|
||||
- [ ] Deploy via Replit deployment interface
|
||||
- [ ] After deployment, ensure seed runs on the production database (may require manual trigger or additional setup)
|
||||
- [ ] Verify live site at https://www.folx.tv shows updated content
|
||||
|
||||
## Important Notes
|
||||
- Tailwind `object-[center_25%]` does NOT work — must use inline `style={{ objectPosition: "center 25%" }}`
|
||||
- Horoscope widget navigates to /horoskop on click (no modal)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -86,6 +86,19 @@ export async function registerRoutes(
|
||||
});
|
||||
|
||||
app.get("/api/articles/category/:category", async (req, res) => {
|
||||
const page = parseInt(req.query.page as string);
|
||||
const limit = parseInt(req.query.limit as string) || 12;
|
||||
if (page && page > 0) {
|
||||
const result = await storage.getArticlesByCategoryPaginated(req.params.category, page, limit);
|
||||
const totalPages = Math.ceil(result.total / limit);
|
||||
return res.json({
|
||||
articles: result.articles,
|
||||
total: result.total,
|
||||
page,
|
||||
totalPages,
|
||||
hasMore: page < totalPages,
|
||||
});
|
||||
}
|
||||
const articles = await storage.getArticlesByCategory(req.params.category);
|
||||
res.json(articles);
|
||||
});
|
||||
|
||||
338
server/seed.ts
338
server/seed.ts
@ -189,6 +189,336 @@ const seedArticles = [
|
||||
"featured": true,
|
||||
"publishedAt": "2026-03-04T14:30:00.000Z",
|
||||
"content": "<p>Nach mehr als drei Jahrzehnten gemeinsamer B\u00fchnenkarriere ist f\u00fcr das bekannte Volksmusik-Duo <strong>Wildecker Herzbuben</strong> ein Kapitel zu Ende gegangen. Die beiden S\u00e4nger <strong>Wolfgang Schwalm</strong> und <strong>Wilfried Gliem</strong> haben ihre gemeinsame musikalische Laufbahn beendet. In den letzten Jahren machten gesundheitliche Probleme von Wilfried Gliem gemeinsame Auftritte zunehmend schwieriger, weshalb sich die beiden schlie\u00dflich entschieden haben, getrennte Wege zu gehen.</p>\n<div style=\"position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;margin:1.5rem 0;\"><iframe style=\"position:absolute;top:0;left:0;width:100%;height:100%;border:0;border-radius:8px;\" src=\"https://www.youtube.com/embed/1-s6pPNRZ1U\" title=\"Wildecker Herzbuben\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>\n<h3>Ein Duo, das Geschichte schrieb</h3>\n<p>Die Wildecker Herzbuben pr\u00e4gten \u00fcber viele Jahre die deutschsprachige Volksmusik- und Schlagerszene. Besonders ihr gro\u00dfer Hit <em>\u201eHerzilein\u201c</em> aus dem Jahr 1989 machte das Duo weit \u00fcber die Grenzen Deutschlands hinaus bekannt. \u00dcber Jahrzehnte hinweg waren sie regelm\u00e4\u00dfig in Fernsehsendungen zu sehen und auf zahlreichen B\u00fchnen im deutschsprachigen Raum unterwegs.</p>\n<h3>\u201eDer Herzbube Wolfgang\u201c \u2013 ein Neuanfang</h3>\n<p>Doch f\u00fcr Wolfgang Schwalm bedeutet das Ende des Duos keineswegs das Ende seiner musikalischen Laufbahn. Er m\u00f6chte weiterhin auf der B\u00fchne stehen und seine Karriere fortsetzen. Unter dem Namen <strong>\u201eDer Herzbube Wolfgang\u201c</strong> plant er neue Auftritte und musikalische Projekte.</p>\n<h3>Neue Zusammenarbeit in Sicht</h3>\n<p>Dabei k\u00f6nnte es k\u00fcnftig auch eine neue Zusammenarbeit geben. Wolfgang Schwalm denkt dar\u00fcber nach, gemeinsam mit dem bekannten Fernsehkoch <strong>Mirko Reeh</strong> aufzutreten. Erste Gespr\u00e4che \u00fcber m\u00f6gliche gemeinsame Konzerte haben bereits stattgefunden, und beide k\u00f6nnen sich vorstellen, k\u00fcnftig auch zusammen auf der B\u00fchne zu stehen.</p>\n<p>F\u00fcr viele Fans der Volksmusik ist das eine erfreuliche Nachricht: Auch wenn das legend\u00e4re Duo nicht mehr gemeinsam auftritt, bleibt Wolfgang Schwalm der Musik treu \u2013 und die beliebten Lieder der Wildecker Herzbuben k\u00f6nnten so weiterhin live zu h\u00f6ren sein.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Kastelruther Spatzen: R\u00fcckblick auf eine unvergessliche Tournee",
|
||||
"slug": "kastelruther-spatzen-rueckblick-tournee-2024",
|
||||
"excerpt": "Ein R\u00fcckblick auf die beeindruckende Tournee der Kastelruther Spatzen. Die Highlights, die sch\u00f6nsten Momente und die Reaktionen der Fans.",
|
||||
"category": "News",
|
||||
"author": "Folx TV Redaktion",
|
||||
"coverImage": "/uploads/kastelruther-spatzen.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-07-10T10:00:00.000Z",
|
||||
"content": "<p>Die Kastelruther Spatzen haben mit ihrer Tournee erneut bewiesen, warum sie zu den beliebtesten Volksmusikgruppen z\u00e4hlen. Ein R\u00fcckblick auf die sch\u00f6nsten Momente und Highlights der vergangenen Konzertreihe.</p><p>Von M\u00fcnchen bis Freiburg begeisterten die Spatzen tausende Fans mit ihrem einzigartigen Mix aus Schlager und Volksmusik. Die Atmosphäre bei jedem Konzert war elektrisierend.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Kastelruther Spatzen Spezial: Die besten Hits aller Zeiten",
|
||||
"slug": "kastelruther-spatzen-spezial-beste-hits",
|
||||
"excerpt": "Eine Zusammenstellung der gr\u00f6\u00dften Hits der Kastelruther Spatzen. Von den Anf\u00e4ngen bis heute \u2013 die Lieder, die Generationen begeistern.",
|
||||
"category": "News",
|
||||
"author": "Folx TV Redaktion",
|
||||
"coverImage": "/uploads/kastelruther-spatzen.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-06-15T10:00:00.000Z",
|
||||
"content": "<p>Die Kastelruther Spatzen haben \u00fcber die Jahre zahlreiche Hits produziert, die aus der Volksmusikszene nicht mehr wegzudenken sind. In diesem Spezial werfen wir einen Blick auf die besten Songs ihrer Karriere.</p><p>Von ihren fr\u00fchen Erfolgen bis zu den neuesten Ver\u00f6ffentlichungen \u2013 jeder Song erz\u00e4hlt eine Geschichte.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Sa\u0161o Avsenik: Update zur neuen CD-Ver\u00f6ffentlichung",
|
||||
"slug": "saso-avsenik-update-neue-cd",
|
||||
"excerpt": "Sa\u0161o Avsenik und seine Oberkrainer pr\u00e4sentieren ihre neueste CD. Ein Update zu den aktuellen Projekten und kommenden Konzerten.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/saso-avsenik.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-07-05T10:00:00.000Z",
|
||||
"content": "<p>Sa\u0161o Avsenik und seine Oberkrainer haben eine neue CD ver\u00f6ffentlicht, die die Tradition der Oberkrainer Musik in die Moderne tr\u00e4gt. Mit schwungvollen Polkas und herzlichen Melodien begeistern sie erneut ihre Fans.</p><p>Die neuen Kompositionen verbinden traditionelle Klangwelten mit frischen Arrangements.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Sa\u0161o Avsenik Teil 2: Die Oberkrainer-Tradition lebt weiter",
|
||||
"slug": "saso-avsenik-teil-2-oberkrainer-tradition",
|
||||
"excerpt": "Im zweiten Teil unserer Serie \u00fcber Sa\u0161o Avsenik erfahren Sie mehr \u00fcber die Zukunftspl\u00e4ne des Ensembles und die Bedeutung der Oberkrainer-Tradition.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/saso-avsenik.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-06-20T10:00:00.000Z",
|
||||
"content": "<p>Sa\u0161o Avsenik f\u00fchrt das Erbe seines Gro\u00dfvaters Slavko Avsenik mit Leidenschaft und Hingabe weiter. In diesem zweiten Teil erfahren Sie, wie er die Oberkrainer-Tradition in die Zukunft tr\u00e4gt.</p><p>Mit Auftritten in ganz Europa und einer wachsenden Fangemeinde bleibt das Ensemble eine feste Gr\u00f6\u00dfe in der Volksmusikszene.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl Spezial: Die besten Momente der Staffel",
|
||||
"slug": "folx-stadl-spezial-beste-momente-staffel",
|
||||
"excerpt": "Ein Spezial mit den unvergesslichsten Momenten aus der aktuellen Folx-Stadl-Staffel. Die Highlights, die lustigsten Sketche und die sch\u00f6nsten Auftritte.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-22.webp",
|
||||
"featured": true,
|
||||
"publishedAt": "2025-07-15T10:00:00.000Z",
|
||||
"content": "<p>Die aktuelle Staffel des <strong>Folx Stadl</strong> hatte so viele unvergessliche Momente, dass wir die besten f\u00fcr Sie zusammengestellt haben. Von humorvollen Sketchen bis zu emotionalen Musikmomenten.</p><p>Lehnen Sie sich zur\u00fcck und genie\u00dfen Sie die Highlights der Staffel.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Kaiser: R\u00fcckblick auf die Fanreise nach Kroatien",
|
||||
"slug": "die-kaiser-rueckblick-fanreise-kroatien",
|
||||
"excerpt": "Die Kaiser nahmen ihre Fans mit auf eine unvergessliche Reise nach Kroatien. Ein R\u00fcckblick auf die sch\u00f6nsten Momente der j\u00e4hrlichen Fanreise.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-kaiser.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-06-25T10:00:00.000Z",
|
||||
"content": "<p>Die j\u00e4hrliche Fanreise der Kaiser nach Kroatien war erneut ein voller Erfolg. Die Fans erlebten unvergessliche Tage voller Musik, Sonnenschein und Gemeinschaft.</p><p>Charly Kaiser und seine Band sorgten f\u00fcr beste Unterhaltung an der kroatischen K\u00fcste.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Kaiser Teil 2: Neue Songs und Tourpl\u00e4ne",
|
||||
"slug": "die-kaiser-teil-2-neue-songs-tourplaene",
|
||||
"excerpt": "Die Kaiser arbeiten an neuen Songs und haben ambitionierte Tourpl\u00e4ne f\u00fcr die kommende Saison. Ein Einblick in die Zukunft des beliebten Sextetts.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-kaiser.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-05-30T10:00:00.000Z",
|
||||
"content": "<p>Die Kaiser sind voller Tatendrang und arbeiten bereits an neuen Songs f\u00fcr ihre Fans. Die kommende Saison verspricht aufregende Konzerte und frische Musik.</p><p>Mit ihrem bew\u00e4hrten Mix aus Herzlichkeit und professionellem Sound bleiben sie eine feste Gr\u00f6\u00dfe.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl \u2013 Sendung 20: R\u00fcckblick und Highlights",
|
||||
"slug": "folx-stadl-sendung-20-rueckblick-highlights",
|
||||
"excerpt": "Die wichtigsten Highlights und sch\u00f6nsten Momente der 20. Folx-Stadl-Sendung zusammengefasst. Ein R\u00fcckblick auf einen gelungenen Abend.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-20.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-05-20T10:00:00.000Z",
|
||||
"content": "<p>Die 20. Ausgabe des Folx Stadl war ein voller Erfolg. In diesem R\u00fcckblick fassen wir die sch\u00f6nsten Momente und Highlights des Abends zusammen.</p><p>Von bodenst\u00e4ndiger Volksmusik bis zu schwungvollen Schlagermomenten \u2013 es war f\u00fcr jeden etwas dabei.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl \u2013 Sendung 21: Die K\u00fcnstler im Interview",
|
||||
"slug": "folx-stadl-sendung-21-kuenstler-interview",
|
||||
"excerpt": "Die K\u00fcnstler der 21. Folx-Stadl-Sendung im exklusiven Interview. Erfahren Sie mehr \u00fcber ihre Musik, ihre Inspiration und ihre Pl\u00e4ne.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-21.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-05-10T10:00:00.000Z",
|
||||
"content": "<p>In exklusiven Interviews erz\u00e4hlen die K\u00fcnstler der 21. Folx-Stadl-Sendung von ihrer Musik und Inspiration. Von Tauern Echo bis Aileen Sager \u2013 jeder bringt seine eigene Geschichte mit.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Oberkrainer: Eine musikalische Reise durch 70 Jahre",
|
||||
"slug": "oberkrainer-musikalische-reise-70-jahre",
|
||||
"excerpt": "Eine umfassende Dokumentation \u00fcber die 70-j\u00e4hrige Geschichte der Oberkrainer Musik. Von den Anf\u00e4ngen bis zur heutigen Generation.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/oberkrainer-geschichte.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-04-25T10:00:00.000Z",
|
||||
"content": "<p>Eine umfassende Dokumentation \u00fcber 70 Jahre Oberkrainer Musik. Von den Br\u00fcdern Avsenik bis zu den heutigen Interpreten \u2013 eine musikalische Reise durch die Jahrzehnte.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Melanie Payer: Portr\u00e4t einer aufstrebenden K\u00fcnstlerin",
|
||||
"slug": "melanie-payer-portrait-aufstrebende-kuenstlerin",
|
||||
"excerpt": "Ein Portr\u00e4t der talentierten S\u00e4ngerin Melanie Payer. Erfahren Sie mehr \u00fcber ihren Werdegang, ihre Musik und ihre Vision f\u00fcr die Zukunft.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/melanie-payer-gipfelstammtisch.jpg",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-04-15T10:00:00.000Z",
|
||||
"content": "<p><strong>Melanie Payer</strong> ist eine der aufstrebenden K\u00fcnstlerinnen der \u00f6sterreichischen Musikszene. Mit ihrer einzigartigen Stimme und authentischen Art hat sie sich einen festen Platz in der Volksmusik erobert.</p><p>In diesem Portr\u00e4t erfahren Sie mehr \u00fcber ihren Werdegang und ihre musikalische Vision.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Oberkrainer goes royal: Teil 2 \u2013 Die Geschichte hinter dem Auftritt",
|
||||
"slug": "oberkrainer-goes-royal-teil-2-geschichte",
|
||||
"excerpt": "Die vollst\u00e4ndige Geschichte hinter dem legend\u00e4ren Auftritt der Oberkrainer im Buckingham Palace. Hintergr\u00fcnde und bisher unbekannte Details.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/oberkrainer-goes-royal.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-04-05T10:00:00.000Z",
|
||||
"content": "<p>Im zweiten Teil unserer Serie \u00fcber den legend\u00e4ren Auftritt der Oberkrainer im Buckingham Palace erfahren Sie die vollst\u00e4ndige Geschichte \u2013 mit bisher unbekannten Details und Hintergr\u00fcnden.</p><p>Wie kam es dazu, dass die Musik von Slavko Avsenik in den k\u00f6niglichen Hallen erklang?</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Edlseer: Update zu neuen Projekten und Konzerten",
|
||||
"slug": "die-edlseer-update-neue-projekte-konzerte",
|
||||
"excerpt": "Die Edlseer arbeiten an spannenden neuen Projekten. Ein Update zu kommenden Konzerten, neuen Songs und Jubil\u00e4umsfeierlichkeiten.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-edlseer.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-03-25T10:00:00.000Z",
|
||||
"content": "<p>Die Edlseer sind weiterhin voller Energie und arbeiten an neuen Projekten. Mit geplanten Konzerten und frischen Songs versprechen sie ihren Fans ein aufregendes Jahr.</p><p>Das steirische Trio bleibt seiner Linie treu und verbindet Tradition mit Moderne.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Pagger Buam: R\u00fcckblick auf das Festivaljahr",
|
||||
"slug": "die-pagger-buam-rueckblick-festivaljahr",
|
||||
"excerpt": "Ein R\u00fcckblick auf das ereignisreiche Festivaljahr der Pagger Buam. Die sch\u00f6nsten Auftritte, die besten Momente und die Pl\u00e4ne f\u00fcr die Zukunft.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-pagger-buam.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-03-15T10:00:00.000Z",
|
||||
"content": "<p>Die Pagger Buam blicken auf ein ereignisreiches Festivaljahr zur\u00fcck. Von Festivals bis zu intimen Konzerten \u2013 das Trio aus der Weststeiermark war \u00fcberall ein Highlight.</p><p>Auch f\u00fcr die Zukunft haben Mario, Franz und Manfred gro\u00dfe Pl\u00e4ne.</p>"
|
||||
},
|
||||
{
|
||||
"title": "70 Jahre Oberkrainer: Spezial \u2013 Die gr\u00f6\u00dften Hits",
|
||||
"slug": "70-jahre-oberkrainer-spezial-groesste-hits",
|
||||
"excerpt": "Zum Jubil\u00e4um pr\u00e4sentieren wir die gr\u00f6\u00dften Hits der Oberkrainer Musik. Von Trompeten-Echo bis zu den neuesten Klassikern.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/70-jahre-oberkrainer.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-03-05T10:00:00.000Z",
|
||||
"content": "<p>Zum 70-j\u00e4hrigen Jubil\u00e4um der Oberkrainer Musik pr\u00e4sentieren wir eine Zusammenstellung der gr\u00f6\u00dften Hits. Das Trompeten-Echo, Na Golici und viele weitere Klassiker, die die Welt bewegt haben.</p>"
|
||||
},
|
||||
{
|
||||
"title": "John Prisco: R\u00fcckblick und neue Projekte",
|
||||
"slug": "john-prisco-rueckblick-neue-projekte",
|
||||
"excerpt": "John Prisco blickt auf seine bisherige Karriere zur\u00fcck und verr\u00e4t Details zu seinen neuen musikalischen Projekten.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/john-prisco.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-02-20T10:00:00.000Z",
|
||||
"content": "<p><strong>John Prisco</strong> blickt auf eine ereignisreiche Karriere zur\u00fcck und gibt Einblicke in seine kommenden Projekte. Der charismatische K\u00fcnstler hat noch viel vor.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Igor und seine Oberkrainer: Live-Konzert Spezial",
|
||||
"slug": "igor-oberkrainer-live-konzert-spezial",
|
||||
"excerpt": "Erleben Sie Igor und seine Oberkrainer in einem exklusiven Live-Konzert Spezial. Die besten Momente des Abends festgehalten auf Folx TV.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/igor-oberkrainer.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-02-10T10:00:00.000Z",
|
||||
"content": "<p>Ein exklusives Live-Konzert von <strong>Igor und seinen Oberkrainern</strong>. Die besten Momente eines unvergesslichen Abends, festgehalten f\u00fcr alle Fans der Oberkrainer Musik.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl \u2013 Sendung 29: Die K\u00fcnstler und ihre Geschichten",
|
||||
"slug": "folx-stadl-sendung-29-kuenstler-geschichten",
|
||||
"excerpt": "Die K\u00fcnstler der 29. Folx-Stadl-Ausgabe erz\u00e4hlen ihre pers\u00f6nlichen Geschichten. Hinter den Kulissen der beliebten Sendung.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-29.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-01-30T10:00:00.000Z",
|
||||
"content": "<p>Hinter den Kulissen der 29. Folx-Stadl-Ausgabe erz\u00e4hlen die K\u00fcnstler ihre pers\u00f6nlichen Geschichten. Von den Anf\u00e4ngen bis zum gro\u00dfen Auftritt.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Linda Feller: Update \u2013 Neue Tournee angek\u00fcndigt",
|
||||
"slug": "linda-feller-update-neue-tournee",
|
||||
"excerpt": "Linda Feller k\u00fcndigt eine neue Tournee an. Die Country-Pionierin aus Th\u00fcringen bringt ihre Musik in zahlreiche St\u00e4dte.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/images/linda-feller.jpg",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-01-20T10:00:00.000Z",
|
||||
"content": "<p><strong>Linda Feller</strong> hat eine neue Tournee angek\u00fcndigt, die sie durch zahlreiche St\u00e4dte f\u00fchren wird. Die Country-Pionierin freut sich auf die Begegnungen mit ihren Fans.</p><p>Mit einer Mischung aus eigenen Hits und neuen Songs verspricht die Tournee unvergessliche Abende.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Wildecker Herzbuben: R\u00fcckblick auf 30 Jahre Musikgeschichte",
|
||||
"slug": "wildecker-herzbuben-rueckblick-30-jahre",
|
||||
"excerpt": "Ein R\u00fcckblick auf 30 Jahre Wildecker Herzbuben. Die gr\u00f6\u00dften Erfolge, die sch\u00f6nsten Erinnerungen und das Verm\u00e4chtnis des legend\u00e4ren Duos.",
|
||||
"category": "News",
|
||||
"author": "Folx TV Redaktion",
|
||||
"coverImage": "/images/wildecker-herzbuben.jpg",
|
||||
"featured": false,
|
||||
"publishedAt": "2025-01-10T10:00:00.000Z",
|
||||
"content": "<p>Die <strong>Wildecker Herzbuben</strong> pr\u00e4gten \u00fcber 30 Jahre die deutschsprachige Musikszene. Ein R\u00fcckblick auf ihre gr\u00f6\u00dften Erfolge und sch\u00f6nsten Erinnerungen.</p><p>Von Herzilein bis zu ihren letzten gemeinsamen Auftritten \u2013 eine Reise durch die Musikgeschichte.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Edlseer: 30 Jahre auf der B\u00fchne \u2013 Ein Jubil\u00e4umsspezial",
|
||||
"slug": "die-edlseer-30-jahre-buehne-jubilaeumsspezial",
|
||||
"excerpt": "Die Edlseer feiern 30 Jahre auf der B\u00fchne mit einem Jubil\u00e4umsspezial. R\u00fcckblick auf drei Jahrzehnte voller Musik und Erfolg.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-edlseer.webp",
|
||||
"featured": true,
|
||||
"publishedAt": "2024-12-28T10:00:00.000Z",
|
||||
"content": "<p>Die Edlseer feiern ihr 30-j\u00e4hriges B\u00fchnenjubil\u00e4um. In diesem Spezial blicken wir auf drei Jahrzehnte voller Musik, Erfolge und unvergessliche Momente zur\u00fcck.</p><p>Fritz Kristoferitsch, Manfred Maier und Andreas Doppelh\u00f6fer erz\u00e4hlen von ihren sch\u00f6nsten Erlebnissen.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Pagger Buam: Portr\u00e4t \u2013 Vom Nachwuchs zu den Profis",
|
||||
"slug": "die-pagger-buam-portrait-nachwuchs-profis",
|
||||
"excerpt": "Wie aus zwei jungen Cousins und einem erfahrenen Musiker eines der beliebtesten Volksmusik-Trios wurde. Die Geschichte der Pagger Buam.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-pagger-buam.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-12-18T10:00:00.000Z",
|
||||
"content": "<p>Die <strong>Pagger Buam</strong> haben einen beeindruckenden Weg zur\u00fcckgelegt \u2013 vom Nachwuchswettbewerb zum gefeierten Volksmusik-Trio. In diesem Portr\u00e4t erz\u00e4hlen sie ihre Geschichte.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Gipfelstammtisch: Die neue Staffel im \u00dcberblick",
|
||||
"slug": "gipfelstammtisch-neue-staffel-ueberblick",
|
||||
"excerpt": "Die neue Staffel des Gipfelstammtischs auf Folx TV steht in den Startl\u00f6chern. Ein \u00dcberblick \u00fcber die G\u00e4ste, Themen und musikalischen Highlights.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/gipfelstammtisch-2024.webp",
|
||||
"featured": true,
|
||||
"publishedAt": "2024-12-08T10:00:00.000Z",
|
||||
"content": "<p>Die neue Staffel des <strong>Gipfelstammtischs</strong> auf Folx TV verspricht spannende G\u00e4ste und musikalische Highlights. Moderiert von <strong>Wijbrand van der Sande</strong> entf\u00fchrt die Sendung in die Welt der alpenl\u00e4ndischen Volksmusik.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Damiano Maiolini: Der vielseitige Musiker im Gespr\u00e4ch",
|
||||
"slug": "damiano-maiolini-vielseitiger-musiker-gespraech",
|
||||
"excerpt": "Damiano Maiolini ist ein vielseitiger Musiker, der Grenzen zwischen Genres m\u00fchelos \u00fcberschreitet. Ein exklusives Gespr\u00e4ch \u00fcber Musik und Leidenschaft.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/damiano-maiolini.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-11-28T10:00:00.000Z",
|
||||
"content": "<p><strong>Damiano Maiolini</strong> ist ein Musiker, der sich nicht auf ein Genre festlegen l\u00e4sst. In diesem exklusiven Gespr\u00e4ch erz\u00e4hlt er von seiner Leidenschaft f\u00fcr die Musik und seinen vielf\u00e4ltigen Projekten.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Lauser: Frischer Wind in der Volksmusikszene",
|
||||
"slug": "die-lauser-frischer-wind-volksmusikszene",
|
||||
"excerpt": "Die Lauser bringen frischen Wind in die Volksmusikszene. Mit ihrer einzigartigen Mischung aus Tradition und Moderne begeistern sie Fans jeden Alters.",
|
||||
"category": "News",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/die-lauser.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-11-18T10:00:00.000Z",
|
||||
"content": "<p><strong>Die Lauser</strong> sind bekannt f\u00fcr ihren frischen und modernen Zugang zur Volksmusik. Mit energiegeladenen Auftritten und eingängigen Melodien haben sie sich einen festen Platz in der Szene erobert.</p><p>Ihre Mischung aus traditionellen Kl\u00e4ngen und modernen Beats spricht ein breites Publikum an.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl Open Air: Das gro\u00dfe Sommerkonzert",
|
||||
"slug": "folx-stadl-open-air-grosses-sommerkonzert",
|
||||
"excerpt": "Das Folx Stadl Open Air bot einen unvergesslichen Sommerabend voller Musik und guter Laune. Die Highlights des gro\u00dfen Open-Air-Konzerts.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-open-air.webp",
|
||||
"featured": true,
|
||||
"publishedAt": "2024-11-08T10:00:00.000Z",
|
||||
"content": "<p>Das <strong>Folx Stadl Open Air</strong> war ein voller Erfolg. Unter freiem Himmel genossen die Zuschauer einen Abend voller Volksmusik, Schlager und bester Unterhaltung.</p><p>Die Highlights des gro\u00dfen Sommerkonzerts in einer Video-Zusammenfassung.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Folx Stadl Staffel 5: Alle Sendungen im R\u00fcckblick",
|
||||
"slug": "folx-stadl-staffel-5-alle-sendungen-rueckblick",
|
||||
"excerpt": "Ein R\u00fcckblick auf alle Sendungen der f\u00fcnften Folx-Stadl-Staffel. Die besten Auftritte, lustigsten Momente und emotionalsten Highlights.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/folx-stadl-staffel5.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-10-28T10:00:00.000Z",
|
||||
"content": "<p>Die f\u00fcnfte Staffel des <strong>Folx Stadl</strong> hat viele unvergessliche Momente hervorgebracht. In diesem R\u00fcckblick fassen wir die besten Auftritte und Highlights aller Sendungen zusammen.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Volksmusik im Wandel: Tradition trifft Moderne",
|
||||
"slug": "volksmusik-im-wandel-tradition-trifft-moderne",
|
||||
"excerpt": "Wie sich die Volksmusik im Laufe der Jahre ver\u00e4ndert hat und welche K\u00fcnstler den Wandel vorantreiben. Eine Analyse der aktuellen Volksmusikszene.",
|
||||
"category": "News",
|
||||
"author": "Folx TV Redaktion",
|
||||
"coverImage": "/uploads/folx-stadl-22.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-10-18T10:00:00.000Z",
|
||||
"content": "<p>Die Volksmusik befindet sich im Wandel. Immer mehr K\u00fcnstler verbinden traditionelle Kl\u00e4nge mit modernen Einfl\u00fcssen und schaffen so einen frischen Sound, der auch j\u00fcngere Generationen anspricht.</p><p>In dieser Analyse beleuchten wir die aktuellen Trends und die K\u00fcnstler, die den Wandel vorantreiben.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Die Geschichte der Oberkrainer: Von Begunje in die Welt",
|
||||
"slug": "geschichte-oberkrainer-begunje-welt",
|
||||
"excerpt": "Die faszinierende Reise der Oberkrainer Musik von dem kleinen Ort Begunje in Slowenien in die ganze Welt. Eine Dokumentation.",
|
||||
"category": "Video",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/oberkrainer-geschichte.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-10-08T10:00:00.000Z",
|
||||
"content": "<p>Von dem kleinen Ort Begunje in Slowenien aus eroberte die Oberkrainer Musik die Welt. Diese Dokumentation erz\u00e4hlt die faszinierende Reise eines einzigartigen Musikstils.</p>"
|
||||
},
|
||||
{
|
||||
"title": "Slavko Avsenik: Das Genie hinter der Oberkrainer Musik",
|
||||
"slug": "slavko-avsenik-genie-hinter-oberkrainer-musik",
|
||||
"excerpt": "Ein Portr\u00e4t des Genies hinter der Oberkrainer Musik: Slavko Avsenik. Seine Vision, sein Talent und sein unvergleichliches Verm\u00e4chtnis.",
|
||||
"category": "Portr\u00e4t",
|
||||
"author": "Folx Music Television",
|
||||
"coverImage": "/uploads/saso-avsenik.webp",
|
||||
"featured": false,
|
||||
"publishedAt": "2024-09-28T10:00:00.000Z",
|
||||
"content": "<p><strong>Slavko Avsenik</strong> war das musikalische Genie hinter der Oberkrainer Musik. Mit \u00fcber 30 Millionen verkauften Tontr\u00e4gern und Auftritten in ganz Europa pr\u00e4gte er eine ganze Musikrichtung.</p><p>Dieses Portr\u00e4t erz\u00e4hlt seine Geschichte \u2013 von den bescheidenen Anf\u00e4ngen bis zum Welterfolg.</p>"
|
||||
}
|
||||
];
|
||||
|
||||
@ -246,4 +576,12 @@ export async function seedDatabase() {
|
||||
content = ${"<p>Die beliebte S\u00e4ngerin <strong>Melanie Payer</strong> stellt ihren Titel \u201eEndlich wieder Gipfelstammtisch\u201c vor, der eigens als Titelsong f\u00fcr die neue Staffel der Sendung <strong>\u201eGipfelstammtisch\u201c</strong> auf Folx TV geschrieben wurde. Im Zuge der Ver\u00f6ffentlichung wurde auch ein offizielles Musikvideo produziert, das die musikalische Idee und die beteiligten K\u00fcnstler in den Mittelpunkt stellt.</p>\n<p>Die Produktion entstand im renommierten Tonstudio FD-Musics in Gmunden. Komponiert wurde der Titel von <strong>Flo Daxner</strong> und <strong>Hanneliese Krei\u00dfl Wurth</strong>, die auch den Text verfasste. Produktion und Arrangement \u00fcbernahm Flo Daxner. Ver\u00f6ffentlicht wurde der Song unter dem Label mymusic.media und ist auf allen g\u00e4ngigen Plattformen als Download und Stream verf\u00fcgbar.</p>\n<p><strong>Song & Musikvideo:</strong><br><a href=\"https://music.imusician.pro/a/K2P_nWeA\" target=\"_blank\" rel=\"noopener noreferrer\">https://music.imusician.pro/a/K2P_nWeA</a></p>\n<div style=\"position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;margin:1.5rem 0;\"><iframe style=\"position:absolute;top:0;left:0;width:100%;height:100%;border:0;border-radius:8px;\" src=\"https://www.youtube.com/embed/38HygQCVFoo\" title=\"Melanie Payer - Endlich wieder Gipfelstammtisch\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>\n<p>Der Titel wurde speziell f\u00fcr die Sendung \u201eGipfelstammtisch\u201c geschrieben, um deren Charakter musikalisch widerzuspiegeln \u2013 Geselligkeit, Tradition und die authentische Volksmusik aus der Region Wilder Kaiser. Im Fokus stehen die Musik und die K\u00fcnstler, die die neue Staffel begleiten.</p>\n<p>Im Musikvideo wirken mit: <strong>Natascha</strong>, <strong>Meli Stein</strong>, <strong>Linda Feller</strong>, <strong>Mark Ed</strong>, <strong>Da Wadltreiber von Amadeus</strong>, <strong>Tauern Echo</strong>, <strong>Melanie Payer</strong>, <strong>Hansi Berger</strong>, <strong>Sanny \u2013 Die Stimme der Berge</strong>, <strong>Julia Raich</strong>, <strong>Spitzbua Markus</strong>, <strong>Meissnitzer Band</strong>, <strong>Brennholz</strong>, <strong>Charly Kaiser</strong>, <strong>Franz Nolf</strong>, <strong>Die Grubertaler</strong>, <strong>Franz Steiner</strong>, <strong>Marlena Martinelli</strong>, <strong>Die 3 Z'widern</strong>, <strong>SUSAL</strong> und <strong>Pfundskerle</strong>. Wijbrand van der Sande ist im Hintergrund zu sehen, w\u00e4hrend der Fokus klar auf dem Titel und den beteiligten K\u00fcnstlern liegt.</p>\n<p>\u201eEndlich wieder Gipfelstammtisch\u201c verbindet die neue Staffel der Sendung mit der Klangwelt der alpenl\u00e4ndischen Volksmusik und unterstreicht die musikalische Identit\u00e4t des Formats.</p>"}
|
||||
WHERE slug = 'melanie-payer-endlich-wieder-gipfelstammtisch'
|
||||
`);
|
||||
|
||||
await db.execute(sql`
|
||||
UPDATE articles SET
|
||||
title = REPLACE(REPLACE(title, '„', '\u201e'), '“', '\u201c'),
|
||||
excerpt = REPLACE(REPLACE(excerpt, '„', '\u201e'), '“', '\u201c')
|
||||
WHERE title LIKE '%„%' OR title LIKE '%“%'
|
||||
OR excerpt LIKE '%„%' OR excerpt LIKE '%“%'
|
||||
`);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { type Article, type InsertArticle, articles } from "@shared/schema";
|
||||
import { db } from "./db";
|
||||
import { eq, desc, sql } from "drizzle-orm";
|
||||
import { eq, desc, sql, count } from "drizzle-orm";
|
||||
|
||||
export interface IStorage {
|
||||
getArticles(): Promise<Article[]>;
|
||||
@ -9,6 +9,7 @@ export interface IStorage {
|
||||
getFeaturedArticles(): Promise<Article[]>;
|
||||
getPopularArticles(limit: number): Promise<Article[]>;
|
||||
getArticlesByCategory(category: string): Promise<Article[]>;
|
||||
getArticlesByCategoryPaginated(category: string, page: number, limit: number): Promise<{ articles: Article[]; total: number }>;
|
||||
createArticle(article: InsertArticle): Promise<Article>;
|
||||
updateArticle(id: number, article: Partial<InsertArticle>): Promise<Article | undefined>;
|
||||
incrementViews(id: number): Promise<void>;
|
||||
@ -42,6 +43,14 @@ export class DatabaseStorage implements IStorage {
|
||||
return db.select().from(articles).where(eq(articles.category, category)).orderBy(desc(articles.publishedAt));
|
||||
}
|
||||
|
||||
async getArticlesByCategoryPaginated(category: string, page: number, limit: number): Promise<{ articles: Article[]; total: number }> {
|
||||
const offset = (page - 1) * limit;
|
||||
const [totalResult] = await db.select({ count: count() }).from(articles).where(eq(articles.category, category));
|
||||
const total = totalResult.count;
|
||||
const items = await db.select().from(articles).where(eq(articles.category, category)).orderBy(desc(articles.publishedAt)).limit(limit).offset(offset);
|
||||
return { articles: items, total };
|
||||
}
|
||||
|
||||
async createArticle(article: InsertArticle): Promise<Article> {
|
||||
const [created] = await db.insert(articles).values(article).returning();
|
||||
return created;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user