videofolxtv/client/src/components/bunny-video-modal.tsx
sebastjanartic 49c2842f74 Improve video modal layout for better mobile and desktop viewing
Adjusted the layout of the video modal to display video information below the player on mobile devices and within a sidebar on desktop, enhancing responsiveness.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 074b0e4c-6171-43bd-aa98-f9e04623ca14
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/074b0e4c-6171-43bd-aa98-f9e04623ca14/DVZN4Rp
2025-08-30 15:07:24 +00:00

352 lines
13 KiB
TypeScript

import { useEffect, useState } from "react";
import { X, Share2, Edit3, ChevronLeft, ChevronRight, Play } from "lucide-react";
import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button";
import { apiRequest } from "@/lib/queryClient";
import {
FacebookIcon,
TwitterIcon,
WhatsappIcon
} from "react-share";
interface BunnyVideoModalProps {
video: Video | null;
isOpen: boolean;
onClose: () => void;
onEdit?: () => void;
videos?: Video[];
onVideoChange?: (video: Video) => void;
}
function formatDuration(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
function formatViews(views: number): string {
if (views >= 1000000) {
return `${(views / 1000000).toFixed(1)}M views`;
} else if (views >= 1000) {
return `${(views / 1000).toFixed(1)}K views`;
}
return `${views} views`;
}
function formatDate(date: Date | string): string {
const now = new Date();
const createdDate = typeof date === 'string' ? new Date(date) : date;
if (!createdDate || isNaN(createdDate.getTime())) {
return "Unknown";
}
const diffTime = Math.abs(now.getTime() - createdDate.getTime());
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Today";
if (diffDays === 1) return "1 day ago";
if (diffDays < 7) return `${diffDays} days ago`;
if (diffDays < 30) return `${Math.floor(diffDays / 7)} week${Math.floor(diffDays / 7) > 1 ? 's' : ''} ago`;
return `${Math.floor(diffDays / 30)} month${Math.floor(diffDays / 30) > 1 ? 's' : ''} ago`;
}
export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos = [], onVideoChange }: BunnyVideoModalProps) {
const [showShareMenu, setShowShareMenu] = useState(false);
// Navigation functions
const getCurrentVideoIndex = () => {
if (!video || !videos.length) return -1;
return videos.findIndex((v: Video) => v.id === video.id);
};
const navigateToVideo = (direction: 'next' | 'prev') => {
const currentIndex = getCurrentVideoIndex();
if (currentIndex === -1 || !onVideoChange) return;
let newIndex;
if (direction === 'next') {
newIndex = currentIndex + 1 >= videos.length ? 0 : currentIndex + 1;
} else {
newIndex = currentIndex - 1 < 0 ? videos.length - 1 : currentIndex - 1;
}
const newVideo = videos[newIndex];
if (newVideo) {
onVideoChange(newVideo);
}
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === "Escape" && isOpen) {
onClose();
}
};
if (isOpen) {
document.addEventListener("keydown", handleEscape);
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.removeEventListener("keydown", handleEscape);
document.body.style.overflow = "";
};
}, [isOpen, onClose]);
const handleVideoPlay = async () => {
if (video) {
try {
await apiRequest("POST", `/api/videos/${video.id}/view`);
} catch (error) {
console.error("Failed to track video view:", error);
}
}
};
const getShareUrl = () => {
if (!video?.id) return window.location.origin;
return `${window.location.origin}?video=${video.id}`;
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(getShareUrl());
const notification = document.createElement('div');
notification.textContent = 'Povezava kopirana!';
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);
const textArea = document.createElement('textarea');
textArea.value = getShareUrl();
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setShowShareMenu(false);
}
};
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
const shareOnFacebook = () => {
const url = `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(getShareUrl())}&t=${encodeURIComponent(video?.title || '')}`;
window.open(url, 'facebook-share', 'width=600,height=400');
setShowShareMenu(false);
};
const shareOnTwitter = () => {
const text = `Poglej si "${video?.title}" na go4.video`;
const url = `https://twitter.com/intent/tweet?text=${encodeURIComponent(text)}&url=${encodeURIComponent(getShareUrl())}`;
window.open(url, 'twitter-share', 'width=600,height=400');
setShowShareMenu(false);
};
const shareOnWhatsApp = () => {
const text = `Poglej si "${video?.title}" na go4.video: ${getShareUrl()}`;
const url = `https://wa.me/?text=${encodeURIComponent(text)}`;
window.open(url, 'whatsapp-share', 'width=600,height=400');
setShowShareMenu(false);
};
if (!isOpen || !video) return null;
return (
<div
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50"
onClick={handleBackdropClick}
style={{ backgroundColor: '#1f2937' }}
>
<div className="relative w-full h-full max-w-7xl mx-auto p-4 flex flex-col">
{/* Header with close button */}
<div className="flex justify-between items-center mb-1 z-10">
<h2 className="text-xl font-bold text-white flex-1 mr-4 leading-tight">
{video.title}
</h2>
<div className="flex gap-2">
{onEdit && (
<Button
variant="ghost"
size="sm"
onClick={onEdit}
className="text-white hover:bg-gray-700"
data-testid="button-edit-video"
>
<Edit3 className="w-4 h-4" />
Edit
</Button>
)}
<div className="relative">
<Button
variant="ghost"
size="sm"
onClick={() => setShowShareMenu(!showShareMenu)}
className="text-white hover:bg-gray-700"
data-testid="button-share-video"
>
<Share2 className="w-4 h-4" />
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]"
style={{ backgroundColor: '#374151' }}
>
<button
onClick={shareOnFacebook}
className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2"
data-testid="button-share-facebook"
>
<FacebookIcon size={16} round />
Facebook
</button>
<button
onClick={shareOnTwitter}
className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2"
data-testid="button-share-twitter"
>
<TwitterIcon size={16} round />
Twitter
</button>
<button
onClick={shareOnWhatsApp}
className="w-full px-4 py-2 text-left text-white hover:bg-gray-700 flex items-center gap-2"
data-testid="button-share-whatsapp"
>
<WhatsappIcon size={16} round />
WhatsApp
</button>
<button
onClick={copyToClipboard}
className="w-full px-4 py-2 text-left text-white hover:bg-gray-700"
data-testid="button-copy-link"
>
Copy Link
</button>
</div>
)}
</div>
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="text-white hover:bg-gray-700"
data-testid="button-close-modal"
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
{/* Video player area */}
<div className="flex-1 flex flex-col lg:flex-row gap-2 min-h-0">
{/* Main video player */}
<div className="flex-1 flex flex-col">
<div className="relative w-full aspect-video bg-black rounded-lg overflow-hidden">
{video.videoUrlIframe ? (
<iframe
src={video.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={video.title}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-white">
<p>Video not available</p>
</div>
)}
{/* Navigation buttons - always visible */}
{videos.length > 1 && (
<>
<Button
onClick={() => navigateToVideo('prev')}
className="absolute left-4 top-1/2 transform -translate-y-1/2 bg-black bg-opacity-60 hover:bg-opacity-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 bg-opacity-60 hover:bg-opacity-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>
</>
)}
</div>
{/* Video info directly under video on mobile, sidebar on desktop */}
<div className="block lg:hidden mt-0">
<div className="p-3 bg-gray-800 rounded-lg text-white max-h-none overflow-visible">
<h3 className="font-bold text-lg mb-2 whitespace-normal break-words" data-testid="text-video-title">
{video.title}
</h3>
<div className="flex flex-wrap gap-4 text-sm text-gray-300 mb-3">
<span data-testid="text-video-views">{formatViews(video.views)}</span>
<span data-testid="text-video-duration">{formatDuration(video.duration)}</span>
<span data-testid="text-video-date">{formatDate(video.createdAt)}</span>
</div>
{video.description && (
<div className="text-sm text-gray-300">
<p className="leading-relaxed whitespace-pre-wrap break-words" data-testid="text-video-description">
{video.description}
</p>
</div>
)}
</div>
</div>
</div>
{/* Video info sidebar - desktop only */}
<div className="hidden lg:block lg:w-80 lg:min-w-80 space-y-0 overflow-y-auto lg:ml-4">
<div className="p-3 bg-gray-800 rounded-lg text-white max-h-none overflow-visible">
<h3 className="font-bold text-lg mb-2 whitespace-normal break-words" data-testid="text-video-title">
{video.title}
</h3>
<div className="flex flex-wrap gap-4 text-sm text-gray-300 mb-3">
<span data-testid="text-video-views">{formatViews(video.views)}</span>
<span data-testid="text-video-duration">{formatDuration(video.duration)}</span>
<span data-testid="text-video-date">{formatDate(video.createdAt)}</span>
</div>
{video.description && (
<div className="text-sm text-gray-300">
<p className="leading-relaxed whitespace-pre-wrap break-words" data-testid="text-video-description">
{video.description}
</p>
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}