videofolxtv/client/src/pages/GipfelstammtischPage.tsx
sebastjanartic c132ac0657 Update legal links to German language for better user experience
Updated privacy policy and terms and conditions links to display "Datenschutz" and "Nutzungsbedingungen" respectively across multiple pages.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 2cd2c0bc-434c-4bc9-ad3f-b99d3897a0d1
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/2cd2c0bc-434c-4bc9-ad3f-b99d3897a0d1/gjpMN2A
2025-09-03 20:33:41 +00:00

345 lines
15 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { Link, useLocation } from 'wouter';
import { ArrowLeft, ChevronLeft, ChevronRight, Menu, X } from 'lucide-react';
import VideoCard from '@/components/video-card';
import BunnyVideoModal from '@/components/bunny-video-modal';
import { Button } from '@/components/ui/button';
import { useState, useEffect } from 'react';
import type { Video } from '@shared/schema';
import { Input } from '@/components/ui/input';
import { Search } from 'lucide-react';
export default function GipfelstammtischPage() {
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [, setLocation] = useLocation();
const [touchStart, setTouchStart] = useState<number>(0);
const [touchEnd, setTouchEnd] = useState<number>(0);
const itemsPerPage = 10;
const { data, isLoading } = useQuery<{videos: Video[], total: number}>({
queryKey: ['/api/videos?limit=200'],
select: (response) => response || { videos: [], total: 0 }
});
const videos = data?.videos || [];
// Filter all Gipfelstammtisch videos
const gipfelVideos = videos.filter(video =>
video.title.includes("Gipfelstammtisch") ||
video.title.includes("GIPFELSTAMMTISCH") ||
video.title.includes("Gipfel Stammtisch")
);
// Pagination logic
const totalPages = Math.ceil(gipfelVideos.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const currentVideos = gipfelVideos.slice(startIndex, endIndex);
const handleVideoClick = (video: Video) => {
// Navigate to individual video page instead of modal
const shortId = video.id.replace(/-/g, '').substring(0, 8);
setLocation(`/video/${shortId}`);
};
const handleCloseModal = () => {
setIsModalOpen(false);
setSelectedVideo(null);
};
// Touch handlers for swipe navigation
const handleTouchStart = (e: React.TouchEvent) => {
setTouchEnd(0);
setTouchStart(e.targetTouches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
e.preventDefault();
setTouchEnd(e.targetTouches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > 30;
const isRightSwipe = distance < -30;
if (isLeftSwipe && currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
if (isRightSwipe && currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 gradient-primary rounded-lg flex items-center justify-center shadow-lg animate-pulse mb-4 mx-auto">
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[9px] border-y-transparent ml-1"></div>
</div>
<h3 className="text-white text-xl font-bold mb-2">go4.video</h3>
<p className="text-bunny-light">Loading GIPFELSTAMMTISCH...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-bunny-dark text-white has-fixed-header">
{/* Header */}
<div className="header-sticky bg-transparent overflow-hidden">
<div className="container py-6">
<div className="flex items-center justify-between">
{/* Left side - Logo */}
<div className="flex items-center space-x-6 flex-1">
<Link href="/" className="flex items-center space-x-3 hover:opacity-80 transition-opacity py-4">
<div className="w-10 h-10 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
<div className="w-0 h-0 border-l-[11px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
</div>
<h1 className="text-2xl font-bold text-white tracking-wide">go4.video</h1>
</Link>
</div>
{/* Right side - Navigation + Search */}
<div className="flex items-center gap-4">
{/* Desktop navigation */}
<div className="hidden md:flex items-center space-x-6">
<nav className="flex space-x-6">
<Link href="/" className="relative text-bunny-light hover:text-bunny-blue transition-colors group">
Startseite
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 via-blue-500 to-purple-600 transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/folx-stadl" className="relative text-bunny-light hover:text-bunny-blue transition-colors group">
FOLX STADL
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 via-blue-500 to-purple-600 transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/geschichte-lied" className="relative text-bunny-light hover:text-bunny-blue transition-colors group">
DIE GESCHICHTE DES LIEDES
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 via-blue-500 to-purple-600 transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/gipfelstammtisch" className="relative text-bunny-light hover:text-bunny-blue transition-colors group">
GIPFELSTAMMTISCH
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-purple-400 via-blue-500 to-purple-600 transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
<div className="relative">
<Input
type="search"
placeholder="Suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white border border-gray-300 rounded-lg px-4 py-1.5 pl-10 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-bunny-blue transition-colors w-56"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
</div>
</div>
{/* Mobile menu button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
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 text-white" />
) : (
<Menu className="w-6 h-6 text-white" />
)}
</button>
</div>
</div>
</div>
{/* Mobile menu dropdown */}
{isMobileMenuOpen && (
<div className="md:hidden border-t border-white/20 bg-bunny-dark/95 backdrop-blur-md">
<div className="px-6 py-4">
{/* Navigation Section */}
<div className="mb-6">
<h3 className="text-white text-xs font-semibold uppercase tracking-wider mb-3 opacity-70">Navigation</h3>
<nav className="flex flex-col space-y-4">
<Link
href="/"
className="text-bunny-light hover:text-bunny-blue transition-colors font-medium py-1 border-l-2 border-transparent hover:border-bunny-blue pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
Startseite
</Link>
<Link
href="/folx-stadl"
className="text-bunny-light hover:text-bunny-blue transition-colors font-medium py-1 border-l-2 border-transparent hover:border-bunny-blue pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
FOLX STADL
</Link>
<Link
href="/geschichte-lied"
className="text-bunny-light hover:text-bunny-blue transition-colors font-medium py-1 border-l-2 border-transparent hover:border-bunny-blue pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
DIE GESCHICHTE DES LIEDES
</Link>
<Link
href="/gipfelstammtisch"
className="text-bunny-light hover:text-bunny-blue transition-colors font-medium py-1 border-l-2 border-transparent hover:border-bunny-blue pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
GIPFELSTAMMTISCH
</Link>
</nav>
</div>
{/* Separator */}
<div className="border-t border-white/10 mb-4"></div>
{/* Search Section */}
<div>
<h3 className="text-white text-xs font-semibold uppercase tracking-wider mb-3 opacity-70">Suchen</h3>
<div className="relative">
<Input
type="search"
placeholder="Suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="bg-white/10 border border-white/20 rounded-lg px-4 py-2.5 pl-10 text-sm text-white placeholder-white/60 focus:outline-none focus:border-bunny-blue focus:bg-white/15 transition-all w-full"
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-white/60 w-4 h-4" />
</div>
</div>
</div>
</div>
)}
</div>
{/* Main Content */}
<div
className="container pt-8 pb-8"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{/* Video List with Descriptions */}
<div className="space-y-6">
{currentVideos.map((video, index) => (
<div key={video.id} className="bg-black/20 backdrop-blur-sm rounded-lg p-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Video Card */}
<div className="md:col-span-1">
<VideoCard
video={video}
onClick={handleVideoClick}
className="w-full hover:scale-102 transition-all duration-300 rounded-lg overflow-hidden"
hideOverlay={true}
/>
</div>
{/* Video Description */}
<div className="md:col-span-2 space-y-3">
<h3 className="text-xl font-bold text-white">{video.title}</h3>
<p className="text-bunny-light text-sm leading-relaxed">
{video.description || "Keine Beschreibung für diese Sendung verfügbar."}
</p>
<div className="flex items-center gap-4 text-xs text-bunny-muted">
<span>{Math.floor(video.duration / 60)}:{(video.duration % 60).toString().padStart(2, '0')} min</span>
<span>{video.views} Aufrufe</span>
</div>
</div>
</div>
</div>
))}
{gipfelVideos.length === 0 && (
<div className="text-center py-16">
<p className="text-bunny-muted text-lg">Keine Gipfelstammtisch Videos gefunden</p>
</div>
)}
</div>
{/* Bottom Pagination */}
{totalPages > 1 && (
<div className="mt-8 flex justify-center gap-1 px-4 overflow-x-auto">
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let pageNum;
if (totalPages <= 5) {
pageNum = i + 1;
} else if (currentPage <= 3) {
pageNum = i + 1;
} else if (currentPage >= totalPages - 2) {
pageNum = totalPages - 4 + i;
} else {
pageNum = currentPage - 2 + i;
}
return (
<Button
key={pageNum}
variant={currentPage === pageNum ? "default" : "outline"}
size="sm"
onClick={() => setCurrentPage(pageNum)}
className={currentPage === pageNum
? "gradient-primary text-white border-none shadow-lg h-10 w-10 p-0 flex items-center justify-center"
: "border-white/20 text-white hover:bg-white/10 h-10 w-10 p-0 flex items-center justify-center"
}
>
{pageNum}
</Button>
);
})}
</div>
)}
</div>
{/* Footer */}
<footer className="bg-bunny-dark/90 border-t border-white/10 py-8 mt-12">
<div className="container">
<div className="flex flex-col items-center justify-center space-y-4">
{/* Logo */}
<div className="flex items-center space-x-2">
<div className="w-8 h-8 bg-gradient-to-r from-purple-600 to-blue-500 rounded-lg flex items-center justify-center shadow-lg">
<div className="w-0 h-0 border-l-[8px] border-l-white border-y-[6px] border-y-transparent ml-1 drop-shadow-sm"></div>
</div>
<h3 className="text-xl font-bold text-white">go4.video</h3>
</div>
{/* Legal Links */}
<div className="flex items-center space-x-6 text-sm">
<Link href="/impressum" className="text-bunny-muted hover:text-bunny-blue transition-colors">
Impressum
</Link>
<Link href="/privacy" className="text-bunny-muted hover:text-bunny-blue transition-colors">
Datenschutz
</Link>
<Link href="/terms" className="text-bunny-muted hover:text-bunny-blue transition-colors">
Nutzungsbedingungen
</Link>
</div>
{/* Copyright */}
<p className="text-bunny-muted text-sm text-center">
© 2025 go4.video. Alle Rechte vorbehalten.
</p>
</div>
</div>
</footer>
{/* Video Modal */}
{selectedVideo && (
<BunnyVideoModal
video={selectedVideo}
isOpen={isModalOpen}
onClose={handleCloseModal}
/>
)}
</div>
);
}