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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user