Complete translation implementation for admin interface
- Fixed all hardcoded strings in admin components to use t() function - Updated BrandingPage.tsx to use translations for watermark settings - Updated EventsListPage.tsx to use translations for status labels - Added missing translation keys to both en.json and de.json - Fixed translations for: - System settings (general, storage, categories tabs) - Branding page (watermark settings, positions, opacity) - Email configuration and templates - Event list view (status labels, filters, actions) - Event detail view (all sections properly translated) - Added comprehensive German translations for all new keys - Ensured consistent translation usage across all admin pages 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ 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';
|
||||
|
||||
// Map API response to component format
|
||||
interface ComponentAnalyticsData {
|
||||
@@ -47,6 +48,7 @@ interface ComponentAnalyticsData {
|
||||
}
|
||||
|
||||
export const AnalyticsPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [dateRange, setDateRange] = useState<'7d' | '30d' | '90d'>('7d');
|
||||
const [isEmbedMode, setIsEmbedMode] = useState(false);
|
||||
|
||||
@@ -158,7 +160,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loading size="lg" text="Loading analytics..." />
|
||||
<Loading size="lg" text={t('analytics.loadingAnalytics')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -169,15 +171,15 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">Analytics Dashboard</h1>
|
||||
<p className="text-neutral-600 mt-1">Detailed analytics powered by Umami</p>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">{t('analytics.title')}</h1>
|
||||
<p className="text-neutral-600 mt-1">{t('analytics.detailedSubtitle')}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsEmbedMode(false)}
|
||||
leftIcon={<BarChart3 className="w-4 h-4" />}
|
||||
>
|
||||
Show Summary View
|
||||
{t('analytics.showSummaryView')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -197,8 +199,8 @@ export const AnalyticsPage: React.FC = () => {
|
||||
{/* Page Header */}
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">Analytics Dashboard</h1>
|
||||
<p className="text-neutral-600 mt-1">Track gallery performance and visitor engagement</p>
|
||||
<h1 className="text-2xl font-bold text-neutral-900">{t('analytics.title')}</h1>
|
||||
<p className="text-neutral-600 mt-1">{t('analytics.subtitle')}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{umamiShareUrl && (
|
||||
@@ -207,7 +209,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
onClick={() => setIsEmbedMode(true)}
|
||||
leftIcon={<Activity className="w-4 h-4" />}
|
||||
>
|
||||
Full Dashboard
|
||||
{t('analytics.fullDashboard')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
@@ -215,16 +217,16 @@ export const AnalyticsPage: React.FC = () => {
|
||||
onClick={() => refetch()}
|
||||
leftIcon={<RefreshCw className="w-4 h-4" />}
|
||||
>
|
||||
Refresh
|
||||
{t('analytics.refresh')}
|
||||
</Button>
|
||||
<select
|
||||
value={dateRange}
|
||||
onChange={(e) => setDateRange(e.target.value as any)}
|
||||
className="px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="7d">Last 7 days</option>
|
||||
<option value="30d">Last 30 days</option>
|
||||
<option value="90d">Last 90 days</option>
|
||||
<option value="7d">{t('analytics.last7Days')}</option>
|
||||
<option value="30d">{t('analytics.last30Days')}</option>
|
||||
<option value="90d">{t('analytics.last90Days')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -234,7 +236,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<Card padding="md">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-600">Page Views</p>
|
||||
<p className="text-sm text-neutral-600">{t('analytics.pageViews')}</p>
|
||||
<p className="text-3xl font-bold text-neutral-900">{analytics?.pageViews.total.toLocaleString()}</p>
|
||||
<div className="mt-1">
|
||||
{renderTrendBadge(analytics?.pageViews.trend || 0)}
|
||||
@@ -251,7 +253,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<Card padding="md">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-600">Unique Visitors</p>
|
||||
<p className="text-sm text-neutral-600">{t('analytics.uniqueVisitors')}</p>
|
||||
<p className="text-3xl font-bold text-neutral-900">{analytics?.uniqueVisitors.total.toLocaleString()}</p>
|
||||
<div className="mt-1">
|
||||
{renderTrendBadge(analytics?.uniqueVisitors.trend || 0)}
|
||||
@@ -268,7 +270,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<Card padding="md">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<p className="text-sm text-neutral-600">Total Downloads</p>
|
||||
<p className="text-sm text-neutral-600">{t('analytics.totalDownloads')}</p>
|
||||
<p className="text-3xl font-bold text-neutral-900">{analytics?.downloads.total.toLocaleString()}</p>
|
||||
<div className="mt-1">
|
||||
{renderTrendBadge(analytics?.downloads.trend || 0)}
|
||||
@@ -277,7 +279,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<Download className="w-8 h-8 text-purple-600" />
|
||||
</div>
|
||||
<div className="mt-4 space-y-2">
|
||||
<p className="text-xs text-neutral-500 uppercase">Top Gallery</p>
|
||||
<p className="text-xs text-neutral-500 uppercase">{t('analytics.topGallery')}</p>
|
||||
<p className="text-sm font-medium text-neutral-900 truncate">
|
||||
{analytics?.downloads.topGalleries[0]?.name}
|
||||
</p>
|
||||
@@ -289,19 +291,19 @@ export const AnalyticsPage: React.FC = () => {
|
||||
{/* Top Pages */}
|
||||
<div className="lg:col-span-2">
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">Top Pages</h2>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.topPages')}</h2>
|
||||
<div className="space-y-3">
|
||||
{analytics?.topPages.map((page, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-neutral-900">{page.path}</p>
|
||||
<p className="text-xs text-neutral-500">
|
||||
{page.uniqueVisitors} unique visitors
|
||||
{page.uniqueVisitors} {t('analytics.visitors')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-semibold text-neutral-900">{page.views}</p>
|
||||
<p className="text-xs text-neutral-500">views</p>
|
||||
<p className="text-xs text-neutral-500">{t('analytics.views')}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -310,7 +312,7 @@ export const AnalyticsPage: React.FC = () => {
|
||||
|
||||
{/* Top Downloads */}
|
||||
<Card padding="md" className="mt-6">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">Top Downloads by Gallery</h2>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.topDownloadsByGallery')}</h2>
|
||||
<div className="space-y-3">
|
||||
{analytics?.downloads.topGalleries.map((gallery, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
@@ -340,26 +342,26 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<div className="space-y-6">
|
||||
{/* Device Breakdown */}
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">Device Breakdown</h2>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.deviceBreakdown')}</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Monitor className="w-5 h-5 text-neutral-600" />
|
||||
<span className="text-sm text-neutral-700">Desktop</span>
|
||||
<span className="text-sm text-neutral-700">{t('analytics.desktop')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{analytics?.devices.desktop}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Smartphone className="w-5 h-5 text-neutral-600" />
|
||||
<span className="text-sm text-neutral-700">Mobile</span>
|
||||
<span className="text-sm text-neutral-700">{t('analytics.mobile')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{analytics?.devices.mobile}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Tablet className="w-5 h-5 text-neutral-600" />
|
||||
<span className="text-sm text-neutral-700">Tablet</span>
|
||||
<span className="text-sm text-neutral-700">{t('analytics.tablet')}</span>
|
||||
</div>
|
||||
<span className="text-sm font-semibold">{analytics?.devices.tablet}%</span>
|
||||
</div>
|
||||
@@ -369,11 +371,11 @@ export const AnalyticsPage: React.FC = () => {
|
||||
{/* Storage Information */}
|
||||
{dashboardStats && (
|
||||
<Card padding="md">
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">Storage Usage</h2>
|
||||
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.storageUsage')}</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-neutral-600">Used</span>
|
||||
<span className="text-neutral-600">{t('analytics.used')}</span>
|
||||
<span className="font-medium">{adminService.formatBytes(dashboardStats.storageUsed)}</span>
|
||||
</div>
|
||||
<div className="w-full bg-neutral-200 rounded-full h-2">
|
||||
@@ -383,16 +385,16 @@ export const AnalyticsPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-neutral-500 mt-1">
|
||||
{Math.round((dashboardStats.storageUsed / (10 * 1024 * 1024 * 1024)) * 100)}% of 10 GB
|
||||
{Math.round((dashboardStats.storageUsed / (10 * 1024 * 1024 * 1024)) * 100)}% {t('analytics.of')} 10 GB
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-neutral-600">Total Photos</span>
|
||||
<span className="text-neutral-600">{t('analytics.totalPhotos')}</span>
|
||||
<span className="font-medium">{dashboardStats.totalPhotos.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm mt-2">
|
||||
<span className="text-neutral-600">Active Events</span>
|
||||
<span className="text-neutral-600">{t('analytics.activeEvents')}</span>
|
||||
<span className="font-medium">{dashboardStats.activeEvents}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -408,9 +410,9 @@ export const AnalyticsPage: React.FC = () => {
|
||||
<div className="flex items-start gap-3">
|
||||
<Activity className="w-5 h-5 text-amber-600 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-amber-900">Umami Analytics Not Configured</p>
|
||||
<p className="text-sm font-medium text-amber-900">{t('analytics.notConfigured')}</p>
|
||||
<p className="text-sm text-amber-700 mt-1">
|
||||
To see real analytics data, configure Umami by setting VITE_UMAMI_URL and VITE_UMAMI_WEBSITE_ID in your environment variables.
|
||||
{t('analytics.configureInstructions')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user