Update home page to use a new grid layout and improve search functionality
Refactors the home page to replace VideoCard with NetflixGrid, removes unused video categorization logic, and enhances search functionality by implementing conditional refetching based on search query changes. 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:
parent
7651f730fd
commit
040ea52a25
@ -1,11 +1,10 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { type Video } from "@shared/schema";
|
import { type Video } from "@shared/schema";
|
||||||
import VideoCard from "@/components/video-card";
|
import NetflixGrid from "@/components/netflix-grid";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Search, Menu, X, ChevronLeft, ChevronRight } from "lucide-react";
|
import { Search, Menu, X } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
interface VideosResponse {
|
interface VideosResponse {
|
||||||
videos: Video[];
|
videos: Video[];
|
||||||
@ -13,18 +12,13 @@ interface VideosResponse {
|
|||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoCategory {
|
|
||||||
title: string;
|
|
||||||
videos: Video[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||||
|
|
||||||
// Fetch videos with optimized loading
|
// Fetch videos with optimized loading
|
||||||
const { data: videosResponse, isLoading } = useQuery<VideosResponse>({
|
const { data: videosResponse, isLoading, refetch } = useQuery<VideosResponse>({
|
||||||
queryKey: ["/api/videos", {
|
queryKey: ["/api/videos", {
|
||||||
limit: 150,
|
limit: 150,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@ -59,70 +53,38 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [videosResponse]);
|
}, [videosResponse]);
|
||||||
|
|
||||||
const handleVideoClick = (video: Video) => {
|
// Only refetch when search changes
|
||||||
window.location.href = `/video/${video.id}`;
|
useEffect(() => {
|
||||||
};
|
if (searchQuery !== undefined) {
|
||||||
|
refetch();
|
||||||
// Organize videos into categories like ZDF
|
}
|
||||||
const getCategories = (): VideoCategory[] => {
|
}, [searchQuery, refetch]);
|
||||||
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-blue-900 to-indigo-900">
|
<div className="has-fixed-header" style={{minHeight: '100vh', background: 'linear-gradient(135deg, hsl(250, 50%, 15%) 0%, hsl(240, 30%, 25%) 100%)', color: 'white'}}>
|
||||||
{/* Header */}
|
{/* STICKY HEADER */}
|
||||||
<header className="sticky top-0 z-50 bg-black/30 backdrop-blur-md border-b border-white/10">
|
<div className="header-sticky bg-transparent overflow-hidden">
|
||||||
<div className="container py-4">
|
<div className="container py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Logo */}
|
{/* Left side - Logo */}
|
||||||
<Link href="/" className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="w-10 h-10 bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg flex items-center justify-center">
|
<Link href="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
|
||||||
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
|
<div className="w-9 h-9 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
|
||||||
</div>
|
<div className="w-0 h-0 border-l-[10px] border-l-white border-y-[7px] border-y-transparent ml-1"></div>
|
||||||
<h1 className="text-2xl font-bold text-white">go4.video</h1>
|
</div>
|
||||||
</Link>
|
<h1 className="text-2xl font-bold text-white tracking-wide">go4.video</h1>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Navigation & Search */}
|
{/* Right side - Navigation + Search */}
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-4">
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop navigation */}
|
||||||
<div className="hidden md:flex items-center space-x-6">
|
<div className="hidden md:flex items-center space-x-6">
|
||||||
<nav className="flex space-x-6">
|
<nav className="flex space-x-6">
|
||||||
<Link href="/" className="text-white/80 hover:text-white transition-colors">
|
<Link href="/" className="text-bunny-light hover:text-bunny-blue transition-colors">
|
||||||
Home
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/folx-stadl" className="text-white/80 hover:text-white transition-colors">
|
<Link href="/folx-stadl" className="text-bunny-light hover:text-bunny-blue transition-colors">
|
||||||
FOLX STADL
|
FOLX STADL
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
@ -133,151 +95,72 @@ export default function Home() {
|
|||||||
placeholder="Videos suchen..."
|
placeholder="Videos suchen..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="bg-white/10 border-white/20 text-white placeholder-white/50 w-64"
|
className="bg-white border border-gray-300 rounded-lg px-4 py-2 pl-10 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-bunny-blue transition-colors w-64"
|
||||||
/>
|
/>
|
||||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 w-4 h-4" />
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile menu button */}
|
{/* Mobile menu button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||||
className="md:hidden p-2 text-white"
|
className="md:hidden p-2 rounded-lg bg-white/10 hover:bg-white/20 transition-colors"
|
||||||
|
data-testid="button-mobile-menu"
|
||||||
>
|
>
|
||||||
{isMobileMenuOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
{isMobileMenuOpen ? (
|
||||||
|
<X className="w-6 h-6 text-white" />
|
||||||
|
) : (
|
||||||
|
<Menu className="w-6 h-6 text-white" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
{/* Mobile menu dropdown */}
|
||||||
{isMobileMenuOpen && (
|
{isMobileMenuOpen && (
|
||||||
<div className="md:hidden mt-4 pt-4 border-t border-white/20">
|
<div className="md:hidden border-t border-white/20 bg-bunny-dark/95 backdrop-blur-md">
|
||||||
<nav className="flex flex-col space-y-3 mb-4">
|
<div className="px-4 py-3">
|
||||||
<Link href="/" className="text-white/80 hover:text-white">Home</Link>
|
<nav className="flex space-x-6 mb-3">
|
||||||
<Link href="/folx-stadl" className="text-white/80 hover:text-white">FOLX STADL</Link>
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-bunny-light hover:text-bunny-blue transition-colors text-sm font-medium"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/folx-stadl"
|
||||||
|
className="text-bunny-light hover:text-bunny-blue transition-colors text-sm font-medium"
|
||||||
|
onClick={() => setIsMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
FOLX STADL
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder="Suchen..."
|
placeholder="Search..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="bg-white/10 border-white/20 text-white placeholder-white/50 w-full"
|
className="bg-white border border-gray-300 rounded-lg px-3 py-2 pl-9 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-bunny-blue transition-colors w-full"
|
||||||
/>
|
/>
|
||||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 text-white/50 w-4 h-4" />
|
<Search className="absolute left-2.5 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<main className="w-full pt-0 pb-8 relative">
|
||||||
|
<div className="container">
|
||||||
|
<NetflixGrid
|
||||||
|
videos={allVideos}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
|
||||||
<main className="container py-8">
|
|
||||||
{isLoading ? (
|
|
||||||
<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="space-y-8">
|
|
||||||
{getCategories().map((category, categoryIndex) => (
|
|
||||||
<CategorySection
|
|
||||||
key={categoryIndex}
|
|
||||||
category={category}
|
|
||||||
onVideoClick={handleVideoClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{allVideos.length === 0 && !isLoading && (
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div className="text-white/60 text-lg">Keine Videos gefunden</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user