import React, { useState, useEffect } from 'react'; import { BarChart3, TrendingUp, Users, Eye, Download, Smartphone, Monitor, Activity, RefreshCw, Tablet } from 'lucide-react'; import { format, parseISO } from 'date-fns'; import { Button, Card, Loading } from '../../components/common'; import { useQuery } from '@tanstack/react-query'; import { adminService } from '../../services/admin.service'; import { useTranslation } from 'react-i18next'; import { api } from '../../config/api'; // Map API response to component format interface ComponentAnalyticsData { pageViews: { total: number; trend: number; chartData: Array<{ date: string; views: number }>; }; uniqueVisitors: { total: number; trend: number; chartData: Array<{ date: string; visitors: number }>; }; downloads: { total: number; trend: number; topGalleries: Array<{ name: string; downloads: number }>; }; devices: { desktop: number; mobile: number; tablet: number; }; topPages: Array<{ path: string; views: number; uniqueVisitors: number; }>; } export const AnalyticsPage: React.FC = () => { const { t } = useTranslation(); const [dateRange, setDateRange] = useState<'7d' | '30d' | '90d'>('7d'); const [isEmbedMode, setIsEmbedMode] = useState(false); // Check if Umami is configured from settings or environment const [umamiConfig, setUmamiConfig] = useState<{ url?: string; shareUrl?: string; enabled?: boolean }>({}); // Fetch analytics data from backend const { data: apiData, isLoading, refetch } = useQuery({ queryKey: ['admin-analytics', dateRange], queryFn: async () => { const days = dateRange === '7d' ? 7 : dateRange === '30d' ? 30 : 90; return adminService.getAnalytics(days); }, refetchInterval: 60000 // Refresh every minute }); // Fetch dashboard stats for additional metrics const { data: dashboardStats } = useQuery({ queryKey: ['admin-dashboard-stats'], queryFn: () => adminService.getDashboardStats(), }); // Fetch Umami config from admin settings since we're in admin panel useEffect(() => { const fetchUmamiConfig = async () => { try { // Use admin API endpoint with auth token since we're in admin area const response = await api.get('/admin/settings'); const settings = response.data; // Transform the settings array to object const settingsMap = settings.reduce((acc: any, setting: any) => { acc[setting.key] = setting.value; return acc; }, {}); // Check if Umami is enabled in admin settings if (settingsMap.analytics_umami_enabled && settingsMap.analytics_umami_url && settingsMap.analytics_umami_website_id) { setUmamiConfig({ url: settingsMap.analytics_umami_url, shareUrl: settingsMap.analytics_umami_share_url, enabled: true }); } else { // Fall back to environment variables if they exist const envUrl = import.meta.env.VITE_UMAMI_URL; const envWebsiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID; if (envUrl && envWebsiteId) { setUmamiConfig({ url: envUrl, shareUrl: import.meta.env.VITE_UMAMI_SHARE_URL, enabled: true }); } else { setUmamiConfig({ enabled: false }); } } } catch (error) { console.error('Failed to fetch Umami config:', error); // Fall back to environment variables if they exist const envUrl = import.meta.env.VITE_UMAMI_URL; const envWebsiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID; if (envUrl && envWebsiteId) { setUmamiConfig({ url: envUrl, shareUrl: import.meta.env.VITE_UMAMI_SHARE_URL, enabled: true }); } else { setUmamiConfig({ enabled: false }); } } }; fetchUmamiConfig(); }, []); // Calculate trends and format data const analytics: ComponentAnalyticsData | undefined = React.useMemo(() => { if (!apiData) return undefined; // Calculate totals from chart data const totalViews = apiData.chartData.reduce((sum, day) => sum + day.views, 0); const totalVisitors = apiData.chartData.reduce((sum, day) => sum + day.uniqueVisitors, 0); const totalDownloads = apiData.chartData.reduce((sum, day) => sum + day.downloads, 0); // Calculate trends (comparing last half to first half) const halfPoint = Math.floor(apiData.chartData.length / 2); const firstHalfViews = apiData.chartData.slice(0, halfPoint).reduce((sum, day) => sum + day.views, 0); const secondHalfViews = apiData.chartData.slice(halfPoint).reduce((sum, day) => sum + day.views, 0); const viewsTrend = firstHalfViews > 0 ? ((secondHalfViews - firstHalfViews) / firstHalfViews) * 100 : 0; const firstHalfVisitors = apiData.chartData.slice(0, halfPoint).reduce((sum, day) => sum + day.uniqueVisitors, 0); const secondHalfVisitors = apiData.chartData.slice(halfPoint).reduce((sum, day) => sum + day.uniqueVisitors, 0); const visitorsTrend = firstHalfVisitors > 0 ? ((secondHalfVisitors - firstHalfVisitors) / firstHalfVisitors) * 100 : 0; const firstHalfDownloads = apiData.chartData.slice(0, halfPoint).reduce((sum, day) => sum + day.downloads, 0); const secondHalfDownloads = apiData.chartData.slice(halfPoint).reduce((sum, day) => sum + day.downloads, 0); const downloadsTrend = firstHalfDownloads > 0 ? ((secondHalfDownloads - firstHalfDownloads) / firstHalfDownloads) * 100 : 0; // Get actual download data for top galleries - sort by downloads const topGalleriesWithDownloads = apiData.topGalleries .filter(gallery => gallery.downloads > 0) // Only show galleries with downloads .sort((a, b) => (b.downloads || 0) - (a.downloads || 0)) // Sort by downloads .slice(0, 5) // Take top 5 .map(gallery => ({ name: gallery.event_name, downloads: gallery.downloads || 0 })); return { pageViews: { total: totalViews, trend: Math.round(viewsTrend * 10) / 10, chartData: apiData.chartData.map(d => ({ date: d.date, views: d.views })) }, uniqueVisitors: { total: totalVisitors, trend: Math.round(visitorsTrend * 10) / 10, chartData: apiData.chartData.map(d => ({ date: d.date, visitors: d.uniqueVisitors })) }, downloads: { total: totalDownloads, trend: Math.round(downloadsTrend * 10) / 10, topGalleries: topGalleriesWithDownloads }, devices: apiData.devices, topPages: apiData.topGalleries.map(gallery => ({ path: `/gallery/${gallery.slug}`, views: gallery.views, uniqueVisitors: gallery.uniqueVisitors || gallery.views // Use actual unique visitors if available })) }; }, [apiData]); const renderTrendBadge = (trend: number) => { const isPositive = trend > 0; return ( {Math.abs(trend)}% ); }; const renderMiniChart = (data: Array<{ date: string; value: number }>, color: string) => { const max = Math.max(...data.map(d => d.value)); const height = 40; return (
{data.map((item, index) => (
))}
); }; if (isLoading) { return (
); } // If Umami is configured and embed mode is enabled, show the Umami dashboard if (isEmbedMode && umamiConfig.shareUrl) { return (

{t('analytics.title')}

{t('analytics.detailedSubtitle')}