Update various text elements on the video page from Slovene to English, including loading messages, error messages, button labels, and social media share descriptions. Also adjusts the query key for fetching video data. 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/SnoheaM
280 lines
11 KiB
TypeScript
280 lines
11 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useRoute } from "wouter";
|
|
import { type Video } from "@shared/schema";
|
|
// 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}`],
|
|
enabled: !!videoId,
|
|
});
|
|
|
|
// Fetch recommended videos (excluding current video)
|
|
const { data: recommendedResponse } = useQuery<VideosResponse>({
|
|
queryKey: ["/api/videos", { limit: 20, offset: 0 }],
|
|
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 bg-bunny-dark">
|
|
{/* Header */}
|
|
<header className="bg-bunny-gray/50 border-b border-gray-700 p-4">
|
|
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-8 h-8 bg-bunny-blue rounded-full flex items-center justify-center">
|
|
<div className="w-0 h-0 border-l-[6px] border-l-white border-y-[4px] border-y-transparent ml-1"></div>
|
|
</div>
|
|
<h1 className="text-xl font-bold text-bunny-light">go4.video</h1>
|
|
</div>
|
|
<Button
|
|
onClick={() => window.history.back()}
|
|
className="bg-gray-700 hover:bg-gray-600 text-white"
|
|
>
|
|
← Back
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="max-w-7xl mx-auto p-4 lg:p-6">
|
|
<div className="flex flex-col lg:flex-row gap-6">
|
|
{/* 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.category && (
|
|
<div className="mb-4">
|
|
<span className="inline-block bg-bunny-blue text-white px-3 py-1 rounded-full text-sm">
|
|
{currentVideo.category}
|
|
</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;
|
|
target.style.display = 'none';
|
|
}}
|
|
/>
|
|
<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>
|
|
</div>
|
|
);
|
|
} |