veza/apps/web/src/components/analytics/TrackAnalyticsView.tsx

183 lines
6.2 KiB
TypeScript
Raw Normal View History

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 '../../components/feedback/ToastProvider';
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-foreground">{trackData.title}</h2>
<p className="text-muted-foreground 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-foreground 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-muted/20 hover:bg-muted/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-background text-foreground 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-foreground mb-4 flex items-center gap-2">
<Map className="w-4 h-4 text-destructive" /> Top Locations
</h3>
<div className="space-y-4">
{trackData.geo.map((g) => (
<div key={g.country} className="flex items-center gap-4">
<div className="w-16 text-sm text-muted-foreground">{g.country}</div>
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-destructive"
style={{ width: `${g.percent}%` }}
></div>
</div>
<div className="w-12 text-right text-sm font-bold text-foreground">
{g.percent}%
</div>
</div>
))}
</div>
</Card>
<Card variant="default">
<h3 className="font-bold text-foreground mb-4 flex items-center gap-2">
<Users className="w-4 h-4 text-warning" /> 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'
refactor(web): migrate components from hardcoded pigment hex to SUMI tokens Kill the drift in 9 components that hardcoded #7c9dd6/#d4634a/#7a9e6c/#c9a84c (the 4 viz pigments) by referencing tokens generated from packages/design-system/tokens/ (single source of truth). apps/web/src/index.css now imports @veza/design-system/tokens.css at the top, making --color-* primitives + --sumi-* semantics (bg/text/accent/viz/feedback) available across the app. Migrated: - charts/{BarChart,LineChart,PieChart}.tsx — defaults use var(--sumi-viz-*) - analytics/TrackAnalyticsView.tsx — JSX inline backgroundColor uses var() - developer/SwaggerUI.tsx — CSS-in-JS uses var() - ui/WaveformVisualizer.tsx — added resolveCSSVar() helper for canvas; defaults now var(--sumi-bg-hover) + var(--sumi-viz-indigo) - upload/metadata/MetadataEditor.tsx — passes var() to WaveformVisualizer - player/AudioVisualizer.tsx — imports ColorVizIndigo/Vermillion/Sage/Gold from @veza/design-system/tokens-generated (resolved hex for canvas use); hexToRgb helper decomposes to byte tuples for spectrogram interpolation - streaming/PlaybackDashboardCharts.tsx — passes var() to LineChart props packages/design-system/package.json: added "./tokens-generated" export pointing to dist/tokens.ts (TS exports of resolved hex values for canvas contexts that need them). Stats: 32 → 13 hardcoded hex literals (4 pigments) across apps/web/src. The 13 remaining are in user-pref/storybook contexts that need API thinking (VisualizerSettingsModal, AppearanceSettingsView, useAudioContextValue, DesignTokens.stories.tsx) — tracked as Sprint 2 follow-up. Build: vite build OK (13s). Typecheck OK. SKIP_TESTS=1: pre-existing LazyDmca mock test failure (legal/dmca feature in flight on main) unrelated to this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 03:07:24 +00:00
? 'var(--sumi-viz-indigo)'
: range === '25-34'
refactor(web): migrate components from hardcoded pigment hex to SUMI tokens Kill the drift in 9 components that hardcoded #7c9dd6/#d4634a/#7a9e6c/#c9a84c (the 4 viz pigments) by referencing tokens generated from packages/design-system/tokens/ (single source of truth). apps/web/src/index.css now imports @veza/design-system/tokens.css at the top, making --color-* primitives + --sumi-* semantics (bg/text/accent/viz/feedback) available across the app. Migrated: - charts/{BarChart,LineChart,PieChart}.tsx — defaults use var(--sumi-viz-*) - analytics/TrackAnalyticsView.tsx — JSX inline backgroundColor uses var() - developer/SwaggerUI.tsx — CSS-in-JS uses var() - ui/WaveformVisualizer.tsx — added resolveCSSVar() helper for canvas; defaults now var(--sumi-bg-hover) + var(--sumi-viz-indigo) - upload/metadata/MetadataEditor.tsx — passes var() to WaveformVisualizer - player/AudioVisualizer.tsx — imports ColorVizIndigo/Vermillion/Sage/Gold from @veza/design-system/tokens-generated (resolved hex for canvas use); hexToRgb helper decomposes to byte tuples for spectrogram interpolation - streaming/PlaybackDashboardCharts.tsx — passes var() to LineChart props packages/design-system/package.json: added "./tokens-generated" export pointing to dist/tokens.ts (TS exports of resolved hex values for canvas contexts that need them). Stats: 32 → 13 hardcoded hex literals (4 pigments) across apps/web/src. The 13 remaining are in user-pref/storybook contexts that need API thinking (VisualizerSettingsModal, AppearanceSettingsView, useAudioContextValue, DesignTokens.stories.tsx) — tracked as Sprint 2 follow-up. Build: vite build OK (13s). Typecheck OK. SKIP_TESTS=1: pre-existing LazyDmca mock test failure (legal/dmca feature in flight on main) unrelated to this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 03:07:24 +00:00
? 'var(--sumi-viz-sage)'
: 'var(--sumi-bg-hover)',
}}
>
<div className="absolute inset-0 flex items-center justify-center text-xs font-bold text-foreground opacity-0 group-hover:opacity-100 transition-opacity">
{range}
</div>
</div>
))}
</div>
<div className="flex justify-between text-xs text-muted-foreground mt-2">
{Object.entries(trackData.demographics).map(([range, val]) => (
<span key={range}>
{range}: {val}%
</span>
))}
</div>
</Card>
</div>
</div>
</div>
);
};