videofolxtv/client/src/components/bunny-video-modal.tsx
sebastjanartic 25562ab317 Update interface text to German for better user experience
Translate UI elements and messages from English to German across various components and pages, including video availability, loading states, and recommendations.

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/9NQBiz8
2025-08-30 22:03:12 +00:00

330 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 {
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;
// Use custom domain if set, otherwise current domain
const baseUrl = import.meta.env.VITE_SHARE_DOMAIN || window.location.origin;
return `${baseUrl}/video/${video.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);
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-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-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>
</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>
);
}