Add explanation for ad indicators and improve video loading
Introduce a new modal to explain the meaning of ad indicators and enhance video loading performance by implementing lazy loading and async decoding. 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/BWSPHB9
This commit is contained in:
parent
635f5eb197
commit
8b694e8a3a
80
client/src/components/ad-explanation.tsx
Normal file
80
client/src/components/ad-explanation.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Info, DollarSign, PlayCircle, Eye } from "lucide-react";
|
||||||
|
|
||||||
|
interface AdExplanationProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AdExplanation({ isOpen, onClose }: AdExplanationProps) {
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="bg-gray-800 border-gray-700 text-white max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-bold flex items-center gap-2">
|
||||||
|
<DollarSign className="w-6 h-6 text-yellow-500" />
|
||||||
|
Kaj pomenijo oznake "💰 OGLAS"?
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<div className="bg-gradient-to-r from-yellow-500 to-orange-500 text-white text-xs px-2 py-1 rounded-full font-bold">
|
||||||
|
💰 OGLAS
|
||||||
|
</div>
|
||||||
|
<span className="text-yellow-300 font-medium">Monetizirani videji</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
Te oznake prikazujejo, da so videji opremljeni z naprednim VAST oglasnim sistemom
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<PlayCircle className="w-5 h-5 text-blue-400" />
|
||||||
|
<h3 className="font-medium">Kako deluje?</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
Pred ali med predvajanjem videa se prikazujejo oglasi, ki omogočajo brezplačno gledanje vsebine
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-700/50 rounded-lg p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Eye className="w-5 h-5 text-green-400" />
|
||||||
|
<h3 className="font-medium">Brezplačno gledanje</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300">
|
||||||
|
Oglasi omogočajo, da lahko vse videji gledate povsem brezplačno brez naročnine
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-4">
|
||||||
|
<h3 className="font-medium text-blue-300 mb-2">VAST oglasni sistem</h3>
|
||||||
|
<p className="text-sm text-gray-300 mb-2">
|
||||||
|
Platforma uporablja profesionalni VAST (Video Ad Serving Template) sistem z 5 oglasnimi mrežami:
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-gray-300 space-y-1">
|
||||||
|
<li>• Publift - Agregator več oglaševalskih omrežij</li>
|
||||||
|
<li>• Vdo.ai - AI ciljanje z visokim eCPM</li>
|
||||||
|
<li>• Primis - Video discovery platforma</li>
|
||||||
|
<li>• AdPlayer.Pro - Outstream rešitve</li>
|
||||||
|
<li>• Aniview - Programmatic oglasne tehnologije</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={onClose} className="bg-bunny-blue hover:bg-blue-600">
|
||||||
|
Razumem
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Search, Play, Menu, Grid3X3, List, DollarSign, Settings } from "lucide-react";
|
import { Search, Play, Menu, Grid3X3, List, DollarSign, Settings, Info } from "lucide-react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
@ -9,13 +9,15 @@ interface SearchHeaderProps {
|
|||||||
onViewChange: (view: "grid" | "list") => void;
|
onViewChange: (view: "grid" | "list") => void;
|
||||||
currentView: "grid" | "list";
|
currentView: "grid" | "list";
|
||||||
onAdSettingsOpen?: () => void;
|
onAdSettingsOpen?: () => void;
|
||||||
|
onAdExplanationOpen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SearchHeader({
|
export default function SearchHeader({
|
||||||
onSearch,
|
onSearch,
|
||||||
onViewChange,
|
onViewChange,
|
||||||
currentView,
|
currentView,
|
||||||
onAdSettingsOpen
|
onAdSettingsOpen,
|
||||||
|
onAdExplanationOpen
|
||||||
}: SearchHeaderProps) {
|
}: SearchHeaderProps) {
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,8 @@ export default function VideoCard({ video, onClick }: VideoCardProps) {
|
|||||||
alt={video.title}
|
alt={video.title}
|
||||||
className="w-full h-full object-cover transition-all duration-300 group-hover:scale-105"
|
className="w-full h-full object-cover transition-all duration-300 group-hover:scale-105"
|
||||||
data-testid={`img-thumbnail-${video.id}`}
|
data-testid={`img-thumbnail-${video.id}`}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
const target = e.target as HTMLImageElement;
|
const target = e.target as HTMLImageElement;
|
||||||
target.style.display = 'none';
|
target.style.display = 'none';
|
||||||
@ -72,7 +74,7 @@ export default function VideoCard({ video, onClick }: VideoCardProps) {
|
|||||||
|
|
||||||
{/* VAST Ad monetization indicator */}
|
{/* VAST Ad monetization indicator */}
|
||||||
<div className="absolute top-2 left-2 bg-gradient-to-r from-yellow-500 to-orange-500 text-white text-xs px-2 py-1 rounded-full font-bold shadow-lg">
|
<div className="absolute top-2 left-2 bg-gradient-to-r from-yellow-500 to-orange-500 text-white text-xs px-2 py-1 rounded-full font-bold shadow-lg">
|
||||||
💰 SPOT
|
💰 OGLAS
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Play button overlay */}
|
{/* Play button overlay */}
|
||||||
|
|||||||
@ -47,8 +47,9 @@ export const queryClient = new QueryClient({
|
|||||||
queryFn: getQueryFn({ on401: "throw" }),
|
queryFn: getQueryFn({ on401: "throw" }),
|
||||||
refetchInterval: false,
|
refetchInterval: false,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
staleTime: Infinity,
|
staleTime: 5 * 60 * 1000, // 5 minutes cache
|
||||||
retry: false,
|
gcTime: 10 * 60 * 1000, // 10 minutes retention
|
||||||
|
retry: 1,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
retry: false,
|
retry: false,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { type Video } from "@shared/schema";
|
|||||||
import SearchHeader from "@/components/search-header";
|
import SearchHeader from "@/components/search-header";
|
||||||
import VideoGrid from "@/components/video-grid";
|
import VideoGrid from "@/components/video-grid";
|
||||||
import AdSettings from "@/components/ad-settings";
|
import AdSettings from "@/components/ad-settings";
|
||||||
|
import AdExplanation from "@/components/ad-explanation";
|
||||||
|
|
||||||
interface VideosResponse {
|
interface VideosResponse {
|
||||||
videos: Video[];
|
videos: Video[];
|
||||||
@ -17,6 +18,7 @@ export default function Home() {
|
|||||||
const [offset, setOffset] = useState(0);
|
const [offset, setOffset] = useState(0);
|
||||||
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
const [allVideos, setAllVideos] = useState<Video[]>([]);
|
||||||
const [showAdSettings, setShowAdSettings] = useState(false);
|
const [showAdSettings, setShowAdSettings] = useState(false);
|
||||||
|
const [showAdExplanation, setShowAdExplanation] = useState(false);
|
||||||
|
|
||||||
// Fetch videos
|
// Fetch videos
|
||||||
const { data: videosResponse, isLoading, refetch } = useQuery<VideosResponse>({
|
const { data: videosResponse, isLoading, refetch } = useQuery<VideosResponse>({
|
||||||
@ -77,6 +79,7 @@ export default function Home() {
|
|||||||
onViewChange={setViewMode}
|
onViewChange={setViewMode}
|
||||||
currentView={viewMode}
|
currentView={viewMode}
|
||||||
onAdSettingsOpen={() => setShowAdSettings(true)}
|
onAdSettingsOpen={() => setShowAdSettings(true)}
|
||||||
|
onAdExplanationOpen={() => setShowAdExplanation(true)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
@ -94,6 +97,12 @@ export default function Home() {
|
|||||||
isOpen={showAdSettings}
|
isOpen={showAdSettings}
|
||||||
onClose={() => setShowAdSettings(false)}
|
onClose={() => setShowAdSettings(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Ad Explanation Modal */}
|
||||||
|
<AdExplanation
|
||||||
|
isOpen={showAdExplanation}
|
||||||
|
onClose={() => setShowAdExplanation(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<footer className="bg-bunny-gray/50 border-t border-gray-700 mt-16">
|
<footer className="bg-bunny-gray/50 border-t border-gray-700 mt-16">
|
||||||
|
|||||||
@ -132,9 +132,9 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Bunny.net administration routes
|
// Bunny.net administration routes
|
||||||
app.get("/api/bunny/stats", async (req, res) => {
|
app.get("/api/bunny/stats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = await storage.getVideos(1, 1000);
|
const result = await storage.getVideos({ limit: 1000, offset: 0 });
|
||||||
const videos = result?.videos || [];
|
const videos = result?.videos || [];
|
||||||
const totalViews = videos.length > 0 ? videos.reduce((sum, video) => sum + video.views, 0) : 0;
|
const totalViews = videos.length > 0 ? videos.reduce((sum: number, video: any) => sum + video.views, 0) : 0;
|
||||||
|
|
||||||
const stats = {
|
const stats = {
|
||||||
totalVideos: videos.length,
|
totalVideos: videos.length,
|
||||||
@ -165,19 +165,26 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
app.get("/api/videos", async (req, res) => {
|
app.get("/api/videos", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Set cache headers for better performance
|
||||||
|
res.set({
|
||||||
|
'Cache-Control': 'public, max-age=300, s-maxage=300', // 5 minutes cache
|
||||||
|
'ETag': `"videos-${Date.now()}"`,
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'X-Frame-Options': 'DENY'
|
||||||
|
});
|
||||||
|
|
||||||
const limit = parseInt(req.query.limit as string) || 20;
|
const limit = parseInt(req.query.limit as string) || 20;
|
||||||
const offset = parseInt(req.query.offset as string) || 0;
|
const offset = parseInt(req.query.offset as string) || 0;
|
||||||
const search = req.query.search as string;
|
const search = req.query.search as string;
|
||||||
|
|
||||||
const videos = await storage.getVideos(limit, offset, search);
|
console.log(`Fetching videos: limit=${limit}, offset=${offset}, search=${search}`);
|
||||||
const total = await storage.getVideoCount(search);
|
|
||||||
|
const result = await storage.getVideos({ limit, offset, search });
|
||||||
res.json({
|
console.log(`Returning ${result.videos.length} videos`);
|
||||||
videos,
|
|
||||||
total,
|
res.json(result);
|
||||||
hasMore: offset + limit < total
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Error fetching videos:", error);
|
||||||
res.status(500).json({ message: "Failed to fetch videos" });
|
res.status(500).json({ message: "Failed to fetch videos" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user