Organize videos into categories with improved navigation and search functionality

Introduces video categorization on the home page, including "Meist Angesehen", "FOLX STADL Shows", "Neue Videos", and "Alle Videos". Adds pagination buttons (ChevronLeft, ChevronRight) and updates placeholder text to Slovenian ("Videos suchen...", "Suchen..."). Refactors the header background and gradient for the main content. Replaces skeleton loading with a grid layout for fetched videos.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 890577b1-c154-40a4-a177-a0c6d55320c3
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/890577b1-c154-40a4-a177-a0c6d55320c3/aC8PHqS
This commit is contained in:
sebastjanartic 2025-09-01 18:14:46 +00:00
parent 901a5685b6
commit 7651f730fd

View File

@ -1,10 +1,11 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useRef } from "react";
import { useQuery } from "@tanstack/react-query";
import { type Video } from "@shared/schema";
import VideoCard from "@/components/video-card";
import { Link } from "wouter";
import { Input } from "@/components/ui/input";
import { Search, Menu, X } from "lucide-react";
import { Search, Menu, X, ChevronLeft, ChevronRight } from "lucide-react";
import { Button } from "@/components/ui/button";
interface VideosResponse {
videos: Video[];
@ -12,6 +13,11 @@ interface VideosResponse {
hasMore: boolean;
}
interface VideoCategory {
title: string;
videos: Video[];
}
export default function Home() {
const [searchQuery, setSearchQuery] = useState("");
const [allVideos, setAllVideos] = useState<Video[]>([]);
@ -57,10 +63,47 @@ export default function Home() {
window.location.href = `/video/${video.id}`;
};
// Organize videos into categories like ZDF
const getCategories = (): VideoCategory[] => {
if (!allVideos.length) return [];
// Sort by views for popular content
const sortedByViews = [...allVideos].sort((a, b) => (b.views || 0) - (a.views || 0));
// Sort by date for recently added
const sortedByDate = [...allVideos].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
// FOLX STADL videos
const folxStadlVideos = allVideos.filter(video =>
video.title.includes("FOLX STADL") || video.title.includes("FOLXSTADL")
);
return [
{
title: "Meist Angesehen",
videos: sortedByViews.slice(0, 12)
},
...(folxStadlVideos.length > 0 ? [{
title: "FOLX STADL Shows",
videos: folxStadlVideos.slice(0, 12)
}] : []),
{
title: "Neue Videos",
videos: sortedByDate.slice(0, 12)
},
{
title: "Alle Videos",
videos: allVideos.slice(0, 15)
}
];
};
return (
<div className="min-h-screen bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900">
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900">
{/* Header */}
<header className="sticky top-0 z-50 bg-black/20 backdrop-blur-md border-b border-white/10">
<header className="sticky top-0 z-50 bg-black/30 backdrop-blur-md border-b border-white/10">
<div className="container py-4">
<div className="flex items-center justify-between">
{/* Logo */}
@ -87,7 +130,7 @@ export default function Home() {
<div className="relative">
<Input
type="search"
placeholder="Search videos..."
placeholder="Videos suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white/10 border-white/20 text-white placeholder-white/50 w-64"
@ -116,7 +159,7 @@ export default function Home() {
<div className="relative">
<Input
type="search"
placeholder="Search..."
placeholder="Suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white/10 border-white/20 text-white placeholder-white/50 w-full"
@ -131,25 +174,31 @@ export default function Home() {
{/* Main Content */}
<main className="container py-8">
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{Array.from({ length: 15 }).map((_, index) => (
<div key={index} className="animate-pulse">
<div className="bg-white/10 aspect-video rounded-lg mb-3"></div>
<div className="space-y-2">
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
<div className="space-y-8">
{Array.from({ length: 3 }).map((_, categoryIndex) => (
<div key={categoryIndex} className="space-y-4">
<div className="h-6 bg-white/10 rounded w-48 animate-pulse"></div>
<div className="flex space-x-4 overflow-hidden">
{Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="flex-shrink-0 w-[280px] animate-pulse">
<div className="bg-white/10 aspect-video rounded-lg mb-3"></div>
<div className="space-y-2">
<div className="h-4 bg-white/10 rounded w-3/4"></div>
<div className="h-3 bg-white/10 rounded w-1/2"></div>
</div>
</div>
))}
</div>
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{allVideos.map((video) => (
<VideoCard
key={video.id}
video={video}
onClick={handleVideoClick}
className="hover:scale-105 transition-transform duration-200"
<div className="space-y-8">
{getCategories().map((category, categoryIndex) => (
<CategorySection
key={categoryIndex}
category={category}
onVideoClick={handleVideoClick}
/>
))}
</div>
@ -157,10 +206,78 @@ export default function Home() {
{allVideos.length === 0 && !isLoading && (
<div className="text-center py-12">
<div className="text-white/60 text-lg">No videos found</div>
<div className="text-white/60 text-lg">Keine Videos gefunden</div>
</div>
)}
</main>
</div>
);
}
interface CategorySectionProps {
category: VideoCategory;
onVideoClick: (video: Video) => void;
}
function CategorySection({ category, onVideoClick }: CategorySectionProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const scroll = (direction: 'left' | 'right') => {
if (scrollRef.current) {
const scrollAmount = 320; // Width of one card + gap
const newScrollLeft = scrollRef.current.scrollLeft + (direction === 'right' ? scrollAmount : -scrollAmount);
scrollRef.current.scrollTo({
left: newScrollLeft,
behavior: 'smooth'
});
}
};
return (
<div className="relative group">
<h2 className="text-xl font-semibold text-white mb-4 px-4">
{category.title}
</h2>
<div className="relative">
{/* Left scroll button */}
<Button
onClick={() => scroll('left')}
className="absolute left-2 top-1/2 -translate-y-1/2 z-40 bg-black/60 hover:bg-black/80 text-white border-none w-12 h-12 rounded-full transition-all duration-300 opacity-0 group-hover:opacity-100"
size="sm"
>
<ChevronLeft className="w-6 h-6" />
</Button>
{/* Right scroll button */}
<Button
onClick={() => scroll('right')}
className="absolute right-2 top-1/2 -translate-y-1/2 z-40 bg-black/60 hover:bg-black/80 text-white border-none w-12 h-12 rounded-full transition-all duration-300 opacity-0 group-hover:opacity-100"
size="sm"
>
<ChevronRight className="w-6 h-6" />
</Button>
{/* Scrollable video row */}
<div
ref={scrollRef}
className="flex gap-4 overflow-x-auto scrollbar-hide pb-4 px-4"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{category.videos.map((video) => (
<div
key={video.id}
className="flex-shrink-0 w-[280px] hover:scale-105 transition-transform duration-300"
>
<VideoCard
video={video}
onClick={onVideoClick}
className="w-full"
/>
</div>
))}
</div>
</div>
</div>
);
}