feat(localization): add i18next extraction helper & refactor backup configuration component to tsx

This commit is contained in:
PiR1
2026-05-09 18:23:12 +02:00
parent 86ee6c80aa
commit e7228b0780
20 changed files with 2093 additions and 517 deletions
+3 -10
View File
@@ -25,15 +25,8 @@ interface AdminSidebarProps {
onClose: () => void;
}
interface NavItem {
nameKey: string;
href: string;
icon: React.ComponentType<{ className?: string }>;
permission?: string;
}
const navigation: NavItem[] = [
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard },
const navigation = [
{ nameKey: 'navigation.dashboard', href: '/admin/dashboard', icon: LayoutDashboard, permission: false },
{ nameKey: 'navigation.events', href: '/admin/events', icon: Calendar, permission: 'events.view' },
{ nameKey: 'navigation.archives', href: '/admin/archives', icon: Archive, permission: 'archives.view' },
{ nameKey: 'admin.analytics', href: '/admin/analytics', icon: BarChart3, permission: 'analytics.view' },
@@ -44,7 +37,7 @@ const navigation: NavItem[] = [
{ nameKey: 'navigation.backup', href: '/admin/backup', icon: HardDrive, permission: 'backup.view' },
{ nameKey: 'navigation.cmsPages', href: '/admin/cms', icon: FileText, permission: 'cms.view' },
{ nameKey: 'navigation.users', href: '/admin/users', icon: Users, permission: 'users.view' },
];
] as const;
export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) => {
const location = useLocation();
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupConfiguration: ComponentType<any>;
@@ -5,18 +5,11 @@ import {
Server,
Cloud,
HardDrive,
Clock,
Calendar,
Shield,
AlertCircle,
Info,
Eye,
EyeOff,
Wifi,
CheckCircle,
XCircle,
Loader2,
FolderOpen,
Database,
Image,
FileArchive
@@ -24,26 +17,58 @@ import {
import { toast } from 'react-toastify';
import { Button, Card, Input } from '../common';
export const BackupConfiguration = ({ config, onSave, isSaving }) => {
interface BackupFormData {
backup_enabled: boolean;
backup_destination_type: 'local' | 'rsync' | 's3';
backup_destination_path: string;
backup_rsync_host: string;
backup_rsync_user: string;
backup_rsync_path: string;
backup_rsync_ssh_key: string;
backup_s3_endpoint: string;
backup_s3_bucket: string;
backup_s3_access_key: string;
backup_s3_secret_key: string;
backup_s3_region: string;
backup_schedule: string;
backup_schedule_cron: string;
backup_retention_days: number;
backup_include_database: boolean;
backup_include_photos: boolean;
backup_include_archives: boolean;
backup_include_thumbnails: boolean;
backup_include_temp: boolean;
backup_compression: boolean;
backup_encryption: boolean;
backup_encryption_passphrase: string;
}
interface BackupConfigurationProps {
config?: Partial<BackupFormData>;
onSave: (data: BackupFormData) => void;
isSaving: boolean;
}
export const BackupConfiguration: React.FC<BackupConfigurationProps> = ({ config, onSave, isSaving }) => {
const { t } = useTranslation();
const destinationTypes = [
{
id: 'local',
id: 'local' as const,
name: t('backup.configuration.destinationTypes.local.name'),
icon: HardDrive,
description: t('backup.configuration.destinationTypes.local.description'),
fields: ['backup_destination_path']
},
{
id: 'rsync',
id: 'rsync' as const,
name: t('backup.configuration.destinationTypes.rsync.name'),
icon: Server,
description: t('backup.configuration.destinationTypes.rsync.description'),
fields: ['backup_rsync_host', 'backup_rsync_user', 'backup_rsync_path', 'backup_rsync_ssh_key']
},
{
id: 's3',
id: 's3' as const,
name: t('backup.configuration.destinationTypes.s3.name'),
icon: Cloud,
description: t('backup.configuration.destinationTypes.s3.description'),
@@ -57,8 +82,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{ value: 'weekly', label: t('backup.configuration.schedule.options.weekly') },
{ value: 'custom', label: t('backup.configuration.schedule.options.custom') }
];
const [formData, setFormData] = useState({
const [formData, setFormData] = useState<BackupFormData>({
backup_enabled: false,
backup_destination_type: 'local',
backup_destination_path: '',
@@ -101,33 +126,32 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
}
}, [config]);
const handleChange = (field, value) => {
const handleChange = <K extends keyof BackupFormData>(field: K, value: BackupFormData[K]) => {
setFormData(prev => ({
...prev,
[field]: value
}));
};
const handleSubmit = (e) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// Validate required fields
const destinationType = destinationTypes.find(t => t.id === formData.backup_destination_type);
const missingFields = [];
const destinationType = destinationTypes.find(dt => dt.id === formData.backup_destination_type);
const missingFields: string[] = [];
if (formData.backup_enabled && destinationType) {
destinationType.fields.forEach(field => {
if (!formData[field] && !field.includes('optional')) {
if (!formData[field as keyof BackupFormData] && !field.includes('optional')) {
missingFields.push(field);
}
});
}
if (missingFields.length > 0) {
toast.error(t('backup.configuration.messages.requiredFields'));
return;
}
onSave(formData);
};
@@ -138,14 +162,12 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success(t('backup.configuration.messages.connectionSuccess'));
} catch (error) {
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + error.message);
toast.error(t('backup.configuration.messages.connectionFailed') + ': ' + (error as Error).message);
} finally {
setTestingConnection(false);
}
};
const selectedDestination = destinationTypes.find(t => t.id === formData.backup_destination_type);
return (
<form onSubmit={handleSubmit} className="space-y-6">
{/* Enable/Disable Toggle */}
@@ -399,7 +421,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Schedule Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.schedule.title')}</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
@@ -456,7 +478,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Backup Content Selection */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.whatToBackup.title')}</h3>
<div className="space-y-3">
<label className="flex items-center">
<input
@@ -527,7 +549,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Advanced Options */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
<div className="space-y-4">
<label className="flex items-center">
<input
@@ -608,4 +630,4 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
</form>
);
};
};
-3
View File
@@ -1,3 +0,0 @@
import type { ComponentType } from 'react';
export const BackupDashboard: ComponentType<any>;
@@ -8,11 +8,9 @@ import {
Clock,
CheckCircle,
AlertCircle,
TrendingUp,
Shield,
Server,
Cloud,
Calendar,
Play,
Loader2,
AlertTriangle,
@@ -21,7 +19,59 @@ import {
import { format, formatDistanceToNow } from 'date-fns';
import { Card, Button } from '../common';
const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
export type HealthStatus = 'excellent' | 'good' | 'warning' | 'critical';
export type BackupDestinationType = 's3' | 'rsync' | 'local';
interface BackupStatistics {
total_size?: number;
files_processed?: number;
database_backed_up?: boolean;
photos_backed_up?: number;
total_photos?: number;
archives_backed_up?: number;
photo_count?: number;
}
interface BackupRecord {
id: number;
status: 'completed' | 'failed' | 'running';
backup_type: string;
created_at: string;
duration_seconds: number;
statistics?: BackupStatistics;
}
interface BackupStatus {
lastBackup?: BackupRecord;
totalBackups?: number;
recentBackups?: BackupRecord[];
}
interface BackupConfig {
backup_destination_type?: BackupDestinationType;
backup_enabled?: boolean;
backup_s3_bucket?: string;
backup_destination_path?: string;
backup_rsync_host?: string;
backup_retention_days?: number;
}
interface StatCardProps {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: string | number;
color?: string;
subtext?: string;
}
interface BackupDashboardProps {
status?: BackupStatus;
config?: BackupConfig;
onRunBackup: () => void;
isBackupRunning: boolean;
}
const StatCard: React.FC<StatCardProps> = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
<Card className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
@@ -38,7 +88,7 @@ const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
</Card>
);
const formatBytes = (bytes) => {
const formatBytes = (bytes: number): string => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
@@ -46,28 +96,34 @@ const formatBytes = (bytes) => {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
};
export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }) => {
const healthColors: Record<HealthStatus, string> = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red',
};
export const BackupDashboard: React.FC<BackupDashboardProps> = ({ status, config, onRunBackup, isBackupRunning }) => {
const { t } = useTranslation();
const lastBackup = status?.lastBackup;
const statistics = lastBackup?.statistics || {};
const statistics = lastBackup?.statistics ?? {};
const isConfigured = config && config.backup_destination_type;
const isEnabled = config?.backup_enabled;
// Calculate backup health score
const getHealthScore = () => {
const getHealthScore = (): { score: number; status: HealthStatus; message: string } => {
if (!lastBackup) return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.noBackups') };
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at)) / (1000 * 60 * 60);
const hoursSinceBackup = (Date.now() - new Date(lastBackup.created_at).getTime()) / (1000 * 60 * 60);
if (lastBackup.status === 'failed') {
return { score: 0, status: 'critical', message: t('backup.dashboard.healthMessages.lastBackupFailed') };
}
if (hoursSinceBackup < 24) {
return { score: 100, status: 'excellent', message: t('backup.dashboard.healthMessages.upToDate') };
} else if (hoursSinceBackup < 48) {
return { score: 75, status: 'good', message: t('backup.dashboard.healthMessages.recent') };
} else if (hoursSinceBackup < 168) { // 1 week
} else if (hoursSinceBackup < 168) {
return { score: 50, status: 'warning', message: t('backup.dashboard.healthMessages.gettingOld') };
} else {
return { score: 25, status: 'critical', message: t('backup.dashboard.healthMessages.outdated') };
@@ -75,12 +131,6 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
};
const health = getHealthScore();
const healthColors = {
excellent: 'green',
good: 'blue',
warning: 'amber',
critical: 'red'
};
return (
<div className="space-y-6">
@@ -109,7 +159,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{t(`backup.dashboard.healthStatus.${health.status}`)}
</span>
</div>
<div className="flex items-center space-x-4">
<div className="relative w-24 h-24">
<svg className="w-24 h-24 transform -rotate-90">
@@ -137,7 +187,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<span className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{health.score}%</span>
</div>
</div>
<div className="flex-1">
<p className="text-neutral-700 dark:text-neutral-300 font-medium">{health.message}</p>
{lastBackup && (
@@ -145,7 +195,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
Last successful backup: {formatDistanceToNow(new Date(lastBackup.created_at), { addSuffix: true })}
</p>
)}
<Button
onClick={onRunBackup}
disabled={!isConfigured || !isEnabled || isBackupRunning}
@@ -177,7 +227,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="blue"
subtext={lastBackup ? `${t('backup.dashboard.stats.last')}: ${format(new Date(lastBackup.created_at), 'PP')}` : t('backup.dashboard.stats.noBackupsYet')}
/>
<StatCard
icon={HardDrive}
label={t('backup.dashboard.stats.backupSize')}
@@ -185,7 +235,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="green"
subtext={`${statistics.files_processed || 0} ${t('backup.dashboard.stats.files')}`}
/>
<StatCard
icon={Clock}
label={t('backup.dashboard.stats.lastDuration')}
@@ -193,7 +243,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
color="purple"
subtext={lastBackup ? format(new Date(lastBackup.created_at), 'p') : ''}
/>
<StatCard
icon={Shield}
label={t('backup.dashboard.stats.backupStatus')}
@@ -324,4 +374,4 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
</div>
</div>
);
};
};
@@ -19,7 +19,7 @@ const RATING_OPTIONS = [
{ value: 3, label: 'filter.threeStarsPlus' },
{ value: 4, label: 'filter.fourStarsPlus' },
{ value: 5, label: 'filter.fiveStarsOnly' },
];
] as const;
export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters,