videofolxtv/client/src/pages/VideoPage.tsx
sebastjanartic 9ceb8d3fc6 Update video sharing to display correct images and text
Introduce a crawler detection middleware to serve Open Graph and Twitter Card metadata for social media sharing, and update the client-side sharing logic to utilize a dedicated share endpoint.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 401e2ec0-e00d-4f10-9d0e-60f3d479f9a5
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1a172d71-eb2c-471b-871f-cbf561747bbf
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/401e2ec0-e00d-4f10-9d0e-60f3d479f9a5/lDvepVp
2026-01-10 16:29:52 +00:00

892 lines
37 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useRoute, useLocation } from "wouter";
import { type Video } from "@shared/schema";
import go4LogoPath from "@assets/go4_1756394900352.png";
// Helper functions
const formatViews = (views: number): string => {
if (views >= 1000000) return `${(views / 1000000).toFixed(1)}M`;
if (views >= 1000) return `${(views / 1000).toFixed(1)}K`;
return views.toString();
};
const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = seconds % 60;
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
return `${minutes}:${secs.toString().padStart(2, '0')}`;
};
const formatDate = (date: Date | string): string => {
const d = typeof date === 'string' ? new Date(date) : date;
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
import { Button } from "@/components/ui/button";
import { Share2, X, Edit3, Menu, Search, ChevronLeft, ChevronRight } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Link } from "wouter";
import { apiRequest } from "@/lib/queryClient";
import {
FacebookShareButton,
TwitterShareButton,
WhatsappShareButton,
FacebookIcon,
TwitterIcon,
WhatsappIcon,
} from "react-share";
import AdSenseAd from "@/components/adsense-ad";
import HeaderAd from "@/components/HeaderAd";
interface VideosResponse {
videos: Video[];
total: number;
hasMore: boolean;
}
export default function VideoPage() {
const [, params] = useRoute("/video/:id");
const [, setLocation] = useLocation();
const videoId = params?.id;
// Get URL search params to determine show context
const urlParams = new URLSearchParams(window.location.search);
const showContext = urlParams.get('show');
const [showShareMenu, setShowShareMenu] = useState(false);
const [touchStart, setTouchStart] = useState(0);
const [touchEnd, setTouchEnd] = useState(0);
const [activeDot, setActiveDot] = useState<'left' | 'center' | 'right'>('center');
const [searchQuery, setSearchQuery] = useState("");
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Fetch current video
const { data: currentVideo, isLoading: videoLoading, error: videoError } = useQuery<Video>({
queryKey: [`/api/videos/${videoId}`],
queryFn: async () => {
const response = await fetch(`/api/videos/${videoId}`);
if (!response.ok) {
throw new Error(`Failed to fetch video: ${response.status}`);
}
return response.json();
},
enabled: !!videoId,
retry: 3,
retryDelay: 1000,
// Real-time refresh for view counts - refresh every 2 minutes for better performance
refetchInterval: 120000,
refetchOnWindowFocus: true,
refetchOnReconnect: true,
});
// Fetch recommended videos (excluding current video)
const { data: recommendedResponse } = useQuery<VideosResponse>({
queryKey: ["/api/videos", "limit=150"],
queryFn: () => fetch("/api/videos?limit=150&offset=0").then(res => res.json()),
enabled: !!videoId,
});
const recommendedVideos = recommendedResponse?.videos?.filter(v => v.id !== currentVideo?.id) || [];
// Filter videos based on show context or current video category for navigation
const getFilteredVideosForNavigation = () => {
const allVideos = recommendedResponse?.videos || [];
if (!currentVideo) return allVideos;
// Check show context from URL parameter first
if (showContext === 'folx-stadl') {
return allVideos.filter(video =>
video.title?.includes("FOLX STADL") || video.title?.includes("FOLXSTADL")
);
}
if (showContext === 'geschichte-lied') {
return allVideos.filter(video =>
video.title?.includes("Die Geschichte des Liedes") ||
video.title?.includes("Geschichte des Liedes") ||
video.title?.includes("GESCHICHTE DES LIEDES")
);
}
if (showContext === 'gipfelstammtisch') {
return allVideos.filter(video =>
video.title?.includes("Gipfelstammtisch") ||
video.title?.includes("GIPFELSTAMMTISCH") ||
video.title?.includes("Gipfel Stammtisch")
);
}
// Fallback to checking video title if no show context
const videoTitle = currentVideo.title || '';
if (videoTitle.includes("FOLX STADL") || videoTitle.includes("FOLXSTADL")) {
return allVideos.filter(video =>
video.title?.includes("FOLX STADL") || video.title?.includes("FOLXSTADL")
);
}
if (videoTitle.includes("Gipfelstammtisch") ||
videoTitle.includes("GIPFELSTAMMTISCH") ||
videoTitle.includes("Gipfel Stammtisch")) {
return allVideos.filter(video =>
video.title?.includes("Gipfelstammtisch") ||
video.title?.includes("GIPFELSTAMMTISCH") ||
video.title?.includes("Gipfel Stammtisch")
);
}
if (videoTitle.includes("Geschichte des Liedes")) {
const filtered = allVideos.filter(video =>
video.title?.includes("Geschichte des Liedes")
);
return filtered;
}
// If not in any specific category, return all videos
return allVideos;
};
const allVideos = getFilteredVideosForNavigation();
// Navigation functions
const getCurrentVideoIndex = () => {
if (!currentVideo || !allVideos.length) return -1;
return allVideos.findIndex((v) => v.id === currentVideo.id);
};
const navigateToVideo = (direction: 'next' | 'prev') => {
const currentIndex = getCurrentVideoIndex();
if (currentIndex === -1) return;
// Show direction feedback on dots
setActiveDot(direction === 'next' ? 'right' : 'left');
// Return to center after 300ms
setTimeout(() => {
setActiveDot('center');
}, 300);
let newIndex;
if (direction === 'next') {
newIndex = currentIndex + 1 >= allVideos.length ? 0 : currentIndex + 1;
} else {
newIndex = currentIndex - 1 < 0 ? allVideos.length - 1 : currentIndex - 1;
}
const newVideo = allVideos[newIndex];
if (newVideo) {
// Generate short ID for navigation
const shortId = newVideo.id.replace(/-/g, '').substring(0, 8);
// Preserve show context in URL when navigating
const showParam = showContext ? `?show=${showContext}` : '';
setLocation(`/video/${shortId}${showParam}`);
}
};
// Update page meta tags for social sharing
useEffect(() => {
if (currentVideo) {
// Update page title
document.title = `${currentVideo.title} | video.folx.tv`;
// Update meta description with original or rich content
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
const descriptionContent = currentVideo.description && currentVideo.description.trim() !== ''
? currentVideo.description
: `${currentVideo.title} - Professionelle Video-Streaming-Plattform mit exklusivem Content von FOLX STADL, Geschichte des Liedes und weiteren Premium-Inhalten auf video.folx.tv.`;
metaDescription.setAttribute('content', descriptionContent);
}
// Update Open Graph tags
const updateMetaTag = (property: string, content: string) => {
let meta = document.querySelector(`meta[property="${property}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('property', property);
document.head.appendChild(meta);
}
meta.setAttribute('content', content);
};
updateMetaTag('og:title', currentVideo.title);
// Use original description if available, otherwise create rich description
const richDescription = currentVideo.description && currentVideo.description.trim() !== ''
? currentVideo.description
: `${currentVideo.title} - Professionelle Video-Streaming-Plattform mit exklusivem Content von FOLX STADL, Geschichte des Liedes und weiteren Premium-Inhalten auf video.folx.tv.`;
updateMetaTag('og:description', richDescription);
// For social media, use direct Bunny.net thumbnail if available, otherwise fallback to our endpoint
const timestamp = new Date().getTime();
const currentDomain = window.location.origin;
// Use original Bunny thumbnail with cache-busting for social media
const socialThumbnail = currentVideo.thumbnailUrl
? `${currentVideo.thumbnailUrl}?t=${timestamp}`
: `${currentDomain}/api/video-thumbnail/${currentVideo.id}?v=${timestamp}`;
updateMetaTag('og:image', socialThumbnail);
updateMetaTag('og:image:width', '1200');
updateMetaTag('og:image:height', '630');
updateMetaTag('og:image:type', 'image/png');
updateMetaTag('og:image:alt', `${currentVideo.title} - video.folx.tv`);
updateMetaTag('og:url', window.location.href);
updateMetaTag('og:type', 'video.other');
updateMetaTag('og:video:duration', currentVideo.duration.toString());
updateMetaTag('og:site_name', 'video.folx.tv');
updateMetaTag('og:locale', 'de_DE');
// Update Twitter Card tags
const updateTwitterTag = (name: string, content: string) => {
let meta = document.querySelector(`meta[name="${name}"]`);
if (!meta) {
meta = document.createElement('meta');
meta.setAttribute('name', name);
document.head.appendChild(meta);
}
meta.setAttribute('content', content);
};
updateTwitterTag('twitter:card', 'summary_large_image');
updateTwitterTag('twitter:title', currentVideo.title);
updateTwitterTag('twitter:description', richDescription);
updateTwitterTag('twitter:image', socialThumbnail);
updateTwitterTag('twitter:image:alt', `${currentVideo.title} - video.folx.tv`);
updateTwitterTag('twitter:site', '@go4video');
}
}, [currentVideo]);
// Track video view
// Handle touch swipe for navigation
const handleTouchStart = (e: React.TouchEvent) => {
setTouchEnd(0);
setTouchStart(e.targetTouches[0].clientX);
};
const handleTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const handleTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > 20;
const isRightSwipe = distance < -20;
if (isLeftSwipe) {
navigateToVideo('next');
}
if (isRightSwipe) {
navigateToVideo('prev');
}
};
const handleVideoPlay = async () => {
if (currentVideo) {
try {
// Use short ID for view tracking
const shortId = currentVideo.id.replace(/-/g, '').substring(0, 8);
await apiRequest("POST", `/api/videos/${shortId}/view`);
} catch (error) {
console.error("Failed to track video view:", error);
}
}
};
// Share URL uses special endpoint with proper OG meta tags for social media previews
// This URL shows thumbnail, title, and description when shared on Facebook, Viber, WhatsApp, etc.
const getShareUrl = () => {
if (!currentVideo?.id) return window.location.origin;
const baseUrl = 'https://video.folx.tv';
return `${baseUrl}/share/video/${currentVideo.id}`;
};
// Direct video URL (for display purposes)
const getVideoUrl = () => {
if (!currentVideo?.id) return window.location.origin;
const baseUrl = 'https://video.folx.tv';
return `${baseUrl}/video/${currentVideo.id}`;
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(getShareUrl());
const notification = document.createElement('div');
notification.textContent = 'Link kopiert!';
notification.className = 'fixed top-4 right-4 bg-green-500 text-white px-4 py-2 rounded-lg z-50 transition-opacity duration-300';
document.body.appendChild(notification);
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => document.body.removeChild(notification), 300);
}, 2000);
setShowShareMenu(false);
} catch (error) {
console.error('Failed to copy link:', error);
}
};
if (videoLoading) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 bg-[#da234d] 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">video.folx.tv</h3>
<p className="text-bunny-light">Video wird geladen...</p>
</div>
</div>
);
}
if (!currentVideo) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="text-center">
<div className="w-12 h-12 bg-[#da234d] rounded-lg flex items-center justify-center shadow-lg mb-4 mx-auto opacity-50">
<div className="w-0 h-0 border-l-[9px] border-l-white border-y-[7px] border-y-transparent ml-1"></div>
</div>
<h3 className="text-white text-lg font-bold mb-2">video.folx.tv</h3>
<p className="text-bunny-light">Video nicht gefunden</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bunny-dark static-triangles 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 bg-[#da234d] 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">video.folx.tv</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-white transition-colors group">
Startseite
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/folx-stadl" className="relative text-bunny-light hover:text-white transition-colors group">
FOLX STADL
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/geschichte-lied" className="relative text-bunny-light hover:text-white transition-colors group">
DIE GESCHICHTE DES LIEDES
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/gipfelstammtisch" className="relative text-bunny-light hover:text-white transition-colors group">
GIPFELSTAMMTISCH
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-[#da234d] transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/live" className="relative text-red-500 hover:text-red-400 transition-colors group font-bold flex items-center space-x-1">
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
<span>LIVE</span>
<span className="absolute bottom-0 left-0 w-0 h-0.5 bg-gradient-to-r from-red-400 via-red-500 to-red-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-[#da234d] 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-video"
>
{isMobileMenuOpen ? (
<X className="w-6 h-6 text-white" />
) : (
<Menu className="w-6 h-6 text-white" />
)}
</button>
</div>
</div>
</div>
{/* Mobile menu dropdown - kompakten */}
{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-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
Startseite
</Link>
<Link
href="/folx-stadl"
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
FOLX STADL
</Link>
<Link
href="/geschichte-lied"
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
DIE GESCHICHTE DES LIEDES
</Link>
<Link
href="/gipfelstammtisch"
className="text-bunny-light hover:text-white transition-colors font-medium py-1 border-l-2 border-transparent hover:border-[#da234d] pl-3"
onClick={() => setIsMobileMenuOpen(false)}
>
GIPFELSTAMMTISCH
</Link>
<Link
href="/live"
className="text-red-500 hover:text-red-400 transition-colors font-bold py-1 border-l-2 border-transparent hover:border-red-500 pl-3 flex items-center space-x-2"
onClick={() => setIsMobileMenuOpen(false)}
>
<span className="w-2 h-2 bg-red-500 rounded-full animate-pulse"></span>
<span>LIVE</span>
</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-[#da234d] 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>
{/* Fixed Header Ad */}
<HeaderAd />
<div className="container p-4 lg:p-6 relative">
{/* Background logo decorations */}
<div
style={{
position: 'absolute',
top: '20%',
right: '8%',
transform: 'rotate(-15deg)',
width: 'clamp(200px, 20vw, 400px)',
height: 'clamp(100px, 10vw, 200px)',
backgroundImage: `url(${go4LogoPath})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
pointerEvents: 'none',
zIndex: 0,
opacity: 0.15,
filter: 'blur(1px)'
}}
/>
<div
style={{
position: 'absolute',
top: '60%',
left: '5%',
transform: 'rotate(20deg)',
width: 'clamp(150px, 15vw, 300px)',
height: 'clamp(75px, 7.5vw, 150px)',
backgroundImage: `url(${go4LogoPath})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
pointerEvents: 'none',
zIndex: 0,
opacity: 0.12,
filter: 'blur(0.5px)'
}}
/>
<div
style={{
position: 'absolute',
top: '80%',
right: '30%',
transform: 'rotate(-25deg)',
width: 'clamp(100px, 10vw, 200px)',
height: 'clamp(50px, 5vw, 100px)',
backgroundImage: `url(${go4LogoPath})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
pointerEvents: 'none',
zIndex: 0,
opacity: 0.08,
filter: 'blur(0.5px)'
}}
/>
<div
style={{
position: 'absolute',
top: '5%',
left: '75%',
transform: 'rotate(40deg)',
width: 'clamp(150px, 15vw, 300px)',
height: 'clamp(75px, 7.5vw, 150px)',
backgroundImage: `url(${go4LogoPath})`,
backgroundSize: 'contain',
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
pointerEvents: 'none',
zIndex: 0,
opacity: 0.12,
filter: 'blur(0.5px)'
}}
/>
<div className="flex flex-col lg:flex-row gap-6 relative z-[40]">
{/* Main video section */}
<div className="flex-1">
{/* Video player */}
<div
className="group relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden mb-4"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{ touchAction: 'pan-y pinch-zoom' }}
>
{/* Navigation arrows - hidden on mobile, visible on desktop */}
{allVideos.length > 1 && (
<>
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigateToVideo('prev');
}}
className="absolute left-2 top-1/2 transform -translate-y-1/2 bg-rose-600 hover:bg-rose-700 text-white border-none p-2 rounded-full z-20 shadow-lg hidden md:group-hover:flex items-center justify-center w-10 h-10 transition-all duration-200 opacity-0 group-hover:opacity-75"
size="sm"
data-testid="button-prev-video"
>
<ChevronLeft className="w-5 h-5" />
</Button>
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigateToVideo('next');
}}
className="absolute right-2 top-1/2 transform -translate-y-1/2 bg-rose-600 hover:bg-rose-700 text-white border-none p-2 rounded-full z-20 shadow-lg hidden md:group-hover:flex items-center justify-center w-10 h-10 transition-all duration-200 opacity-0 group-hover:opacity-75"
size="sm"
data-testid="button-next-video"
>
<ChevronRight className="w-5 h-5" />
</Button>
</>
)}
{/* Touch overlay for mobile swipe detection - positioned on edges only, avoiding bottom controls */}
<div
className="absolute left-0 top-0 w-16 h-3/4 z-10 md:hidden"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
pointerEvents: 'auto',
touchAction: 'none'
}}
/>
<div
className="absolute right-0 top-0 w-16 h-3/4 z-10 md:hidden"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
pointerEvents: 'auto',
touchAction: 'none'
}}
/>
{currentVideo.videoUrlIframe ? (
<iframe
src={currentVideo.videoUrlIframe}
className="absolute inset-0 w-full h-full"
style={{ pointerEvents: 'auto' }}
frameBorder="0"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
sandbox="allow-scripts allow-same-origin allow-presentation"
onLoad={handleVideoPlay}
title={currentVideo.title}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-white">
<p>Video nicht verfügbar</p>
</div>
)}
</div>
{/* Mobile navigation dots - only visible on mobile */}
{allVideos.length > 1 && (
<div className="md:hidden flex justify-center mt-4 mb-2 gap-1 py-1">
{/* Left dot */}
<button
onClick={() => navigateToVideo('prev')}
className={`rounded-full transition-colors duration-300 ease-in-out ${
activeDot === 'left'
? 'bg-gradient-to-r from-[#da234d] to-[#da234d]'
: 'bg-white/25 hover:bg-white/40'
}`}
style={{
width: '8px',
height: '8px',
minWidth: '8px',
minHeight: '8px'
}}
aria-label="Previous video"
/>
{/* Center dot */}
<button
className={`rounded-full transition-colors duration-300 ease-in-out ${
activeDot === 'center'
? 'bg-gradient-to-r from-[#da234d] to-[#da234d]'
: 'bg-white/25'
}`}
style={{
width: '8px',
height: '8px',
minWidth: '8px',
minHeight: '8px'
}}
aria-label="Current video"
/>
{/* Right dot */}
<button
onClick={() => navigateToVideo('next')}
className={`rounded-full transition-colors duration-300 ease-in-out ${
activeDot === 'right'
? 'bg-gradient-to-r from-[#da234d] to-[#da234d]'
: 'bg-white/25 hover:bg-white/40'
}`}
style={{
width: '8px',
height: '8px',
minWidth: '8px',
minHeight: '8px'
}}
aria-label="Next video"
/>
</div>
)}
{/* Video info */}
<div className="bg-bunny-gray/50 rounded-lg p-4">
<div className="flex justify-between items-start mb-4">
<h1 className="text-xl font-bold text-bunny-light flex-1 pr-4">
{currentVideo.title}
</h1>
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={() => setShowShareMenu(!showShareMenu)}
className="text-white hover:bg-gray-700"
>
<Share2 className="w-4 h-4 mr-2" />
Share
</Button>
{showShareMenu && (
<div className="absolute right-0 top-full mt-2 bg-gray-800 rounded-lg shadow-lg py-2 z-50 min-w-[200px]">
<FacebookShareButton url={getShareUrl()}>
<div className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2 cursor-pointer">
<FacebookIcon size={16} round />
Facebook
</div>
</FacebookShareButton>
<TwitterShareButton url={getShareUrl()} title={`Watch "${currentVideo.title}" on video.folx.tv`}>
<div className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2 cursor-pointer">
<TwitterIcon size={16} round />
Twitter
</div>
</TwitterShareButton>
<WhatsappShareButton url={getShareUrl()} title={`Watch "${currentVideo.title}" on video.folx.tv`}>
<div className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2 cursor-pointer">
<WhatsappIcon size={16} round />
WhatsApp
</div>
</WhatsappShareButton>
<button
onClick={copyToClipboard}
className="w-full px-4 py-2 text-left text-white hover:bg-gray-700"
>
Copy Link
</button>
</div>
)}
</div>
</div>
<div className="flex flex-wrap gap-4 text-sm text-bunny-muted mb-4">
<span>{formatViews(currentVideo.views)} views</span>
<span>{formatDuration(currentVideo.duration)}</span>
<span>{formatDate(currentVideo.createdAt)}</span>
</div>
{currentVideo.description ? (
<div className="text-bunny-light">
<p className="text-sm leading-relaxed">{currentVideo.description}</p>
</div>
) : (
<div className="text-bunny-muted text-sm">
<p>Video description not available.</p>
</div>
)}
</div>
</div>
{/* Recommended videos sidebar */}
<div className="w-full lg:w-96">
<h2 className="text-lg font-semibold text-bunny-light mb-4">Empfohlene Videos</h2>
<div className="space-y-2">
{recommendedVideos.slice(0, 10).map((video) => (
<div
key={video.id}
onClick={() => {
const shortId = video.id.replace(/-/g, '').substring(0, 8);
setLocation(`/video/${shortId}`);
}}
className="flex gap-3 p-2 bg-bunny-gray/30 hover:bg-bunny-gray/50 rounded-lg cursor-pointer transition-colors"
>
<div className="relative w-24 h-16 bg-gray-700 rounded overflow-hidden flex-shrink-0">
<img
src={video.thumbnailUrl}
alt={video.title}
className="w-full h-full object-cover"
onError={(e) => {
const target = e.target as HTMLImageElement;
console.log('Sidebar thumbnail failed:', target.src);
target.style.display = 'none';
if (target.parentElement && !target.parentElement.querySelector('.thumbnail-placeholder')) {
target.parentElement.style.background = 'linear-gradient(135deg, #1f2937, #374151)';
const placeholder = document.createElement('div');
placeholder.className = 'thumbnail-placeholder absolute inset-0 flex items-center justify-center text-white text-xs';
placeholder.innerHTML = '<div>🎬</div>';
target.parentElement.appendChild(placeholder);
}
}}
/>
<div className="absolute bottom-1 right-1 bg-black/70 text-white text-xs px-1 py-0.5 rounded">
{formatDuration(video.duration)}
</div>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-sm font-medium text-bunny-light mb-1 line-clamp-2"
style={{
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden'
}}>
{video.title}
</h3>
<div className="text-xs text-bunny-muted">
<div>{formatViews(video.views)} views {formatDate(video.createdAt)}</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* Footer - same as home page */}
<footer className="bunny-gray/50 border-t border-white/10 mt-16 relative overflow-hidden">
{/* Triangle decorations in footer - manjši in bolje pozicionirani */}
<div className="absolute top-2 right-10 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-b-[25px] border-b-[#da234d]/5 rotate-12 z-0"></div>
<div className="absolute top-1 left-10 w-0 h-0 border-l-[15px] border-l-transparent border-r-[15px] border-r-transparent border-b-[20px] border-b-[#da234d]/4 -rotate-12 z-0"></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative z-10">
<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-[#da234d] rounded-lg flex items-center justify-center shadow-md">
<div className="w-0 h-0 border-l-[8px] border-l-white border-y-[6px] border-y-transparent ml-0.5"></div>
</div>
<span className="text-lg font-semibold text-white">video.folx.tv</span>
</div>
{/* Legal Links */}
<div className="flex items-center space-x-6 text-sm">
<Link href="/impressum" className="text-bunny-muted hover:text-white transition-colors">
Impressum
</Link>
<Link href="/privacy" className="text-bunny-muted hover:text-white transition-colors">
Datenschutz
</Link>
<Link href="/terms" className="text-bunny-muted hover:text-white transition-colors">
Nutzungsbedingungen
</Link>
</div>
{/* Copyright */}
<div className="text-sm text-bunny-muted text-center">
© 2025 video.folx.tv. Alle Rechte vorbehalten.
</div>
</div>
</div>
</footer>
</div>
);
}