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:
parent
d8fa916902
commit
635f5eb197
281
client/src/components/ad-networks.tsx
Normal file
281
client/src/components/ad-networks.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
123
client/src/components/vast-ad-player.tsx
Normal file
123
client/src/components/vast-ad-player.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user