videofolxtv/client/src/pages/VideoPage.tsx
sebastjanartic e7fe1e6016 Make the header stay in place when scrolling down the page
Update CSS to change the header's position from sticky to fixed, ensuring it remains visible at the top of the viewport. Add padding to the body to prevent content from being obscured by the fixed header.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 051a65da-1176-4478-a61c-c662f2a15536
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/051a65da-1176-4478-a61c-c662f2a15536/BesXHXW
2025-08-30 21:23:51 +00:00

518 lines
21 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useRoute } 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 videoId = params?.id;
const [showShareMenu, setShowShareMenu] = useState(false);
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) {
window.location.href = `/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
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 copied!';
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">Nalagam video...</div>
</div>
);
}
if (!currentVideo) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="text-white">Video not found</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="Search videos..."
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>
</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">
{/* Navigation arrows */}
{allVideos.length > 1 && (
<>
<Button
onClick={() => navigateToVideo('prev')}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black/60 hover:bg-black/80 text-white border-none p-2 rounded-full z-20"
size="sm"
data-testid="button-prev-video"
>
<ChevronLeft className="w-6 h-6" />
</Button>
<Button
onClick={() => navigateToVideo('next')}
className="absolute right-4 top-1/2 transform -translate-y-1/2 bg-black/60 hover:bg-black/80 text-white border-none p-2 rounded-full z-20"
size="sm"
data-testid="button-next-video"
>
<ChevronRight className="w-6 h-6" />
</Button>
</>
)}
{currentVideo.videoUrlIframe ? (
<iframe
src={currentVideo.videoUrlIframe}
className="absolute inset-0 w-full h-full"
frameBorder="0"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onLoad={handleVideoPlay}
title={currentVideo.title}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-white">
<p>Video not available</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">Recommended Videos</h2>
<div className="space-y-3">
{recommendedVideos.slice(0, 10).map((video) => (
<div
key={video.id}
onClick={() => window.location.href = `/video/${video.id}`}
className="flex gap-3 p-3 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 space-y-0.5">
<div>{formatViews(video.views)} views</div>
<div>{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. All rights reserved.
</div>
</div>
</div>
</footer>
</div>
);
}