veza/apps/web/src/pages/AnalyticsPage.tsx
senke 6974c12a25 aesthetic-improvements: align spacing to 8px grid (Action 11.2.1.3)
- Created automated script (scripts/align-8px-grid.py) to align all spacing to 8px grid
- Replaced non-8px-aligned spacing: gap-3/p-3/m-3 (12px) → gap-4/p-4/m-4 (16px), gap-5/p-5/m-5 (20px) → gap-6/p-6/m-6 (24px), gap-10/p-10/m-10 (40px) → gap-12/p-12/m-12 (48px), gap-20/p-20/m-20 (80px) → gap-24/p-24/m-24 (96px)
- Preserved: 4px values (gap-1, p-1, m-1) as they may be intentional fine-tuning, responsive breakpoints (sm:, md:, lg:), test files, documentation
- Modified files across all components to ensure consistent 8px grid alignment
- Action 11.2.1.3: Align all elements to 8px grid - COMPLETE
2026-01-16 11:50:46 +01:00

379 lines
13 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Select } from '@/components/ui/select';
import { logger } from '@/utils/logger';
import { parseApiError } from '@/utils/apiErrorHandler';
import {
Music,
Play,
Heart,
Download,
TrendingUp,
BarChart3,
ListMusic,
Share2,
} from 'lucide-react';
import {
getAnalyticsData,
type AnalyticsData,
} from '@/features/analytics/services/analyticsService';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
/**
* FE-PAGE-015: Analytics page for track and playlist statistics
*/
export function AnalyticsPage() {
const [analytics, setAnalytics] = useState<AnalyticsData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [period, setPeriod] = useState<number>(30);
const navigate = useNavigate();
useEffect(() => {
loadAnalytics();
}, [period]);
const loadAnalytics = async () => {
setLoading(true);
setError(null);
try {
const data = await getAnalyticsData(period);
setAnalytics(data);
} catch (err: unknown) {
const apiError = parseApiError(err);
logger.error('Failed to load analytics', {
error: apiError.message,
period,
});
setError(apiError.message);
} finally {
setLoading(false);
}
};
const formatNumber = (num: number): string => {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`;
}
return num.toString();
};
if (loading) {
return (
<div className="container mx-auto px-4 py-8">
<div className="flex items-center justify-center h-[400px]">
<LoadingSpinner />
</div>
</div>
);
}
if (error) {
return (
<div className="container mx-auto px-4 py-8">
<Card>
<CardHeader>
<CardTitle>Erreur</CardTitle>
<CardDescription>{error}</CardDescription>
</CardHeader>
<CardContent>
<Button onClick={loadAnalytics}>Réessayer</Button>
</CardContent>
</Card>
</div>
);
}
if (!analytics) {
return null;
}
return (
<div className="container mx-auto px-4 py-8">
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">Analytics</h1>
<p className="text-muted-foreground">
Statistiques et métriques de performance pour vos pistes et playlists
</p>
</div>
{/* Period selector */}
<div className="mb-6 flex items-center gap-4">
<label htmlFor="period" className="text-sm font-medium">
Période:
</label>
<Select
options={[
{ value: '7', label: '7 derniers jours' },
{ value: '30', label: '30 derniers jours' },
{ value: '90', label: '90 derniers jours' },
{ value: '365', label: '1 an' },
]}
value={period.toString()}
onChange={(value) => setPeriod(parseInt(value as string, 10))}
placeholder="Sélectionner une période"
className="w-[180px]"
/>
</div>
{/* Track Analytics */}
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<Music className="h-6 w-6" />
Statistiques des pistes
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des pistes
</CardTitle>
<Music className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.tracks.total_tracks)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des lectures
</CardTitle>
<Play className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.tracks.total_plays)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des likes
</CardTitle>
<Heart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.tracks.total_likes)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des téléchargements
</CardTitle>
<Download className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.tracks.total_downloads)}
</div>
</CardContent>
</Card>
</div>
{/* Top Tracks */}
{analytics.tracks.top_tracks.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Top 5 pistes
</CardTitle>
<CardDescription>Vos pistes les plus écoutées</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{analytics.tracks.top_tracks.map((track, index) => (
<div
key={track.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent cursor-pointer"
onClick={() => navigate(`/tracks/${track.id}`)}
>
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold">
{index + 1}
</div>
<div>
<p className="font-medium">{track.title}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Play className="h-3 w-3" />
{formatNumber(track.play_count)}
</span>
<span className="flex items-center gap-1">
<Heart className="h-3 w-3" />
{formatNumber(track.like_count)}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
{/* Playlist Analytics */}
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4 flex items-center gap-2">
<ListMusic className="h-6 w-6" />
Statistiques des playlists
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des playlists
</CardTitle>
<ListMusic className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.playlists.total_playlists)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des lectures
</CardTitle>
<Play className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.playlists.total_plays)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des likes
</CardTitle>
<Heart className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.playlists.total_likes)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
Total des partages
</CardTitle>
<Share2 className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{formatNumber(analytics.playlists.total_shares)}
</div>
</CardContent>
</Card>
</div>
{/* Top Playlists */}
{analytics.playlists.top_playlists.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Top 5 playlists
</CardTitle>
<CardDescription>Vos playlists les plus écoutées</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{analytics.playlists.top_playlists.map((playlist, index) => (
<div
key={playlist.id}
className="flex items-center justify-between p-4 border rounded-lg hover:bg-accent cursor-pointer"
onClick={() => navigate(`/playlists/${playlist.id}`)}
>
<div className="flex items-center gap-4">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary font-semibold">
{index + 1}
</div>
<div>
<p className="font-medium">{playlist.name}</p>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span className="flex items-center gap-1">
<Play className="h-3 w-3" />
{formatNumber(playlist.play_count)}
</span>
<span className="flex items-center gap-1">
<Heart className="h-3 w-3" />
{formatNumber(playlist.like_count)}
</span>
</div>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
{/* Summary Card */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Résumé
</CardTitle>
<CardDescription>Vue d'ensemble de vos performances</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground mb-1">
Lectures moyennes par piste
</p>
<p className="text-2xl font-bold">
{Math.round(analytics.tracks.average_play_count)}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">
Lectures moyennes par playlist
</p>
<p className="text-2xl font-bold">
{Math.round(analytics.playlists.average_play_count)}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
);
}