videofolxtv/client/src/pages/VideoPage.tsx
sebastjanartic 83a93bf190 Add dynamic background logo elements to enhance page visuals
Replaced static background logo classes with inline styles for dynamic positioning and sizing, importing the logo asset to all relevant pages (VideoPage, Home, NotFound).

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: d7424866-83d1-4486-a212-ac12b4c7becf
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/d7424866-83d1-4486-a212-ac12b4c7becf/ccgD6tv
2025-08-28 15:36:44 +00:00

409 lines
17 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('sl-SI', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
import { Button } from "@/components/ui/button";
import { Share2, X, Edit3 } from "lucide-react";
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);
// 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) || [];
// 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;
return `${window.location.origin}/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">Loading 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 floating-triangles">
{/* Header with triangle design */}
<div className="bunny-gray border-b border-white/20 sticky top-0 z-50 backdrop-blur-md relative overflow-hidden">
{/* Triangle decorations in header - same as home page */}
<div className="absolute top-2 right-20 w-0 h-0 border-l-[35px] border-l-transparent border-r-[35px] border-r-transparent border-b-[50px] border-b-blue-400/15 rotate-12"></div>
<div className="absolute top-3 left-1/3 w-0 h-0 border-l-[25px] border-l-transparent border-r-[25px] border-r-transparent border-b-[35px] border-b-purple-400/12 -rotate-6"></div>
<div className="absolute top-1 right-1/2 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-b-[30px] border-b-cyan-400/10 rotate-45"></div>
<div className="triangle-decoration-2 opacity-30" style={{top: '10px', right: '15%'}}></div>
<div className="triangle-decoration-3 opacity-20" style={{top: '30px', left: '70%'}}></div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
<div className="flex items-center justify-between h-20">
<div className="flex items-center space-x-4">
<a 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>
</a>
</div>
<div className="hidden md:flex items-center space-x-6">
<nav className="flex space-x-6">
<a href="/" className="text-bunny-light hover:text-bunny-blue transition-colors" data-testid="link-home">
Home
</a>
</nav>
<Button
onClick={() => window.history.back()}
className="bg-white/10 hover:bg-white/20 text-white border border-white/20 backdrop-blur-sm transition-all duration-200"
>
Back
</Button>
</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-10">
{/* 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">
{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()}>
<button className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2">
<FacebookIcon size={16} round />
Facebook
</button>
</FacebookShareButton>
<TwitterShareButton url={getShareUrl()} title={`Watch "${currentVideo.title}" on go4.video`}>
<button className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2">
<TwitterIcon size={16} round />
Twitter
</button>
</TwitterShareButton>
<WhatsappShareButton url={getShareUrl()} title={`Watch "${currentVideo.title}" on go4.video`}>
<button className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2">
<WhatsappIcon size={16} round />
WhatsApp
</button>
</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>
</div>
{/* Recommended videos sidebar */}
<div className="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 */}
<div className="absolute bottom-2 left-10 w-0 h-0 border-l-[30px] border-l-transparent border-r-[30px] border-r-transparent border-b-[40px] border-b-purple-400/8 rotate-12"></div>
<div className="absolute bottom-1 right-1/4 w-0 h-0 border-l-[20px] border-l-transparent border-r-[20px] border-r-transparent border-b-[30px] border-b-blue-400/6 -rotate-12"></div>
<div className="absolute bottom-3 left-1/2 w-0 h-0 border-l-[25px] border-l-transparent border-r-[25px] border-r-transparent border-b-[35px] border-b-cyan-400/7 rotate-45"></div>
<div className="triangle-decoration-1 opacity-20" style={{bottom: '10px', right: '20%'}}></div>
<div className="triangle-decoration-3 opacity-15" style={{bottom: '20px', left: '15%'}}></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>
);
}