Add null check for video player element before setting opacity in VideoPage.tsx to prevent potential runtime errors. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 344ec1e0-1186-4058-bbff-2e9619a7b1e0 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/344ec1e0-1186-4058-bbff-2e9619a7b1e0/FgaI1Sc
610 lines
24 KiB
TypeScript
610 lines
24 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";
|
|
|
|
interface VideosResponse {
|
|
videos: Video[];
|
|
total: number;
|
|
hasMore: boolean;
|
|
}
|
|
|
|
export default function VideoPage() {
|
|
const [, params] = useRoute("/video/:id");
|
|
const [, setLocation] = useLocation();
|
|
const videoId = params?.id;
|
|
const [showShareMenu, setShowShareMenu] = useState(false);
|
|
const [touchStart, setTouchStart] = useState(0);
|
|
const [touchEnd, setTouchEnd] = useState(0);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [viewMode, setViewMode] = useState<"grid" | "list">("grid");
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
|
|
// Fetch current video
|
|
const { data: currentVideo, isLoading: videoLoading } = useQuery<Video>({
|
|
queryKey: [`/api/videos/${videoId}`],
|
|
queryFn: () => fetch(`/api/videos/${videoId}`).then(res => res.json()),
|
|
enabled: !!videoId,
|
|
});
|
|
|
|
// Fetch recommended videos (excluding current video)
|
|
const { data: recommendedResponse } = useQuery<VideosResponse>({
|
|
queryKey: ["/api/videos"],
|
|
queryFn: () => fetch("/api/videos?limit=20&offset=0").then(res => res.json()),
|
|
enabled: !!videoId,
|
|
});
|
|
|
|
const recommendedVideos = recommendedResponse?.videos?.filter(v => v.id !== videoId) || [];
|
|
const allVideos = recommendedResponse?.videos || [];
|
|
|
|
// 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;
|
|
|
|
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) {
|
|
setLocation(`/video/${newVideo.id}`);
|
|
}
|
|
};
|
|
|
|
|
|
// Update page meta tags for social sharing
|
|
useEffect(() => {
|
|
if (currentVideo) {
|
|
// Update page title
|
|
document.title = `${currentVideo.title} | go4.video`;
|
|
|
|
// Update meta description
|
|
const metaDescription = document.querySelector('meta[name="description"]');
|
|
if (metaDescription) {
|
|
metaDescription.setAttribute('content', currentVideo.description || `Watch ${currentVideo.title} on go4.video`);
|
|
}
|
|
|
|
// 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);
|
|
updateMetaTag('og:description', currentVideo.description || `Watch ${currentVideo.title} on go4.video`);
|
|
// Use custom thumbnail if available, otherwise default thumbnail
|
|
const thumbnailForSharing = currentVideo.customThumbnailUrl || currentVideo.thumbnailUrl;
|
|
updateMetaTag('og:image', thumbnailForSharing);
|
|
updateMetaTag('og:url', window.location.href);
|
|
updateMetaTag('og:type', 'video.other');
|
|
updateMetaTag('og:video:duration', currentVideo.duration.toString());
|
|
|
|
// 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:title', currentVideo.title);
|
|
updateTwitterTag('twitter:description', currentVideo.description || `Watch ${currentVideo.title} on go4.video`);
|
|
updateTwitterTag('twitter:image', thumbnailForSharing);
|
|
}
|
|
}, [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 > 50;
|
|
const isRightSwipe = distance < -50;
|
|
|
|
if (isLeftSwipe) {
|
|
navigateToVideo('next');
|
|
}
|
|
if (isRightSwipe) {
|
|
navigateToVideo('prev');
|
|
}
|
|
};
|
|
|
|
const handleVideoPlay = async () => {
|
|
if (currentVideo) {
|
|
try {
|
|
await apiRequest("POST", `/api/videos/${currentVideo.id}/view`);
|
|
} catch (error) {
|
|
console.error("Failed to track video view:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getShareUrl = () => {
|
|
if (!currentVideo?.id) return window.location.origin;
|
|
// Use custom domain if set, otherwise current domain
|
|
const baseUrl = import.meta.env.VITE_SHARE_DOMAIN || window.location.origin;
|
|
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-white">Lade Video...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!currentVideo) {
|
|
return (
|
|
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
|
|
<div className="text-white">Video nicht gefunden</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="max-w-7xl mx-auto px-4 py-4">
|
|
<div className="flex items-center justify-between">
|
|
{/* Left side - Logo */}
|
|
<div className="flex items-center space-x-4">
|
|
<Link href="/" className="flex items-center space-x-2 hover:opacity-80 transition-opacity">
|
|
<div className="w-9 h-9 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
|
|
<div className="w-0 h-0 border-l-[10px] border-l-white border-y-[7px] 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="text-bunny-light hover:text-bunny-blue transition-colors">
|
|
Home
|
|
</Link>
|
|
<Link href="/folx-stadl" className="text-bunny-light hover:text-bunny-blue transition-colors">
|
|
FOLX STADL
|
|
</Link>
|
|
</nav>
|
|
|
|
<div className="relative">
|
|
<Input
|
|
type="search"
|
|
placeholder="Videos suchen..."
|
|
value={searchQuery}
|
|
onChange={(e) => {
|
|
setSearchQuery(e.target.value);
|
|
if (e.target.value) window.location.href = `/?search=${encodeURIComponent(e.target.value)}`;
|
|
}}
|
|
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 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-4 py-3">
|
|
{/* Mobile navigation links - horizontal */}
|
|
<nav className="flex space-x-6 mb-3">
|
|
<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>
|
|
|
|
{/* Mobile search - manjši */}
|
|
<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-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 left-2.5 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto 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="relative w-full h-0 pb-[56.25%] bg-black rounded-lg overflow-hidden mb-4"
|
|
onTouchStart={handleTouchStart}
|
|
onTouchMove={handleTouchMove}
|
|
onTouchEnd={handleTouchEnd}
|
|
>
|
|
{/* Navigation arrows - hidden on mobile, visible on desktop */}
|
|
{allVideos.length > 1 && (
|
|
<>
|
|
<Button
|
|
onClick={() => navigateToVideo('prev')}
|
|
className="absolute left-1 top-1/2 transform -translate-y-1/2 bg-black/70 hover:bg-black/90 text-white border-none p-3 rounded-full z-20 shadow-lg hidden md:flex items-center justify-center"
|
|
size="sm"
|
|
data-testid="button-prev-video"
|
|
>
|
|
<ChevronLeft className="w-5 h-5" />
|
|
</Button>
|
|
<Button
|
|
onClick={() => navigateToVideo('next')}
|
|
className="absolute right-1 top-1/2 transform -translate-y-1/2 bg-black/70 hover:bg-black/90 text-white border-none p-3 rounded-full z-20 shadow-lg hidden md:flex items-center justify-center"
|
|
size="sm"
|
|
data-testid="button-next-video"
|
|
>
|
|
<ChevronRight className="w-5 h-5" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
{currentVideo.videoUrlIframe ? (
|
|
<div className="relative w-full h-full">
|
|
<iframe
|
|
src={`${currentVideo.videoUrlIframe}${currentVideo.videoUrlIframe.includes('?') ? '&' : '?'}autoplay=1`}
|
|
className="absolute inset-0 w-full h-full opacity-0"
|
|
frameBorder="0"
|
|
allowFullScreen
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
onLoad={(e) => {
|
|
setTimeout(() => {
|
|
if (e.currentTarget) {
|
|
e.currentTarget.style.opacity = '1';
|
|
}
|
|
}, 500);
|
|
handleVideoPlay();
|
|
}}
|
|
title={currentVideo.title}
|
|
/>
|
|
<div className="absolute inset-0 bg-black"></div>
|
|
</div>
|
|
) : (
|
|
<div className="absolute inset-0 flex items-center justify-center text-white">
|
|
<p>Video nicht verfügbar</p>
|
|
</div>
|
|
)}
|
|
</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 go4.video`}>
|
|
<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 go4.video`}>
|
|
<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={() => setLocation(`/video/${video.id}`)}
|
|
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-purple-400/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-blue-400/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 md:flex-row items-center justify-between">
|
|
<div className="flex items-center space-x-2 mb-4 md:mb-0">
|
|
<div className="w-8 h-8 gradient-primary 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">go4.video</span>
|
|
</div>
|
|
|
|
<div className="text-sm text-bunny-muted">
|
|
© 2024 go4.video. Alle Rechte vorbehalten.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
);
|
|
} |