feat: add admin dark mode and SEO/robots.txt settings

Admin Dark Mode:
- Add AdminDarkModeContext with light/dark/system preference
- Update all admin components with Tailwind dark: classes
- Add dark mode toggle in admin header
- Persist preference in localStorage

SEO Settings:
- Add robots.txt configuration in Settings > SEO tab
- Block AI crawlers (GPTBot, ChatGPT-User, etc.) with toggle
- Custom robots.txt rules management
- Add RobotsMetaTags component for gallery pages
- Backend service for dynamic robots.txt generation
- Database migration for SEO settings storage

UI/UX Improvements:
- Consistent dark mode styling across all admin pages
- Update gallery components with themed CSS classes
- Fix input, card, and button styling for dark mode
This commit is contained in:
Paul Nothaft
2026-02-06 23:26:01 +01:00
parent 4912e2bccf
commit 9c2a0d272a
79 changed files with 2467 additions and 1475 deletions
@@ -1,12 +1,15 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
import { AdminAuthProvider, PermissionsProvider } from '../../contexts';
import { AdminDarkModeProvider } from '../../contexts/AdminDarkModeContext';
export const AdminAuthWrapper: React.FC = () => {
return (
<AdminAuthProvider>
<PermissionsProvider>
<Outlet />
<AdminDarkModeProvider>
<Outlet />
</AdminDarkModeProvider>
</PermissionsProvider>
</AdminAuthProvider>
);
+39 -28
View File
@@ -1,12 +1,13 @@
import React, { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { Menu, User, LogOut, Settings, Bell, Lock, CheckCircle, Trash2 } from 'lucide-react';
import { Menu, User, LogOut, Settings, Bell, Lock, CheckCircle, Trash2, Sun, Moon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useLocalizedDate } from '../../hooks/useLocalizedDate';
import { useLocalizedTimeAgo } from '../../hooks/useLocalizedTimeAgo';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useAdminAuth } from '../../contexts';
import { useAdminDarkMode } from '../../contexts/AdminDarkModeContext';
import { useOnClickOutside } from '../../hooks/useOnClickOutside';
import { PasswordChangeModal } from './PasswordChangeModal';
import { LanguageSelector } from '../common';
@@ -20,6 +21,7 @@ interface AdminHeaderProps {
export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
const navigate = useNavigate();
const { user, logout } = useAdminAuth();
const { isDark, toggle: toggleDarkMode } = useAdminDarkMode();
const { t } = useTranslation();
const { format } = useLocalizedDate();
const { formatTimeAgo } = useLocalizedTimeAgo();
@@ -68,7 +70,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
const unreadCount = notificationsData?.unreadCount || 0;
return (
<header className="sticky top-0 z-30 bg-white border-b border-neutral-200">
<header className="sticky top-0 z-30 bg-white dark:bg-neutral-900 border-b border-neutral-200 dark:border-neutral-700">
<div className="px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Left side - Menu button, Logo, and Date */}
@@ -87,8 +89,8 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
</div>
{/* Date display - hidden on smaller screens */}
<div className="hidden xl:block pl-3 border-l border-neutral-200 ml-1">
<p className="text-base text-neutral-700">
<div className="hidden xl:block pl-3 border-l border-neutral-200 dark:border-neutral-700 ml-1">
<p className="text-base text-neutral-700 dark:text-neutral-300">
{format(new Date(), 'PPPP')}
</p>
</div>
@@ -98,12 +100,21 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
<div className="flex items-center gap-3">
{/* Language Selector */}
<LanguageSelector />
{/* Dark Mode Toggle */}
<button
onClick={toggleDarkMode}
className="p-2 text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
title={isDark ? t('admin.lightMode', 'Switch to light mode') : t('admin.darkMode', 'Switch to dark mode')}
>
{isDark ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
{/* Notifications */}
<div className="relative" ref={notificationRef}>
<button
onClick={() => setShowNotifications(!showNotifications)}
className="relative p-2 text-neutral-500 hover:text-neutral-700 hover:bg-neutral-100 rounded-lg transition-colors"
className="relative p-2 text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
>
<Bell className="w-5 h-5" />
{unreadCount > 0 && (
@@ -113,14 +124,14 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
{/* Notifications dropdown */}
{showNotifications && (
<div className="absolute right-0 mt-2 w-96 bg-white rounded-lg shadow-lg border border-neutral-200">
<div className="px-4 py-3 border-b border-neutral-100 flex items-center justify-between">
<h3 className="text-sm font-semibold text-neutral-900">{t('admin.notifications')}</h3>
<div className="absolute right-0 mt-2 w-96 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700">
<div className="px-4 py-3 border-b border-neutral-100 dark:border-neutral-700 flex items-center justify-between">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">{t('admin.notifications')}</h3>
<div className="flex items-center gap-2">
{unreadCount > 0 && (
<button
onClick={() => markAllAsReadMutation.mutate()}
className="text-xs text-primary-600 hover:text-primary-700 flex items-center gap-1"
className="text-xs text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 flex items-center gap-1"
title={t('admin.markAllRead')}
>
<CheckCircle className="w-3 h-3" />
@@ -129,7 +140,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
)}
<button
onClick={() => clearAllMutation.mutate()}
className="text-xs text-neutral-600 hover:text-neutral-700 flex items-center gap-1"
className="text-xs text-neutral-600 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 flex items-center gap-1"
title={t('admin.clearAll')}
>
<Trash2 className="w-3 h-3" />
@@ -139,7 +150,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
</div>
<div className="max-h-96 overflow-y-auto">
{notifications.length === 0 ? (
<div className="px-4 py-8 text-center text-sm text-neutral-500">
<div className="px-4 py-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
{t('admin.noNotificationsMessage')}
</div>
) : (
@@ -148,7 +159,7 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
return (
<div
key={notification.id}
className={`px-4 py-3 hover:bg-neutral-50 cursor-pointer border-l-4 ${
className={`px-4 py-3 hover:bg-neutral-50 dark:hover:bg-neutral-700 cursor-pointer border-l-4 ${
notification.isRead ? 'border-transparent opacity-75' : 'border-primary-500'
}`}
>
@@ -157,10 +168,10 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
<Bell className="w-4 h-4" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-neutral-900">
<p className="text-sm text-neutral-900 dark:text-neutral-100">
{notificationsService.formatNotificationMessage(notification)}
</p>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{formatTimeAgo(notification.createdAt)}
</p>
</div>
@@ -171,10 +182,10 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
)}
</div>
{notifications.length > 0 && (
<div className="px-4 py-2 border-t border-neutral-100 text-center">
<button
<div className="px-4 py-2 border-t border-neutral-100 dark:border-neutral-700 text-center">
<button
onClick={() => setShowNotifications(false)}
className="text-sm text-primary-600 hover:text-primary-700"
className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300"
>
{t('admin.close')}
</button>
@@ -188,11 +199,11 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
<div className="relative" ref={userMenuRef}>
<button
onClick={() => setShowUserMenu(!showUserMenu)}
className="flex items-center gap-3 p-2 hover:bg-neutral-100 rounded-lg transition-colors"
className="flex items-center gap-3 p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors"
>
<div className="text-right hidden sm:block">
<p className="text-sm font-medium text-neutral-900">{user?.username}</p>
<p className="text-xs text-neutral-500">{user?.email}</p>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{user?.username}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{user?.email}</p>
</div>
<div className="w-8 h-8 bg-primary-600 rounded-full flex items-center justify-center">
<User className="w-5 h-5 text-white" />
@@ -201,17 +212,17 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
{/* User dropdown */}
{showUserMenu && (
<div className="absolute right-0 mt-2 w-56 bg-white rounded-lg shadow-lg border border-neutral-200 py-1">
<div className="px-4 py-2 border-b border-neutral-100 sm:hidden">
<p className="text-sm font-medium text-neutral-900">{user?.username}</p>
<p className="text-xs text-neutral-500">{user?.email}</p>
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 py-1">
<div className="px-4 py-2 border-b border-neutral-100 dark:border-neutral-700 sm:hidden">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{user?.username}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{user?.email}</p>
</div>
<button
onClick={() => {
setShowUserMenu(false);
navigate('/admin/settings');
}}
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-3"
className="w-full px-4 py-2 text-left text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700 flex items-center gap-3"
>
<Settings className="w-4 h-4" />
{t('navigation.settings')}
@@ -221,14 +232,14 @@ export const AdminHeader: React.FC<AdminHeaderProps> = ({ onMenuClick }) => {
setShowUserMenu(false);
setShowPasswordModal(true);
}}
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-3"
className="w-full px-4 py-2 text-left text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700 flex items-center gap-3"
>
<Lock className="w-4 h-4" />
{t('admin.changePassword')}
</button>
<button
onClick={handleLogout}
className="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-50 flex items-center gap-3"
className="w-full px-4 py-2 text-left text-sm text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700 flex items-center gap-3"
>
<LogOut className="w-4 h-4" />
{t('common.logout')}
@@ -17,7 +17,7 @@ export const AdminLayout: React.FC = () => {
if (isLoading) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-950 flex items-center justify-center">
<div className="text-center">
<div className="w-16 h-16 border-4 border-primary-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<p className="text-neutral-600">Loading...</p>
@@ -31,7 +31,7 @@ export const AdminLayout: React.FC = () => {
}
return (
<div className="h-screen bg-neutral-50 flex overflow-hidden">
<div className="h-screen bg-neutral-50 dark:bg-neutral-950 flex overflow-hidden">
{/* Mandatory Password Change Modal */}
{mustChangePassword && <MandatoryPasswordChangeModal />}
@@ -190,7 +190,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
{selectedPhotos.size > 0 && (
<>
<span className="text-sm text-neutral-600">
<span className="text-sm text-neutral-600 dark:text-neutral-400">
{t('gallery.photosSelected', { count: selectedPhotos.size })}
</span>
<Button
@@ -215,7 +215,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
)}
</div>
<div className="text-sm text-neutral-600">
<div className="text-sm text-neutral-600 dark:text-neutral-400">
{t('gallery.photosCount', { count: photos.length })}
</div>
</div>
@@ -234,7 +234,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
<div
key={photo.id}
data-testid={`admin-photo-tile-${photo.id}`}
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 transition-opacity ${
className={`relative group cursor-pointer rounded-lg overflow-hidden bg-neutral-100 dark:bg-neutral-800 transition-opacity ${
isSelectionMode ? 'ring-2 ring-offset-2 ' + (selectedPhotos.has(photo.id) ? 'ring-primary-500' : 'ring-transparent') : ''
} ${isDeleting ? 'opacity-50' : ''}`}
onClick={() => !isDeleting && onPhotoClick(photo, index)}
@@ -353,7 +353,7 @@ export const AdminPhotoGrid: React.FC<AdminPhotoGridProps> = ({
{photos.length === 0 && (
<div className="text-center py-12">
<p className="text-neutral-500">{t('gallery.noMedia', 'No media uploaded yet')}</p>
<p className="text-neutral-500 dark:text-neutral-400">{t('gallery.noMedia', 'No media uploaded yet')}</p>
</div>
)}
+13 -13
View File
@@ -59,15 +59,15 @@ export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) =
return (
<div
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white border-r border-neutral-200 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 lg:h-screen ${
className={`fixed inset-y-0 left-0 z-50 w-64 bg-white dark:bg-neutral-900 border-r border-neutral-200 dark:border-neutral-700 transform transition-transform duration-200 ease-in-out lg:relative lg:translate-x-0 lg:h-screen ${
isOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex flex-col h-screen lg:h-full">
{/* Brand */}
<div className="flex items-center justify-between h-16 px-6 border-b border-neutral-200 flex-shrink-0">
<div className="flex items-center justify-between h-16 px-6 border-b border-neutral-200 dark:border-neutral-700 flex-shrink-0">
<div className="flex items-center">
<span className="text-xl font-bold text-neutral-900">{t('admin.title')}</span>
<span className="text-xl font-bold text-neutral-900 dark:text-neutral-100">{t('admin.title')}</span>
</div>
<button
onClick={onClose}
@@ -90,8 +90,8 @@ export const AdminSidebar: React.FC<AdminSidebarProps> = ({ isOpen, onClose }) =
onClick={() => onClose()}
className={`flex items-center px-3 py-2 text-sm font-medium rounded-lg transition-colors ${
isActive
? 'bg-primary-50 text-primary-700'
: 'text-neutral-700 hover:bg-neutral-100 hover:text-neutral-900'
? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-400'
: 'text-neutral-700 dark:text-neutral-300 hover:bg-neutral-100 dark:hover:bg-neutral-800 hover:text-neutral-900 dark:hover:text-neutral-100'
}`}
>
<item.icon className={`w-5 h-5 mr-3 ${
@@ -138,26 +138,26 @@ const StorageInfo: React.FC = () => {
const isOverSoftLimit = limitInUse && storageInfo.total_used >= limitInUse;
const progressBarClass = isOverSoftLimit ? 'bg-red-600' : 'bg-primary-600';
const containerClass = isOverSoftLimit
? 'bg-red-50 border border-red-200'
: 'bg-neutral-100';
? 'bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800'
: 'bg-neutral-100 dark:bg-neutral-800';
const softLimitDisplay = settingsService.formatBytes(limitInUse);
return (
<div className="p-4 border-t border-neutral-200">
<div className="p-4 border-t border-neutral-200 dark:border-neutral-700">
<div className={`${containerClass} rounded-lg p-3 transition-colors duration-300`}>
<div className="flex items-center justify-between text-sm">
<span className="text-neutral-700">{t('admin.storageUsed')}</span>
<span className="font-medium text-neutral-900">
<span className="text-neutral-700 dark:text-neutral-300">{t('admin.storageUsed')}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{settingsService.formatBytes(storageInfo.total_used)}
</span>
</div>
<div className="mt-2 w-full bg-neutral-200 rounded-full h-2">
<div
<div className="mt-2 w-full bg-neutral-200 dark:bg-neutral-700 rounded-full h-2">
<div
className={`${progressBarClass} h-2 rounded-full transition-all duration-300`}
style={{ width: `${Math.min(usagePercent, 100)}%` }}
/>
</div>
<p className="text-xs text-neutral-600 mt-1">
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">
{t('admin.storagePercent', { percent: usagePercent, limit: softLimitDisplay })}
</p>
</div>
@@ -152,8 +152,8 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<Card className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">{t('backup.configuration.enableBackup')}</h3>
<p className="mt-1 text-sm text-gray-600">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('backup.configuration.enableBackup')}</h3>
<p className="mt-1 text-sm text-neutral-600 dark:text-neutral-400">
{t('backup.configuration.enableBackupHelp')}
</p>
</div>
@@ -164,15 +164,15 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
onChange={(e) => handleChange('backup_enabled', e.target.checked)}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
<div className="w-11 h-6 bg-neutral-200 dark:bg-neutral-600 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-primary-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-neutral-300 dark:after:border-neutral-500 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"></div>
</label>
</div>
</Card>
{/* Destination Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.destinationType')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.configuration.destinationType')}</h3>
{/* Destination Type Selection */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
{destinationTypes.map((type) => {
@@ -184,17 +184,17 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
onClick={() => handleChange('backup_destination_type', type.id)}
className={`p-4 rounded-lg border-2 transition-all ${
formData.backup_destination_type === type.id
? 'border-primary bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-primary bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
<Icon className={`h-8 w-8 mb-2 mx-auto ${
formData.backup_destination_type === type.id
? 'text-primary'
: 'text-gray-400'
: 'text-neutral-400'
}`} />
<h4 className="font-medium text-gray-900">{type.name}</h4>
<p className="text-xs text-gray-500 mt-1">{type.description}</p>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{type.name}</h4>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{type.description}</p>
</button>
);
})}
@@ -205,7 +205,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{formData.backup_destination_type === 'local' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.destinationPath')}
</label>
<Input
@@ -215,7 +215,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
placeholder={t('backup.configuration.fields.destinationPathPlaceholder')}
required
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.configuration.fields.destinationPathHelp')}
</p>
</div>
@@ -226,7 +226,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.rsyncHost')}
</label>
<Input
@@ -238,7 +238,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.rsyncUser')}
</label>
<Input
@@ -251,7 +251,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.rsyncPath')}
</label>
<Input
@@ -263,7 +263,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.rsyncSshKey')}
</label>
<div className="relative">
@@ -271,18 +271,18 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
value={formData.backup_rsync_ssh_key}
onChange={(e) => handleChange('backup_rsync_ssh_key', e.target.value)}
placeholder={t('backup.configuration.fields.rsyncSshKeyPlaceholder')}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary font-mono text-sm"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md focus:outline-none focus:ring-primary focus:border-primary font-mono text-sm"
rows={4}
/>
<button
type="button"
onClick={() => setShowSecrets(prev => ({ ...prev, ssh_key: !prev.ssh_key }))}
className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
className="absolute top-2 right-2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
>
{showSecrets.ssh_key ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
</div>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.configuration.fields.rsyncSshKeyHelp')}
</p>
</div>
@@ -292,7 +292,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{formData.backup_destination_type === 's3' && (
<>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.s3Endpoint')}
</label>
<Input
@@ -302,13 +302,13 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
placeholder="https://s3.amazonaws.com"
required
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.configuration.fields.s3EndpointHelp')}
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.s3Bucket')}
</label>
<Input
@@ -320,7 +320,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.s3Region')}
</label>
<Input
@@ -333,7 +333,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.s3AccessKey')}
</label>
<Input
@@ -345,7 +345,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.fields.s3SecretKey')}
</label>
<div className="relative">
@@ -359,7 +359,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<button
type="button"
onClick={() => setShowSecrets(prev => ({ ...prev, s3_secret_key: !prev.s3_secret_key }))}
className="absolute top-1/2 -translate-y-1/2 right-2 text-gray-400 hover:text-gray-600"
className="absolute top-1/2 -translate-y-1/2 right-2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
>
{showSecrets.s3_secret_key ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
@@ -398,17 +398,17 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Schedule Configuration */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.schedule.title')}</h3>
<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-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.schedule.scheduleType')}
</label>
<select
value={formData.backup_schedule}
onChange={(e) => handleChange('backup_schedule', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
>
{scheduleOptions.map(option => (
<option key={option.value} value={option.value}>
@@ -420,7 +420,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{formData.backup_schedule === 'custom' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.schedule.customCron')}
</label>
<Input
@@ -429,14 +429,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
onChange={(e) => handleChange('backup_schedule_cron', e.target.value)}
placeholder="0 3 * * *"
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.configuration.schedule.customCronHelp')}
</p>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.schedule.retention')}
</label>
<Input
@@ -446,7 +446,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
min="1"
max="365"
/>
<p className="mt-1 text-xs text-gray-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.configuration.schedule.retentionHelp')}
</p>
</div>
@@ -455,7 +455,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Backup Content Selection */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.whatToBackup.title')}</h3>
<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">
@@ -463,14 +463,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_include_database}
onChange={(e) => handleChange('backup_include_database', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<div className="flex items-center space-x-2">
<Database className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.database')}</span>
<Database className="h-4 w-4 text-neutral-400" />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.database')}</span>
</div>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.databaseHelp')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.whatToBackup.databaseHelp')}</p>
</div>
</label>
@@ -479,14 +479,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_include_photos}
onChange={(e) => handleChange('backup_include_photos', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<div className="flex items-center space-x-2">
<Image className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.photos')}</span>
<Image className="h-4 w-4 text-neutral-400" />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.photos')}</span>
</div>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.photosHelp')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.whatToBackup.photosHelp')}</p>
</div>
</label>
@@ -495,14 +495,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_include_archives}
onChange={(e) => handleChange('backup_include_archives', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<div className="flex items-center space-x-2">
<FileArchive className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.archives')}</span>
<FileArchive className="h-4 w-4 text-neutral-400" />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.archives')}</span>
</div>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.archivesHelp')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.whatToBackup.archivesHelp')}</p>
</div>
</label>
@@ -511,14 +511,14 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_include_thumbnails}
onChange={(e) => handleChange('backup_include_thumbnails', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<div className="flex items-center space-x-2">
<Image className="h-4 w-4 text-gray-400" />
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.whatToBackup.thumbnails')}</span>
<Image className="h-4 w-4 text-neutral-400" />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.thumbnails')}</span>
</div>
<p className="text-xs text-gray-500">{t('backup.configuration.whatToBackup.thumbnailsHelp')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.whatToBackup.thumbnailsHelp')}</p>
</div>
</label>
</div>
@@ -526,7 +526,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
{/* Advanced Options */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.configuration.advancedOptions.title')}</h3>
<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">
@@ -534,11 +534,11 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_compression}
onChange={(e) => handleChange('backup_compression', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.advancedOptions.compression')}</span>
<p className="text-xs text-gray-500">{t('backup.configuration.advancedOptions.compressionHelp')}</p>
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.advancedOptions.compression')}</span>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.advancedOptions.compressionHelp')}</p>
</div>
</label>
@@ -548,17 +548,17 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
type="checkbox"
checked={formData.backup_encryption}
onChange={(e) => handleChange('backup_encryption', e.target.checked)}
className="h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div className="ml-3">
<span className="text-sm font-medium text-gray-700">{t('backup.configuration.advancedOptions.encryption')}</span>
<p className="text-xs text-gray-500">{t('backup.configuration.advancedOptions.encryptionHelp')}</p>
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.configuration.advancedOptions.encryption')}</span>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('backup.configuration.advancedOptions.encryptionHelp')}</p>
</div>
</label>
{formData.backup_encryption && (
<div className="ml-7">
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('backup.configuration.advancedOptions.encryptionPassphrase')}
</label>
<div className="relative">
@@ -572,7 +572,7 @@ export const BackupConfiguration = ({ config, onSave, isSaving }) => {
<button
type="button"
onClick={() => setShowSecrets(prev => ({ ...prev, encryption_passphrase: !prev.encryption_passphrase }))}
className="absolute top-1/2 -translate-y-1/2 right-2 text-gray-400 hover:text-gray-600"
className="absolute top-1/2 -translate-y-1/2 right-2 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
>
{showSecrets.encryption_passphrase ? <EyeOff size={20} /> : <Eye size={20} />}
</button>
@@ -25,14 +25,14 @@ const StatCard = ({ icon: Icon, label, value, color = 'blue', subtext }) => (
<Card className="p-6">
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-gray-600">{label}</p>
<p className="mt-2 text-3xl font-semibold text-gray-900">{value}</p>
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">{label}</p>
<p className="mt-2 text-3xl font-semibold text-neutral-900 dark:text-neutral-100">{value}</p>
{subtext && (
<p className="mt-1 text-sm text-gray-500">{subtext}</p>
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">{subtext}</p>
)}
</div>
<div className={`p-3 bg-${color}-100 rounded-lg`}>
<Icon className={`h-6 w-6 text-${color}-600`} />
<div className={`p-3 bg-${color}-100 dark:bg-${color}-900/40 rounded-lg`}>
<Icon className={`h-6 w-6 text-${color}-600 dark:text-${color}-400`} />
</div>
</div>
</Card>
@@ -86,14 +86,14 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<div className="space-y-6">
{/* Configuration Alert */}
{!isConfigured && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<div className="flex">
<AlertTriangle className="h-5 w-5 text-amber-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
{t('backup.dashboard.notConfigured.title')}
</h3>
<p className="mt-1 text-sm text-amber-700">
<p className="mt-1 text-sm text-amber-700 dark:text-amber-300">
{t('backup.dashboard.notConfigured.message')}
</p>
</div>
@@ -104,8 +104,8 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{/* Health Score Card */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">{t('backup.dashboard.health.title')}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium bg-${healthColors[health.status]}-100 text-${healthColors[health.status]}-700`}>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('backup.dashboard.health.title')}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium bg-${healthColors[health.status]}-100 dark:bg-${healthColors[health.status]}-900/40 text-${healthColors[health.status]}-700 dark:text-${healthColors[health.status]}-300`}>
{health.status.charAt(0).toUpperCase() + health.status.slice(1)}
</span>
</div>
@@ -120,7 +120,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
stroke="currentColor"
strokeWidth="8"
fill="none"
className="text-gray-200"
className="text-neutral-200 dark:text-neutral-700"
/>
<circle
cx="48"
@@ -134,14 +134,14 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-gray-900">{health.score}%</span>
<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-gray-700 font-medium">{health.message}</p>
<p className="text-neutral-700 dark:text-neutral-300 font-medium">{health.message}</p>
{lastBackup && (
<p className="text-sm text-gray-500 mt-1">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">
Last successful backup: {formatDistanceToNow(new Date(lastBackup.created_at), { addSuffix: true })}
</p>
)}
@@ -206,10 +206,10 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{/* Recent Activity */}
{status?.recentBackups && status.recentBackups.length > 0 && (
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.recentActivity.title')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.dashboard.recentActivity.title')}</h3>
<div className="space-y-3">
{status.recentBackups.slice(0, 5).map((backup) => (
<div key={backup.id} className="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
<div key={backup.id} className="flex items-center justify-between py-3 border-b border-neutral-100 dark:border-neutral-700 last:border-0">
<div className="flex items-center space-x-3">
{backup.status === 'completed' ? (
<CheckCircle className="h-5 w-5 text-green-500" />
@@ -219,19 +219,19 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
)}
<div>
<p className="font-medium text-gray-900">
<p className="font-medium text-neutral-900 dark:text-neutral-100">
{t('backup.dashboard.backupType', { type: backup.backup_type })}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{format(new Date(backup.created_at), 'PPp')}
</p>
</div>
</div>
<div className="text-right">
<p className="text-sm font-medium text-gray-900">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{formatBytes(backup.statistics?.total_size || 0)}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{backup.statistics?.files_processed || 0} files
</p>
</div>
@@ -244,36 +244,36 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
{/* Storage Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.coverage.title')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.dashboard.coverage.title')}</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Database className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">Database</span>
<Database className="h-5 w-5 text-neutral-400" />
<span className="text-neutral-700 dark:text-neutral-300">Database</span>
</div>
<span className={`px-2 py-1 rounded text-xs font-medium ${
statistics.database_backed_up ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
statistics.database_backed_up ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300' : 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300'
}`}>
{statistics.database_backed_up ? t('backup.dashboard.coverage.included') : t('backup.dashboard.coverage.excluded')}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<Image className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">{t('backup.configuration.whatToBackup.photos')}</span>
<Image className="h-5 w-5 text-neutral-400" />
<span className="text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.photos')}</span>
</div>
<span className="text-sm text-gray-500">
<span className="text-sm text-neutral-500 dark:text-neutral-400">
{statistics.photos_backed_up || 0} {t('common.of')} {statistics.total_photos || 0}
</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<FileArchive className="h-5 w-5 text-gray-400" />
<span className="text-gray-700">{t('backup.configuration.whatToBackup.archives')}</span>
<FileArchive className="h-5 w-5 text-neutral-400" />
<span className="text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.archives')}</span>
</div>
<span className="text-sm text-gray-500">
<span className="text-sm text-neutral-500 dark:text-neutral-400">
{statistics.archives_backed_up || 0} {t('backup.dashboard.stats.files')}
</span>
</div>
@@ -281,7 +281,7 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
</Card>
<Card className="p-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">{t('backup.dashboard.storageDestination')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.dashboard.storageDestination')}</h3>
<div className="space-y-3">
<div className="flex items-center space-x-3">
{config?.backup_destination_type === 's3' ? (
@@ -289,15 +289,15 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
) : config?.backup_destination_type === 'rsync' ? (
<Server className="h-5 w-5 text-purple-500" />
) : (
<HardDrive className="h-5 w-5 text-gray-500" />
<HardDrive className="h-5 w-5 text-neutral-500" />
)}
<div>
<p className="font-medium text-gray-900">
{config?.backup_destination_type
? t(`backup.configuration.destinationTypes.${config.backup_destination_type}.name`)
<p className="font-medium text-neutral-900 dark:text-neutral-100">
{config?.backup_destination_type
? t(`backup.configuration.destinationTypes.${config.backup_destination_type}.name`)
: t('backup.dashboard.notConfigured.title')}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{config?.backup_destination_type === 's3' && config?.backup_s3_bucket
? `Bucket: ${config.backup_s3_bucket}`
: config?.backup_destination_type === 'local' && config?.backup_destination_path
@@ -308,12 +308,12 @@ export const BackupDashboard = ({ status, config, onRunBackup, isBackupRunning }
</p>
</div>
</div>
{config?.backup_retention_days && (
<div className="mt-4 p-3 bg-gray-50 rounded-lg">
<div className="mt-4 p-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<div className="flex items-center space-x-2">
<Info className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600">
<Info className="h-4 w-4 text-neutral-400" />
<span className="text-sm text-neutral-600 dark:text-neutral-300">
{t('backup.configuration.schedule.retentionDays')} {config.backup_retention_days} {t('backup.configuration.schedule.retentionHelp').replace('days (older backups will be automatically deleted)', '')}
</span>
</div>
+52 -52
View File
@@ -109,7 +109,7 @@ export const BackupHistory = () => {
<div className="flex flex-col sm:flex-row gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-neutral-400" />
<Input
type="text"
placeholder={t('backup.history.searchPlaceholder')}
@@ -124,7 +124,7 @@ export const BackupHistory = () => {
<select
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-md focus:outline-none focus:ring-primary focus:border-primary"
>
<option value="all">All Status</option>
<option value="completed">Completed</option>
@@ -149,33 +149,33 @@ export const BackupHistory = () => {
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="bg-gray-50 border-b border-gray-200">
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<tr className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.status')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.dateTime')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.type')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.size')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.duration')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
<th className="px-6 py-3 text-right text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{t('backup.history.columns.actions')}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{backups.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-12 text-center text-gray-500">
<FileArchive className="h-12 w-12 mx-auto mb-3 text-gray-300" />
<p className="text-lg font-medium">No backups found</p>
<td colSpan={6} className="px-6 py-12 text-center text-neutral-500 dark:text-neutral-400">
<FileArchive className="h-12 w-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" />
<p className="text-lg font-medium text-neutral-900 dark:text-neutral-100">No backups found</p>
<p className="text-sm mt-1">Backups will appear here once created</p>
</td>
</tr>
@@ -188,40 +188,40 @@ export const BackupHistory = () => {
return (
<React.Fragment key={backup.id}>
<tr className="hover:bg-gray-50">
<tr className="hover:bg-neutral-50 dark:hover:bg-neutral-700/50">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<StatusIcon className={`h-5 w-5 ${statusColor}`} />
<span className="ml-2 text-sm font-medium text-gray-900 capitalize">
<span className="ml-2 text-sm font-medium text-neutral-900 dark:text-neutral-100 capitalize">
{backup.status}
</span>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<p className="text-sm font-medium text-gray-900">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{format(new Date(backup.created_at), 'PPP')}
</p>
<p className="text-xs text-gray-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{format(new Date(backup.created_at), 'p')} {formatDistanceToNow(new Date(backup.created_at), { addSuffix: true })}
</p>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 capitalize">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300 capitalize">
{backup.backup_type || 'Manual'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<p className="text-sm text-gray-900">
<p className="text-sm text-neutral-900 dark:text-neutral-100">
{formatBytes(stats.total_size || 0)}
</p>
<p className="text-xs text-gray-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{stats.files_processed || 0} {t('backup.dashboard.stats.files')}
</p>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{backup.duration_seconds
<td className="px-6 py-4 whitespace-nowrap text-sm text-neutral-900 dark:text-neutral-100">
{backup.duration_seconds
? `${Math.round(backup.duration_seconds / 60)}m ${backup.duration_seconds % 60}s`
: '-'}
</td>
@@ -229,7 +229,7 @@ export const BackupHistory = () => {
<div className="flex items-center justify-end space-x-2">
<button
onClick={() => toggleRowExpansion(backup.id)}
className="text-gray-400 hover:text-gray-600"
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
title={t('backup.actions.view')}
>
{isExpanded ? <ChevronUp size={20} /> : <ChevronDown size={20} />}
@@ -237,7 +237,7 @@ export const BackupHistory = () => {
{backup.manifest_path && (
<button
onClick={() => window.open(`/admin/backup/download/${backup.id}`, '_blank')}
className="text-gray-400 hover:text-gray-600"
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300"
title={t('backup.actions.download')}
>
<Download size={20} />
@@ -245,7 +245,7 @@ export const BackupHistory = () => {
)}
<button
onClick={() => handleDelete(backup)}
className="text-gray-400 hover:text-red-600"
className="text-neutral-400 hover:text-red-600"
title={t('backup.actions.delete')}
disabled={deleteMutation.isLoading}
>
@@ -258,24 +258,24 @@ export const BackupHistory = () => {
{/* Expanded Details Row */}
{isExpanded && (
<tr>
<td colSpan={6} className="px-6 py-4 bg-gray-50">
<td colSpan={6} className="px-6 py-4 bg-neutral-50 dark:bg-neutral-700/50">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Backup Details */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">{t('backup.history.details.backupDetails')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.history.details.backupDetails')}</h4>
<div className="text-sm space-y-1">
<div className="flex justify-between">
<span className="text-gray-500">{t('backup.history.details.destination')}:</span>
<span className="text-gray-900">{backup.destination_type || 'Unknown'}</span>
<span className="text-neutral-500 dark:text-neutral-400">{t('backup.history.details.destination')}:</span>
<span className="text-neutral-900 dark:text-neutral-100">{backup.destination_type || 'Unknown'}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-500">{t('backup.history.details.started')}:</span>
<span className="text-gray-900">{format(new Date(backup.created_at), 'p')}</span>
<span className="text-neutral-500 dark:text-neutral-400">{t('backup.history.details.started')}:</span>
<span className="text-neutral-900 dark:text-neutral-100">{format(new Date(backup.created_at), 'p')}</span>
</div>
{backup.completed_at && (
<div className="flex justify-between">
<span className="text-gray-500">{t('backup.history.details.completed')}:</span>
<span className="text-gray-900">{format(new Date(backup.completed_at), 'p')}</span>
<span className="text-neutral-500 dark:text-neutral-400">{t('backup.history.details.completed')}:</span>
<span className="text-neutral-900 dark:text-neutral-100">{format(new Date(backup.completed_at), 'p')}</span>
</div>
)}
</div>
@@ -283,21 +283,21 @@ export const BackupHistory = () => {
{/* Content Backed Up */}
<div className="space-y-2">
<h4 className="font-medium text-gray-900">{t('backup.history.details.contentBackedUp')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.history.details.contentBackedUp')}</h4>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Database className={`h-4 w-4 ${stats.database_backed_up ? 'text-green-500' : 'text-gray-300'}`} />
<span className="text-sm text-gray-700">{t('backup.configuration.whatToBackup.database')}</span>
<Database className={`h-4 w-4 ${stats.database_backed_up ? 'text-green-500' : 'text-neutral-300 dark:text-neutral-600'}`} />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('backup.configuration.whatToBackup.database')}</span>
</div>
<div className="flex items-center space-x-2">
<Image className={`h-4 w-4 ${stats.photos_backed_up > 0 ? 'text-green-500' : 'text-gray-300'}`} />
<span className="text-sm text-gray-700">
<Image className={`h-4 w-4 ${stats.photos_backed_up > 0 ? 'text-green-500' : 'text-neutral-300 dark:text-neutral-600'}`} />
<span className="text-sm text-neutral-700 dark:text-neutral-300">
Photos ({stats.photos_backed_up || 0} of {stats.total_photos || 0})
</span>
</div>
<div className="flex items-center space-x-2">
<FileArchive className={`h-4 w-4 ${stats.archives_backed_up > 0 ? 'text-green-500' : 'text-gray-300'}`} />
<span className="text-sm text-gray-700">
<FileArchive className={`h-4 w-4 ${stats.archives_backed_up > 0 ? 'text-green-500' : 'text-neutral-300 dark:text-neutral-600'}`} />
<span className="text-sm text-neutral-700 dark:text-neutral-300">
Archives ({stats.archives_backed_up || 0})
</span>
</div>
@@ -307,8 +307,8 @@ export const BackupHistory = () => {
{/* Error Information */}
{backup.error_message && (
<div className="space-y-2">
<h4 className="font-medium text-red-900">{t('backup.history.details.errorDetails')}</h4>
<p className="text-sm text-red-700 bg-red-50 p-2 rounded">
<h4 className="font-medium text-red-900 dark:text-red-200">{t('backup.history.details.errorDetails')}</h4>
<p className="text-sm text-red-700 dark:text-red-300 bg-red-50 dark:bg-red-900/30 p-2 rounded">
{backup.error_message}
</p>
</div>
@@ -317,8 +317,8 @@ export const BackupHistory = () => {
{/* Manifest Path */}
{backup.manifest_path && (
<div className="space-y-2">
<h4 className="font-medium text-gray-900">{t('backup.history.details.manifest')}</h4>
<p className="text-sm text-gray-600 font-mono break-all">
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.history.details.manifest')}</h4>
<p className="text-sm text-neutral-600 dark:text-neutral-400 font-mono break-all">
{backup.manifest_path}
</p>
</div>
@@ -337,7 +337,7 @@ export const BackupHistory = () => {
{/* Pagination */}
{pagination.pages > 1 && (
<div className="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
<div className="bg-white dark:bg-neutral-800 px-4 py-3 border-t border-neutral-200 dark:border-neutral-700 sm:px-6">
<div className="flex items-center justify-between">
<div className="flex-1 flex justify-between sm:hidden">
<Button
@@ -359,7 +359,7 @@ export const BackupHistory = () => {
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
<p className="text-sm text-neutral-700 dark:text-neutral-300">
{t('backup.history.pagination.showing', {
from: (currentPage - 1) * pagination.limit + 1,
to: Math.min(currentPage * pagination.limit, pagination.total),
@@ -372,11 +372,11 @@ export const BackupHistory = () => {
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-sm font-medium text-neutral-500 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('backup.history.pagination.previous')}
</button>
{[...Array(Math.min(5, pagination.pages))].map((_, i) => {
const pageNum = i + 1;
return (
@@ -385,19 +385,19 @@ export const BackupHistory = () => {
onClick={() => setCurrentPage(pageNum)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
currentPage === pageNum
? 'z-10 bg-primary-50 border-primary text-primary'
: 'bg-white border-gray-300 text-gray-700 hover:bg-gray-50'
? 'z-10 bg-primary-50 dark:bg-primary-900/30 border-primary text-primary'
: 'bg-white dark:bg-neutral-800 border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-700'
}`}
>
{pageNum}
</button>
);
})}
<button
onClick={() => setCurrentPage(p => Math.min(pagination.pages, p + 1))}
disabled={currentPage === pagination.pages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-sm font-medium text-neutral-500 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{t('backup.history.pagination.next')}
</button>
@@ -44,27 +44,27 @@ export const BulkCategoryModal: React.FC<BulkCategoryModalProps> = ({
<Card className="w-full max-w-md">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-neutral-900">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{t('photos.moveToCategory', 'Move {{count}} photos to category', { count: photoCount })}
</h2>
<button
onClick={handleClose}
className="p-1 hover:bg-neutral-100 rounded-lg transition-colors"
className="p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg transition-colors"
disabled={isLoading}
>
<X className="w-5 h-5 text-neutral-500" />
<X className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
</button>
</div>
<div className="mb-6">
<label htmlFor="category-select" className="block text-sm font-medium text-neutral-700 mb-2">
<label htmlFor="category-select" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('photos.selectCategory', 'Select category')}
</label>
<select
id="category-select"
value={selectedCategoryId ?? ''}
onChange={(e) => setSelectedCategoryId(e.target.value === '' ? null : Number(e.target.value))}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
disabled={isLoading}
>
<option value="">{t('photos.uncategorized', 'Uncategorized')}</option>
@@ -101,7 +101,7 @@ export const CategoryManager: React.FC = () => {
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-lg font-semibold text-neutral-900">{t('categories.title')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('categories.title')}</h3>
{!isAdding && (
<Button
variant="primary"
@@ -116,14 +116,14 @@ export const CategoryManager: React.FC = () => {
{/* Add new category form */}
{isAdding && (
<div className="flex gap-2 p-3 bg-neutral-50 rounded-lg">
<div className="flex gap-2 p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
<input
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleCreate()}
placeholder={t('categories.categoryName')}
className="flex-1 px-3 py-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-primary-500"
className="flex-1 px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500"
autoFocus
/>
<Button
@@ -154,14 +154,14 @@ export const CategoryManager: React.FC = () => {
{/* Categories list */}
<div className="space-y-2">
{categories.length === 0 ? (
<p className="text-neutral-500 text-center py-8">
<p className="text-neutral-500 dark:text-neutral-400 text-center py-8">
{t('categories.noCategoriesYet')}
</p>
) : (
categories.map((category) => (
<div
key={category.id}
className="flex items-center justify-between p-3 bg-white rounded-lg border border-neutral-200 hover:border-neutral-300 transition-colors"
className="flex items-center justify-between p-3 bg-white dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600 transition-colors"
>
{editingId === category.id ? (
<div className="flex gap-2 flex-1">
@@ -173,7 +173,7 @@ export const CategoryManager: React.FC = () => {
if (e.key === 'Enter') handleUpdate(category.id);
if (e.key === 'Escape') cancelEdit();
}}
className="flex-1 px-3 py-1 border border-neutral-300 rounded-md focus:ring-2 focus:ring-primary-500"
className="flex-1 px-3 py-1 border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500"
autoFocus
/>
<Button
@@ -199,20 +199,20 @@ export const CategoryManager: React.FC = () => {
) : (
<>
<div>
<p className="font-medium text-neutral-900">{category.name}</p>
<p className="text-sm text-neutral-500">/{category.slug}</p>
<p className="font-medium text-neutral-900 dark:text-neutral-100">{category.name}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">/{category.slug}</p>
</div>
<div className="flex gap-1">
<button
onClick={() => startEdit(category)}
className="p-1.5 text-neutral-600 hover:text-primary-600 hover:bg-primary-50 rounded transition-colors"
className="p-1.5 text-neutral-600 dark:text-neutral-400 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded transition-colors"
title={t('common.edit')}
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(category)}
className="p-1.5 text-neutral-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
className="p-1.5 text-neutral-600 dark:text-neutral-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded transition-colors"
title={t('common.delete')}
disabled={deleteMutation.isPending}
>
@@ -92,14 +92,14 @@ export const CssTemplateEditor: React.FC = () => {
<Card>
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-neutral-900 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Code className="w-5 h-5" />
{t('cssTemplates.title', 'Custom CSS Templates')}
</h2>
</div>
{/* Tab Navigation */}
<div className="flex border-b border-neutral-200 mb-6">
<div className="flex border-b border-neutral-200 dark:border-neutral-700 mb-6">
{[1, 2, 3].map(slot => {
const template = localTemplates.find(t => t.slot_number === slot);
return (
@@ -109,7 +109,7 @@ export const CssTemplateEditor: React.FC = () => {
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeSlot === slot
? 'border-primary-600 text-primary-600'
: 'border-transparent text-neutral-600 hover:text-neutral-900 hover:border-neutral-300'
: 'border-transparent text-neutral-600 dark:text-neutral-400 hover:text-neutral-900 dark:hover:text-neutral-100 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
{t('cssTemplates.template', 'Template')} {slot}
@@ -130,7 +130,7 @@ export const CssTemplateEditor: React.FC = () => {
<div className="space-y-6">
{/* Template Name */}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 dark:text-neutral-300 mb-2">
{t('cssTemplates.templateName', 'Template Name')}
</label>
<input
@@ -138,7 +138,7 @@ export const CssTemplateEditor: React.FC = () => {
value={activeTemplate.name}
onChange={(e) => updateLocalTemplate({ name: e.target.value })}
maxLength={50}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
@@ -151,18 +151,18 @@ export const CssTemplateEditor: React.FC = () => {
onChange={(e) => updateLocalTemplate({ is_enabled: e.target.checked })}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('cssTemplates.enableTemplate', 'Enable this template')}
</span>
</label>
<p className="text-xs text-neutral-500 mt-1 ml-6">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1 ml-6">
{t('cssTemplates.enableHint', 'Enabled templates can be selected when creating events')}
</p>
</div>
{/* CSS Editor */}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 dark:text-neutral-300 mb-2">
{t('cssTemplates.cssContent', 'CSS Content')}
</label>
<div className="relative">
@@ -177,22 +177,22 @@ export const CssTemplateEditor: React.FC = () => {
{(activeTemplate.css_content?.length || 0).toLocaleString()} / 102,400 {t('common.characters', 'characters')}
</div>
</div>
<p className="text-xs text-neutral-500 mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('cssTemplates.cssHint', 'Use .gallery-page to scope styles to the gallery. Available variables: --gallery-bg, --gallery-text, --gallery-accent')}
</p>
</div>
{/* Security Notice */}
<div className="flex items-start gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<AlertTriangle className="w-4 h-4 text-amber-600 mt-0.5 flex-shrink-0" />
<div className="text-xs text-amber-800">
<div className="flex items-start gap-2 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
<AlertTriangle className="w-4 h-4 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
<div className="text-xs text-amber-800 dark:text-amber-200">
<strong>{t('cssTemplates.securityNotice', 'Security Notice')}:</strong>{' '}
{t('cssTemplates.securityText', 'CSS is sanitized to prevent malicious code. External URLs, @import, and JavaScript expressions are blocked.')}
</div>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-between pt-4 border-t border-neutral-100">
<div className="flex items-center justify-between pt-4 border-t border-neutral-100 dark:border-neutral-700">
<div className="flex items-center gap-3">
{activeSlot === 1 && activeTemplate.is_default && (
<Button
@@ -227,7 +227,7 @@ export const CssTemplateEditor: React.FC = () => {
{/* Last Updated */}
{activeTemplate.updated_at && (
<p className="text-xs text-neutral-400 text-right">
<p className="text-xs text-neutral-400 dark:text-neutral-500 text-right">
{t('cssTemplates.lastUpdated', 'Last updated')}: {new Date(activeTemplate.updated_at).toLocaleString()}
</p>
)}
@@ -25,27 +25,27 @@ export const EmailPreviewModal: React.FC<EmailPreviewModalProps> = ({
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<Card className="w-full max-w-4xl max-h-[90vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
<div className="flex items-center justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
<div className="flex items-center gap-3">
<Mail className="w-6 h-6 text-primary-600" />
<h2 className="text-xl font-semibold text-neutral-900">Email Preview</h2>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">Email Preview</h2>
</div>
<button
onClick={onClose}
className="text-neutral-400 hover:text-neutral-600 transition-colors"
className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
{/* Subject */}
<div className="px-6 py-4 border-b border-neutral-200 bg-neutral-50">
<p className="text-sm font-medium text-neutral-600">Subject:</p>
<p className="text-lg font-semibold text-neutral-900 mt-1">{subject}</p>
<div className="px-6 py-4 border-b border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800">
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">Subject:</p>
<p className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mt-1">{subject}</p>
</div>
{/* View mode toggle */}
<div className="px-6 py-3 border-b border-neutral-200">
<div className="px-6 py-3 border-b border-neutral-200 dark:border-neutral-700">
<div className="flex gap-2">
<Button
variant={viewMode === 'html' ? 'primary' : 'outline'}
@@ -71,7 +71,7 @@ export const EmailPreviewModal: React.FC<EmailPreviewModalProps> = ({
{/* Content */}
<div className="flex-1 overflow-auto p-6">
{viewMode === 'html' ? (
<div className="bg-white border border-neutral-200 rounded-lg shadow-sm">
<div className="bg-white border border-neutral-200 dark:border-neutral-700 rounded-lg shadow-sm">
<iframe
srcDoc={htmlContent}
className="w-full h-[600px] border-0"
@@ -79,8 +79,8 @@ export const EmailPreviewModal: React.FC<EmailPreviewModalProps> = ({
/>
</div>
) : (
<div className="bg-neutral-50 border border-neutral-200 rounded-lg p-6">
<pre className="whitespace-pre-wrap font-mono text-sm text-neutral-700">
<div className="bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg p-6">
<pre className="whitespace-pre-wrap font-mono text-sm text-neutral-700 dark:text-neutral-300">
{textContent}
</pre>
</div>
@@ -88,7 +88,7 @@ export const EmailPreviewModal: React.FC<EmailPreviewModalProps> = ({
</div>
{/* Footer */}
<div className="flex justify-end gap-3 p-6 border-t border-neutral-200">
<div className="flex justify-end gap-3 p-6 border-t border-neutral-200 dark:border-neutral-700">
<Button variant="outline" onClick={onClose}>
Close
</Button>
@@ -110,7 +110,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
return (
<div className="space-y-3">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium text-neutral-700">{t('categories.eventSpecificCategories')}</h3>
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('categories.eventSpecificCategories')}</h3>
{!isAdding && (
<Button
variant="outline"
@@ -132,7 +132,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
onChange={(e) => setNewCategoryName(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleCreate()}
placeholder={t('categories.categoryName')}
className="flex-1 px-3 py-1.5 text-sm border border-neutral-300 rounded-md focus:ring-2 focus:ring-primary-500"
className="flex-1 px-3 py-1.5 text-sm border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500"
autoFocus
/>
<Button
@@ -162,7 +162,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
{/* Event categories list */}
{eventCategories.length === 0 ? (
<p className="text-sm text-neutral-500 italic">
<p className="text-sm text-neutral-500 dark:text-neutral-400 italic">
{t('categories.noEventSpecificCategories')}
</p>
) : (
@@ -174,13 +174,13 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
return (
<div
key={category.id}
className="flex items-center justify-between px-3 py-2 bg-neutral-50 rounded-md"
className="flex items-center justify-between px-3 py-2 bg-neutral-50 dark:bg-neutral-800 rounded-md"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
{/* Hero photo thumbnail */}
<button
onClick={() => setHeroPickerCategoryId(category.id)}
className="flex-shrink-0 w-10 h-10 rounded border border-neutral-200 overflow-hidden bg-neutral-100 hover:border-primary-400 transition-colors flex items-center justify-center"
className="flex-shrink-0 w-10 h-10 rounded border border-neutral-200 dark:border-neutral-700 overflow-hidden bg-neutral-100 dark:bg-neutral-700 hover:border-primary-400 transition-colors flex items-center justify-center"
title={t('categories.setCoverPhoto')}
>
{heroPhoto ? (
@@ -195,11 +195,11 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
<ImageIcon className="w-4 h-4 text-neutral-300" />
)}
</button>
<span className="text-sm text-neutral-700 truncate">{category.name}</span>
<span className="text-sm text-neutral-700 dark:text-neutral-300 truncate">{category.name}</span>
</div>
<button
onClick={() => handleDelete(category)}
className="p-1 text-neutral-400 hover:text-red-600 transition-colors"
className="p-1 text-neutral-400 dark:text-neutral-500 hover:text-red-600 dark:hover:text-red-400 transition-colors"
title={t('categories.deleteCategoryTitle')}
disabled={deleteMutation.isPending}
>
@@ -216,8 +216,8 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
)}
{/* Show available global categories */}
<div className="mt-4 pt-3 border-t border-neutral-200">
<p className="text-xs font-medium text-neutral-500 mb-2">{t('categories.globalCategoriesAlwaysAvailable')}</p>
<div className="mt-4 pt-3 border-t border-neutral-200 dark:border-neutral-700">
<p className="text-xs font-medium text-neutral-500 dark:text-neutral-400 mb-2">{t('categories.globalCategoriesAlwaysAvailable')}</p>
<div className="space-y-2">
{categories
.filter(cat => cat.is_global)
@@ -226,10 +226,10 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
? photos.find(p => p.id === cat.hero_photo_id)
: null;
return (
<div key={cat.id} className="flex items-center gap-3 px-3 py-2 bg-neutral-50 rounded-md">
<div key={cat.id} className="flex items-center gap-3 px-3 py-2 bg-neutral-50 dark:bg-neutral-800 rounded-md">
<button
onClick={() => setHeroPickerCategoryId(cat.id)}
className="flex-shrink-0 w-10 h-10 rounded border border-neutral-200 overflow-hidden bg-neutral-100 hover:border-primary-400 transition-colors flex items-center justify-center"
className="flex-shrink-0 w-10 h-10 rounded border border-neutral-200 dark:border-neutral-700 overflow-hidden bg-neutral-100 dark:bg-neutral-700 hover:border-primary-400 transition-colors flex items-center justify-center"
title={t('categories.setCoverPhoto')}
>
{heroPhoto ? (
@@ -244,7 +244,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
<ImageIcon className="w-4 h-4 text-neutral-300" />
)}
</button>
<span className="text-sm text-neutral-600">{cat.name}</span>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{cat.name}</span>
</div>
);
})}
@@ -255,12 +255,12 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
{heroPickerCategoryId !== null && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<Card className="max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div className="p-6 border-b border-neutral-200">
<div className="p-6 border-b border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">{t('categories.setCoverPhoto')}</h2>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{t('categories.setCoverPhoto')}</h2>
<button
onClick={() => setHeroPickerCategoryId(null)}
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors"
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
@@ -269,7 +269,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
<div className="p-6 overflow-y-auto max-h-[calc(90vh-180px)]">
{photos.length === 0 ? (
<p className="text-center text-neutral-500 py-8">
<p className="text-center text-neutral-500 dark:text-neutral-400 py-8">
{t('events.noPhotosAvailable')}
</p>
) : (
@@ -287,7 +287,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
: 'border-transparent hover:border-neutral-300'
}`}
>
<div className="aspect-square bg-neutral-100">
<div className="aspect-square bg-neutral-100 dark:bg-neutral-700">
<AuthenticatedImage
src={photo.thumbnail_url || photo.url}
alt={photo.filename}
@@ -309,7 +309,7 @@ export const EventCategoryManager: React.FC<EventCategoryManagerProps> = ({ even
)}
</div>
<div className="p-6 border-t border-neutral-200 flex justify-between gap-3">
<div className="p-6 border-t border-neutral-200 dark:border-neutral-700 flex justify-between gap-3">
{categories.find(c => c.id === heroPickerCategoryId)?.hero_photo_id && (
<Button
variant="outline"
@@ -51,7 +51,7 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
<Card className={className}>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-neutral-900 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<MessageSquare className="w-5 h-5" />
{t('feedback.settings.title', 'Guest Feedback Settings')}
</h2>
@@ -62,7 +62,7 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
onChange={() => handleToggle('feedback_enabled')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('feedback.settings.enableFeedback', 'Enable feedback')}
</span>
</label>
@@ -72,77 +72,77 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
<>
{/* Feedback Types */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-700">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('feedback.settings.feedbackTypes', 'Feedback Types')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<label className="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700">
<input
type="checkbox"
checked={settings.allow_ratings}
onChange={() => handleToggle('allow_ratings')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Star className="w-5 h-5 text-neutral-600" />
<Star className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.ratings', 'Star Ratings')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.ratingsDesc', 'Allow guests to rate photos (1-5 stars)')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<label className="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700">
<input
type="checkbox"
checked={settings.allow_likes}
onChange={() => handleToggle('allow_likes')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Heart className="w-5 h-5 text-neutral-600" />
<Heart className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.likes', 'Likes')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.likesDesc', 'Simple like/unlike functionality')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<label className="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700">
<input
type="checkbox"
checked={settings.allow_comments}
onChange={() => handleToggle('allow_comments')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<MessageSquare className="w-5 h-5 text-neutral-600" />
<MessageSquare className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.comments', 'Comments')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.commentsDesc', 'Text comments on photos')}
</div>
</div>
</label>
<label className="flex items-center gap-3 p-3 bg-neutral-50 rounded-lg cursor-pointer hover:bg-neutral-100">
<label className="flex items-center gap-3 p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg cursor-pointer hover:bg-neutral-100 dark:hover:bg-neutral-700">
<input
type="checkbox"
checked={settings.allow_favorites}
onChange={() => handleToggle('allow_favorites')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Bookmark className="w-5 h-5 text-neutral-600" />
<Bookmark className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.favorites', 'Favorites')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.favoritesDesc', 'Mark photos as favorites')}
</div>
</div>
@@ -150,11 +150,11 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
</div>
</div>
<div className="border-t pt-4" />
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4" />
{/* Privacy & Moderation */}
<div className="space-y-4">
<h3 className="text-sm font-medium text-neutral-700">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('feedback.settings.privacyModeration', 'Privacy & Moderation')}
</h3>
<div className="space-y-3">
@@ -166,10 +166,10 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.requireInfo', 'Require Name & Email')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.requireInfoDesc', 'Guests must provide name and email to leave feedback')}
</div>
</div>
@@ -183,12 +183,12 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
disabled={!settings.allow_comments}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500 disabled:opacity-50"
/>
<Shield className="w-5 h-5 text-neutral-600" />
<Shield className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.moderateComments', 'Moderate Comments')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.moderateCommentsDesc', 'Comments require approval before being visible')}
</div>
</div>
@@ -201,12 +201,12 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
onChange={() => handleToggle('show_feedback_to_guests')}
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<Eye className="w-5 h-5 text-neutral-600" />
<Eye className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.showToGuests', 'Show Feedback to Guests')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.showToGuestsDesc', 'Other guests can see ratings, likes, and approved comments')}
</div>
</div>
@@ -214,7 +214,7 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
</div>
</div>
<div className="border-t pt-4" />
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4" />
{/* Rate Limiting */}
<div className="space-y-4">
@@ -226,10 +226,10 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
className="w-4 h-4 text-primary-600 bg-neutral-100 border-neutral-300 rounded focus:ring-primary-500"
/>
<div className="flex-1">
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('feedback.settings.enableRateLimiting', 'Enable Rate Limiting')}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{t('feedback.settings.rateLimitingDesc', 'Prevent spam by limiting feedback frequency')}
</div>
</div>
@@ -238,7 +238,7 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
{settings.enable_rate_limiting && (
<div className="grid grid-cols-2 gap-4 ml-7">
<div>
<label className="block text-xs font-medium text-neutral-600 mb-1">
<label className="block text-xs font-medium text-neutral-600 dark:text-neutral-400 mb-1">
{t('feedback.settings.timeWindow', 'Time Window (minutes)')}
</label>
<input
@@ -247,11 +247,11 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
max="60"
value={settings.rate_limit_window_minutes || 15}
onChange={(e) => handleNumberChange('rate_limit_window_minutes', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-neutral-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
className="w-full px-3 py-1.5 text-sm border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
<div>
<label className="block text-xs font-medium text-neutral-600 mb-1">
<label className="block text-xs font-medium text-neutral-600 dark:text-neutral-400 mb-1">
{t('feedback.settings.maxRequests', 'Max Requests')}
</label>
<input
@@ -260,7 +260,7 @@ export const FeedbackSettings: React.FC<FeedbackSettingsProps> = ({
max="100"
value={settings.rate_limit_max_requests || 10}
onChange={(e) => handleNumberChange('rate_limit_max_requests', e.target.value)}
className="w-full px-3 py-1.5 text-sm border border-neutral-300 rounded-md focus:ring-primary-500 focus:border-primary-500"
className="w-full px-3 py-1.5 text-sm border border-neutral-300 dark:border-neutral-600 rounded-md bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
</div>
@@ -37,7 +37,7 @@ export const HeroPhotoSelector: React.FC<HeroPhotoSelectorProps> = ({
if (!isEditing) {
return (
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('events.heroPhoto')}
</label>
{currentHeroPhoto ? (
@@ -57,10 +57,10 @@ export const HeroPhotoSelector: React.FC<HeroPhotoSelectorProps> = ({
return (
<div>
<label className="block text-sm font-medium text-neutral-700 mb-1">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('events.heroPhoto')}
</label>
<p className="text-xs text-neutral-500 mb-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mb-2">
{t('events.heroPhotoHelp')}
</p>
@@ -107,12 +107,12 @@ export const HeroPhotoSelector: React.FC<HeroPhotoSelectorProps> = ({
{isOpen && (
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<Card className="max-w-4xl w-full max-h-[90vh] overflow-hidden">
<div className="p-6 border-b border-neutral-200">
<div className="p-6 border-b border-neutral-200 dark:border-neutral-700">
<div className="flex items-center justify-between">
<h2 className="text-xl font-semibold">{t('events.selectHeroPhoto')}</h2>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{t('events.selectHeroPhoto')}</h2>
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-neutral-100 rounded-lg transition-colors"
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg transition-colors"
>
<X className="w-5 h-5" />
</button>
@@ -157,7 +157,7 @@ export const HeroPhotoSelector: React.FC<HeroPhotoSelectorProps> = ({
)}
</div>
<div className="p-6 border-t border-neutral-200 flex justify-end gap-3">
<div className="p-6 border-t border-neutral-200 dark:border-neutral-700 flex justify-end gap-3">
<Button
variant="outline"
onClick={() => setIsOpen(false)}
@@ -103,10 +103,10 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
<Card className="w-full max-w-md">
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-neutral-900">{t('passwordChange.title')}</h2>
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{t('passwordChange.title')}</h2>
<button
onClick={onClose}
className="p-1 hover:bg-neutral-100 rounded-lg transition-colors"
className="p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-neutral-500" />
</button>
@@ -115,7 +115,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
<form onSubmit={handleSubmit} className="space-y-4">
{/* Current Password */}
<div>
<label htmlFor="currentPassword" className="block text-sm font-medium text-neutral-700 mb-1">
<label htmlFor="currentPassword" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('passwordChange.currentPassword')}
</label>
<div className="relative">
@@ -131,7 +131,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, current: !prev.current }))}
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded"
>
{showPasswords.current ?
<EyeOff className="w-4 h-4 text-neutral-500" /> :
@@ -143,7 +143,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
{/* New Password */}
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-neutral-700 mb-1">
<label htmlFor="newPassword" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('passwordChange.newPassword')}
</label>
<div className="relative">
@@ -159,7 +159,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, new: !prev.new }))}
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded"
>
{showPasswords.new ?
<EyeOff className="w-4 h-4 text-neutral-500" /> :
@@ -171,7 +171,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
{/* Confirm Password */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-neutral-700 mb-1">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-1">
{t('passwordChange.confirmPassword')}
</label>
<div className="relative">
@@ -187,7 +187,7 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
<button
type="button"
onClick={() => setShowPasswords(prev => ({ ...prev, confirm: !prev.confirm }))}
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 rounded"
className="absolute right-3 top-2 p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded"
>
{showPasswords.confirm ?
<EyeOff className="w-4 h-4 text-neutral-500" /> :
@@ -198,10 +198,10 @@ export const PasswordChangeModal: React.FC<PasswordChangeModalProps> = ({ isOpen
</div>
{/* Password Requirements */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3">
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-3">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800">
<AlertCircle className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div className="text-sm text-blue-800 dark:text-blue-200">
<p className="font-medium">{t('passwordChange.requirements')}</p>
<ul className="list-disc list-inside mt-1 space-y-1">
<li>{t('passwordChange.minLength')}</li>
@@ -101,8 +101,8 @@ export const PhotoExportMenu: React.FC<PhotoExportMenuProps> = ({
inline-flex items-center gap-2 px-4 py-2 rounded-lg border font-medium text-sm
transition-colors
${isDisabled
? 'bg-neutral-100 text-neutral-400 border-neutral-200 cursor-not-allowed'
: 'bg-white text-neutral-700 border-neutral-300 hover:bg-neutral-50'
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-400 border-neutral-200 dark:border-neutral-700 cursor-not-allowed'
: 'bg-white dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 border-neutral-300 dark:border-neutral-600 hover:bg-neutral-50 dark:hover:bg-neutral-700'
}
`}
>
@@ -129,9 +129,9 @@ export const PhotoExportMenu: React.FC<PhotoExportMenuProps> = ({
/>
{/* Dropdown Menu */}
<div className="absolute right-0 mt-2 w-72 bg-white rounded-lg shadow-lg border border-neutral-200 z-20">
<div className="absolute right-0 mt-2 w-72 bg-white dark:bg-neutral-800 rounded-lg shadow-lg border border-neutral-200 dark:border-neutral-700 z-20">
<div className="p-2">
<p className="px-3 py-2 text-xs font-medium text-neutral-500 uppercase tracking-wider">
<p className="px-3 py-2 text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider">
{hasSelection
? t('export.exportSelected', 'Export {{count}} selected', { count: selectedPhotoIds.length })
: t('export.exportFiltered', 'Export filtered photos')
@@ -145,14 +145,14 @@ export const PhotoExportMenu: React.FC<PhotoExportMenuProps> = ({
key={format.value}
onClick={() => handleExport(format.value as 'txt' | 'csv' | 'xmp' | 'json')}
disabled={exportMutation.isPending}
className="w-full flex items-start gap-3 px-3 py-2 rounded-md hover:bg-neutral-50 text-left transition-colors"
className="w-full flex items-start gap-3 px-3 py-2 rounded-md hover:bg-neutral-50 dark:hover:bg-neutral-700 text-left transition-colors"
>
<Icon className="w-5 h-5 text-neutral-500 mt-0.5" />
<Icon className="w-5 h-5 text-neutral-500 dark:text-neutral-400 mt-0.5" />
<div>
<div className="text-sm font-medium text-neutral-900">
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{format.label}
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
{format.description}
</div>
</div>
@@ -165,7 +165,7 @@ export const PhotoExportMenu: React.FC<PhotoExportMenuProps> = ({
)}
{!hasSelection && !hasFilters && (
<p className="mt-1 text-xs text-neutral-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('export.hint', 'Select photos or apply filters to export')}
</p>
)}
@@ -57,9 +57,9 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
filters.hasComments;
return (
<div className="bg-white rounded-lg border border-neutral-200 p-4 mb-4">
<div className="bg-white dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-700 p-4 mb-4">
<div className="flex items-center justify-between mb-4">
<h3 className="font-medium text-neutral-900 flex items-center gap-2">
<h3 className="font-medium text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Filter className="w-4 h-4" />
{t('filter.feedbackFilters', 'Feedback Filters')}
</h3>
@@ -78,14 +78,14 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
<div className="space-y-4">
{/* Rating Filter */}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
<Star className="w-4 h-4 inline mr-1" />
{t('filter.rating', 'Rating')}
</label>
<select
value={filters.minRating ?? ''}
onChange={(e) => handleRatingChange(e.target.value === '' ? null : parseFloat(e.target.value))}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
disabled={isLoading}
>
{RATING_OPTIONS.map(option => (
@@ -107,10 +107,10 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
disabled={isLoading}
/>
<Heart className="w-4 h-4 text-red-500" />
<span className="text-sm text-neutral-700">
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('filter.hasLikes', 'Has likes')}
{summary && (
<span className="text-neutral-500 ml-1">({summary.withLikes})</span>
<span className="text-neutral-500 dark:text-neutral-400 ml-1">({summary.withLikes})</span>
)}
</span>
</label>
@@ -124,10 +124,10 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
disabled={isLoading}
/>
<Bookmark className="w-4 h-4 text-yellow-500" />
<span className="text-sm text-neutral-700">
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('filter.hasFavorites', 'Has favorites')}
{summary && (
<span className="text-neutral-500 ml-1">({summary.withFavorites})</span>
<span className="text-neutral-500 dark:text-neutral-400 ml-1">({summary.withFavorites})</span>
)}
</span>
</label>
@@ -141,10 +141,10 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
disabled={isLoading}
/>
<MessageCircle className="w-4 h-4 text-blue-500" />
<span className="text-sm text-neutral-700">
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('filter.hasComments', 'Has comments')}
{summary && (
<span className="text-neutral-500 ml-1">({summary.withComments})</span>
<span className="text-neutral-500 dark:text-neutral-400 ml-1">({summary.withComments})</span>
)}
</span>
</label>
@@ -153,15 +153,15 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
{/* Logic Toggle */}
{(filters.hasLikes || filters.hasFavorites || filters.hasComments) && (
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-600">{t('filter.combineWith', 'Combine with')}:</span>
<div className="flex rounded-lg border border-neutral-200 overflow-hidden">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('filter.combineWith', 'Combine with')}:</span>
<div className="flex rounded-lg border border-neutral-200 dark:border-neutral-700 overflow-hidden">
<button
type="button"
onClick={() => handleLogicChange('AND')}
className={`px-3 py-1 text-sm font-medium transition-colors ${
filters.logic === 'AND' || !filters.logic
? 'bg-primary-600 text-white'
: 'bg-white text-neutral-600 hover:bg-neutral-50'
: 'bg-white dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-700'
}`}
disabled={isLoading}
>
@@ -173,7 +173,7 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
className={`px-3 py-1 text-sm font-medium transition-colors ${
filters.logic === 'OR'
? 'bg-primary-600 text-white'
: 'bg-white text-neutral-600 hover:bg-neutral-50'
: 'bg-white dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 hover:bg-neutral-50 dark:hover:bg-neutral-700'
}`}
disabled={isLoading}
>
@@ -185,7 +185,7 @@ export const PhotoFilterPanel: React.FC<PhotoFilterPanelProps> = ({
{/* Summary */}
{summary && (
<div className="pt-2 border-t border-neutral-100 text-sm text-neutral-600">
<div className="pt-2 border-t border-neutral-100 dark:border-neutral-700 text-sm text-neutral-600 dark:text-neutral-400">
{t('filter.showingPhotos', 'Total photos')}: {summary.total}
{summary.withRatings > 0 && (
<span className="ml-2">
@@ -36,7 +36,7 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
};
return (
<div className="bg-white border border-neutral-200 rounded-lg p-4 mb-6">
<div className="bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg p-4 mb-6">
<div className="flex flex-col lg:flex-row gap-4">
{/* Search */}
<div className="flex-1">
@@ -60,7 +60,7 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
const numeric = Number(raw);
onCategoryChange(Number.isNaN(numeric) ? raw : numeric);
}}
className="px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="">{t('gallery.allCategories', 'All Categories')}</option>
<option value="0">{t('gallery.uncategorized', 'Uncategorized')}</option>
@@ -78,7 +78,7 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
<select
value={mediaType}
onChange={(e) => onMediaTypeChange(e.target.value as 'all' | 'photo' | 'video')}
className="px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="all">{t('gallery.allMedia', 'All media')}</option>
<option value="photo">{t('gallery.photosOnly', 'Photos only')}</option>
@@ -92,7 +92,7 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value as 'date' | 'name' | 'size' | 'rating', sortOrder)}
className="px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="date">{t('gallery.sortByDate', 'Sort by Date')}</option>
<option value="name">{t('gallery.sortByName', 'Sort by Name')}</option>
@@ -102,13 +102,13 @@ export const PhotoFilters: React.FC<PhotoFiltersProps> = ({
<button
onClick={handleSortToggle}
className="p-2 border border-neutral-300 rounded-lg hover:bg-neutral-50 transition-colors"
className="p-2 border border-neutral-300 dark:border-neutral-600 rounded-lg hover:bg-neutral-50 dark:hover:bg-neutral-700 transition-colors"
aria-label={sortOrder === 'asc' ? t('gallery.sortDescending', 'Sort descending') : t('gallery.sortAscending', 'Sort ascending')}
>
{sortOrder === 'asc' ? (
<SortAsc className="w-5 h-5 text-neutral-600" />
<SortAsc className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
) : (
<SortDesc className="w-5 h-5 text-neutral-600" />
<SortDesc className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
)}
</button>
</div>
+15 -15
View File
@@ -203,13 +203,13 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
<div className="space-y-4">
{/* Category Selection */}
<div>
<label className="block text-sm font-medium text-neutral-700 mb-2">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('upload.photoCategory')}
</label>
<select
value={selectedCategoryId || ''}
onChange={(e) => setSelectedCategoryId(e.target.value ? Number(e.target.value) : null)}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:ring-2 focus:ring-primary-500"
>
<option value="">{t('upload.noCategory')}</option>
{categories.map((category) => (
@@ -225,21 +225,21 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
className={clsx(
"border-2 border-dashed rounded-lg p-8 text-center transition-colors",
"hover:border-primary-400 hover:bg-primary-50/50",
selectedFiles.length > 0 ? "border-primary-400 bg-primary-50/30" : "border-neutral-300"
selectedFiles.length > 0 ? "border-primary-400 bg-primary-50/30 dark:bg-primary-900/20" : "border-neutral-300 dark:border-neutral-600"
)}
onClick={() => fileInputRef.current?.click()}
>
<Upload className="w-12 h-12 mx-auto text-neutral-400 mb-4" />
<p className="text-neutral-700 font-medium mb-1">
<Upload className="w-12 h-12 mx-auto text-neutral-400 dark:text-neutral-500 mb-4" />
<p className="text-neutral-700 dark:text-neutral-300 font-medium mb-1">
{t('upload.clickToUpload')}
</p>
<p className="text-sm text-neutral-500">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('upload.fileRequirements', { limit: maxFilesPerUpload })}
</p>
<p
className={clsx(
"text-xs mt-2",
remainingSlots === 0 ? "text-red-600" : "text-neutral-500"
remainingSlots === 0 ? "text-red-600" : "text-neutral-500 dark:text-neutral-400"
)}
>
{remainingSlots === 0
@@ -263,22 +263,22 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
{/* Selected Files */}
{selectedFiles.length > 0 && (
<div className="space-y-2">
<p className="text-sm font-medium text-neutral-700">
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('upload.selectedFiles')} ({selectedFiles.length})
</p>
<div className="max-h-48 overflow-y-auto space-y-2">
{selectedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between p-2 bg-neutral-50 rounded-lg"
className="flex items-center justify-between p-2 bg-neutral-50 dark:bg-neutral-800 rounded-lg"
>
<div className="flex items-center gap-3">
<Image className="w-5 h-5 text-neutral-400" />
<div>
<p className="text-sm font-medium text-neutral-700 truncate max-w-xs">
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300 truncate max-w-xs">
{file.name}
</p>
<p className="text-xs text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{formatFileSize(file.size)}
</p>
</div>
@@ -288,7 +288,7 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
e.stopPropagation();
removeFile(index);
}}
className="p-1 hover:bg-neutral-200 rounded"
className="p-1 hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded"
>
<X className="w-4 h-4" />
</button>
@@ -313,21 +313,21 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
{/* Progress Bar */}
{isUploading && (
<div className="mt-4">
<div className="flex justify-between text-sm text-neutral-600 mb-1">
<div className="flex justify-between text-sm text-neutral-600 dark:text-neutral-400 mb-1">
<span>
{t('upload.uploading')}
{totalChunks > 1 && ` (${t('common.chunk')} ${currentChunk}/${totalChunks})`}
</span>
<span>{uploadProgress}%</span>
</div>
<div className="w-full bg-neutral-200 rounded-full h-2">
<div className="w-full bg-neutral-200 dark:bg-neutral-700 rounded-full h-2">
<div
className="bg-primary-600 h-2 rounded-full transition-all duration-300"
style={{ width: `${uploadProgress}%` }}
/>
</div>
{totalChunks > 1 && (
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('upload.uploadingChunks', { count: selectedFiles.length, total: totalChunks })}
</p>
)}
@@ -30,10 +30,10 @@ export const PhotoUploadModal: React.FC<PhotoUploadModalProps> = ({
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[90vh]">
<div className="bg-white dark:bg-neutral-800 rounded-lg shadow-xl w-full max-w-2xl flex flex-col max-h-[90vh]">
{/* Fixed Header */}
<div className="flex items-center justify-between p-6 border-b border-neutral-200">
<h2 className="text-xl font-semibold text-neutral-900">{t('upload.uploadMedia', t('events.uploadPhotos'))}</h2>
<div className="flex items-center justify-between p-6 border-b border-neutral-200 dark:border-neutral-700">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{t('upload.uploadMedia', t('events.uploadPhotos'))}</h2>
<Button
variant="ghost"
size="sm"
+102 -102
View File
@@ -38,7 +38,7 @@ export const RestoreWizard = () => {
{ id: 'backup', title: t('backup.restore.steps.chooseBackup') },
{ id: 'options', title: t('backup.restore.steps.restoreOptions') },
{ id: 'confirm', title: t('backup.restore.steps.reviewConfirm') },
{ id: 'progress', title: t('backup.restore.steps.restoreProgress') }
{ id: 'progress', title: t('backup.restore.steps.progress') }
];
const restoreTypes = [
@@ -187,8 +187,8 @@ export const RestoreWizard = () => {
const renderSourceSelection = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.source.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.source.subtitle')}</p>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.restore.source.title')}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.source.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@@ -196,52 +196,52 @@ export const RestoreWizard = () => {
onClick={() => setRestoreData(prev => ({ ...prev, source: 'local' }))}
className={`p-6 rounded-lg border-2 transition-all ${
restoreData.source === 'local'
? 'border-primary bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-primary bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
<HardDrive className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 'local' ? 'text-primary' : 'text-gray-400'
restoreData.source === 'local' ? 'text-primary' : 'text-neutral-400'
}`} />
<h4 className="font-medium text-gray-900">{t('backup.restore.source.local.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.local.description')}</p>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.restore.source.local.name')}</h4>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('backup.restore.source.local.description')}</p>
</button>
<button
onClick={() => setRestoreData(prev => ({ ...prev, source: 's3' }))}
className={`p-6 rounded-lg border-2 transition-all ${
restoreData.source === 's3'
? 'border-primary bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-primary bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
<Cloud className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 's3' ? 'text-primary' : 'text-gray-400'
restoreData.source === 's3' ? 'text-primary' : 'text-neutral-400'
}`} />
<h4 className="font-medium text-gray-900">{t('backup.restore.source.s3.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.s3.description')}</p>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.restore.source.s3.name')}</h4>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('backup.restore.source.s3.description')}</p>
</button>
<button
onClick={() => setRestoreData(prev => ({ ...prev, source: 'upload' }))}
className={`p-6 rounded-lg border-2 transition-all ${
restoreData.source === 'upload'
? 'border-primary bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-primary bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
<Upload className={`h-12 w-12 mb-3 mx-auto ${
restoreData.source === 'upload' ? 'text-primary' : 'text-gray-400'
restoreData.source === 'upload' ? 'text-primary' : 'text-neutral-400'
}`} />
<h4 className="font-medium text-gray-900">{t('backup.restore.source.upload.name')}</h4>
<p className="text-xs text-gray-500 mt-1">{t('backup.restore.source.upload.description')}</p>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.restore.source.upload.name')}</h4>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">{t('backup.restore.source.upload.description')}</p>
</button>
</div>
{/* Source-specific configuration */}
{restoreData.source === 's3' && (
<Card className="p-4 space-y-4">
<h4 className="font-medium text-gray-900">{t('backup.restore.source.configuration.s3')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.restore.source.configuration.s3')}</h4>
<div className="grid grid-cols-2 gap-4">
<Input
placeholder={t('backup.restore.source.configuration.endpoint')}
@@ -283,8 +283,8 @@ export const RestoreWizard = () => {
{restoreData.source === 'upload' && (
<Card className="p-4">
<div className="text-center py-8">
<Upload className="h-12 w-12 mx-auto mb-3 text-gray-400" />
<p className="text-sm text-gray-600">{t('backup.restore.source.upload.comingSoon')}</p>
<Upload className="h-12 w-12 mx-auto mb-3 text-neutral-400" />
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.source.upload.comingSoon')}</p>
</div>
</Card>
)}
@@ -294,16 +294,16 @@ export const RestoreWizard = () => {
const renderBackupSelection = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.backup.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.backup.subtitle')}</p>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.restore.backup.title')}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.backup.subtitle')}</p>
</div>
{loadingBackups ? (
<Loading />
) : availableBackups?.length === 0 ? (
<Card className="p-8 text-center">
<FileArchive className="h-12 w-12 mx-auto mb-3 text-gray-300" />
<p className="text-gray-500">{t('backup.restore.backup.noBackupsFound')}</p>
<FileArchive className="h-12 w-12 mx-auto mb-3 text-neutral-300 dark:text-neutral-600" />
<p className="text-neutral-500 dark:text-neutral-400">{t('backup.restore.backup.noBackupsFound')}</p>
</Card>
) : (
<div className="space-y-3">
@@ -312,7 +312,7 @@ export const RestoreWizard = () => {
key={backup.id}
className={`p-4 cursor-pointer transition-all ${
restoreData.selectedBackup?.id === backup.id
? 'ring-2 ring-primary bg-primary-50'
? 'ring-2 ring-primary bg-primary-50 dark:bg-primary-900/30'
: 'hover:shadow-md'
}`}
onClick={() => setRestoreData(prev => ({ ...prev, selectedBackup: backup }))}
@@ -320,25 +320,25 @@ export const RestoreWizard = () => {
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<div className={`p-2 rounded-lg ${
backup.status === 'completed' ? 'bg-green-100' : 'bg-amber-100'
backup.status === 'completed' ? 'bg-green-100 dark:bg-green-900/40' : 'bg-amber-100 dark:bg-amber-900/40'
}`}>
{backup.status === 'completed' ? (
<CheckCircle className="h-6 w-6 text-green-600" />
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
) : (
<AlertCircle className="h-6 w-6 text-amber-600" />
<AlertCircle className="h-6 w-6 text-amber-600 dark:text-amber-400" />
)}
</div>
<div>
<p className="font-medium text-gray-900">
<p className="font-medium text-neutral-900 dark:text-neutral-100">
{format(new Date(backup.created_at), 'PPP')} {t('backup.restore.backup.at')} {format(new Date(backup.created_at), 'p')}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
{t('backup.dashboard.backupType', { type: backup.backup_type })} {formatBytes(backup.total_size || 0)}
</p>
</div>
</div>
{backup.encrypted && (
<Shield className="h-5 w-5 text-gray-400" />
<Shield className="h-5 w-5 text-neutral-400" />
)}
</div>
</Card>
@@ -347,12 +347,12 @@ export const RestoreWizard = () => {
)}
{restoreData.selectedBackup?.encrypted && (
<Card className="p-4 bg-amber-50 border-amber-200">
<Card className="p-4 bg-amber-50 dark:bg-amber-900/30 border-amber-200 dark:border-amber-800">
<div className="flex items-start space-x-3">
<Shield className="h-5 w-5 text-amber-600 mt-0.5" />
<Shield className="h-5 w-5 text-amber-600 dark:text-amber-400 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-amber-900">{t('backup.restore.backup.encrypted')}</p>
<p className="text-sm text-amber-700 mt-1">
<p className="text-sm font-medium text-amber-900 dark:text-amber-200">{t('backup.restore.backup.encrypted')}</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
{t('backup.restore.backup.encryptedMessage')}
</p>
<Input
@@ -375,8 +375,8 @@ export const RestoreWizard = () => {
const renderRestoreOptions = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.options.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.options.subtitle')}</p>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.restore.options.title')}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.options.subtitle')}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -388,18 +388,18 @@ export const RestoreWizard = () => {
onClick={() => setRestoreData(prev => ({ ...prev, restoreType: type.id }))}
className={`p-4 rounded-lg border-2 text-left transition-all ${
restoreData.restoreType === type.id
? 'border-primary bg-primary-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-primary bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
<div className="flex items-start space-x-3">
<Icon className={`h-6 w-6 mt-1 ${
restoreData.restoreType === type.id ? 'text-primary' : 'text-gray-400'
restoreData.restoreType === type.id ? 'text-primary' : 'text-neutral-400'
}`} />
<div className="flex-1">
<h4 className="font-medium text-gray-900">{type.name}</h4>
<p className="text-sm text-gray-600 mt-1">{type.description}</p>
<p className="text-xs text-amber-600 mt-2">
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{type.name}</h4>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">{type.description}</p>
<p className="text-xs text-amber-600 dark:text-amber-400 mt-2">
<AlertTriangle className="inline h-3 w-3 mr-1" />
{type.warning}
</p>
@@ -412,8 +412,8 @@ export const RestoreWizard = () => {
{/* Additional Options */}
<Card className="p-4 space-y-4">
<h4 className="font-medium text-gray-900">{t('backup.restore.options.additionalOptions.title')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100">{t('backup.restore.options.additionalOptions.title')}</h4>
<label className="flex items-start space-x-3">
<input
type="checkbox"
@@ -422,11 +422,11 @@ export const RestoreWizard = () => {
...prev,
skipPreBackup: e.target.checked
}))}
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div>
<p className="text-sm font-medium text-gray-700">{t('backup.restore.options.additionalOptions.skipPreBackup')}</p>
<p className="text-xs text-gray-500">
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.restore.options.additionalOptions.skipPreBackup')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.restore.options.additionalOptions.skipPreBackupHelp')}
</p>
</div>
@@ -440,11 +440,11 @@ export const RestoreWizard = () => {
...prev,
force: e.target.checked
}))}
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-gray-300 rounded"
className="mt-1 h-4 w-4 text-primary focus:ring-primary border-neutral-300 dark:border-neutral-600 rounded bg-white dark:bg-neutral-700"
/>
<div>
<p className="text-sm font-medium text-gray-700">{t('backup.restore.options.additionalOptions.force')}</p>
<p className="text-xs text-gray-500">
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('backup.restore.options.additionalOptions.force')}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('backup.restore.options.additionalOptions.forceHelp')}
</p>
</div>
@@ -456,17 +456,17 @@ export const RestoreWizard = () => {
const renderConfirmation = () => (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.confirmation.title')}</h3>
<p className="text-sm text-gray-600">{t('backup.restore.confirmation.subtitle')}</p>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.restore.confirmation.title')}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.subtitle')}</p>
</div>
{validationResult ? (
<>
{/* Validation Results */}
<Card className={`p-4 ${
validationResult.validation?.isValid
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
validationResult.validation?.isValid
? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800'
}`}>
<div className="flex items-start space-x-3">
{validationResult.validation?.isValid ? (
@@ -476,14 +476,14 @@ export const RestoreWizard = () => {
)}
<div className="flex-1">
<p className={`text-sm font-medium ${
validationResult.validation?.isValid ? 'text-green-900' : 'text-red-900'
validationResult.validation?.isValid ? 'text-green-900 dark:text-green-200' : 'text-red-900 dark:text-red-200'
}`}>
{validationResult.validation?.isValid
? t('backup.restore.confirmation.validation.passed')
: t('backup.restore.confirmation.validation.failed')}
</p>
{validationResult.validation?.errors?.length > 0 && (
<ul className="mt-2 text-sm text-red-700 list-disc list-inside">
<ul className="mt-2 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
{validationResult.validation.errors.map((error, idx) => (
<li key={idx}>{error}</li>
))}
@@ -496,17 +496,17 @@ export const RestoreWizard = () => {
{/* Space Check */}
{validationResult.spaceCheck && (
<Card className="p-4">
<h4 className="font-medium text-gray-900 mb-3">{t('backup.restore.confirmation.spaceCheck.title')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100 mb-3">{t('backup.restore.confirmation.spaceCheck.title')}</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-gray-600">{t('backup.restore.confirmation.spaceCheck.required')}:</span>
<span className="font-medium">
<span className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.spaceCheck.required')}:</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{validationResult.spaceCheck.requiredFormatted || formatBytes(validationResult.spaceCheck.required || 0)}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">{t('backup.restore.confirmation.spaceCheck.available')}:</span>
<span className="font-medium">
<span className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.spaceCheck.available')}:</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">
{validationResult.spaceCheck.availableFormatted ||
(validationResult.spaceCheck.available != null ? formatBytes(validationResult.spaceCheck.available) : t('common.unknown', 'Unknown'))}
</span>
@@ -523,38 +523,38 @@ export const RestoreWizard = () => {
{/* Summary */}
<Card className="p-4">
<h4 className="font-medium text-gray-900 mb-3">{t('backup.restore.confirmation.summary.title')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100 mb-3">{t('backup.restore.confirmation.summary.title')}</h4>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.source')}:</dt>
<dd className="font-medium capitalize">{restoreData.source}</dd>
<dt className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.summary.source')}:</dt>
<dd className="font-medium text-neutral-900 dark:text-neutral-100 capitalize">{restoreData.source}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.backupDate')}:</dt>
<dd className="font-medium">
<dt className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.summary.backupDate')}:</dt>
<dd className="font-medium text-neutral-900 dark:text-neutral-100">
{format(new Date(restoreData.selectedBackup.created_at), 'PPp')}
</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.restoreType')}:</dt>
<dd className="font-medium capitalize">{restoreData.restoreType}</dd>
<dt className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.summary.restoreType')}:</dt>
<dd className="font-medium text-neutral-900 dark:text-neutral-100 capitalize">{restoreData.restoreType}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-600">{t('backup.restore.confirmation.summary.preBackup')}:</dt>
<dd className="font-medium">{restoreData.skipPreBackup ? t('backup.restore.confirmation.summary.skipped') : t('backup.restore.confirmation.summary.enabled')}</dd>
<dt className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.summary.preBackup')}:</dt>
<dd className="font-medium text-neutral-900 dark:text-neutral-100">{restoreData.skipPreBackup ? t('backup.restore.confirmation.summary.skipped') : t('backup.restore.confirmation.summary.enabled')}</dd>
</div>
</dl>
</Card>
{/* Warning */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<div className="bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg p-4">
<div className="flex">
<AlertTriangle className="h-5 w-5 text-amber-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-amber-800">
<h3 className="text-sm font-medium text-amber-800 dark:text-amber-200">
{t('backup.restore.confirmation.warning.title')}
</h3>
<p className="mt-1 text-sm text-amber-700">
<p className="mt-1 text-sm text-amber-700 dark:text-amber-300">
{t('backup.restore.confirmation.warning.message')}
</p>
</div>
@@ -564,7 +564,7 @@ export const RestoreWizard = () => {
) : (
<div className="text-center py-8">
<Loader2 className="h-8 w-8 animate-spin mx-auto text-primary" />
<p className="mt-2 text-sm text-gray-600">{t('backup.restore.confirmation.validation.checking')}</p>
<p className="mt-2 text-sm text-neutral-600 dark:text-neutral-400">{t('backup.restore.confirmation.validation.checking')}</p>
</div>
)}
</div>
@@ -577,8 +577,8 @@ export const RestoreWizard = () => {
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{t('backup.restore.progress.title')}</h3>
<p className="text-sm text-gray-600">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.restore.progress.title')}</h3>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{isRunning ? t('backup.restore.progress.inProgress') : t('backup.restore.progress.completed')}
</p>
</div>
@@ -587,17 +587,17 @@ export const RestoreWizard = () => {
<Card className="p-6">
<div className="space-y-4">
<div className="flex justify-between text-sm">
<span className="text-gray-600">{t('backup.restore.progress.overallProgress')}</span>
<span className="font-medium">{progress.percentage || 0}%</span>
<span className="text-neutral-600 dark:text-neutral-400">{t('backup.restore.progress.overallProgress')}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">{progress.percentage || 0}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-3">
<div className="w-full bg-neutral-200 dark:bg-neutral-700 rounded-full h-3">
<div
className="bg-primary h-3 rounded-full transition-all duration-500"
style={{ width: `${progress.percentage || 0}%` }}
/>
</div>
{progress.currentFile && (
<p className="text-sm text-gray-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('backup.restore.progress.current')}: {progress.currentFile}
</p>
)}
@@ -606,7 +606,7 @@ export const RestoreWizard = () => {
{/* Status Details */}
<Card className="p-6">
<h4 className="font-medium text-gray-900 mb-4">{t('backup.restore.progress.statusDetails')}</h4>
<h4 className="font-medium text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.restore.progress.statusDetails')}</h4>
<div className="space-y-3">
{progress.steps?.map((step, idx) => (
<div key={idx} className="flex items-center space-x-3">
@@ -617,16 +617,16 @@ export const RestoreWizard = () => {
) : step.status === 'failed' ? (
<XCircle className="h-5 w-5 text-red-500" />
) : (
<Clock className="h-5 w-5 text-gray-300" />
<Clock className="h-5 w-5 text-neutral-300 dark:text-neutral-600" />
)}
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{step.name}</p>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{step.name}</p>
{step.message && (
<p className="text-xs text-gray-500">{step.message}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{step.message}</p>
)}
</div>
{step.duration && (
<span className="text-xs text-gray-500">{step.duration}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">{step.duration}</span>
)}
</div>
))}
@@ -636,9 +636,9 @@ export const RestoreWizard = () => {
{/* Logs */}
{progress.logs && progress.logs.length > 0 && (
<Card className="p-6">
<h4 className="font-medium text-gray-900 mb-4">{t('backup.restore.progress.restoreLogs')}</h4>
<div className="bg-gray-900 rounded-lg p-4 max-h-64 overflow-y-auto">
<pre className="text-xs text-gray-300 font-mono">
<h4 className="font-medium text-neutral-900 dark:text-neutral-100 mb-4">{t('backup.restore.progress.restoreLogs')}</h4>
<div className="bg-neutral-900 rounded-lg p-4 max-h-64 overflow-y-auto">
<pre className="text-xs text-neutral-300 font-mono">
{progress.logs.join('\n')}
</pre>
</div>
@@ -647,14 +647,14 @@ export const RestoreWizard = () => {
{/* Completion Actions */}
{!isRunning && progress.status === 'completed' && (
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg p-4">
<div className="flex">
<CheckCircle className="h-5 w-5 text-green-400 mt-0.5" />
<div className="ml-3">
<h3 className="text-sm font-medium text-green-800">
<h3 className="text-sm font-medium text-green-800 dark:text-green-200">
{t('backup.restore.progress.success.title')}
</h3>
<p className="mt-1 text-sm text-green-700">
<p className="mt-1 text-sm text-green-700 dark:text-green-300">
{t('backup.restore.progress.success.message')}
</p>
</div>
@@ -684,11 +684,11 @@ export const RestoreWizard = () => {
<div className="flex items-center">
<div className={`
relative flex h-8 w-8 items-center justify-center rounded-full
${currentStep > stepIdx
? 'bg-primary'
: currentStep === stepIdx
? 'bg-primary'
: 'bg-gray-300'
${currentStep > stepIdx
? 'bg-primary'
: currentStep === stepIdx
? 'bg-primary'
: 'bg-neutral-300 dark:bg-neutral-600'
}
`}>
{currentStep > stepIdx ? (
@@ -699,14 +699,14 @@ export const RestoreWizard = () => {
</div>
{stepIdx !== steps.length - 1 && (
<div className={`
absolute top-4 w-full h-0.5
${currentStep > stepIdx ? 'bg-primary' : 'bg-gray-300'}
absolute top-4 w-full h-0.5
${currentStep > stepIdx ? 'bg-primary' : 'bg-neutral-300 dark:bg-neutral-600'}
`} style={{ left: '2rem', right: '-2rem' }} />
)}
</div>
<span className={`
mt-2 text-xs font-medium
${currentStep >= stepIdx ? 'text-gray-900' : 'text-gray-500'}
${currentStep >= stepIdx ? 'text-neutral-900 dark:text-neutral-100' : 'text-neutral-500 dark:text-neutral-400'}
`}>
{step.title}
</span>
+26 -22
View File
@@ -7,7 +7,9 @@ import {
Play,
Clock,
LayoutGrid,
Layout
Layout,
Columns,
Film
} from 'lucide-react';
import { ThemeConfig, GalleryLayoutType, GALLERY_THEME_PRESETS } from '../../types/theme.types';
import { useTranslation } from 'react-i18next';
@@ -24,7 +26,9 @@ const layoutIcons: Record<GalleryLayoutType, React.ReactNode> = {
masonry: <Layers className="w-4 h-4" />,
carousel: <Play className="w-4 h-4" />,
timeline: <Clock className="w-4 h-4" />,
mosaic: <LayoutGrid className="w-4 h-4" />
mosaic: <LayoutGrid className="w-4 h-4" />,
'gallery-premium': <Columns className="w-4 h-4" />,
'gallery-story': <Film className="w-4 h-4" />
};
export const ThemeDisplay: React.FC<ThemeDisplayProps> = ({
@@ -78,64 +82,64 @@ export const ThemeDisplay: React.FC<ThemeDisplayProps> = ({
{/* Theme Name & Layout */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Layout className="w-4 h-4 text-neutral-500" />
<span className="text-sm font-medium text-neutral-700">{themeName}</span>
<Layout className="w-4 h-4 text-neutral-500 dark:text-neutral-300" />
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-100">{themeName}</span>
</div>
<div className="flex items-center gap-2 text-sm text-neutral-600">
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-neutral-200">
{layoutIcons[galleryLayout]}
<span className="capitalize">{t(`branding.layoutDescriptions.${galleryLayout}`)}</span>
</div>
</div>
{showDetails && (
<>
{/* Color Palette */}
<div className="flex items-center gap-2">
<Palette className="w-4 h-4 text-neutral-500" />
<span className="text-sm text-neutral-600">{t('branding.colors')}:</span>
<Palette className="w-4 h-4 text-neutral-500 dark:text-neutral-300" />
<span className="text-sm text-neutral-600 dark:text-neutral-200">{t('branding.colors')}:</span>
<div className="flex gap-1">
{themeConfig.primaryColor && (
<div
className="w-6 h-6 rounded border border-neutral-300"
<div
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.primaryColor }}
title={t('branding.primaryColor')}
/>
)}
{themeConfig.accentColor && (
<div
className="w-6 h-6 rounded border border-neutral-300"
<div
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.accentColor }}
title={t('branding.accentColor')}
/>
)}
{themeConfig.backgroundColor && (
<div
className="w-6 h-6 rounded border border-neutral-300"
<div
className="w-6 h-6 rounded border border-neutral-300 dark:border-neutral-600"
style={{ backgroundColor: themeConfig.backgroundColor }}
title={t('branding.backgroundColor')}
/>
)}
</div>
</div>
{/* Typography */}
{themeConfig.fontFamily && (
<div className="flex items-center gap-2">
<Type className="w-4 h-4 text-neutral-500" />
<span className="text-sm text-neutral-600">{t('branding.bodyFont')}:</span>
<span className="text-sm font-medium" style={{ fontFamily: themeConfig.fontFamily }}>
<Type className="w-4 h-4 text-neutral-500 dark:text-neutral-300" />
<span className="text-sm text-neutral-600 dark:text-neutral-200">{t('branding.bodyFont')}:</span>
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-100" style={{ fontFamily: themeConfig.fontFamily }}>
{themeConfig.fontFamily}
</span>
</div>
)}
{/* Layout Settings */}
{themeConfig.gallerySettings && (
<div className="text-sm text-neutral-600">
<div className="text-sm text-neutral-600 dark:text-neutral-200">
{themeConfig.gallerySettings.spacing && (
<span className="inline-flex items-center gap-1 mr-3">
<span>{t('branding.photoSpacing')}:</span>
<span className="font-medium capitalize">
<span className="font-medium capitalize text-neutral-700 dark:text-neutral-100">
{t(`branding.spacing.${themeConfig.gallerySettings.spacing}`)}
</span>
</span>
@@ -143,7 +147,7 @@ export const ThemeDisplay: React.FC<ThemeDisplayProps> = ({
{themeConfig.gallerySettings.photoAnimation && themeConfig.gallerySettings.photoAnimation !== 'none' && (
<span className="inline-flex items-center gap-1">
<span>{t('branding.photoAnimation')}:</span>
<span className="font-medium capitalize">
<span className="font-medium capitalize text-neutral-700 dark:text-neutral-100">
{t(`branding.animation.${themeConfig.gallerySettings.photoAnimation}`)}
</span>
</span>
@@ -56,25 +56,25 @@ export const UpdateNotification: React.FC<UpdateNotificationProps> = ({ onDismis
: t('admin.updates.channelStable', 'Stable');
return (
<div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-4 rounded-r-lg">
<div className="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 mb-4 rounded-r-lg">
<div className="flex items-start justify-between">
<div className="flex items-start">
<ArrowUpCircle className="w-5 h-5 text-blue-500 mt-0.5 mr-3 flex-shrink-0" />
<div>
<h4 className="text-sm font-semibold text-blue-800">
<h4 className="text-sm font-semibold text-blue-800 dark:text-blue-200">
{t('admin.updates.available', 'Update Available')}
</h4>
<p className="text-sm text-blue-700 mt-1">
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
{t('admin.updates.newVersion', 'Version {{version}} is available', {
version: updateInfo.latest.forChannel
})}
<span className="text-blue-500 ml-2">
<span className="text-blue-500 dark:text-blue-400 ml-2">
({t('admin.updates.currentVersion', 'Current: {{version}}', {
version: updateInfo.current
})})
</span>
</p>
<p className="text-xs text-blue-600 mt-1">
<p className="text-xs text-blue-600 dark:text-blue-400 mt-1">
{t('admin.updates.channel', 'Channel: {{channel}}', {
channel: channelLabel
})}
@@ -83,7 +83,7 @@ export const UpdateNotification: React.FC<UpdateNotificationProps> = ({ onDismis
href="https://github.com/the-luap/picpeak/releases"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 mt-2"
className="inline-flex items-center text-xs text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 mt-2"
>
{t('admin.updates.viewReleaseNotes', 'View Release Notes')}
<ExternalLink className="w-3 h-3 ml-1" />
@@ -92,7 +92,7 @@ export const UpdateNotification: React.FC<UpdateNotificationProps> = ({ onDismis
</div>
<button
onClick={handleDismiss}
className="text-blue-400 hover:text-blue-600 p-1"
className="text-blue-400 hover:text-blue-600 dark:hover:text-blue-300 p-1"
aria-label={t('common.close', 'Close')}
>
<X className="w-4 h-4" />
@@ -43,23 +43,23 @@ export const WelcomeMessageEditor: React.FC<WelcomeMessageEditorProps> = ({
onChange={handleChange}
placeholder={placeholder}
rows={rows}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors resize-none font-mono text-sm"
className="w-full px-3 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 dark:placeholder-neutral-500 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors resize-none font-mono text-sm"
/>
<div className="absolute top-2 right-2 text-neutral-400" title="Line breaks will be preserved in emails">
<HelpCircle className="w-4 h-4" aria-hidden="true" />
</div>
</div>
<div className="text-xs text-neutral-500">
<div className="text-xs text-neutral-500 dark:text-neutral-400">
Tip: Press Enter to create a new line. Each line will appear as a separate paragraph in emails.
</div>
{value && (
<div className="mt-4">
<p className="text-sm font-medium text-neutral-700 mb-2">Preview:</p>
<div className="p-4 bg-neutral-50 rounded-lg border border-neutral-200">
<div
className="text-sm text-neutral-700 whitespace-pre-wrap"
<p className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">Preview:</p>
<div className="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg border border-neutral-200 dark:border-neutral-700">
<div
className="text-sm text-neutral-700 dark:text-neutral-300 whitespace-pre-wrap"
dangerouslySetInnerHTML={{ __html: getPreviewHtml() }}
/>
</div>
@@ -151,15 +151,15 @@ export const WordFilterManager: React.FC = () => {
const getSeverityBadgeClass = (severity: string) => {
switch (severity) {
case 'low':
return 'bg-blue-100 text-blue-800';
return 'bg-blue-100 dark:bg-blue-900/40 text-blue-800 dark:text-blue-300';
case 'moderate':
return 'bg-yellow-100 text-yellow-800';
return 'bg-yellow-100 dark:bg-yellow-900/40 text-yellow-800 dark:text-yellow-300';
case 'high':
return 'bg-orange-100 text-orange-800';
return 'bg-orange-100 dark:bg-orange-900/40 text-orange-800 dark:text-orange-300';
case 'block':
return 'bg-red-100 text-red-800';
return 'bg-red-100 dark:bg-red-900/40 text-red-800 dark:text-red-300';
default:
return 'bg-gray-100 text-gray-800';
return 'bg-neutral-100 dark:bg-neutral-700 text-neutral-800 dark:text-neutral-300';
}
};
@@ -182,17 +182,17 @@ export const WordFilterManager: React.FC = () => {
<Card>
<div className="p-6">
<div className="mb-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">
{t('settings.moderation.wordFilters', 'Word Filters')}
</h2>
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('settings.moderation.description', 'Manage words that should be filtered or blocked in comments')}
</p>
</div>
{/* Add new filter */}
<div className="mb-6 p-4 bg-neutral-50 rounded-lg">
<h3 className="text-sm font-medium text-neutral-900 mb-3">
<div className="mb-6 p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
<h3 className="text-sm font-medium text-neutral-900 dark:text-neutral-100 dark:text-neutral-100 mb-3">
{t('settings.moderation.addFilter', 'Add New Filter')}
</h3>
<div className="flex gap-3">
@@ -207,7 +207,7 @@ export const WordFilterManager: React.FC = () => {
<select
value={newSeverity}
onChange={(e) => setNewSeverity(e.target.value as any)}
className="px-3 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="low">{t('settings.moderation.severityLow', 'Low')}</option>
<option value="moderate">{t('settings.moderation.severityModerate', 'Moderate')}</option>
@@ -239,7 +239,7 @@ export const WordFilterManager: React.FC = () => {
{/* Filters list */}
<div className="space-y-2">
{filteredFilters.length === 0 ? (
<div className="text-center py-8 text-neutral-500">
<div className="text-center py-8 text-neutral-500 dark:text-neutral-400">
{searchTerm ?
t('settings.moderation.noMatchingFilters', 'No matching filters found') :
t('settings.moderation.noFilters', 'No word filters configured yet')
@@ -250,7 +250,7 @@ export const WordFilterManager: React.FC = () => {
<div
key={filter.id}
className={`flex items-center justify-between p-3 rounded-lg border ${
filter.is_active ? 'border-neutral-200 bg-white' : 'border-neutral-100 bg-neutral-50 opacity-60'
filter.is_active ? 'border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-800' : 'border-neutral-100 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-900 opacity-60'
}`}
>
{editingId === filter.id ? (
@@ -265,7 +265,7 @@ export const WordFilterManager: React.FC = () => {
<select
value={editSeverity}
onChange={(e) => setEditSeverity(e.target.value as any)}
className="px-3 py-2 border border-neutral-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
className="px-3 py-2 border border-neutral-300 dark:border-neutral-600 rounded-lg bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="low">{t('settings.moderation.severityLow', 'Low')}</option>
<option value="moderate">{t('settings.moderation.severityModerate', 'Moderate')}</option>
@@ -302,7 +302,7 @@ export const WordFilterManager: React.FC = () => {
onChange={() => handleToggleActive(filter)}
className="w-4 h-4 text-primary-600 rounded focus:ring-primary-500"
/>
<span className="font-medium text-neutral-900">{filter.word}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">{filter.word}</span>
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${getSeverityBadgeClass(filter.severity)}`}>
{getSeverityIcon(filter.severity)}
{filter.severity}
@@ -340,15 +340,15 @@ export const WordFilterManager: React.FC = () => {
{/* Severity explanation */}
<Card>
<div className="p-6">
<h3 className="text-sm font-semibold text-neutral-900 mb-3">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3">
{t('settings.moderation.severityLevels', 'Severity Levels')}
</h3>
<div className="space-y-2 text-sm">
<div className="flex items-start gap-3">
{getSeverityIcon('low')}
<div>
<span className="font-medium text-neutral-900">{t('settings.moderation.severityLow', 'Low')}: </span>
<span className="text-neutral-600">
<span className="font-medium text-neutral-900 dark:text-neutral-100">{t('settings.moderation.severityLow', 'Low')}: </span>
<span className="text-neutral-600 dark:text-neutral-400">
{t('settings.moderation.lowDescription', 'Word is flagged for review but not automatically blocked')}
</span>
</div>
@@ -356,8 +356,8 @@ export const WordFilterManager: React.FC = () => {
<div className="flex items-start gap-3">
{getSeverityIcon('moderate')}
<div>
<span className="font-medium text-neutral-900">{t('settings.moderation.severityModerate', 'Moderate')}: </span>
<span className="text-neutral-600">
<span className="font-medium text-neutral-900 dark:text-neutral-100">{t('settings.moderation.severityModerate', 'Moderate')}: </span>
<span className="text-neutral-600 dark:text-neutral-400">
{t('settings.moderation.moderateDescription', 'Comment requires manual approval before being visible')}
</span>
</div>
@@ -365,8 +365,8 @@ export const WordFilterManager: React.FC = () => {
<div className="flex items-start gap-3">
{getSeverityIcon('high')}
<div>
<span className="font-medium text-neutral-900">{t('settings.moderation.severityHigh', 'High')}: </span>
<span className="text-neutral-600">
<span className="font-medium text-neutral-900 dark:text-neutral-100">{t('settings.moderation.severityHigh', 'High')}: </span>
<span className="text-neutral-600 dark:text-neutral-400">
{t('settings.moderation.highDescription', 'Comment is automatically hidden and requires admin review')}
</span>
</div>
@@ -374,8 +374,8 @@ export const WordFilterManager: React.FC = () => {
<div className="flex items-start gap-3">
{getSeverityIcon('block')}
<div>
<span className="font-medium text-neutral-900">{t('settings.moderation.severityBlock', 'Block')}: </span>
<span className="text-neutral-600">
<span className="font-medium text-neutral-900 dark:text-neutral-100">{t('settings.moderation.severityBlock', 'Block')}: </span>
<span className="text-neutral-600 dark:text-neutral-400">
{t('settings.moderation.blockDescription', 'Comment is rejected immediately and cannot be submitted')}
</span>
</div>