Add monetization indicators and ad network management features

Introduce a new component for managing ad networks with detailed analytics and integrate VAST ad playback with Video.js, including an indicator for monetized content.

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/A9pVG1H
This commit is contained in:
sebastjanartic 2025-08-08 21:38:22 +00:00
parent d8fa916902
commit 635f5eb197
3 changed files with 409 additions and 0 deletions

View File

@ -0,0 +1,281 @@
import { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge';
import {
DollarSign,
TrendingUp,
Settings,
Eye,
BarChart3,
Target,
Zap
} from 'lucide-react';
interface AdNetwork {
id: string;
name: string;
enabled: boolean;
priority: number;
tagUrl: string;
fillRate: number;
eCPM: number;
revenue: number;
impressions: number;
}
const defaultAdNetworks: AdNetwork[] = [
{
id: 'publift',
name: 'Publift',
enabled: true,
priority: 1,
tagUrl: 'https://ads.publift.com/vast/tag?zone=12345',
fillRate: 85,
eCPM: 2.45,
revenue: 245.50,
impressions: 10000
},
{
id: 'vdo',
name: 'Vdo.ai',
enabled: true,
priority: 2,
tagUrl: 'https://delivery.vdo.ai/vast/tag?id=67890',
fillRate: 78,
eCPM: 1.85,
revenue: 185.20,
impressions: 8500
},
{
id: 'primis',
name: 'Primis',
enabled: true,
priority: 3,
tagUrl: 'https://tags.primis.tech/vast/tag?pid=54321',
fillRate: 72,
eCPM: 1.65,
revenue: 165.80,
impressions: 7200
},
{
id: 'adplayer',
name: 'AdPlayer.Pro',
enabled: false,
priority: 4,
tagUrl: 'https://vast.adplayer.pro/vast?id=98765',
fillRate: 68,
eCPM: 1.25,
revenue: 125.40,
impressions: 6800
},
{
id: 'aniview',
name: 'Aniview',
enabled: false,
priority: 5,
tagUrl: 'https://player.aniview.com/vast/tag?id=11111',
fillRate: 65,
eCPM: 1.15,
revenue: 115.30,
impressions: 6500
}
];
export default function AdNetworks() {
const [networks, setNetworks] = useState<AdNetwork[]>(defaultAdNetworks);
const [editingNetwork, setEditingNetwork] = useState<string | null>(null);
const toggleNetwork = (id: string) => {
setNetworks(prev =>
prev.map(network =>
network.id === id
? { ...network, enabled: !network.enabled }
: network
)
);
};
const updateTagUrl = (id: string, tagUrl: string) => {
setNetworks(prev =>
prev.map(network =>
network.id === id
? { ...network, tagUrl }
: network
)
);
};
const totalRevenue = networks.reduce((sum, network) => sum + network.revenue, 0);
const totalImpressions = networks.reduce((sum, network) => sum + network.impressions, 0);
const averageECPM = totalRevenue / (totalImpressions / 1000);
return (
<div className="space-y-6">
{/* Revenue Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="bg-gray-800 border-gray-700">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Celotni prihodek</CardTitle>
<DollarSign className="h-4 w-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
${totalRevenue.toFixed(2)}
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Povprečni eCPM</CardTitle>
<TrendingUp className="h-4 w-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
${averageECPM.toFixed(2)}
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Skupno prikazov</CardTitle>
<Eye className="h-4 w-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
{totalImpressions.toLocaleString()}
</div>
</CardContent>
</Card>
<Card className="bg-gray-800 border-gray-700">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Aktivne mreže</CardTitle>
<Zap className="h-4 w-4 text-orange-500" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">
{networks.filter(n => n.enabled).length}
</div>
</CardContent>
</Card>
</div>
{/* Ad Networks List */}
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Target className="w-5 h-5" />
Oglasne mreže
</CardTitle>
<CardDescription className="text-gray-400">
Upravljajte svoje oglasne mreže in optimizirajte prihodke
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{networks.map((network) => (
<div
key={network.id}
className="flex items-center justify-between p-4 bg-gray-700 rounded-lg"
>
<div className="flex items-center gap-4">
<Switch
checked={network.enabled}
onCheckedChange={() => toggleNetwork(network.id)}
data-testid={`switch-${network.id}`}
/>
<div className="space-y-1">
<div className="flex items-center gap-2">
<h3 className="font-medium text-white">{network.name}</h3>
<Badge
variant={network.enabled ? "default" : "secondary"}
className={network.enabled ? "bg-green-600" : "bg-gray-600"}
>
Prioriteta {network.priority}
</Badge>
</div>
{editingNetwork === network.id ? (
<div className="flex gap-2 mt-2">
<Input
value={network.tagUrl}
onChange={(e) => updateTagUrl(network.id, e.target.value)}
className="bg-gray-600 border-gray-500 text-white text-sm"
placeholder="VAST tag URL"
/>
<Button
size="sm"
onClick={() => setEditingNetwork(null)}
className="bg-blue-600 hover:bg-blue-700"
>
Shrani
</Button>
</div>
) : (
<p className="text-sm text-gray-400 truncate max-w-md">
{network.tagUrl}
</p>
)}
</div>
</div>
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-sm text-gray-400">Fill Rate</p>
<p className="text-white font-medium">{network.fillRate}%</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-400">eCPM</p>
<p className="text-white font-medium">${network.eCPM}</p>
</div>
<div className="text-right">
<p className="text-sm text-gray-400">Prihodek</p>
<p className="text-green-400 font-medium">${network.revenue}</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setEditingNetwork(
editingNetwork === network.id ? null : network.id
)}
className="text-gray-400 hover:text-white"
data-testid={`button-edit-${network.id}`}
>
<Settings className="w-4 h-4" />
</Button>
</div>
</div>
))}
</CardContent>
</Card>
{/* Performance Chart Placeholder */}
<Card className="bg-gray-800 border-gray-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<BarChart3 className="w-5 h-5" />
Učinkovitost mreže
</CardTitle>
</CardHeader>
<CardContent>
<div className="h-64 flex items-center justify-center border-2 border-dashed border-gray-600 rounded-lg">
<div className="text-center text-gray-400">
<BarChart3 className="w-12 h-12 mx-auto mb-2" />
<p>Graf učinkovitosti oglasnih mrež</p>
<p className="text-sm">Prikazuje eCPM in fill rate trends</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,123 @@
import { useEffect, useRef, useState } from 'react';
import videojs from 'video.js';
// @ts-ignore
import 'videojs-ima';
// @ts-ignore
import 'videojs-contrib-ads';
import 'video.js/dist/video-js.css';
interface VASTAdPlayerProps {
videoId: string;
videoUrl: string;
posterUrl?: string;
adTagUrl?: string;
onAdStart?: () => void;
onAdEnd?: () => void;
onAdError?: (error: any) => void;
}
export default function VASTAdPlayer({
videoId,
videoUrl,
posterUrl,
adTagUrl = 'https://pubads.g.doubleclick.net/gampad/ads?sz=640x480&iu=/124319096/external/single_ad_samples&ciu_szs=300x250&impl=s&gdfp_req=1&env=vp&output=vast&unviewed_position_start=1&cust_params=deployment%3Ddevsite%26sample_ct%3Dlinear&correlator=',
onAdStart,
onAdEnd,
onAdError
}: VASTAdPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
const playerRef = useRef<any>(null);
const [isAdPlaying, setIsAdPlaying] = useState(false);
useEffect(() => {
if (!videoRef.current) return;
// Initialize Video.js player with IMA plugin
const player = videojs(videoRef.current, {
controls: true,
responsive: true,
fluid: true,
playbackRates: [0.5, 1, 1.25, 1.5, 2],
poster: posterUrl,
sources: [{
src: videoUrl,
type: 'video/mp4'
}]
});
playerRef.current = player;
// Initialize ads
player.ready(() => {
// @ts-ignore
player.ads({
debug: false,
timeout: 5000
});
// Initialize IMA
// @ts-ignore
player.ima({
adTagUrl: adTagUrl,
adsManagerLoadedCallback: () => {
console.log('IMA ads manager loaded');
},
adsManagerErrorCallback: (error: any) => {
console.error('IMA ads manager error:', error);
onAdError?.(error);
}
});
// Ad event listeners
player.on('ads-ad-started', () => {
console.log('Ad started');
setIsAdPlaying(true);
onAdStart?.();
});
player.on('ads-ad-ended', () => {
console.log('Ad ended');
setIsAdPlaying(false);
onAdEnd?.();
});
player.on('adserror', (event: any) => {
console.error('Ad error:', event);
onAdError?.(event);
});
player.on('ads-request', () => {
console.log('Ad request made');
});
player.on('ads-load', () => {
console.log('Ad loaded');
});
});
return () => {
if (playerRef.current) {
playerRef.current.dispose();
playerRef.current = null;
}
};
}, [videoId, videoUrl, adTagUrl, posterUrl, onAdStart, onAdEnd, onAdError]);
return (
<div className="relative w-full">
{isAdPlaying && (
<div className="absolute top-2 right-2 z-50 bg-black bg-opacity-75 text-white text-xs px-2 py-1 rounded">
Oglas
</div>
)}
<video
ref={videoRef}
className="video-js vjs-default-skin w-full"
controls
preload="auto"
data-setup="{}"
data-testid={`video-player-${videoId}`}
/>
</div>
);
}

View File

@ -70,6 +70,11 @@ export default function VideoCard({ video, onClick }: VideoCardProps) {
{formatDuration(video.duration)}
</div>
{/* 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">
💰 SPOT
</div>
{/* Play button overlay */}
<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">