Restored to 'e087324c769fdb5e778619a362ad06f5e61119bf'

Replit-Restored-To: e087324c76
This commit is contained in:
sebastjanartic
2025-08-04 20:11:57 +00:00
parent 6593691ab3
commit 018f559617
11 changed files with 15 additions and 620 deletions
-268
View File
@@ -1,268 +0,0 @@
import { useParams } from "wouter";
import { useLocation } from "wouter";
import { useQuery } from "@tanstack/react-query";
import { type Video } from "@shared/schema";
import { Button } from "@/components/ui/button";
import { ArrowLeft, Share2, Eye, Clock } from "lucide-react";
import { Link } from "wouter";
import { useState, useEffect } from "react";
import ShareModal from "@/components/share-modal";
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 ogledov`;
} else if (views >= 1000) {
return `${(views / 1000).toFixed(1)}K ogledov`;
}
return `${views} ogledov`;
}
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 "Neznano";
}
const diffTime = Math.abs(now.getTime() - createdDate.getTime());
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) return "Danes";
if (diffDays === 1) return "Pred 1 dnem";
if (diffDays < 7) return `Pred ${diffDays} dnevi`;
if (diffDays < 30) return `Pred ${Math.floor(diffDays / 7)} tednom${Math.floor(diffDays / 7) > 1 ? 'i' : ''}`;
return `Pred ${Math.floor(diffDays / 30)} mesec${Math.floor(diffDays / 30) > 1 ? 'i' : 'em'}`;
}
export default function VideoPage() {
const params = useParams();
const [location] = useLocation();
const videoId = params.id;
const [isShareModalOpen, setIsShareModalOpen] = useState(false);
const [viewTracked, setViewTracked] = useState(false);
// Fetch video data
const { data: video, isLoading, error } = useQuery<Video>({
queryKey: ["/api/videos", videoId],
queryFn: async () => {
const response = await fetch(`/api/videos/${videoId}`);
if (!response.ok) {
throw new Error('Video not found');
}
return response.json();
},
enabled: !!videoId
});
// Track video view
useEffect(() => {
if (video && !viewTracked) {
fetch(`/api/videos/${video.id}/view`, { method: 'POST' })
.catch(console.error);
setViewTracked(true);
}
}, [video, viewTracked]);
// Set page title and meta tags
useEffect(() => {
if (video) {
document.title = `${video.title} - VideoStream`;
// Update meta tags for social sharing
const updateMeta = (name: string, content: string, property = true) => {
const selector = property ? `meta[property="${name}"]` : `meta[name="${name}"]`;
let meta = document.querySelector(selector) as HTMLMetaElement;
if (!meta) {
meta = document.createElement('meta');
if (property) {
meta.setAttribute('property', name);
} else {
meta.setAttribute('name', name);
}
document.head.appendChild(meta);
}
meta.content = content;
};
const shareUrl = `${window.location.origin}/video/${video.id}`;
// Open Graph tags for Facebook
updateMeta('og:title', video.title);
updateMeta('og:description', video.description || `Oglej si ta video na VideoStream`);
updateMeta('og:image', video.thumbnailUrl);
updateMeta('og:image:width', '1200');
updateMeta('og:image:height', '630');
updateMeta('og:image:type', 'image/jpeg');
updateMeta('og:url', shareUrl);
updateMeta('og:type', 'video.other');
updateMeta('og:site_name', 'VideoStream');
// Standard meta tags
updateMeta('description', video.description || `Oglej si ta video na VideoStream`, false);
// Twitter Card tags
updateMeta('twitter:card', 'summary_large_image', false);
updateMeta('twitter:title', video.title, false);
updateMeta('twitter:description', video.description || `Oglej si ta video na VideoStream`, false);
updateMeta('twitter:image', video.thumbnailUrl, false);
}
}, [video]);
if (isLoading) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-bunny-blue"></div>
</div>
);
}
if (error || !video) {
return (
<div className="min-h-screen bg-bunny-dark flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-bunny-light mb-4">Video ni najden</h1>
<p className="text-bunny-muted mb-6">Video, ki ga iščete, ne obstaja ali je bil odstranjen.</p>
<Link href="/">
<Button className="bg-bunny-blue hover:bg-blue-600 text-white">
<ArrowLeft className="w-4 h-4 mr-2" />
Nazaj na domačo stran
</Button>
</Link>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-bunny-dark">
{/* Header */}
<header className="bg-bunny-gray/80 backdrop-blur-sm border-b border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<Link href="/">
<Button variant="ghost" className="text-bunny-light hover:text-bunny-blue">
<ArrowLeft className="w-5 h-5 mr-2" />
Nazaj na seznam
</Button>
</Link>
<Button
onClick={() => setIsShareModalOpen(true)}
className="bg-bunny-blue hover:bg-blue-600 text-white"
data-testid="button-share-video"
>
<Share2 className="w-4 h-4 mr-2" />
Deli
</Button>
</div>
</div>
</header>
{/* Video Player */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2">
<div className="bg-black rounded-lg overflow-hidden aspect-video">
{video.videoUrl.includes('iframe.mediadelivery.net') ? (
<iframe
src={video.videoUrl}
className="w-full h-full"
allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture; fullscreen;"
allowFullScreen={true}
frameBorder="0"
data-testid="video-iframe"
/>
) : (
<video
className="w-full h-full"
controls
preload="metadata"
data-testid="video-player"
>
<source src={video.videoUrl} type="video/mp4" />
Your browser does not support the video tag.
</video>
)}
</div>
<div className="mt-6">
<h1 className="text-2xl font-bold text-bunny-light mb-4" data-testid="text-video-title">
{video.title}
</h1>
<div className="flex items-center space-x-6 text-bunny-muted mb-6">
<div className="flex items-center space-x-2">
<Eye className="w-4 h-4" />
<span data-testid="text-video-views">{formatViews(video.views)}</span>
</div>
<div className="flex items-center space-x-2">
<Clock className="w-4 h-4" />
<span data-testid="text-video-duration">{formatDuration(video.duration)}</span>
</div>
<span data-testid="text-video-date">{formatDate(video.createdAt)}</span>
{video.category && (
<span className="bg-bunny-gray px-3 py-1 rounded-full text-sm" data-testid="text-video-category">
{video.category}
</span>
)}
</div>
{video.description && (
<div className="bg-bunny-gray rounded-lg p-4">
<h3 className="font-semibold text-bunny-light mb-2">Opis</h3>
<p className="text-bunny-muted leading-relaxed" data-testid="text-video-description">
{video.description}
</p>
</div>
)}
</div>
</div>
<div className="lg:col-span-1">
<div className="bg-bunny-gray rounded-lg p-6">
<h3 className="font-semibold text-bunny-light mb-4">Deli ta video</h3>
<Button
onClick={() => setIsShareModalOpen(true)}
className="w-full bg-bunny-blue hover:bg-blue-600 text-white mb-4"
data-testid="button-share-sidebar"
>
<Share2 className="w-4 h-4 mr-2" />
Deli video
</Button>
<div className="space-y-3 text-sm text-bunny-muted">
<div>
<strong className="text-bunny-light">Trajanje:</strong> {formatDuration(video.duration)}
</div>
<div>
<strong className="text-bunny-light">Ogledi:</strong> {formatViews(video.views)}
</div>
<div>
<strong className="text-bunny-light">Dodano:</strong> {formatDate(video.createdAt)}
</div>
{video.category && (
<div>
<strong className="text-bunny-light">Kategorija:</strong> {video.category}
</div>
)}
</div>
</div>
</div>
</div>
</main>
<ShareModal
video={video}
isOpen={isShareModalOpen}
onClose={() => setIsShareModalOpen(false)}
/>
</div>
);
}