137 lines
6.2 KiB
TypeScript
137 lines
6.2 KiB
TypeScript
|
|
|
||
|
|
import React from 'react';
|
||
|
|
import { Card } from '../ui/card';
|
||
|
|
import { Button } from '../ui/button';
|
||
|
|
import { StatCard } from '../dashboard/StatCard';
|
||
|
|
import { Play, SkipForward, Clock, Users, Map, ArrowLeft, Download } from 'lucide-react';
|
||
|
|
import { useToast } from '../../context/ToastContext';
|
||
|
|
|
||
|
|
interface TrackAnalyticsViewProps {
|
||
|
|
trackId: string;
|
||
|
|
onBack: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
export const TrackAnalyticsView: React.FC<TrackAnalyticsViewProps> = ({ trackId: _trackId, onBack }) => {
|
||
|
|
const { addToast } = useToast();
|
||
|
|
|
||
|
|
// Mock Track Data
|
||
|
|
const trackData = {
|
||
|
|
title: 'Neon Nights',
|
||
|
|
artist: 'Cyber_Producer',
|
||
|
|
plays: 15420,
|
||
|
|
skips: 320,
|
||
|
|
avgListen: '2:45', // vs 3:45 total
|
||
|
|
completion: 78,
|
||
|
|
demographics: { '18-24': 45, '25-34': 30, '35+': 25 },
|
||
|
|
geo: [
|
||
|
|
{ country: 'USA', percent: 40 },
|
||
|
|
{ country: 'Japan', percent: 25 },
|
||
|
|
{ country: 'Germany', percent: 15 },
|
||
|
|
{ country: 'UK', percent: 10 },
|
||
|
|
]
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-8 animate-fadeIn pb-20">
|
||
|
|
<div className="flex items-center justify-between mb-6">
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<Button variant="ghost" size="icon" onClick={onBack}><ArrowLeft className="w-5 h-5" /></Button>
|
||
|
|
<div>
|
||
|
|
<h2 className="text-2xl font-bold text-white">{trackData.title}</h2>
|
||
|
|
<p className="text-gray-400 text-sm">Analytics Report</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button variant="secondary" icon={<Download className="w-4 h-4" />} onClick={() => addToast("Report downloaded")}>Export CSV</Button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Key Metrics */}
|
||
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||
|
|
<StatCard
|
||
|
|
label="Total Plays"
|
||
|
|
value={trackData.plays.toLocaleString()}
|
||
|
|
icon={<Play className="w-5 h-5" />}
|
||
|
|
color="cyan"
|
||
|
|
trend={12}
|
||
|
|
sparklineData={[10, 15, 12, 20, 25, 30, 28, 35, 40]}
|
||
|
|
/>
|
||
|
|
<StatCard
|
||
|
|
label="Completion Rate"
|
||
|
|
value={`${trackData.completion}%`}
|
||
|
|
icon={<Clock className="w-5 h-5" />}
|
||
|
|
color="lime"
|
||
|
|
trend={2.5}
|
||
|
|
/>
|
||
|
|
<StatCard
|
||
|
|
label="Skip Rate"
|
||
|
|
value={`${((trackData.skips / trackData.plays) * 100).toFixed(1)}%`}
|
||
|
|
icon={<SkipForward className="w-5 h-5" />}
|
||
|
|
color="red"
|
||
|
|
trend={-0.5} // Negative is good for skip rate, logic in StatCard handles green/red based on +/-. might need tweak for inverse metrics
|
||
|
|
/>
|
||
|
|
<StatCard
|
||
|
|
label="Avg Listen Time"
|
||
|
|
value={trackData.avgListen}
|
||
|
|
icon={<Users className="w-5 h-5" />}
|
||
|
|
color="gold"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||
|
|
|
||
|
|
{/* Plays Over Time Graph Placeholder */}
|
||
|
|
<Card variant="default">
|
||
|
|
<h3 className="font-bold text-white mb-6">Plays Over Time (30 Days)</h3>
|
||
|
|
<div className="h-64 flex items-end gap-2 px-4 pb-4">
|
||
|
|
{Array.from({length: 30}).map((_, i) => {
|
||
|
|
const h = Math.random() * 100;
|
||
|
|
return (
|
||
|
|
<div key={i} className="flex-1 bg-kodo-cyan/20 hover:bg-kodo-cyan/50 transition-colors rounded-t relative group" style={{height: `${h}%`}}>
|
||
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 bg-black text-white text-xs px-2 py-1 rounded opacity-0 group-hover:opacity-100 pointer-events-none">
|
||
|
|
{Math.floor(h * 10)} plays
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
{/* Demographics & Geo */}
|
||
|
|
<div className="space-y-6">
|
||
|
|
<Card variant="default">
|
||
|
|
<h3 className="font-bold text-white mb-4 flex items-center gap-2"><Map className="w-4 h-4 text-kodo-magenta" /> Top Locations</h3>
|
||
|
|
<div className="space-y-3">
|
||
|
|
{trackData.geo.map(g => (
|
||
|
|
<div key={g.country} className="flex items-center gap-4">
|
||
|
|
<div className="w-16 text-sm text-gray-400">{g.country}</div>
|
||
|
|
<div className="flex-1 h-2 bg-kodo-steel rounded-full overflow-hidden">
|
||
|
|
<div className="h-full bg-kodo-magenta" style={{width: `${g.percent}%`}}></div>
|
||
|
|
</div>
|
||
|
|
<div className="w-12 text-right text-sm font-bold text-white">{g.percent}%</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card variant="default">
|
||
|
|
<h3 className="font-bold text-white mb-4 flex items-center gap-2"><Users className="w-4 h-4 text-kodo-gold" /> Listeners Age</h3>
|
||
|
|
<div className="flex gap-2 h-8">
|
||
|
|
{Object.entries(trackData.demographics).map(([range, val]) => (
|
||
|
|
<div key={range} className="h-full first:rounded-l last:rounded-r relative group" style={{width: `${val}%`, backgroundColor: range === '18-24' ? '#66FCF1' : range === '25-34' ? '#36E5D1' : '#1F2833'}}>
|
||
|
|
<div className="absolute inset-0 flex items-center justify-center text-[10px] font-bold text-black opacity-0 group-hover:opacity-100 transition-opacity">
|
||
|
|
{range}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
||
|
|
{Object.entries(trackData.demographics).map(([range, val]) => (
|
||
|
|
<span key={range}>{range}: {val}%</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|