This commit updates all instances of the old domain name "go4.video" to the new domain name "video.folx.tv" across various frontend components, including modals, headers, cards, and page metadata. It also includes updates to sharing intent URLs for Twitter and WhatsApp to use the new domain. Additionally, CSS for Video.js loading animations has been added to `index.css`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 946a0075-7e32-454b-b348-9e7f576d7f45 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/60d372ff-2c10-46c7-b01b-10c3435136b0/946a0075-7e32-454b-b348-9e7f576d7f45/jh6R7y2
333 lines
12 KiB
TypeScript
333 lines
12 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 Aufrufe`;
|
|
} else if (views >= 1000) {
|
|
return `${(views / 1000).toFixed(1)}K Aufrufe`;
|
|
}
|
|
return `${views} Aufrufe`;
|
|
}
|
|
|
|
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 "Heute";
|
|
if (diffDays === 1) return "vor 1 Tag";
|
|
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
|
if (diffDays < 30) return `vor ${Math.floor(diffDays / 7)} Woche${Math.floor(diffDays / 7) > 1 ? 'n' : ''}`;
|
|
return `vor ${Math.floor(diffDays / 30)} Monat${Math.floor(diffDays / 30) > 1 ? 'en' : ''}`;
|
|
}
|
|
|
|
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 {
|
|
// Use short ID for view tracking
|
|
const shortId = video.id.replace(/-/g, '').substring(0, 8);
|
|
await apiRequest("POST", `/api/videos/${shortId}/view`);
|
|
} catch (error) {
|
|
console.error("Failed to track video view:", error);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getShareUrl = () => {
|
|
if (!video?.id) return window.location.origin;
|
|
// Use custom domain if set, otherwise current domain
|
|
const baseUrl = import.meta.env.VITE_SHARE_DOMAIN || window.location.origin;
|
|
const shortId = video.id.replace(/-/g, '').substring(0, 8);
|
|
return `${baseUrl}/video/${shortId}`;
|
|
};
|
|
|
|
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);
|
|
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 video.folx.tv`;
|
|
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 video.folx.tv: ${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-end items-center mb-1 z-10">
|
|
<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" />
|
|
Bearbeiten
|
|
</Button>
|
|
)}
|
|
<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 gap-3 min-h-0">
|
|
{/* Main video player */}
|
|
<div className="w-full max-w-lg mx-auto md:ml-8 md:mx-0">
|
|
<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 nicht verfügbar</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-rose-600 hover:bg-rose-700 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-rose-600 hover:bg-rose-700 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>
|
|
</div>
|
|
|
|
{/* Video info always below video */}
|
|
<div className="w-full max-w-lg mx-auto md:ml-8 md:mx-0">
|
|
<div className="p-4 bg-gray-800 rounded-lg text-white relative">
|
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center mb-3 gap-2 md:gap-0">
|
|
<h3 className="font-bold text-xl whitespace-normal break-words flex-1 md:pr-4" data-testid="text-video-title">
|
|
{video.title}
|
|
</h3>
|
|
|
|
{/* Share button next to title */}
|
|
<div className="relative">
|
|
<button
|
|
onClick={() => setShowShareMenu(!showShareMenu)}
|
|
className="flex items-center gap-1 text-gray-300 hover:text-white text-sm transition-colors"
|
|
data-testid="button-share-video"
|
|
>
|
|
<Share2 className="w-4 h-4" />
|
|
Teilen
|
|
</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"
|
|
>
|
|
Link kopieren
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
);
|
|
} |