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 (
{t('analytics.detailedSubtitle')}
{t('analytics.subtitle')}
{t('analytics.pageViews')}
{analytics?.pageViews.total.toLocaleString()}
{t('analytics.uniqueVisitors')}
{analytics?.uniqueVisitors.total.toLocaleString()}
{t('analytics.totalDownloads')}
{analytics?.downloads.total.toLocaleString()}
{t('analytics.topGallery')}
{analytics?.downloads.topGalleries[0]?.name}
{page.path}
{page.uniqueVisitors} {t('analytics.visitors')}
{page.views}
{t('analytics.views')}
{gallery.name}
{gallery.downloads}
{Math.round((dashboardStats.storageUsed / (10 * 1024 * 1024 * 1024)) * 100)}% {t('analytics.of')} 10 GB
{t('analytics.notConfigured')}
{t('analytics.configureInstructions')}