Restored to '7fd7127003398a269f1f765208c8baced728aafb'

Replit-Restored-To: 7fd7127003
This commit is contained in:
sebastjanartic 2025-08-30 12:39:34 +00:00
parent a424d51560
commit 2355fc4da6
30 changed files with 111 additions and 602 deletions

View File

@ -40,3 +40,4 @@ args = "npm run dev"
waitForPort = 5000
[agent]
integrations = ["javascript_log_in_with_replit==1.0.0", "javascript_database==1.0.0", "javascript_object_storage==1.0.0"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 529 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

View File

@ -26,19 +26,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="theme-color" content="#3b82f6" />
<!-- Oswald Google Font z improved loading -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Oswald:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Fallback font za desktop če se Oswald ne naloži */
@font-face {
font-family: 'Oswald-fallback';
src: local('Arial Black'), local('Helvetica');
font-weight: 600;
}
</style>
</head>
<body>
<div id="root"></div>

View File

@ -116,7 +116,7 @@ export default function AdSettings({ isOpen, onClose }: AdSettingsProps) {
if (!isOpen) return null;
return (
<div className="modal-overlay fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4" style={{ zIndex: 2147483647, position: 'fixed' }}>
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-gray-900 rounded-lg max-w-4xl w-full max-h-[90vh] overflow-y-auto">
<div className="sticky top-0 bg-white dark:bg-gray-900 p-6 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between">

View File

@ -168,19 +168,9 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
return (
<div
className="modal-overlay fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center"
style={{
zIndex: 2147483647,
backgroundColor: '#1f2937',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100vw',
height: '100vh'
}}
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 */}
@ -275,7 +265,7 @@ export default function BunnyVideoModal({ video, isOpen, onClose, onEdit, videos
className="absolute inset-0 w-full h-full"
frameBorder="0"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onLoad={handleVideoPlay}
title={video.title}
/>

View File

@ -1,10 +1,9 @@
import { useState, useRef, useEffect } from "react";
import { useState, useRef } from "react";
import { type Video } from "@shared/schema";
import VideoCard from "./video-card";
import BunnyVideoModal from "./bunny-video-modal";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight } from "lucide-react";
import SimpleCarousel from "./simple-carousel";
interface VideoCategory {
title: string;
@ -106,9 +105,9 @@ export default function NetflixGrid({ videos, isLoading }: NetflixGridProps) {
return (
<>
<div className="space-y-12 relative">
<div className="space-y-12">
{categories.map((category, categoryIndex) => (
<SimpleCarousel
<CategoryRow
key={categoryIndex}
category={category}
onVideoClick={handleVideoClick}
@ -132,88 +131,23 @@ interface CategoryRowProps {
onVideoClick: (video: Video) => void;
}
function CategoryRowOLD_BROKEN({ category, onVideoClick }: CategoryRowProps) {
function CategoryRow({ category, onVideoClick }: CategoryRowProps) {
const scrollRef = useRef<HTMLDivElement>(null);
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [showLeftButton, setShowLeftButton] = useState(false);
const [showRightButton, setShowRightButton] = useState(true);
const [isScrolling, setIsScrolling] = useState(false);
const [speedMode, setSpeedMode] = useState<'normal' | 'fast'>('normal');
const scroll = (direction: 'left' | 'right') => {
if (!scrollContainerRef.current) return;
const scrollAmount = direction === 'right' ? 300 : -300;
scrollContainerRef.current.scrollBy({
left: scrollAmount,
behavior: 'smooth'
});
};
const toggleSpeed = (direction: 'left' | 'right') => {
// Toggle speed mode and restart animation with new speed
const newSpeed = speedMode === 'normal' ? 'fast' : 'normal';
setSpeedMode(newSpeed);
// Restart the animation with new speed
if (isScrolling && scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
// Start new interval with updated speed immediately
const baseSpeed = newSpeed === 'fast' ? 2.5 : 0.8;
const interval = 16; // Fixed interval - speed controlled by pixel movement only
scrollIntervalRef.current = setInterval(() => {
if (!scrollContainerRef.current) return;
if (scrollRef.current) {
// Responsive scroll amount - wider cards on desktop
const isMobile = window.innerWidth < 768;
const scrollAmount = isMobile ? 240 : 336; // Mobile: 224px + gap, Desktop: 320px + gap
const currentScroll = scrollRef.current.scrollLeft;
const targetScroll = direction === 'left'
? currentScroll - scrollAmount
: currentScroll + scrollAmount;
const currentSpeed = newSpeed === 'fast' ? 5 : 2;
const scrollAmount = direction === 'right' ? currentSpeed : -currentSpeed;
scrollContainerRef.current.scrollBy({
left: scrollAmount,
behavior: 'auto'
});
}, 16);
}
};
const startAutoScroll = (direction: 'left' | 'right') => {
// Clear any existing interval
if (scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
}
setIsScrolling(true);
// Start continuous smooth scrolling with variable speed
scrollIntervalRef.current = setInterval(() => {
if (!scrollContainerRef.current) return;
const baseSpeed = 3; // Faster speed on hover
const scrollAmount = direction === 'right' ? baseSpeed : -baseSpeed;
scrollContainerRef.current.scrollBy({
left: scrollAmount,
behavior: 'auto'
scrollRef.current.scrollTo({
left: targetScroll,
behavior: 'smooth'
});
}, 16);
};
// Initialize with first video on the left side
useEffect(() => {
// No need to set initial position for scroll - browser handles it
}, [category.videos.length]);
// Always show both buttons
useEffect(() => {
setShowLeftButton(true);
setShowRightButton(true);
}, []);
const stopAutoScroll = () => {
setIsScrolling(false);
if (scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
scrollIntervalRef.current = null;
}
};
@ -223,125 +157,50 @@ function CategoryRowOLD_BROKEN({ category, onVideoClick }: CategoryRowProps) {
{category.title}
</h2>
<div className="relative">
{/* Left scroll button - positioned at video thumbnail center */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
scroll('left');
}}
onMouseEnter={(e) => {
e.stopPropagation();
startAutoScroll('left');
}}
onMouseLeave={(e) => {
e.stopPropagation();
stopAutoScroll();
}}
className="flex absolute left-2 top-[45%] -translate-y-1/2 w-12 h-12 z-30 bg-black/80 hover:bg-black/95 rounded-full items-center justify-center transition-all duration-300 cursor-pointer border border-white/30 shadow-lg opacity-80 hover:!opacity-100"
data-testid="button-scroll-left"
<div className="relative px-16">
{/* Left scroll button */}
<Button
onClick={() => scroll('left')}
className="absolute -left-2 top-1/2 -translate-y-1/2 z-30 bg-black/80 hover:bg-black/95 text-white border-none w-12 h-12 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center shadow-xl"
size="sm"
>
<ChevronLeft className="w-6 h-6 text-white" />
</button>
<ChevronLeft className="w-5 h-5" />
</Button>
{/* Right scroll button - positioned at video thumbnail center */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
scroll('right');
}}
onMouseEnter={(e) => {
e.stopPropagation();
startAutoScroll('right');
}}
onMouseLeave={(e) => {
e.stopPropagation();
stopAutoScroll();
}}
className="flex absolute right-2 top-[45%] -translate-y-1/2 w-12 h-12 z-30 bg-black/80 hover:bg-black/95 rounded-full items-center justify-center transition-all duration-300 cursor-pointer border border-white/30 shadow-lg opacity-80 hover:!opacity-100"
data-testid="button-scroll-right"
{/* Right scroll button */}
<Button
onClick={() => scroll('right')}
className="absolute -right-2 top-1/2 -translate-y-1/2 z-30 bg-black/80 hover:bg-black/95 text-white border-none w-12 h-12 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center shadow-xl"
size="sm"
>
<ChevronRight className="w-6 h-6 text-white" />
</button>
<ChevronRight className="w-5 h-5" />
</Button>
{/* Mobile scroll buttons */}
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
scroll('left');
}}
onTouchStart={(e) => {
e.stopPropagation();
startAutoScroll('left');
}}
onTouchEnd={(e) => {
e.stopPropagation();
stopAutoScroll();
}}
className="md:hidden absolute left-1 top-[45%] -translate-y-1/2 w-10 h-10 z-40 bg-black/80 rounded-full flex items-center justify-center border border-white/30 shadow-lg"
data-testid="button-mobile-scroll-left"
>
<ChevronLeft className="w-5 h-5 text-white" />
</button>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
scroll('right');
}}
onTouchStart={(e) => {
e.stopPropagation();
startAutoScroll('right');
}}
onTouchEnd={(e) => {
e.stopPropagation();
stopAutoScroll();
}}
className="md:hidden absolute right-1 top-[45%] -translate-y-1/2 w-10 h-10 z-40 bg-black/80 rounded-full flex items-center justify-center border border-white/30 shadow-lg"
data-testid="button-mobile-scroll-right"
>
<ChevronRight className="w-5 h-5 text-white" />
</button>
{/* Simple horizontal scroll carousel */}
<div
ref={scrollContainerRef}
className="overflow-x-auto scrollbar-hide"
{/* Scrollable video row */}
<div
ref={scrollRef}
className="flex space-x-4 overflow-x-auto scrollbar-hide pb-4"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
<div className="flex space-x-4 pb-4 w-max">
{/* ULTRA SIMPLE - just many copies */}
{Array.from({ length: 50 }).map((_, copyIndex) =>
category.videos.map((video, videoIndex) => (
<div key={`${video.id}-${copyIndex}-${videoIndex}`} className="flex-shrink-0 w-28 md:w-52 relative group">
{/* Top 10 Number overlay for first category */}
{category.title.includes("Top 10") && (
<div className="absolute top-1 left-1 z-30 text-white font-black text-3xl md:text-5xl drop-shadow-2xl pointer-events-none"
style={{
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
WebkitTextStroke: '2px rgba(0,0,0,0.8)',
transform: 'none',
transition: 'none'
}}>
{videoIndex + 1}
</div>
)}
<VideoCard
video={video}
onClick={onVideoClick}
className="w-full hover:scale-105 hover:z-10 transition-all duration-300 group-hover:shadow-xl rounded-md overflow-hidden"
/>
{/* Simple debug number */}
<div className="absolute bottom-0 right-0 bg-green-500 text-white text-xs px-1 rounded">
{videoIndex + 1}
</div>
{category.videos.map((video, index) => (
<div key={video.id} className="flex-shrink-0 w-56 md:w-80 relative group">
{/* Top 10 Number overlay for first category */}
{category.title.includes("Top 10") && index < 10 && (
<div className="absolute top-2 left-2 z-20 text-white font-black text-5xl md:text-7xl drop-shadow-2xl"
style={{
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
WebkitTextStroke: '2px rgba(0,0,0,0.8)'
}}>
{index + 1}
</div>
))
).flat()}
</div>
)}
<VideoCard
video={video}
onClick={onVideoClick}
className="w-full hover:scale-110 hover:z-10 transition-all duration-500 group-hover:shadow-2xl rounded-lg overflow-hidden"
/>
</div>
))}
</div>
</div>
</div>

View File

@ -59,7 +59,7 @@ export default function SearchHeader({
<div className="w-9 h-9 gradient-primary rounded-lg flex items-center justify-center shadow-lg">
<div className="w-0 h-0 border-l-[10px] border-l-white border-y-[7px] border-y-transparent ml-1"></div>
</div>
<h1 className="oswald-text text-2xl text-white">go4.video</h1>
<h1 className="text-2xl font-bold text-white tracking-wide">go4.video</h1>
</a>
</div>

View File

@ -1,128 +0,0 @@
import { useState, useRef, useEffect } from "react";
import { type Video } from "@shared/schema";
import VideoCard from "./video-card";
import { ChevronLeft, ChevronRight } from "lucide-react";
interface SimpleCarouselProps {
category: {
title: string;
videos: Video[];
};
onVideoClick: (video: Video) => void;
}
export default function SimpleCarousel({ category, onVideoClick }: SimpleCarouselProps) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const scrollIntervalRef = useRef<NodeJS.Timeout | null>(null);
const [isScrolling, setIsScrolling] = useState(false);
const [currentDirection, setCurrentDirection] = useState<'left' | 'right' | null>(null);
const [speed, setSpeed] = useState<'normal' | 'fast'>('normal');
const scroll = (direction: 'left' | 'right') => {
// If already scrolling in same direction, toggle speed
if (isScrolling && currentDirection === direction) {
const newSpeed = speed === 'normal' ? 'fast' : 'normal';
setSpeed(newSpeed);
// Restart with new speed immediately
if (scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
}
const speedValue = newSpeed === 'fast' ? 1.8 : 0.8;
scrollIntervalRef.current = setInterval(() => {
if (!scrollContainerRef.current) return;
const scrollAmount = direction === 'right' ? speedValue : -speedValue;
scrollContainerRef.current.scrollBy({
left: scrollAmount,
behavior: 'auto'
});
}, 8);
} else {
// If not scrolling or different direction, start scrolling
startAutoScroll(direction);
}
};
const startAutoScroll = (direction: 'left' | 'right') => {
if (scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
}
setIsScrolling(true);
setCurrentDirection(direction);
setSpeed('normal'); // Reset to normal speed when starting
const speedValue = 0.8; // Always start with normal speed
scrollIntervalRef.current = setInterval(() => {
if (!scrollContainerRef.current) return;
const scrollAmount = direction === 'right' ? speedValue : -speedValue;
scrollContainerRef.current.scrollBy({
left: scrollAmount,
behavior: 'auto'
});
}, 8);
};
const stopAutoScroll = () => {
if (scrollIntervalRef.current) {
clearInterval(scrollIntervalRef.current);
scrollIntervalRef.current = null;
}
setIsScrolling(false);
setCurrentDirection(null);
setSpeed('normal');
};
// Initialize scroll position in the middle so we can scroll both ways
useEffect(() => {
if (scrollContainerRef.current && category.videos.length > 0) {
// Wait for content to load, then scroll to middle
setTimeout(() => {
if (scrollContainerRef.current) {
const containerWidth = scrollContainerRef.current.scrollWidth;
const viewportWidth = scrollContainerRef.current.clientWidth;
const middlePosition = (containerWidth - viewportWidth) / 2;
scrollContainerRef.current.scrollTo({
left: middlePosition,
behavior: 'auto'
});
}
}, 100);
}
}, [category.videos.length]);
return (
<div className="relative group mb-12">
<h2 className="oswald-text text-2xl text-bunny-light mb-6 px-4">
{category.title}
</h2>
{/* Container z flex wrap */}
<div className="container flex flex-wrap justify-center gap-2.5 p-2.5 md:flex-row flex-col items-center">
{category.videos.map((video, videoIndex) => (
<div key={video.id} className="relative">
{/* Top 10 Number overlay for first category */}
{category.title.includes("Top 10") && (
<div className="absolute top-1 left-1 z-30 text-white font-black text-xl md:text-2xl drop-shadow-2xl pointer-events-none"
style={{
textShadow: '4px 4px 8px rgba(0,0,0,0.8), -2px -2px 4px rgba(0,0,0,0.6)',
WebkitTextStroke: '1px rgba(0,0,0,0.8)',
}}>
{videoIndex + 1}
</div>
)}
<VideoCard
video={video}
onClick={onVideoClick}
className="poster w-[150px] md:w-[150px] sm:w-[120px] h-auto hover:scale-105 transition-all duration-300 hover:shadow-2xl rounded-md overflow-hidden relative"
/>
</div>
))}
</div>
</div>
);
}

View File

@ -270,7 +270,7 @@ export default function VASTPlayer({ video, onClose, vastTagUrl, enableAds = tru
};
return (
<div className="modal-overlay fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center" style={{ zIndex: 2147483647, position: 'fixed' }}>
<div className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50">
<div className="relative w-full max-w-6xl mx-4">
{/* Close button */}
<Button

View File

@ -1,9 +1,7 @@
import { Play, Plus, ThumbsUp, ChevronDown } from "lucide-react";
import { Play } from "lucide-react";
import { type Video } from "@shared/schema";
import HLSPreviewThumbnail from "./hls-preview-thumbnail";
import { useState, useRef, useEffect } from "react";
// @ts-ignore
import Hls from 'hls.js';
interface VideoCardProps {
video: Video;
@ -48,43 +46,10 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
const [isHovered, setIsHovered] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const hoverTimeoutRef = useRef<NodeJS.Timeout>();
const videoRef = useRef<HTMLVideoElement>(null);
const hlsRef = useRef<any>(null);
const animationFrameRef = useRef<number>();
// Handle mouse scrubbing for video preview with throttling for smoothness
const lastScrubTime = useRef(0);
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (!showPreview || !videoRef.current) return;
const now = Date.now();
// Throttle scrubbing to ~60fps for smoother experience
if (now - lastScrubTime.current < 16) return;
lastScrubTime.current = now;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const progress = Math.max(0, Math.min(1, x / rect.width));
// Scrub video based on mouse position with smooth seeking
if (videoRef.current.duration && !isNaN(videoRef.current.duration)) {
const targetTime = progress * videoRef.current.duration;
// Use requestAnimationFrame for smoother seeking
requestAnimationFrame(() => {
if (videoRef.current) {
videoRef.current.currentTime = targetTime;
}
});
}
};
// Delay preview start to avoid loading on quick mouse passes
// Only enable previews on desktop devices with mouse
useEffect(() => {
const isMobile = window.innerWidth < 768 || 'ontouchstart' in window;
if (isHovered && !isMobile) {
if (isHovered) {
hoverTimeoutRef.current = setTimeout(() => {
setShowPreview(true);
}, 800); // Start preview after 800ms hover
@ -102,89 +67,23 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
};
}, [isHovered]);
// Setup HLS when preview is shown
useEffect(() => {
if (showPreview && videoRef.current && video.videoUrl) {
const videoElement = videoRef.current;
if (Hls.isSupported()) {
console.log('Setting up HLS preview for:', video.title);
hlsRef.current = new Hls({
enableWorker: false,
lowLatencyMode: true,
backBufferLength: 10,
maxBufferLength: 15,
maxMaxBufferLength: 30,
maxBufferSize: 30 * 1000 * 1000,
maxBufferHole: 0.1,
startLevel: -1, // Auto select lowest quality for fast start
autoStartLoad: true,
debug: false,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 10,
startFragPrefetch: true,
testBandwidth: false,
});
hlsRef.current.loadSource(video.videoUrl);
hlsRef.current.attachMedia(videoElement);
hlsRef.current.on(Hls.Events.MANIFEST_PARSED, () => {
console.log('HLS manifest parsed, starting playback');
// Enable audio and play
videoElement.muted = false;
videoElement.volume = 0.3; // Low volume for preview
videoElement.play().catch(e => console.log('Autoplay failed:', e));
});
hlsRef.current.on(Hls.Events.ERROR, (event: any, data: any) => {
console.log('HLS error:', data);
});
} else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
// Safari native HLS support
videoElement.src = video.videoUrl;
videoElement.muted = false;
videoElement.volume = 0.3;
videoElement.play().catch(e => console.log('Autoplay failed:', e));
}
}
return () => {
if (hlsRef.current) {
hlsRef.current.destroy();
hlsRef.current = null;
}
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
};
}, [showPreview, video.videoUrl]);
return (
<div
data-testid={`card-video-${video.id}`}
className={`video-card transition-all duration-300 ease-out hover:scale-110 ${className}`}
style={{
transformStyle: 'preserve-3d',
transition: 'transform 0.3s ease',
willChange: 'transform',
zIndex: isHovered ? 2147483647 : 1,
position: 'relative'
}}
className={`video-card transition-transform duration-200 hover:scale-[1.02] p-3 ${className}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* Video preview container */}
<div
className="relative overflow-hidden cursor-pointer group aspect-[16/9]"
className="relative gradient-card rounded-xl overflow-hidden mb-4 aspect-[9/16] md:aspect-[16/9] cursor-pointer group"
onClick={() => onClick?.(video)}
onMouseMove={handleMouseMove}
>
{/* Static thumbnail - always visible */}
<img
src={video.thumbnailUrl}
alt={video.title}
className={`w-full h-full object-cover transition-all duration-500 ease-out ${showPreview ? 'opacity-0' : 'opacity-100 group-hover:scale-105'}`}
className={`w-full h-full object-cover transition-all duration-300 ${showPreview ? 'opacity-0' : 'opacity-100 group-hover:scale-105'}`}
style={{
objectPosition: video.faceCenterPosition || 'center center',
objectFit: 'cover'
@ -212,34 +111,63 @@ export default function VideoCard({ video, onClick, className = "" }: VideoCardP
{showPreview && (
<div className="absolute inset-0">
<video
ref={videoRef}
className="w-full h-full object-cover"
style={{
objectPosition: video.faceCenterPosition || 'center center',
objectFit: 'cover'
}}
muted={false} // Enable audio for preview
autoPlay
muted
loop
playsInline
preload="none"
/>
onLoadStart={() => console.log('Preview loading for:', video.title)}
onError={(e) => console.log('Preview failed for:', video.title)}
>
{/* Try MP4 source first for faster loading */}
{video.mp4Url && (
<source src={video.mp4Url} type="video/mp4" />
)}
{/* Fallback to HLS if MP4 fails */}
{video.hlsUrl && (
<source src={video.hlsUrl} type="application/x-mpegURL" />
)}
</video>
</div>
)}
{/* Title overlay at bottom */}
<div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/90 via-black/50 to-transparent p-3">
<h3
className="oswald-text text-white text-sm md:text-base line-clamp-2 hover:text-bunny-blue transition-colors duration-300 cursor-pointer"
onClick={() => onClick?.(video)}
data-testid={`text-title-${video.id}`}
>
{video.title}
</h3>
{/* Duration badge */}
<div className="absolute bottom-2 right-2 bg-black/70 text-white text-xs px-2 py-1 rounded z-10">
{formatDuration(video.duration)}
</div>
{/* Play button overlay - hidden during preview */}
{!showPreview && (
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/40 transition-all duration-300 flex items-center justify-center">
<div className="bg-white/20 backdrop-blur-sm rounded-full w-16 h-16 flex items-center justify-center group-hover:bg-white/30 group-hover:scale-110 transition-all duration-300">
<div className="w-0 h-0 border-l-[12px] border-l-white border-y-[8px] border-y-transparent ml-1"></div>
</div>
</div>
)}
</div>
<div className="space-y-2">
<h3
className="font-semibold line-clamp-2 hover:text-bunny-blue transition-colors text-bunny-light cursor-pointer"
onClick={() => onClick?.(video)}
data-testid={`text-title-${video.id}`}
>
{video.title}
</h3>
<div className="flex items-center space-x-3 text-sm text-bunny-muted">
<span data-testid={`text-views-${video.id}`}>
{formatViews(video.views)}
</span>
<span data-testid={`text-date-${video.id}`}>
{formatDate(video.createdAt)}
</span>
</div>
</div>
</div>
);
}

View File

@ -437,8 +437,7 @@ export default function VideoModal({ video, isOpen, onClose, enableAds = true }:
return (
<div
className="modal-overlay fixed inset-0 bg-black/90 backdrop-blur-sm flex items-center justify-center p-4"
style={{ zIndex: 2147483647, position: 'fixed' }}
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={handleBackdropClick}
data-testid="modal-video"
>

View File

@ -2,32 +2,6 @@
@tailwind components;
@tailwind utilities;
/* Hide scrollbars only on mobile devices */
@media (max-width: 768px) {
* {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
*::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
html, body {
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
}
:root {
--background: hsl(222, 84%, 4.9%);
--foreground: hsl(210, 40%, 98%);
@ -189,27 +163,6 @@
.animation-delay-150 {
animation-delay: 150ms;
}
/* Force modal z-index above everything */
.modal-overlay {
z-index: 2147483647 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
}
/* Keep video cards low */
.video-card {
z-index: 1 !important;
}
.video-card:hover {
z-index: 5 !important;
}
}
/* Video edit modal styles */
@ -380,82 +333,4 @@ input[data-testid*="search"]::placeholder {
user-select: none;
opacity: 0.08;
filter: blur(0.5px);
}
/* Individual video hover effect for Top 10 numbers */
.individual-video-hover:hover .individual-video-hover\:opacity-0 {
opacity: 0;
}
/* Maximum possible z-index for video card hover */
.video-card:hover {
z-index: 2147483647 !important;
}
/* Oswald font povsod z fallback za desktop */
.oswald-text {
font-family: 'Oswald', 'Arial Black', 'Helvetica', sans-serif;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
font-display: swap;
}
/* Container flex wrap styles */
.container {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px; /* Razmik med slikami */
padding: 10px;
}
.poster {
width: 150px; /* Velikost slike */
height: auto; /* Ohranja razmerje */
}
@media (max-width: 768px) {
.container {
flex-direction: column; /* Slike ena pod drugo */
align-items: center;
}
.poster {
width: 120px; /* Manjša velikost za mobilne naprave */
}
}
/* Hide picture-in-picture button on all video elements */
video::-webkit-media-controls-picture-in-picture-button {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
video::-moz-picture-in-picture-button {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}
/* Hide Video.js picture-in-picture button */
.video-js .vjs-picture-in-picture-control {
display: none !important;
visibility: hidden !important;
}
/* Hide all picture-in-picture related elements */
*[aria-label*="picture"],
*[title*="picture"],
*[data-title*="picture"],
*[class*="picture"],
*[class*="pip"],
.vjs-picture-in-picture-control,
.vjs-pip-button {
display: none !important;
visibility: hidden !important;
opacity: 0 !important;
pointer-events: none !important;
}

View File

@ -252,7 +252,7 @@ export default function VideoPage() {
className="absolute inset-0 w-full h-full"
frameBorder="0"
allowFullScreen
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
onLoad={handleVideoPlay}
title={currentVideo.title}
/>

View File

@ -78,7 +78,7 @@ export default function Home() {
currentView={viewMode}
/>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative overflow-visible netflix-grid-container">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 relative">
{/* Trikotniki na robovih - ne prekrivajo video kartic */}

View File

@ -73,11 +73,9 @@ export class BunnyService {
private bunnyVideoToVideo(bunnyVideo: BunnyVideo | BunnyVideoDetails): Video {
// Generate optimized thumbnail URL from Bunny CDN with WebP format for better performance
// Add cache busting timestamp to ensure fresh thumbnails
const timestamp = Date.now();
const thumbnailUrl = bunnyVideo.thumbnailFileName
? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp&t=${timestamp}`
: `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp&t=${timestamp}`;
? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp`
: `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp`;
// Generate signed HLS URL for private video access
const hlsUrl = this.generateSignedUrl(bunnyVideo.guid);