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

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

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

UI/UX Improvements:
- Consistent dark mode styling across all admin pages
- Update gallery components with themed CSS classes
- Fix input, card, and button styling for dark mode
This commit is contained in:
Paul Nothaft
2026-02-06 23:26:01 +01:00
parent 4912e2bccf
commit 9c2a0d272a
79 changed files with 2467 additions and 1475 deletions
+25 -25
View File
@@ -148,8 +148,8 @@ export const AdminDashboard: React.FC = () => {
{/* Page Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h1 className="text-2xl font-bold text-neutral-900">{t('navigation.dashboard')}</h1>
<p className="text-neutral-600 mt-1">{t('admin.dashboardSubtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('navigation.dashboard')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('admin.dashboardSubtitle')}</p>
</div>
<Button
variant="primary"
@@ -166,13 +166,13 @@ export const AdminDashboard: React.FC = () => {
<Card key={stat.title} className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-neutral-600">{stat.title}</p>
<p className="text-2xl font-bold text-neutral-900 mt-1">{stat.value}</p>
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">{stat.title}</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100 mt-1">{stat.value}</p>
{stat.change && (
<p className="text-sm text-neutral-500 mt-1">{stat.change}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1">{stat.change}</p>
)}
</div>
<div className={`p-3 rounded-full bg-neutral-100 ${stat.color}`}>
<div className={`p-3 rounded-full bg-neutral-100 dark:bg-neutral-700 ${stat.color}`}>
<stat.icon className="w-6 h-6" />
</div>
</div>
@@ -186,36 +186,36 @@ export const AdminDashboard: React.FC = () => {
<div className="lg:col-span-2">
<Card padding="md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">{t('admin.eventsExpiringSoon')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('admin.eventsExpiringSoon')}</h2>
<AlertTriangle className="w-5 h-5 text-orange-600" />
</div>
{expiringEvents.length === 0 ? (
<p className="text-neutral-600 py-8 text-center">{t('admin.noEventsExpiring')}</p>
<p className="text-neutral-600 dark:text-neutral-400 py-8 text-center">{t('admin.noEventsExpiring')}</p>
) : (
<div className="space-y-3">
{expiringEvents.slice(0, 5).map((event) => {
const daysLeft = differenceInDays(parseISO(event.expires_at!), new Date());
return (
<div
key={event.id}
className="flex items-center justify-between p-4 bg-orange-50 rounded-lg border border-orange-200 cursor-pointer hover:bg-orange-100 transition-colors"
className="flex items-center justify-between p-4 bg-orange-50 dark:bg-orange-900/30 rounded-lg border border-orange-200 dark:border-orange-800 cursor-pointer hover:bg-orange-100 dark:hover:bg-orange-900/50 transition-colors"
onClick={() => navigate(`/admin/events/${event.id}`)}
>
<div>
<h3 className="font-medium text-neutral-900">{event.event_name}</h3>
<h3 className="font-medium text-neutral-900 dark:text-neutral-100">{event.event_name}</h3>
{event.event_date && (
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{format(parseISO(event.event_date), 'PP')}
</p>
)}
</div>
<div className="text-right">
<p className="text-sm font-medium text-orange-600">
<p className="text-sm font-medium text-orange-600 dark:text-orange-400">
{t('admin.daysLeft', { count: daysLeft })}
</p>
<p className="text-xs text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('gallery.expires')} {format(parseISO(event.expires_at!), 'PP')}
</p>
</div>
@@ -224,11 +224,11 @@ export const AdminDashboard: React.FC = () => {
})}
</div>
)}
{expiringEvents.length > 5 && (
<button
onClick={() => navigate('/admin/events?filter=expiring')}
className="w-full mt-4 text-sm text-primary-600 hover:text-primary-700 font-medium"
className="w-full mt-4 text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium"
>
{t('admin.viewAllExpiringEvents', { count: expiringEvents.length })}
</button>
@@ -239,13 +239,13 @@ export const AdminDashboard: React.FC = () => {
{/* Recent Activity */}
<Card padding="md">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-neutral-900">{t('admin.recentActivity')}</h2>
<Clock className="w-5 h-5 text-neutral-500" />
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('admin.recentActivity')}</h2>
<Clock className="w-5 h-5 text-neutral-500 dark:text-neutral-400" />
</div>
<div className="space-y-4">
{!recentActivity || recentActivity.length === 0 ? (
<p className="text-sm text-neutral-500 text-center py-4">{t('admin.noRecentActivity')}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400 text-center py-4">{t('admin.noRecentActivity')}</p>
) : (
recentActivity.slice(0, 5).map((activity) => {
// Get color based on activity type
@@ -274,7 +274,7 @@ export const AdminDashboard: React.FC = () => {
template: activity.metadata?.template_key || '',
categoryName: activity.metadata?.category_name || ''
};
// Translate; if key missing i18n returns the key string itself
const translated = t(translationKey, params) as string;
if (!translated || translated === translationKey) {
@@ -288,11 +288,11 @@ export const AdminDashboard: React.FC = () => {
<div key={activity.id} className="flex items-start gap-3">
<div className={`w-2 h-2 rounded-full mt-1.5 flex-shrink-0 ${getActivityColor(activity.type)}`} />
<div className="flex-1 min-w-0">
<p className="text-sm text-neutral-900 break-words">
<p className="text-sm text-neutral-900 dark:text-neutral-100 break-words">
{getActivityMessage()}
</p>
<p className="text-xs text-neutral-500">{activity.actorName}</p>
<p className="text-xs text-neutral-400 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400">{activity.actorName}</p>
<p className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">
{formatDistanceToNow(parseISO(activity.createdAt), { addSuffix: true })}
</p>
</div>
+50 -50
View File
@@ -197,7 +197,7 @@ export const AnalyticsPage: React.FC = () => {
const isPositive = trend > 0;
return (
<span className={`inline-flex items-center text-xs font-medium ${
isPositive ? 'text-green-700' : 'text-red-700'
isPositive ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'
}`}>
<TrendingUp className={`w-3 h-3 mr-1 ${!isPositive ? 'rotate-180' : ''}`} />
{Math.abs(trend)}%
@@ -237,8 +237,8 @@ export const AnalyticsPage: React.FC = () => {
<div>
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">{t('analytics.title')}</h1>
<p className="text-neutral-600 mt-1">{t('analytics.detailedSubtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('analytics.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('analytics.detailedSubtitle')}</p>
</div>
<Button
variant="outline"
@@ -265,8 +265,8 @@ export const AnalyticsPage: React.FC = () => {
{/* Page Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">{t('analytics.title')}</h1>
<p className="text-neutral-600 mt-1">{t('analytics.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('analytics.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('analytics.subtitle')}</p>
</div>
<div className="flex items-center gap-3">
{umamiConfig.shareUrl && (
@@ -288,7 +288,7 @@ export const AnalyticsPage: React.FC = () => {
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value as any)}
className="px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500"
className="px-4 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="7d">{t('analytics.last7Days')}</option>
<option value="30d">{t('analytics.last30Days')}</option>
@@ -302,8 +302,8 @@ export const AnalyticsPage: React.FC = () => {
<Card padding="md">
<div className="flex items-start justify-between mb-4">
<div>
<p className="text-sm text-neutral-600">{t('analytics.pageViews')}</p>
<p className="text-3xl font-bold text-neutral-900">{analytics?.pageViews.total.toLocaleString()}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('analytics.pageViews')}</p>
<p className="text-3xl font-bold text-neutral-900 dark:text-neutral-100">{analytics?.pageViews.total.toLocaleString()}</p>
<div className="mt-1">
{renderTrendBadge(analytics?.pageViews.trend || 0)}
</div>
@@ -319,8 +319,8 @@ export const AnalyticsPage: React.FC = () => {
<Card padding="md">
<div className="flex items-start justify-between mb-4">
<div>
<p className="text-sm text-neutral-600">{t('analytics.uniqueVisitors')}</p>
<p className="text-3xl font-bold text-neutral-900">{analytics?.uniqueVisitors.total.toLocaleString()}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('analytics.uniqueVisitors')}</p>
<p className="text-3xl font-bold text-neutral-900 dark:text-neutral-100">{analytics?.uniqueVisitors.total.toLocaleString()}</p>
<div className="mt-1">
{renderTrendBadge(analytics?.uniqueVisitors.trend || 0)}
</div>
@@ -336,8 +336,8 @@ export const AnalyticsPage: React.FC = () => {
<Card padding="md">
<div className="flex items-start justify-between mb-4">
<div>
<p className="text-sm text-neutral-600">{t('analytics.totalDownloads')}</p>
<p className="text-3xl font-bold text-neutral-900">{analytics?.downloads.total.toLocaleString()}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('analytics.totalDownloads')}</p>
<p className="text-3xl font-bold text-neutral-900 dark:text-neutral-100">{analytics?.downloads.total.toLocaleString()}</p>
<div className="mt-1">
{renderTrendBadge(analytics?.downloads.trend || 0)}
</div>
@@ -345,8 +345,8 @@ export const AnalyticsPage: React.FC = () => {
<Download className="w-8 h-8 text-purple-600" />
</div>
<div className="mt-4 space-y-2">
<p className="text-xs text-neutral-500 uppercase">{t('analytics.topGallery')}</p>
<p className="text-sm font-medium text-neutral-900 truncate">
<p className="text-xs text-neutral-500 dark:text-neutral-400 uppercase">{t('analytics.topGallery')}</p>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100 truncate">
{analytics?.downloads.topGalleries[0]?.name}
</p>
</div>
@@ -357,19 +357,19 @@ export const AnalyticsPage: React.FC = () => {
{/* Top Pages */}
<div className="lg:col-span-2">
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.topPages')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('analytics.topPages')}</h2>
<div className="space-y-3">
{analytics?.topPages.map((page, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">{page.path}</p>
<p className="text-xs text-neutral-500">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{page.path}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{page.uniqueVisitors} {t('analytics.visitors')}
</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-neutral-900">{page.views}</p>
<p className="text-xs text-neutral-500">{t('analytics.views')}</p>
<p className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">{page.views}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{t('analytics.views')}</p>
</div>
</div>
))}
@@ -378,23 +378,23 @@ export const AnalyticsPage: React.FC = () => {
{/* Top Downloads */}
<Card padding="md" className="mt-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.topDownloadsByGallery')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('analytics.topDownloadsByGallery')}</h2>
<div className="space-y-3">
{analytics?.downloads.topGalleries.map((gallery, index) => (
<div key={index} className="flex items-center justify-between">
<div className="flex-1">
<p className="text-sm font-medium text-neutral-900">{gallery.name}</p>
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{gallery.name}</p>
</div>
<div className="flex items-center gap-4">
<div className="flex-1 bg-neutral-200 rounded-full h-2 max-w-[100px]">
<div className="flex-1 bg-neutral-200 dark:bg-neutral-700 rounded-full h-2 max-w-[100px]">
<div
className="bg-purple-600 h-2 rounded-full"
style={{
width: `${(gallery.downloads / (analytics.downloads.topGalleries[0]?.downloads || 1)) * 100}%`
style={{
width: `${(gallery.downloads / (analytics.downloads.topGalleries[0]?.downloads || 1)) * 100}%`
}}
/>
</div>
<p className="text-sm font-semibold text-neutral-900 w-12 text-right">
<p className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 w-12 text-right">
{gallery.downloads}
</p>
</div>
@@ -408,28 +408,28 @@ export const AnalyticsPage: React.FC = () => {
<div className="space-y-6">
{/* Device Breakdown */}
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.deviceBreakdown')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('analytics.deviceBreakdown')}</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Monitor className="w-5 h-5 text-neutral-600" />
<span className="text-sm text-neutral-700">{t('analytics.desktop')}</span>
<Monitor className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('analytics.desktop')}</span>
</div>
<span className="text-sm font-semibold">{analytics?.devices.desktop}%</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">{analytics?.devices.desktop}%</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Smartphone className="w-5 h-5 text-neutral-600" />
<span className="text-sm text-neutral-700">{t('analytics.mobile')}</span>
<Smartphone className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('analytics.mobile')}</span>
</div>
<span className="text-sm font-semibold">{analytics?.devices.mobile}%</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">{analytics?.devices.mobile}%</span>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Tablet className="w-5 h-5 text-neutral-600" />
<span className="text-sm text-neutral-700">{t('analytics.tablet')}</span>
<Tablet className="w-5 h-5 text-neutral-600 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('analytics.tablet')}</span>
</div>
<span className="text-sm font-semibold">{analytics?.devices.tablet}%</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-neutral-100">{analytics?.devices.tablet}%</span>
</div>
</div>
</Card>
@@ -463,34 +463,34 @@ export const AnalyticsPage: React.FC = () => {
return (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('analytics.storageUsage')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('analytics.storageUsage')}</h2>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-neutral-600">{t('analytics.used')}</span>
<span className="font-medium">{adminService.formatBytes(dashboardStats.storageUsed)}</span>
<span className="text-neutral-600 dark:text-neutral-400">{t('analytics.used')}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">{adminService.formatBytes(dashboardStats.storageUsed)}</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={`${progressColor} h-2 rounded-full transition-all`}
style={{ width: `${usageWidth}%` }}
/>
</div>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{usagePercent}% {t('analytics.of')} {limitDisplay}
</p>
<p className={`text-xs mt-1 ${overSoftLimit ? 'text-red-600 font-semibold' : 'text-red-500 font-medium'}`}>
<p className={`text-xs mt-1 ${overSoftLimit ? 'text-red-600 dark:text-red-400 font-semibold' : 'text-red-500 dark:text-red-400 font-medium'}`}>
{limitDescriptor}
</p>
</div>
<div className="pt-2 border-t">
<div className="pt-2 border-t border-neutral-200 dark:border-neutral-700">
<div className="flex justify-between text-sm">
<span className="text-neutral-600">{t('analytics.totalPhotos')}</span>
<span className="font-medium">{dashboardStats.totalPhotos.toLocaleString()}</span>
<span className="text-neutral-600 dark:text-neutral-400">{t('analytics.totalPhotos')}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">{dashboardStats.totalPhotos.toLocaleString()}</span>
</div>
<div className="flex justify-between text-sm mt-2">
<span className="text-neutral-600">{t('analytics.activeEvents')}</span>
<span className="font-medium">{dashboardStats.activeEvents}</span>
<span className="text-neutral-600 dark:text-neutral-400">{t('analytics.activeEvents')}</span>
<span className="font-medium text-neutral-900 dark:text-neutral-100">{dashboardStats.activeEvents}</span>
</div>
</div>
</div>
@@ -502,12 +502,12 @@ export const AnalyticsPage: React.FC = () => {
{/* Configuration Notice */}
{umamiConfig.enabled === false && (
<Card padding="md" className="mt-6 bg-amber-50 border-amber-200">
<Card padding="md" className="mt-6 bg-amber-50 dark:bg-amber-900/30 border-amber-200 dark:border-amber-800">
<div className="flex items-start gap-3">
<Activity className="w-5 h-5 text-amber-600 flex-shrink-0" />
<Activity className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-900">{t('analytics.notConfigured')}</p>
<p className="text-sm text-amber-700 mt-1">
<p className="text-sm font-medium text-amber-900 dark:text-amber-200">{t('analytics.notConfigured')}</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
{t('analytics.configureInstructions')}
</p>
</div>
+38 -38
View File
@@ -138,8 +138,8 @@ export const ArchivesPage: React.FC = () => {
<div>
{/* Page Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">{t('archives.title')}</h1>
<p className="text-neutral-600 mt-1">{t('archives.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('archives.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('archives.subtitle')}</p>
</div>
{/* Statistics Cards */}
@@ -147,28 +147,28 @@ export const ArchivesPage: React.FC = () => {
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('archives.totalArchives')}</p>
<p className="text-2xl font-bold text-neutral-900">{archives.length}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('archives.totalArchives')}</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{archives.length}</p>
</div>
<Archive className="w-8 h-8 text-primary-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('archives.storageUsed')}</p>
<p className="text-2xl font-bold text-neutral-900">{archiveService.formatBytes(getTotalSize())}</p>
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('archives.storageUsed')}</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{archiveService.formatBytes(getTotalSize())}</p>
</div>
<HardDrive className="w-8 h-8 text-blue-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('archives.totalPhotos')}</p>
<p className="text-2xl font-bold text-neutral-900">
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('archives.totalPhotos')}</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{(() => {
const total = archives.reduce((sum, a) => sum + (parseInt(String(a.photoCount)) || 0), 0);
return total === 0 ? '0' : total.toLocaleString();
@@ -178,13 +178,13 @@ export const ArchivesPage: React.FC = () => {
<FileArchive className="w-8 h-8 text-green-600" />
</div>
</Card>
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">{t('archives.avgArchiveSize')}</p>
<p className="text-2xl font-bold text-neutral-900">
{archives.length > 0
<p className="text-sm text-neutral-600 dark:text-neutral-400">{t('archives.avgArchiveSize')}</p>
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{archives.length > 0
? archiveService.formatBytes(getTotalSize() / archives.length)
: '0 Bytes'
}
@@ -212,7 +212,7 @@ export const ArchivesPage: React.FC = () => {
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="px-4 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="all">{t('archives.allTypes')}</option>
<option value="wedding">{t('archives.wedding')}</option>
@@ -221,11 +221,11 @@ export const ArchivesPage: React.FC = () => {
<option value="party">Party</option>
<option value="other">{t('archives.other')}</option>
</select>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-4 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
className="px-4 py-2 border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="date">{t('archives.sortByDate')}</option>
<option value="name">{t('archives.sortByName')}</option>
@@ -239,61 +239,61 @@ export const ArchivesPage: React.FC = () => {
<Card className="overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-neutral-50 border-b border-neutral-200">
<thead className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('archives.tableHeaders.event')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('archives.tableHeaders.type')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('archives.tableHeaders.archivedDate')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('archives.tableHeaders.size')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('archives.tableHeaders.photos')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-neutral-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('archives.tableHeaders.actions')}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-neutral-200">
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{filteredArchives.length === 0 ? (
<tr>
<td colSpan={6} className="px-6 py-12 text-center text-neutral-500">
<td colSpan={6} className="px-6 py-12 text-center text-neutral-500 dark:text-neutral-400">
{t('archives.noArchivesFound')}
</td>
</tr>
) : (
filteredArchives.map((archive) => (
<tr key={archive.id} className="hover:bg-neutral-50">
<tr key={archive.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700/50">
<td className="px-6 py-4">
<div>
<p className="text-sm font-medium text-neutral-900">{archive.eventName}</p>
<p className="text-xs text-neutral-500">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{archive.eventName}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{t('archives.eventDateNA').replace('N/A', formatDate(archive.eventDate, 'MMM d, yyyy') || 'N/A')}
</p>
</div>
</td>
<td className="px-6 py-4 text-sm text-neutral-700 capitalize">
<td className="px-6 py-4 text-sm text-neutral-700 dark:text-neutral-300 capitalize">
{archive.eventType}
</td>
<td className="px-6 py-4 text-sm text-neutral-700">
<td className="px-6 py-4 text-sm text-neutral-700 dark:text-neutral-300">
<div>
<p>{formatDate(archive.archivedAt, 'MMM d, yyyy') || t('archives.processing')}</p>
<p className="text-xs text-neutral-500">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
{formatDate(archive.archivedAt, 'h:mm a')}
</p>
</div>
</td>
<td className="px-6 py-4 text-sm text-neutral-700">
<td className="px-6 py-4 text-sm text-neutral-700 dark:text-neutral-300">
{archiveService.formatBytes(archive.archiveSize)}
</td>
<td className="px-6 py-4 text-sm text-neutral-700">
<td className="px-6 py-4 text-sm text-neutral-700 dark:text-neutral-300">
{archive.photoCount}
</td>
<td className="px-6 py-4 text-right">
@@ -383,12 +383,12 @@ export const ArchivesPage: React.FC = () => {
)}
{/* Storage Warning */}
<div className="mt-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div className="mt-6 p-4 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0" />
<AlertCircle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-900">{t('archives.storageManagement')}</p>
<p className="text-sm text-amber-700 mt-1">
<p className="text-sm font-medium text-amber-900 dark:text-amber-200">{t('archives.storageManagement')}</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-1">
{t('archives.storageInfo')}
</p>
</div>
+13 -13
View File
@@ -112,8 +112,8 @@ export const BackupManagement = () => {
<div className="p-8 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">{t('backup.title')}</h1>
<p className="text-gray-600">
<h1 className="text-3xl font-bold text-neutral-900 dark:text-neutral-100 mb-2">{t('backup.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400">
{t('backup.subtitle')}
</p>
</div>
@@ -126,27 +126,27 @@ export const BackupManagement = () => {
{backupStatus?.isRunning ? (
<>
<Loader2 className="h-5 w-5 text-blue-500 animate-spin" />
<span className="text-blue-600 font-medium">{t('backup.status.inProgress')}</span>
<span className="text-blue-600 dark:text-blue-400 font-medium">{t('backup.status.inProgress')}</span>
</>
) : backupStatus?.lastBackup ? (
<>
<CheckCircle className="h-5 w-5 text-green-500" />
<span className="text-gray-700">
<span className="text-neutral-700 dark:text-neutral-300">
{t('backup.status.lastBackup')}: {format(new Date(backupStatus.lastBackup.created_at), 'PPp')}
</span>
</>
) : (
<>
<AlertCircle className="h-5 w-5 text-amber-500" />
<span className="text-gray-700">{t('backup.status.noBackups')}</span>
<span className="text-neutral-700 dark:text-neutral-300">{t('backup.status.noBackups')}</span>
</>
)}
</div>
{backupConfig?.backup_enabled && (
<div className="flex items-center space-x-2">
<Clock className="h-5 w-5 text-gray-400" />
<span className="text-sm text-gray-600">
<Clock className="h-5 w-5 text-neutral-400" />
<span className="text-sm text-neutral-600 dark:text-neutral-400">
{t('backup.status.nextBackup')}: {backupStatus?.nextBackup || t('backup.status.notScheduled')}
</span>
</div>
@@ -174,9 +174,9 @@ export const BackupManagement = () => {
</Button>
<div className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm font-medium ${
backupConfig?.backup_enabled
? 'bg-green-100 text-green-700'
: 'bg-gray-100 text-gray-700'
backupConfig?.backup_enabled
? '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'
}`}>
<Shield className="h-4 w-4" />
<span>{backupConfig?.backup_enabled ? t('backup.status.enabled') : t('backup.status.disabled')}</span>
@@ -186,7 +186,7 @@ export const BackupManagement = () => {
</Card>
{/* Tabs */}
<div className="border-b border-gray-200 mb-6">
<div className="border-b border-neutral-200 dark:border-neutral-700 mb-6">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => {
const Icon = tab.icon;
@@ -197,8 +197,8 @@ export const BackupManagement = () => {
className={`
py-2 px-1 border-b-2 font-medium text-sm flex items-center space-x-2
${activeTab === tab.id
? 'border-primary text-primary'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
? 'border-primary-600 dark:border-primary-400 text-primary-600 dark:text-primary-400'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 hover:border-neutral-300 dark:hover:border-neutral-600'
}
`}
>
+60 -60
View File
@@ -251,8 +251,8 @@ export const BrandingPage: React.FC = () => {
{/* Page Header */}
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">{t('branding.title')}</h1>
<p className="text-neutral-600 mt-1">{t('branding.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('branding.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('branding.subtitle')}</p>
</div>
<div className="flex items-center gap-3">
<Button
@@ -274,7 +274,7 @@ export const BrandingPage: React.FC = () => {
{/* Company Branding */}
<Card padding="md" className="mb-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('branding.companyInfo')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('branding.companyInfo')}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Input
label={t('branding.companyName')}
@@ -299,22 +299,22 @@ export const BrandingPage: React.FC = () => {
helperText={t('branding.supportEmailHelp')}
/>
<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('branding.footerText')}
</label>
<textarea
value={brandingSettings.footer_text}
onChange={(e) => handleBrandingChange('footer_text', e.target.value)}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500"
rows={2}
placeholder={`© ${new Date().getFullYear()} Your Company. All rights reserved.`}
/>
</div>
</div>
<div className="mt-6 pt-6 border-t border-neutral-200">
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
<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('branding.favicon')}
</label>
<div className="space-y-2">
@@ -325,7 +325,7 @@ export const BrandingPage: React.FC = () => {
alt="Current favicon"
className="w-8 h-8"
/>
<span className="text-sm text-neutral-600">{t('branding.currentFavicon')}</span>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('branding.currentFavicon')}</span>
<Button
variant="ghost"
size="sm"
@@ -351,20 +351,20 @@ export const BrandingPage: React.FC = () => {
>
{t('branding.uploadFavicon')}
</Button>
<p className="text-xs text-neutral-600 mt-1">{t('branding.faviconHelp')}</p>
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">{t('branding.faviconHelp')}</p>
</div>
</div>
</div>
</div>
{/* Logo Customization Settings */}
<div className="mt-6 pt-6 border-t border-neutral-200">
<h3 className="text-md font-semibold text-neutral-900 mb-4">{t('branding.logoCustomization', 'Logo Customization')}</h3>
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('branding.logoCustomization', 'Logo Customization')}</h3>
<div className="space-y-4">
{/* Logo Upload */}
<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('branding.logo', 'Logo')}
</label>
<div className="flex items-center gap-4">
@@ -373,7 +373,7 @@ export const BrandingPage: React.FC = () => {
<img
src={brandingSettings.logo_url.startsWith('http') ? brandingSettings.logo_url : buildResourceUrl(brandingSettings.logo_url)}
alt="Logo"
className="h-16 object-contain bg-neutral-100 rounded p-2"
className="h-16 object-contain bg-neutral-100 dark:bg-neutral-700 rounded p-2"
/>
<button
type="button"
@@ -402,19 +402,19 @@ export const BrandingPage: React.FC = () => {
</Button>
</div>
</div>
<p className="text-xs text-neutral-600 mt-1">
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">
{t('branding.logoHelp', 'PNG, JPG or SVG format, recommended width: 200px')}
</p>
</div>
{/* Logo Size */}
<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('branding.logoSize', 'Logo Size')}
</label>
<select
value={brandingSettings.logo_size || 'medium'}
onChange={(e) => handleBrandingChange('logo_size', e.target.value)}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="small">{t('branding.logoSizeSmall', 'Small (32px)')}</option>
<option value="medium">{t('branding.logoSizeMedium', 'Medium (48px)')}</option>
@@ -427,7 +427,7 @@ export const BrandingPage: React.FC = () => {
{/* Custom Height (only shown when size is custom) */}
{brandingSettings.logo_size === 'custom' && (
<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('branding.logoMaxHeight', 'Maximum Height (pixels)')}
</label>
<input
@@ -436,9 +436,9 @@ export const BrandingPage: React.FC = () => {
max="200"
value={brandingSettings.logo_max_height || 48}
onChange={(e) => handleBrandingChange('logo_max_height', parseInt(e.target.value))}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
<p className="text-xs text-neutral-600 mt-1">
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">
{t('branding.logoMaxHeightHelp', 'Set a custom maximum height for the logo (20-200 pixels)')}
</p>
</div>
@@ -446,7 +446,7 @@ export const BrandingPage: React.FC = () => {
{/* Logo Position */}
<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('branding.logoPosition', 'Logo Position in Header')}
</label>
<div className="flex gap-2">
@@ -458,7 +458,7 @@ export const BrandingPage: React.FC = () => {
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
brandingSettings.logo_position === position
? 'bg-primary-600 text-white'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600'
}`}
>
{t(`branding.position${position.charAt(0).toUpperCase() + position.slice(1)}`, position.charAt(0).toUpperCase() + position.slice(1))}
@@ -469,13 +469,13 @@ export const BrandingPage: React.FC = () => {
{/* Display Mode */}
<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('branding.logoDisplayMode', 'Display Mode')}
</label>
<select
value={brandingSettings.logo_display_mode || 'logo_and_text'}
onChange={(e) => handleBrandingChange('logo_display_mode', e.target.value)}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="logo_only">{t('branding.logoOnly', 'Logo Only')}</option>
<option value="text_only">{t('branding.textOnly', 'Company Name Only')}</option>
@@ -490,13 +490,13 @@ export const BrandingPage: React.FC = () => {
type="checkbox"
checked={brandingSettings.logo_display_header !== false}
onChange={(e) => handleBrandingChange('logo_display_header', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('branding.showLogoInHeader', 'Show logo in gallery header')}
</span>
<p className="text-xs text-neutral-600">
<p className="text-xs text-neutral-600 dark:text-neutral-400">
{t('branding.showLogoInHeaderHelp', 'Display the logo in the main header bar')}
</p>
</div>
@@ -507,13 +507,13 @@ export const BrandingPage: React.FC = () => {
type="checkbox"
checked={brandingSettings.logo_display_hero !== false}
onChange={(e) => handleBrandingChange('logo_display_hero', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('branding.showLogoInHero', 'Show logo in hero section')}
</span>
<p className="text-xs text-neutral-600">
<p className="text-xs text-neutral-600 dark:text-neutral-400">
{t('branding.showLogoInHeroHelp', 'Display the logo in hero sections (for non-grid layouts)')}
</p>
</div>
@@ -523,49 +523,49 @@ export const BrandingPage: React.FC = () => {
</div>
{/* White Label Settings */}
<div className="mt-6 pt-6 border-t border-neutral-200">
<h3 className="text-md font-semibold text-neutral-900 mb-4">{t('branding.whiteLabel', 'White Label')}</h3>
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('branding.whiteLabel', 'White Label')}</h3>
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={brandingSettings.hide_powered_by === true}
onChange={(e) => handleBrandingChange('hide_powered_by', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{t('branding.hidePoweredBy', 'Hide "Powered by PicPeak" branding')}
</span>
<p className="text-xs text-neutral-600">
<p className="text-xs text-neutral-600 dark:text-neutral-400">
{t('branding.hidePoweredByHelp', 'Remove the PicPeak attribution from gallery footers for a fully white-labeled experience')}
</p>
</div>
</label>
</div>
<div className="mt-6 pt-6 border-t border-neutral-200">
<div className="mt-6 pt-6 border-t border-neutral-200 dark:border-neutral-700">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={brandingSettings.watermark_enabled}
onChange={(e) => handleBrandingChange('watermark_enabled', e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-900">{t('branding.enableWatermarks')}</span>
<p className="text-xs text-neutral-600">{t('branding.watermarkHelp')}</p>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{t('branding.enableWatermarks')}</span>
<p className="text-xs text-neutral-600 dark:text-neutral-400">{t('branding.watermarkHelp')}</p>
</div>
</label>
</div>
{/* Watermark Settings */}
{brandingSettings.watermark_enabled && (
<div className="mt-6 space-y-6 border-t border-neutral-200 pt-6">
<h3 className="text-md font-semibold text-neutral-900">{t('branding.watermarkSettings')}</h3>
<div className="mt-6 space-y-6 border-t border-neutral-200 dark:border-neutral-700 pt-6">
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100">{t('branding.watermarkSettings')}</h3>
{/* Watermark Logo Upload */}
<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('branding.watermarkLogo')}
</label>
<div className="space-y-2">
@@ -574,9 +574,9 @@ export const BrandingPage: React.FC = () => {
<img
src={brandingSettings.watermark_logo_url.startsWith('http') ? brandingSettings.watermark_logo_url : buildResourceUrl(brandingSettings.watermark_logo_url)}
alt="Current watermark"
className="h-16 w-auto object-contain bg-neutral-100 p-2 rounded"
className="h-16 w-auto object-contain bg-neutral-100 dark:bg-neutral-700 p-2 rounded"
/>
<span className="text-sm text-neutral-600">{t('branding.currentWatermark')}</span>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('branding.currentWatermark')}</span>
<Button
variant="ghost"
size="sm"
@@ -604,14 +604,14 @@ export const BrandingPage: React.FC = () => {
{t('branding.uploadWatermarkLogo')}
</Button>
</label>
<p className="text-xs text-neutral-600 mt-1">{t('branding.watermarkHelp')}</p>
<p className="text-xs text-neutral-600 dark:text-neutral-400 mt-1">{t('branding.watermarkHelp')}</p>
</div>
</div>
</div>
{/* Position Selector */}
<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('branding.watermarkPosition')}
</label>
<div className="grid grid-cols-3 gap-2 max-w-xs">
@@ -629,7 +629,7 @@ export const BrandingPage: React.FC = () => {
className={`px-3 py-2 text-sm rounded-lg border transition-colors ${
brandingSettings.watermark_position === position.value
? 'bg-primary-600 text-white border-primary-600'
: 'bg-white text-neutral-700 border-neutral-300 hover:bg-neutral-50'
: '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'
}`}
>
{position.label}
@@ -640,7 +640,7 @@ export const BrandingPage: React.FC = () => {
{/* Opacity Slider */}
<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('branding.watermarkOpacity')}: {brandingSettings.watermark_opacity || 50}%
</label>
<input
@@ -660,7 +660,7 @@ export const BrandingPage: React.FC = () => {
outline: 'none'
}}
/>
<div className="flex justify-between text-xs text-neutral-500 mt-1">
<div className="flex justify-between text-xs text-neutral-500 dark:text-neutral-400 mt-1">
<span>10%</span>
<span>50%</span>
<span>100%</span>
@@ -669,7 +669,7 @@ export const BrandingPage: React.FC = () => {
{/* Size Slider */}
<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('branding.watermarkSize')}: {brandingSettings.watermark_size || 15}%
</label>
<input
@@ -689,7 +689,7 @@ export const BrandingPage: React.FC = () => {
outline: 'none'
}}
/>
<div className="flex justify-between text-xs text-neutral-500 mt-1">
<div className="flex justify-between text-xs text-neutral-500 dark:text-neutral-400 mt-1">
<span>5%</span>
<span>15%</span>
<span>30%</span>
@@ -701,7 +701,7 @@ export const BrandingPage: React.FC = () => {
{/* Theme Customization */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-4 flex items-center gap-2">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
<Palette className="w-5 h-5" />
{t('branding.galleryTheme')}
</h2>
@@ -711,9 +711,9 @@ export const BrandingPage: React.FC = () => {
type="checkbox"
checked={isPreviewMode}
onChange={(e) => setIsPreviewMode(e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm text-neutral-700">{t('branding.applyLivePreview')}</span>
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('branding.applyLivePreview')}</span>
</label>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
@@ -733,7 +733,7 @@ export const BrandingPage: React.FC = () => {
{/* Right side - Gallery Preview */}
<div className="lg:sticky lg:top-4 lg:h-fit">
<Card className="p-4">
<h3 className="text-sm font-medium text-neutral-700 mb-3">
<h3 className="text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-3">
{t('branding.livePreview')}
</h3>
<GalleryPreview
@@ -747,12 +747,12 @@ export const BrandingPage: React.FC = () => {
</div>
{/* Event-Specific Themes Info */}
<Card padding="md" className="bg-blue-50 border-blue-200">
<Card padding="md" className="bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800">
<div className="flex items-start gap-3">
<Palette className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" />
<Palette className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-sm font-medium text-blue-900">{t('branding.eventSpecificThemes')}</h3>
<p className="text-sm text-blue-700 mt-1">
<h3 className="text-sm font-medium text-blue-900 dark:text-blue-200">{t('branding.eventSpecificThemes')}</h3>
<p className="text-sm text-blue-700 dark:text-blue-300 mt-1">
{t('branding.eventThemesInfo')}
</p>
</div>
+29 -29
View File
@@ -325,8 +325,8 @@ export const CMSPage: React.FC = () => {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">{t('cms.title')}</h1>
<p className="text-neutral-600 mt-1">{t('cms.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('cms.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('cms.subtitle')}</p>
</div>
<div className="mb-8">
@@ -337,8 +337,8 @@ export const CMSPage: React.FC = () => {
<Globe className="w-5 h-5" />
<span className="text-sm font-semibold uppercase tracking-wide">{t('settings.publicSite.badge')}</span>
</div>
<h2 className="text-2xl font-semibold text-neutral-900">{t('settings.publicSite.title')}</h2>
<p className="text-neutral-600 mt-1 max-w-2xl">{t('settings.publicSite.subtitle')}</p>
<h2 className="text-2xl font-semibold text-neutral-900 dark:text-neutral-100">{t('settings.publicSite.title')}</h2>
<p className="text-neutral-600 dark:text-neutral-400 mt-1 max-w-2xl">{t('settings.publicSite.subtitle')}</p>
</div>
<label className="flex items-center gap-2 cursor-pointer select-none">
<input
@@ -359,7 +359,7 @@ export const CMSPage: React.FC = () => {
}`}
/>
</span>
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{publicSiteEnabled ? t('settings.publicSite.enabled') : t('settings.publicSite.disabled')}
</span>
</label>
@@ -373,35 +373,35 @@ export const CMSPage: React.FC = () => {
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<label className="flex items-center gap-2 text-sm font-medium text-neutral-800 mb-2">
<label className="flex items-center gap-2 text-sm font-medium text-neutral-800 dark:text-neutral-200 mb-2">
<Sparkles className="w-4 h-4 text-primary-500" />
{t('settings.publicSite.htmlLabel')}
</label>
<textarea
className="w-full h-64 font-mono text-sm rounded-lg border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:bg-neutral-100 disabled:text-neutral-500"
className="w-full h-64 font-mono text-sm rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:bg-neutral-100 dark:disabled:bg-neutral-700 disabled:text-neutral-500 dark:disabled:text-neutral-400"
value={publicSiteHtml}
onChange={(event) => setPublicSiteHtml(event.target.value)}
disabled={!publicSiteEnabled}
placeholder={t('settings.publicSite.htmlPlaceholder') || ''}
/>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('settings.publicSite.htmlHelp')}
</p>
</div>
<div>
<label className="flex items-center gap-2 text-sm font-medium text-neutral-800 mb-2">
<label className="flex items-center gap-2 text-sm font-medium text-neutral-800 dark:text-neutral-200 mb-2">
<ShieldCheck className="w-4 h-4 text-primary-500" />
{t('settings.publicSite.cssLabel')}
</label>
<textarea
className="w-full h-48 font-mono text-sm rounded-lg border border-neutral-300 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:bg-neutral-100 disabled:text-neutral-500"
className="w-full h-48 font-mono text-sm rounded-lg border border-neutral-300 dark:border-neutral-600 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 disabled:bg-neutral-100 dark:disabled:bg-neutral-700 disabled:text-neutral-500 dark:disabled:text-neutral-400"
value={publicSiteCss}
onChange={(event) => setPublicSiteCss(event.target.value)}
disabled={!publicSiteEnabled}
placeholder={t('settings.publicSite.cssPlaceholder') || ''}
/>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('settings.publicSite.cssHelp')}
</p>
</div>
@@ -425,7 +425,7 @@ export const CMSPage: React.FC = () => {
</Button>
</div>
<div className="rounded-lg bg-neutral-50 border border-neutral-200 p-3 text-xs text-neutral-600 leading-relaxed">
<div className="rounded-lg bg-neutral-50 dark:bg-neutral-800/50 border border-neutral-200 dark:border-neutral-700 p-3 text-xs text-neutral-600 dark:text-neutral-400 leading-relaxed">
<p className="font-semibold mb-1">{t('settings.publicSite.sanitizationNotice')}</p>
<p>{t('settings.publicSite.htmlHelp')}</p>
</div>
@@ -433,10 +433,10 @@ export const CMSPage: React.FC = () => {
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-neutral-800 uppercase tracking-wide">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-200 uppercase tracking-wide">
{t('settings.publicSite.previewTitle')}
</h3>
<span className="text-xs text-neutral-500">{t('settings.publicSite.previewSandboxed')}</span>
<span className="text-xs text-neutral-500 dark:text-neutral-400">{t('settings.publicSite.previewSandboxed')}</span>
</div>
{publicSiteEnabled ? (
<div className="rounded-xl border border-neutral-200 overflow-hidden shadow-sm bg-white">
@@ -448,7 +448,7 @@ export const CMSPage: React.FC = () => {
/>
</div>
) : (
<div className="rounded-xl border border-dashed border-neutral-300 bg-neutral-50 p-8 text-center text-sm text-neutral-500">
<div className="rounded-xl border border-dashed border-neutral-300 dark:border-neutral-600 bg-neutral-50 dark:bg-neutral-800/50 p-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
{t('settings.publicSite.previewDisabled')}
</div>
)}
@@ -462,7 +462,7 @@ export const CMSPage: React.FC = () => {
{/* Page Selection */}
<div className="lg:col-span-1">
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('cms.pages')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('cms.pages')}</h2>
<div className="space-y-2">
{pages?.map((page) => (
<button
@@ -477,14 +477,14 @@ export const CMSPage: React.FC = () => {
}}
className={`w-full text-left px-4 py-3 rounded-lg transition-colors flex items-center gap-3 ${
selectedPage === page.slug
? 'bg-primary-100 text-primary-700 border border-primary-300'
: 'bg-white border border-neutral-200 hover:bg-neutral-50'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300 border border-primary-300 dark:border-primary-700'
: 'bg-white dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 hover:bg-neutral-50 dark:hover:bg-neutral-700 text-neutral-900 dark:text-neutral-100'
}`}
>
<FileText className="w-5 h-5 flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-medium truncate">{t(`legal.${page.slug}`)}</p>
<p className="text-sm text-neutral-500">/{page.slug}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">/{page.slug}</p>
</div>
{selectedPage === page.slug && hasUnsavedChanges && (
<div className="w-2 h-2 bg-yellow-500 rounded-full flex-shrink-0" />
@@ -495,7 +495,7 @@ export const CMSPage: React.FC = () => {
</Card>
<Card padding="md" className="mt-4">
<h3 className="text-sm font-semibold text-neutral-900 mb-3">{t('cms.previewLinks')}</h3>
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3">{t('cms.previewLinks')}</h3>
<div className="space-y-2 text-sm">
<a
href={`${window.location.origin}/${selectedPage}?lang=en`}
@@ -549,18 +549,18 @@ export const CMSPage: React.FC = () => {
<div className="lg:col-span-3">
<Card padding="md">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-neutral-900">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
{t('cms.editPage', { page: t(`legal.${selectedPage}`) })}
</h2>
{/* Language Tabs */}
<div className="flex gap-2">
<button
onClick={() => setEditingLang('en')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'en'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600'
}`}
>
English
@@ -569,8 +569,8 @@ export const CMSPage: React.FC = () => {
onClick={() => setEditingLang('de')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'de'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600'
}`}
>
Deutsch
@@ -581,7 +581,7 @@ export const CMSPage: React.FC = () => {
<div className="space-y-4">
{/* Title */}
<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('cms.pageTitle')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<Input
@@ -593,7 +593,7 @@ export const CMSPage: React.FC = () => {
{/* Content */}
<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('cms.pageContent')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<CMSEditor
@@ -606,7 +606,7 @@ export const CMSPage: React.FC = () => {
</div>
{currentPage?.updated_at && (
<p className="text-xs text-neutral-500 mt-4">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-4">
{t('cms.lastUpdated')} {new Date(currentPage.updated_at).toLocaleString()}
</p>
)}
+43 -48
View File
@@ -365,7 +365,7 @@ export const CreateEventPage: React.FC = () => {
>
{t('common.back')}
</Button>
<h1 className="text-2xl font-bold text-neutral-900">{t('events.create')}</h1>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('events.create')}</h1>
</div>
</div>
@@ -373,13 +373,13 @@ export const CreateEventPage: React.FC = () => {
{/* Event Details */}
<Card>
<div className="p-6 space-y-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">
<Calendar className="w-5 h-5" />
{t('events.eventDetails')}
</h2>
<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('events.eventType')}
</label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
@@ -390,12 +390,12 @@ export const CreateEventPage: React.FC = () => {
onClick={() => setFormData({ ...formData, event_type: type.value })}
className={`p-4 rounded-lg border-2 transition-all ${
formData.event_type === type.value
? 'border-primary-600 bg-primary-50'
: 'border-neutral-200 hover:border-neutral-300'
? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
<div className="text-2xl mb-1">{type.emoji}</div>
<div className="text-sm font-medium">{type.name}</div>
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{type.name}</div>
</button>
))}
</div>
@@ -422,7 +422,7 @@ export const CreateEventPage: React.FC = () => {
</div>
<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('events.welcomeMessage')}
</label>
<WelcomeMessageEditor
@@ -439,7 +439,7 @@ export const CreateEventPage: React.FC = () => {
<Card>
<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">
<Palette className="w-5 h-5" />
{t('events.themeAndStyle')}
</h2>
@@ -456,14 +456,9 @@ export const CreateEventPage: React.FC = () => {
{/* Quick Theme Preview */}
{!showThemeCustomizer && (
<div className="p-4 rounded-lg border border-neutral-200"
style={{
backgroundColor: formData.theme_config.backgroundColor,
color: formData.theme_config.textColor
}}
>
<div className="p-4 rounded-lg border border-neutral-200 dark:border-neutral-700 bg-neutral-50 dark:bg-neutral-800">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold" style={{ fontFamily: formData.theme_config.fontFamily }}>
<h3 className="font-semibold text-neutral-900 dark:text-neutral-100" style={{ fontFamily: formData.theme_config.fontFamily }}>
{GALLERY_THEME_PRESETS[formData.theme_preset]?.name || 'Custom Theme'}
</h3>
<div className="flex gap-2">
@@ -477,7 +472,7 @@ export const CreateEventPage: React.FC = () => {
/>
</div>
</div>
<p className="text-sm opacity-80">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Gallery Layout: <span className="font-medium capitalize">{formData.theme_config.galleryLayout || 'grid'}</span>
</p>
</div>
@@ -511,12 +506,12 @@ export const CreateEventPage: React.FC = () => {
{/* Custom CSS Template Selection */}
{cssTemplates && cssTemplates.length > 0 && (
<div className="pt-6 border-t border-neutral-200">
<h3 className="text-md font-semibold text-neutral-900 mb-3 flex items-center gap-2">
<div className="pt-6 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-md font-semibold text-neutral-900 dark:text-neutral-100 mb-3 flex items-center gap-2">
<Code className="w-4 h-4" />
{t('events.customCssTemplate', 'Custom CSS Template')}
</h3>
<p className="text-sm text-neutral-600 mb-4">
<p className="text-sm text-neutral-600 dark:text-neutral-400 mb-4">
{t('events.customCssTemplateDesc', 'Apply a custom CSS template to style the gallery with unique visual effects.')}
</p>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
@@ -526,12 +521,12 @@ export const CreateEventPage: React.FC = () => {
onClick={() => setFormData({ ...formData, css_template_id: null })}
className={`p-4 rounded-lg border-2 transition-all text-left ${
formData.css_template_id === null
? 'border-primary-600 bg-primary-50'
: 'border-neutral-200 hover:border-neutral-300'
? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
<div className="font-medium text-sm">{t('events.noTemplate', 'No Template')}</div>
<div className="text-xs text-neutral-500 mt-1">
<div className="font-medium text-sm text-neutral-900 dark:text-neutral-100">{t('events.noTemplate', 'No Template')}</div>
<div className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.useThemeOnly', 'Use theme preset only')}
</div>
</button>
@@ -544,12 +539,12 @@ export const CreateEventPage: React.FC = () => {
onClick={() => setFormData({ ...formData, css_template_id: template.id })}
className={`p-4 rounded-lg border-2 transition-all text-left ${
formData.css_template_id === template.id
? 'border-primary-600 bg-primary-50'
: 'border-neutral-200 hover:border-neutral-300'
? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
<div className="font-medium text-sm">{template.name}</div>
<div className="text-xs text-neutral-500 mt-1">
<div className="font-medium text-sm text-neutral-900 dark:text-neutral-100">{template.name}</div>
<div className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.customTemplate', 'Custom Template')} {template.slot_number}
</div>
</button>
@@ -563,7 +558,7 @@ export const CreateEventPage: React.FC = () => {
{/* Access & Security */}
<Card>
<div className="p-6 space-y-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">
<Lock className="w-5 h-5" />
{t('events.accessAndSecurity')}
</h2>
@@ -605,7 +600,7 @@ export const CreateEventPage: React.FC = () => {
<label className="flex items-start gap-2">
<input
type="checkbox"
className="mt-1 w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="mt-1 w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
checked={formData.require_password}
onChange={(e) => {
const checked = e.target.checked;
@@ -621,17 +616,17 @@ export const CreateEventPage: React.FC = () => {
}}
/>
<div>
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('events.requirePasswordToggle')}
</span>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.requirePasswordToggleHelp', 'Disable this if you want to share the gallery without a password. Anyone with the link will be able to view the photos.')}
</p>
</div>
</label>
{!formData.require_password && (
<div className="rounded-md border border-orange-200 bg-orange-50 p-3 text-xs text-orange-800">
<div className="rounded-md border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-900/30 p-3 text-xs text-orange-800 dark:text-orange-300">
{t('events.publicGalleryWarning', 'Public galleries are accessible to anyone with the link. Consider enabling download watermarks and monitoring activity.')}
</div>
)}
@@ -687,7 +682,7 @@ export const CreateEventPage: React.FC = () => {
{requireExpiration ? (
<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('events.galleryExpiration')}
</label>
<div className="flex items-center gap-2">
@@ -702,40 +697,40 @@ export const CreateEventPage: React.FC = () => {
leftIcon={<Clock className="w-5 h-5" />}
/>
</div>
<span className="text-sm text-neutral-600">{t('events.daysAfterEvent')}</span>
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.daysAfterEvent')}</span>
</div>
{formData.event_date && (
<p className="mt-2 text-sm text-neutral-500">
<p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400">
{t('events.expiresOn')}: {format(addDays(new Date(formData.event_date), formData.expires_in_days))}
</p>
)}
</div>
) : (
<div className="rounded-md border border-blue-200 bg-blue-50 p-3">
<div className="flex items-center gap-2 text-blue-800">
<div className="rounded-md border border-blue-200 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/30 p-3">
<div className="flex items-center gap-2 text-blue-800 dark:text-blue-300">
<Clock className="w-4 h-4" />
<span className="text-sm font-medium">{t('events.noExpiration', 'No Expiration')}</span>
</div>
<p className="mt-1 text-xs text-blue-700">
<p className="mt-1 text-xs text-blue-700 dark:text-blue-400">
{t('events.noExpirationHelp', 'This gallery will remain active until manually archived.')}
</p>
</div>
)}
{/* User Upload Settings */}
<div className="pt-4 border-t border-neutral-200">
<div className="pt-4 border-t border-neutral-200 dark:border-neutral-700">
<label className="flex items-center gap-3">
<input
type="checkbox"
checked={formData.allow_user_uploads}
onChange={(e) => setFormData({ ...formData, allow_user_uploads: e.target.checked })}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<div>
<span className="text-sm font-medium text-neutral-700">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">
{t('events.allowUserUploads')}
</span>
<p className="text-xs text-neutral-500 mt-0.5">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-0.5">
{t('events.allowUserUploadsDescription')}
</p>
</div>
@@ -743,16 +738,16 @@ export const CreateEventPage: React.FC = () => {
{formData.allow_user_uploads && categories && categories.length > 0 && (
<div className="mt-4 ml-7">
<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('events.uploadCategory')}
</label>
<select
value={formData.upload_category_id || ''}
onChange={(e) => setFormData({
...formData,
upload_category_id: e.target.value ? Number(e.target.value) : null
onChange={(e) => setFormData({
...formData,
upload_category_id: 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 focus:ring-2 focus:ring-primary-500 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100"
>
<option value="">{t('events.selectCategory')}</option>
{categories.map(category => (
@@ -761,7 +756,7 @@ export const CreateEventPage: React.FC = () => {
</option>
))}
</select>
<p className="mt-1 text-xs text-neutral-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('events.uploadCategoryHelp')}
</p>
</div>
+52 -52
View File
@@ -256,16 +256,16 @@ export const EmailConfigPage: React.FC = () => {
const renderVariableHelp = () => {
const variables = editedTemplate.variables || [];
return (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-sm font-semibold text-blue-900 mb-2">{t('email.templateVariables')}</h4>
<div className="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h4 className="text-sm font-semibold text-blue-900 dark:text-blue-200 mb-2">{t('email.templateVariables')}</h4>
<div className="grid grid-cols-2 gap-2 text-sm">
{variables.map(variable => (
<code key={variable} className="text-blue-700 bg-blue-100 px-2 py-1 rounded">
<code key={variable} className="text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900/50 px-2 py-1 rounded">
{`{{${variable}}}`}
</code>
))}
</div>
<p className="text-xs text-blue-700 mt-2">
<p className="text-xs text-blue-700 dark:text-blue-300 mt-2">
{t('email.variableHelp')}
</p>
</div>
@@ -283,19 +283,19 @@ export const EmailConfigPage: React.FC = () => {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">{t('email.title')}</h1>
<p className="text-neutral-600 mt-1">{t('email.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('email.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('email.subtitle')}</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-neutral-200 mb-6">
<div className="border-b border-neutral-200 dark:border-neutral-700 mb-6">
<nav className="-mb-px flex gap-6">
<button
onClick={() => setActiveTab('smtp')}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'smtp'
? 'border-primary-600 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
}`}
>
{t('email.smtpSettings')}
@@ -305,7 +305,7 @@ export const EmailConfigPage: React.FC = () => {
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === 'templates'
? 'border-primary-600 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
}`}
>
{t('email.emailTemplates')}
@@ -317,11 +317,11 @@ export const EmailConfigPage: React.FC = () => {
{activeTab === 'smtp' && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('email.smtpConfiguration')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('email.smtpConfiguration')}</h2>
<div className="space-y-4">
<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('email.smtpHost')} <span className="text-red-500">*</span>
</label>
<Input
@@ -335,7 +335,7 @@ export const EmailConfigPage: React.FC = () => {
<div className="grid grid-cols-2 gap-4">
<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('email.port')} <span className="text-red-500">*</span>
</label>
<Input
@@ -347,13 +347,13 @@ export const EmailConfigPage: React.FC = () => {
</div>
<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('email.security')}
</label>
<select
value={smtpConfig.smtp_secure ? 'ssl' : 'tls'}
onChange={(e) => setSmtpConfig(prev => ({ ...prev, smtp_secure: e.target.value === 'ssl' }))}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
@@ -368,17 +368,17 @@ export const EmailConfigPage: React.FC = () => {
type="checkbox"
checked={!smtpConfig.tls_reject_unauthorized}
onChange={(e) => setSmtpConfig(prev => ({ ...prev, tls_reject_unauthorized: !e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 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('email.ignoreSslErrors')}
</span>
</label>
{!smtpConfig.tls_reject_unauthorized && (
<div className="mt-2 p-3 bg-amber-50 border border-amber-200 rounded-lg">
<div className="mt-2 p-3 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-start gap-2">
<ShieldAlert className="w-4 h-4 text-amber-600 flex-shrink-0 mt-0.5" />
<p className="text-xs text-amber-800">
<ShieldAlert className="w-4 h-4 text-amber-600 dark:text-amber-400 flex-shrink-0 mt-0.5" />
<p className="text-xs text-amber-800 dark:text-amber-300">
{t('email.ignoreSslWarning')}
</p>
</div>
@@ -386,7 +386,7 @@ export const EmailConfigPage: React.FC = () => {
)}</div>
<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('email.username')}
</label>
<Input
@@ -399,7 +399,7 @@ export const EmailConfigPage: React.FC = () => {
</div>
<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('email.password')}
</label>
<div className="relative">
@@ -421,7 +421,7 @@ export const EmailConfigPage: React.FC = () => {
</div>
<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('email.fromEmail')} <span className="text-red-500">*</span>
</label>
<Input
@@ -434,7 +434,7 @@ export const EmailConfigPage: React.FC = () => {
</div>
<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('email.fromName')}
</label>
<Input
@@ -458,12 +458,12 @@ export const EmailConfigPage: React.FC = () => {
</Card>
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('email.testEmailSection')}</h2>
<div className="mb-4 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('email.testEmailSection')}</h2>
<div className="mb-4 p-4 bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-lg">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0" />
<div className="text-sm text-amber-800">
<AlertCircle className="w-5 h-5 text-amber-600 dark:text-amber-400 flex-shrink-0" />
<div className="text-sm text-amber-800 dark:text-amber-300">
<p className="font-medium">{t('email.beforeTesting')}</p>
<ul className="list-disc list-inside mt-1">
<li>{t('email.saveSmtpFirst')}</li>
@@ -476,7 +476,7 @@ export const EmailConfigPage: React.FC = () => {
<div className="space-y-4">
<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('email.testEmailAddressLabel')}
</label>
<Input
@@ -499,10 +499,10 @@ export const EmailConfigPage: React.FC = () => {
</Button>
</div>
<div className="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="mt-6 p-4 bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800 rounded-lg">
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 flex-shrink-0" />
<div className="text-sm text-green-800">
<CheckCircle className="w-5 h-5 text-green-600 dark:text-green-400 flex-shrink-0" />
<div className="text-sm text-green-800 dark:text-green-300">
<p className="font-medium">{t('email.commonSmtpSettings')}</p>
<ul className="mt-2 space-y-1">
<li><strong>Gmail:</strong> smtp.gmail.com:587 (TLS)</li>
@@ -520,7 +520,7 @@ export const EmailConfigPage: React.FC = () => {
{activeTab === 'templates' && (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<Card padding="sm">
<h3 className="text-lg font-semibold text-neutral-900 mb-4">{t('email.templates')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('email.templates')}</h3>
<div className="space-y-2">
{templates.map(template => {
const templateInfo = defaultTemplateKeys.find(t => t.key === template.template_key);
@@ -533,14 +533,14 @@ export const EmailConfigPage: React.FC = () => {
}}
className={`w-full text-left p-3 rounded-lg transition-colors ${
selectedTemplateKey === template.template_key
? 'bg-primary-50 border-2 border-primary-600'
: 'bg-neutral-50 border-2 border-transparent hover:bg-neutral-100'
? 'bg-primary-50 dark:bg-primary-900/30 border-2 border-primary-600'
: 'bg-neutral-50 dark:bg-neutral-700 border-2 border-transparent hover:bg-neutral-100 dark:hover:bg-neutral-600'
}`}
>
<p className="font-medium text-neutral-900">
<p className="font-medium text-neutral-900 dark:text-neutral-100">
{templateInfo?.name || template.template_key}
</p>
<p className="text-sm text-neutral-500 mt-1 truncate">
<p className="text-sm text-neutral-500 dark:text-neutral-400 mt-1 truncate">
{template.subject_en || template.subject}
</p>
</button>
@@ -552,15 +552,15 @@ export const EmailConfigPage: React.FC = () => {
<div className="lg:col-span-2">
<Card padding="md">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-neutral-900">{t('email.editTemplate')}</h3>
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{t('email.editTemplate')}</h3>
<div className="flex gap-2">
<div className="flex gap-1 mr-4">
<button
onClick={() => setEditingLang('en')}
className={`px-3 py-1 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'en'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600'
}`}
>
🇬🇧 English
@@ -569,8 +569,8 @@ export const EmailConfigPage: React.FC = () => {
onClick={() => setEditingLang('de')}
className={`px-3 py-1 text-sm font-medium rounded-lg transition-colors ${
editingLang === 'de'
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-700 hover:bg-neutral-200'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-600'
}`}
>
🇩🇪 Deutsch
@@ -598,19 +598,19 @@ export const EmailConfigPage: React.FC = () => {
<div className="space-y-4">
<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('email.templateName')}
</label>
<Input
type="text"
value={defaultTemplateKeys.find(t => t.key === selectedTemplateKey)?.name || selectedTemplateKey}
disabled
className="bg-neutral-50"
className="bg-neutral-50 dark:bg-neutral-700"
/>
</div>
<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('email.subjectLine')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<Input
@@ -629,21 +629,21 @@ export const EmailConfigPage: React.FC = () => {
</div>
<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('email.emailBody')} ({editingLang === 'en' ? 'English' : 'German'})
</label>
<textarea
value={
editingLang === 'en'
editingLang === 'en'
? (editedTemplate.body_html_en || editedTemplate.body_html || '')
: (editedTemplate.body_html_de || '')
}
onChange={(e) => setEditedTemplate(prev => ({
...prev,
[editingLang === 'en' ? 'body_html_en' : 'body_html_de']: e.target.value
onChange={(e) => setEditedTemplate(prev => ({
...prev,
[editingLang === 'en' ? 'body_html_en' : 'body_html_de']: e.target.value
}))}
rows={15}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 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-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 font-mono text-sm"
/>
</div>
+188 -188
View File
@@ -101,23 +101,23 @@ const ExternalFolderPicker: React.FC<{ value: string; onChange: (p: string) => v
};
return (
<div className="mt-2 border rounded-lg p-3">
<div className="mt-2 border border-neutral-200 dark:border-neutral-700 rounded-lg p-3">
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-neutral-600">/external-media/{entries?.path || ''}</div>
<div className="text-sm text-neutral-600 dark:text-neutral-400">/external-media/{entries?.path || ''}</div>
<div className="flex gap-2">
<button className="text-sm underline" onClick={navigateUp} disabled={!entries?.canNavigateUp}>{t('common.up', 'Up')}</button>
<button className="text-sm underline" onClick={() => onChange(entries?.path || '')}>{t('common.select', 'Select')}</button>
<button className="text-sm underline text-neutral-700 dark:text-neutral-300" onClick={navigateUp} disabled={!entries?.canNavigateUp}>{t('common.up', 'Up')}</button>
<button className="text-sm underline text-neutral-700 dark:text-neutral-300" onClick={() => onChange(entries?.path || '')}>{t('common.select', 'Select')}</button>
</div>
</div>
{loading ? (
<div className="text-sm text-neutral-500">{t('common.loading', 'Loading...')}</div>
<div className="text-sm text-neutral-500 dark:text-neutral-400">{t('common.loading', 'Loading...')}</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{entries?.entries?.filter((e: any) => e.type === 'dir').map((e: any) => (
<button
key={e.name}
onClick={() => load([entries?.path, e.name].filter(Boolean).join('/'))}
className="px-3 py-2 border rounded text-left hover:bg-neutral-50"
className="px-3 py-2 border border-neutral-200 dark:border-neutral-600 rounded text-left text-neutral-900 dark:text-neutral-100 hover:bg-neutral-50 dark:hover:bg-neutral-700"
>
📁 {e.name}
</button>
@@ -125,7 +125,7 @@ const ExternalFolderPicker: React.FC<{ value: string; onChange: (p: string) => v
</div>
)}
{value && (
<div className="mt-2 text-xs text-neutral-600">{t('common.selected', 'Selected')}: /external-media/{value}</div>
<div className="mt-2 text-xs text-neutral-600 dark:text-neutral-400">{t('common.selected', 'Selected')}: /external-media/{value}</div>
)}
</div>
);
@@ -654,8 +654,8 @@ export const EventDetailsPage: React.FC = () => {
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold text-neutral-900">{event.event_name}</h1>
<div className="flex items-center gap-4 mt-2 text-sm text-neutral-600">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{event.event_name}</h1>
<div className="flex items-center gap-4 mt-2 text-sm text-neutral-600 dark:text-neutral-400">
{event.event_date && (
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
@@ -666,14 +666,14 @@ export const EventDetailsPage: React.FC = () => {
<span
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${
isGalleryPublic(event.require_password)
? 'bg-green-100 text-green-700'
: 'bg-neutral-100 text-neutral-700'
? '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'
}`}
>
{isGalleryPublic(event.require_password) ? t('events.publicAccess', 'Public access') : t('events.passwordProtected', 'Password protected')}
</span>
{event.is_archived ? (
<span className="text-neutral-500 flex items-center">
<span className="text-neutral-500 dark:text-neutral-400 flex items-center">
<Archive className="w-4 h-4 mr-1" />
{t('events.archived')}
</span>
@@ -787,14 +787,14 @@ export const EventDetailsPage: React.FC = () => {
)}
{/* Tabs */}
<div className="mb-6 border-b border-neutral-200">
<div className="mb-6 border-b border-neutral-200 dark:border-neutral-700">
<nav className="-mb-px flex space-x-8">
<button
onClick={() => setActiveTab('overview')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'overview'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
{t('events.overview')}
@@ -803,14 +803,14 @@ export const EventDetailsPage: React.FC = () => {
onClick={() => setActiveTab('photos')}
className={`py-2 px-1 border-b-2 font-medium text-sm flex items-center gap-2 ${
activeTab === 'photos'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
<Image className="w-4 h-4" />
<span>{t('events.photos')}</span>
{event.photo_count !== undefined && event.photo_count > 0 && (
<span className="ml-1 px-2 py-0.5 text-xs font-medium bg-neutral-100 text-neutral-700 rounded-full">
<span className="ml-1 px-2 py-0.5 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded-full">
{event.photo_count}
</span>
)}
@@ -819,8 +819,8 @@ export const EventDetailsPage: React.FC = () => {
onClick={() => setActiveTab('categories')}
className={`py-2 px-1 border-b-2 font-medium text-sm ${
activeTab === 'categories'
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700 hover:border-neutral-300'
? 'border-primary-500 text-primary-600 dark:text-primary-400'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300 hover:border-neutral-300 dark:hover:border-neutral-600'
}`}
>
{t('events.categories')}
@@ -835,25 +835,25 @@ export const EventDetailsPage: React.FC = () => {
<div className="space-y-6">
{/* Event Information */}
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.eventInformation')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.eventInformation')}</h2>
{isEditing ? (
<div className="space-y-4">
<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.welcomeMessageLabel')}
</label>
<textarea
value={editForm.welcome_message}
onChange={(e) => setEditForm(prev => ({ ...prev, welcome_message: 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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
rows={3}
placeholder={t('events.welcomeMessage')}
/>
</div>
<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.hostName')}
</label>
<Input
@@ -865,7 +865,7 @@ export const EventDetailsPage: React.FC = () => {
</div>
<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.expirationDate')}
</label>
<Input
@@ -891,10 +891,10 @@ export const EventDetailsPage: React.FC = () => {
if (!heroImageUrl) return null;
return (
<div className="ml-6 mt-2">
<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.heroImageAnchor', 'Hero Image Crop Position')}
</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.heroImageAnchorDescription', 'Click on the image to set the focal point for cropping.')}
</p>
<FocalPointPicker
@@ -911,7 +911,7 @@ export const EventDetailsPage: React.FC = () => {
<label className="flex items-start gap-2">
<input
type="checkbox"
className="mt-1 w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="mt-1 w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
checked={editForm.require_password}
onChange={(e) => {
const checked = e.target.checked;
@@ -927,15 +927,15 @@ export const EventDetailsPage: React.FC = () => {
}}
/>
<div>
<span className="text-sm font-medium text-neutral-700">{t('events.requirePasswordToggle')}</span>
<p className="text-xs text-neutral-500 mt-1">
<span className="text-sm font-medium text-neutral-700 dark:text-neutral-300">{t('events.requirePasswordToggle')}</span>
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.requirePasswordToggleHelp', 'Disable this if you want to share the gallery without a password. Anyone with the link will be able to view the photos.')}
</p>
</div>
</label>
{!editForm.require_password && (
<div className="mt-2 rounded-md border border-orange-200 bg-orange-50 p-3 text-xs text-orange-800">
<div className="mt-2 rounded-md border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-900/30 p-3 text-xs text-orange-800 dark:text-orange-300">
{t('events.publicGalleryWarning', 'Public galleries are accessible to anyone with the link. Consider enabling download watermarks and monitoring activity.')}
</div>
)}
@@ -944,7 +944,7 @@ export const EventDetailsPage: React.FC = () => {
{editForm.require_password && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<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.newPasswordLabel', 'New gallery password')}
</label>
<div className="relative">
@@ -962,16 +962,16 @@ export const EventDetailsPage: React.FC = () => {
className="absolute inset-y-0 right-0 pr-3 flex items-center"
>
{showNewPassword ? (
<EyeOff className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
<EyeOff className="w-5 h-5 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300" />
) : (
<Eye className="w-5 h-5 text-neutral-400 hover:text-neutral-600" />
<Eye className="w-5 h-5 text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300" />
)}
</button>
</div>
</div>
<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.confirmPassword')}
</label>
<Input
@@ -986,7 +986,7 @@ export const EventDetailsPage: React.FC = () => {
)}
<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.sourceMode', 'Source Mode')}
</label>
<select
@@ -1001,26 +1001,26 @@ export const EventDetailsPage: React.FC = () => {
: ''
}));
}}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="managed">{t('events.sourceModeManaged', 'Managed (upload to PicPeak)')}</option>
<option value="reference">{t('events.sourceModeReference', 'Reference external folder')}</option>
</select>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.sourceModeHelp', 'Use managed mode for direct uploads or reference an external folder that is mounted at /external-media in Docker.')}
</p>
</div>
{editForm.source_mode === 'reference' && (
<div className="mt-3">
<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('events.externalFolder', 'External Folder')}
</label>
<ExternalFolderPicker
value={editForm.external_path || ''}
onChange={(folder) => setEditForm(prev => ({ ...prev, external_path: folder }))}
/>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.externalFolderHint', 'These folders come from the /external-media mount inside the container. Ensure it is accessible to the backend process.')}
</p>
</div>
@@ -1032,27 +1032,27 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.allow_user_uploads}
onChange={(e) => setEditForm(prev => ({ ...prev, allow_user_uploads: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<span className="ml-2 text-sm text-neutral-700">{t('events.allowUserUploads')}</span>
<span className="ml-2 text-sm text-neutral-700 dark:text-neutral-300">{t('events.allowUserUploads')}</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('events.allowUserUploadsHelp')}
</p>
</div>
{editForm.allow_user_uploads && (
<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.uploadCategory')}
</label>
<select
value={editForm.upload_category_id || ''}
onChange={(e) => setEditForm(prev => ({
...prev,
upload_category_id: e.target.value ? parseInt(e.target.value) : null
onChange={(e) => setEditForm(prev => ({
...prev,
upload_category_id: e.target.value ? parseInt(e.target.value) : null
}))}
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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
<option value="">{t('events.selectCategory')}</option>
{categories?.map(category => (
@@ -1061,15 +1061,15 @@ export const EventDetailsPage: React.FC = () => {
</option>
))}
</select>
<p className="text-xs text-neutral-500 mt-1">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-1">
{t('events.uploadCategoryHelp')}
</p>
</div>
)}
{/* Feedback Settings */}
<div className="mt-4 pt-4 border-t border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-900 mb-3">{t('feedback.settings.title', 'Guest Feedback Settings')}</h3>
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3">{t('feedback.settings.title', 'Guest Feedback Settings')}</h3>
<FeedbackSettings
settings={feedbackSettings}
onChange={setFeedbackSettings}
@@ -1077,8 +1077,8 @@ export const EventDetailsPage: React.FC = () => {
</div>
{/* Download Protection Settings */}
<div className="mt-4 pt-4 border-t border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-900 mb-3 flex items-center gap-2">
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3 flex items-center gap-2">
<Shield className="w-4 h-4 text-primary-600" />
{t('events.downloadProtection', 'Download Protection')}
</h3>
@@ -1089,10 +1089,10 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.allow_downloads}
onChange={(e) => setEditForm(prev => ({ ...prev, allow_downloads: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Download className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.allowDownloads', 'Allow photo downloads')}</span>
<Download className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.allowDownloads', 'Allow photo downloads')}</span>
</label>
<label className="flex items-center">
@@ -1100,10 +1100,10 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.disable_right_click}
onChange={(e) => setEditForm(prev => ({ ...prev, disable_right_click: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<MousePointer className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.disableRightClick', 'Block right-click menu')}</span>
<MousePointer className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.disableRightClick', 'Block right-click menu')}</span>
</label>
<label className="flex items-center">
@@ -1111,10 +1111,10 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.watermark_downloads}
onChange={(e) => setEditForm(prev => ({ ...prev, watermark_downloads: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Droplets className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.watermarkDownloads', 'Add watermark to downloads')}</span>
<Droplets className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.watermarkDownloads', 'Add watermark to downloads')}</span>
</label>
<label className="flex items-center">
@@ -1122,10 +1122,10 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.enable_devtools_protection}
onChange={(e) => setEditForm(prev => ({ ...prev, enable_devtools_protection: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Monitor className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.enableDevtoolsProtection', 'Detect developer tools')}</span>
<Monitor className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.enableDevtoolsProtection', 'Detect developer tools')}</span>
</label>
<label className="flex items-center">
@@ -1133,21 +1133,21 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.use_canvas_rendering}
onChange={(e) => setEditForm(prev => ({ ...prev, use_canvas_rendering: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.useCanvasRendering', 'Canvas rendering (advanced protection)')}</span>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.useCanvasRendering', 'Canvas rendering (advanced protection)')}</span>
</label>
<p className="text-xs text-neutral-500 mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('events.protectionInfo', 'Protection features help prevent unauthorized downloads but cannot block all methods.')}
</p>
</div>
</div>
{/* Hero Logo Settings */}
<div className="mt-4 pt-4 border-t border-neutral-200">
<h3 className="text-sm font-semibold text-neutral-900 mb-3 flex items-center gap-2">
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<h3 className="text-sm font-semibold text-neutral-900 dark:text-neutral-100 mb-3 flex items-center gap-2">
<Layout className="w-4 h-4 text-primary-600" />
{t('events.heroLogoSettings', 'Hero Logo Settings')}
</h3>
@@ -1158,22 +1158,22 @@ export const EventDetailsPage: React.FC = () => {
type="checkbox"
checked={editForm.hero_logo_visible}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_visible: e.target.checked }))}
className="w-4 h-4 text-primary-600 border-neutral-300 rounded focus:ring-primary-500"
className="w-4 h-4 text-primary-600 border-neutral-300 dark:border-neutral-600 rounded focus:ring-primary-500"
/>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500" />
<span className="text-sm text-neutral-700">{t('events.heroLogoVisible', 'Display logo in hero section')}</span>
<Image className="w-4 h-4 ml-2 mr-1 text-neutral-500 dark:text-neutral-400" />
<span className="text-sm text-neutral-700 dark:text-neutral-300">{t('events.heroLogoVisible', 'Display logo in hero section')}</span>
</label>
{editForm.hero_logo_visible && (
<>
<div className="ml-6">
<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.heroLogoSize', 'Logo Size')}
</label>
<select
value={editForm.hero_logo_size}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_size: e.target.value as 'small' | 'medium' | 'large' | 'xlarge' }))}
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
className="w-full sm:w-48 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 shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
>
<option value="small">{t('events.heroLogoSizeSmall', 'Small')}</option>
<option value="medium">{t('events.heroLogoSizeMedium', 'Medium')}</option>
@@ -1183,13 +1183,13 @@ export const EventDetailsPage: React.FC = () => {
</div>
<div className="ml-6">
<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.heroLogoPosition', 'Logo Position')}
</label>
<select
value={editForm.hero_logo_position}
onChange={(e) => setEditForm(prev => ({ ...prev, hero_logo_position: e.target.value as 'top' | 'center' | 'bottom' }))}
className="w-full sm:w-48 px-3 py-2 border border-neutral-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
className="w-full sm:w-48 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 shadow-sm focus:ring-primary-500 focus:border-primary-500 text-sm"
>
<option value="top">{t('events.heroLogoPositionTop', 'Top (above title)')}</option>
<option value="center">{t('events.heroLogoPositionCenter', 'Center (between title and dates)')}</option>
@@ -1198,17 +1198,17 @@ export const EventDetailsPage: React.FC = () => {
</div>
{/* Custom Event Logo Upload */}
<div className="ml-6 mt-3 pt-3 border-t border-neutral-100">
<label className="block text-sm font-medium text-neutral-700 mb-2">
<div className="ml-6 mt-3 pt-3 border-t border-neutral-100 dark:border-neutral-700">
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
{t('events.eventCustomLogo', 'Custom Event Logo')}
</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.eventCustomLogoDescription', 'Upload a custom logo for this event. This overrides the global branding logo for this gallery only.')}
</p>
{event.hero_logo_url ? (
<div className="flex items-center gap-3">
<div className="w-16 h-16 border border-neutral-200 rounded-md flex items-center justify-center bg-neutral-50 overflow-hidden">
<div className="w-16 h-16 border border-neutral-200 dark:border-neutral-600 rounded-md flex items-center justify-center bg-neutral-50 dark:bg-neutral-700 overflow-hidden">
<img
src={buildResourceUrl(event.hero_logo_url)}
alt={t('events.eventCustomLogo', 'Custom Event Logo')}
@@ -1245,7 +1245,7 @@ export const EventDetailsPage: React.FC = () => {
</div>
) : (
<div className="flex items-center gap-2">
<label className={`cursor-pointer inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium border border-neutral-300 rounded-md hover:bg-neutral-50 ${logoUploading ? 'opacity-50 pointer-events-none' : ''}`}>
<label className={`cursor-pointer inline-flex items-center gap-2 px-3 py-1.5 text-xs font-medium border border-neutral-300 dark:border-neutral-600 text-neutral-700 dark:text-neutral-300 rounded-md hover:bg-neutral-50 dark:hover:bg-neutral-700 ${logoUploading ? 'opacity-50 pointer-events-none' : ''}`}>
<Upload className="w-3.5 h-3.5" />
{t('events.uploadEventLogo', 'Upload Logo')}
<input
@@ -1267,7 +1267,7 @@ export const EventDetailsPage: React.FC = () => {
</>
)}
<p className="text-xs text-neutral-500 mt-2">
<p className="text-xs text-neutral-500 dark:text-neutral-400 mt-2">
{t('events.heroLogoInfo', 'These settings apply when the gallery uses the Hero layout. You can hide the logo or customize its size and position.')}
</p>
</div>
@@ -1276,94 +1276,94 @@ export const EventDetailsPage: React.FC = () => {
) : (
<dl className="space-y-4">
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.sourceMode', 'Source Mode')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.sourceMode', 'Source Mode')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.source_mode === 'reference' ? t('events.sourceModeReference', 'Reference external folder') : t('events.sourceModeManaged', 'Managed (upload to PicPeak)')}
{event.source_mode === 'reference' && event.external_path ? (
<span className="text-neutral-500 ml-2">/external-media/{event.external_path}</span>
<span className="text-neutral-500 dark:text-neutral-400 ml-2">/external-media/{event.external_path}</span>
) : null}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.welcomeMessage')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.welcomeMessage')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.welcome_message || <span className="text-neutral-400">{t('events.noWelcomeMessageSet')}</span>}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.hostName')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.hostName')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.customer_name || <span className="text-neutral-400">{t('common.notSet')}</span>}
</dd>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.hostEmail')}</dt>
<dd className="mt-1 text-sm text-neutral-900">{event.customer_email}</dd>
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.hostEmail')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">{event.customer_email}</dd>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.adminEmail')}</dt>
<dd className="mt-1 text-sm text-neutral-900">{event.admin_email}</dd>
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.adminEmail')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">{event.admin_email}</dd>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.created')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.created')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.created_at && format(safeParseDate(event.created_at)!, 'PP')}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.expires')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.expires')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.expires_at ? (
<>
{format(safeParseDate(event.expires_at)!, 'PP')}
{!event.is_archived && daysUntilExpiration !== null && daysUntilExpiration > 0 && (
<span className="text-neutral-500 ml-1">
<span className="text-neutral-500 dark:text-neutral-400 ml-1">
{t('events.daysLeft', { count: daysUntilExpiration })}
</span>
)}
</>
) : (
<span className="text-neutral-500">{t('events.neverExpires', 'Never')}</span>
<span className="text-neutral-500 dark:text-neutral-400">{t('events.neverExpires', 'Never')}</span>
)}
</dd>
</div>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.heroPhoto')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.heroPhoto')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.hero_photo_id ? (
<span className="text-primary-600">{t('events.heroPhotoSelected')}</span>
<span className="text-primary-600 dark:text-primary-400">{t('events.heroPhotoSelected')}</span>
) : (
<span className="text-neutral-400">{t('events.noHeroPhotoSelected')}</span>
)}
</dd>
</div>
<div>
<dt className="text-sm font-medium text-neutral-500">{t('events.userUploads')}</dt>
<dd className="mt-1 text-sm text-neutral-900">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.userUploads')}</dt>
<dd className="mt-1 text-sm text-neutral-900 dark:text-neutral-100">
{event.allow_user_uploads ? (
<div className="space-y-1">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium text-green-700 bg-green-100 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/40 rounded">
{t('common.yes')}
</span>
{event.upload_category_id && (
<p className="text-xs text-neutral-600">
<p className="text-xs text-neutral-600 dark:text-neutral-400">
{t('events.uploadCategory')}: {categories.find(c => c.id === event.upload_category_id)?.name || 'N/A'}
</p>
)}
</div>
) : (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium text-neutral-700 bg-neutral-100 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium text-neutral-700 dark:text-neutral-300 bg-neutral-100 dark:bg-neutral-700 rounded">
{t('common.no')}
</span>
)}
@@ -1371,41 +1371,41 @@ export const EventDetailsPage: React.FC = () => {
</div>
{/* Download Protection Display */}
<div className="pt-3 mt-3 border-t border-neutral-200">
<dt className="text-sm font-medium text-neutral-500 flex items-center gap-2">
<div className="pt-3 mt-3 border-t border-neutral-200 dark:border-neutral-700">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400 flex items-center gap-2">
<Shield className="w-4 h-4" />
{t('events.downloadProtection', 'Download Protection')}
</dt>
<dd className="mt-2 text-sm text-neutral-900">
<dd className="mt-2 text-sm text-neutral-900 dark:text-neutral-100">
<div className="flex flex-wrap gap-2">
<span className={`inline-flex items-center px-2 py-1 text-xs font-medium rounded ${
event.protection_level === 'maximum' ? 'bg-red-100 text-red-700' :
event.protection_level === 'enhanced' ? 'bg-orange-100 text-orange-700' :
event.protection_level === 'standard' ? 'bg-blue-100 text-blue-700' :
'bg-neutral-100 text-neutral-700'
event.protection_level === 'maximum' ? 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300' :
event.protection_level === 'enhanced' ? 'bg-orange-100 dark:bg-orange-900/40 text-orange-700 dark:text-orange-300' :
event.protection_level === 'standard' ? 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300' :
'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300'
}`}>
{event.protection_level || 'standard'}
</span>
{event.disable_right_click && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
<MousePointer className="w-3 h-3 mr-1" />
{t('events.rightClickBlocked', 'Right-click blocked')}
</span>
)}
{event.enable_devtools_protection && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
<Monitor className="w-3 h-3 mr-1" />
{t('events.devtoolsDetection', 'DevTools detection')}
</span>
)}
{!event.allow_downloads && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 text-red-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 rounded">
<Download className="w-3 h-3 mr-1" />
{t('events.downloadsDisabled', 'Downloads disabled')}
</span>
)}
{event.watermark_downloads && (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
<Droplets className="w-3 h-3 mr-1" />
{t('events.watermarked', 'Watermarked')}
</span>
@@ -1415,28 +1415,28 @@ export const EventDetailsPage: React.FC = () => {
</div>
{/* Hero Logo Settings Display */}
<div className="pt-3 mt-3 border-t border-neutral-200">
<dt className="text-sm font-medium text-neutral-500 flex items-center gap-2">
<div className="pt-3 mt-3 border-t border-neutral-200 dark:border-neutral-700">
<dt className="text-sm font-medium text-neutral-500 dark:text-neutral-400 flex items-center gap-2">
<Layout className="w-4 h-4" />
{t('events.heroLogoSettings', 'Hero Logo Settings')}
</dt>
<dd className="mt-2 text-sm text-neutral-900">
<dd className="mt-2 text-sm text-neutral-900 dark:text-neutral-100">
<div className="flex flex-wrap gap-2">
{event.hero_logo_visible !== false ? (
<>
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 text-green-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 rounded">
<Image className="w-3 h-3 mr-1" />
{t('events.heroLogoVisibleLabel', 'Logo visible')}
</span>
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
{t('events.heroLogoSizeLabel', 'Size')}: {event.hero_logo_size || 'medium'}
</span>
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
{t('events.heroLogoPositionLabel', 'Position')}: {event.hero_logo_position || 'top'}
</span>
</>
) : (
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 text-neutral-700 rounded">
<span className="inline-flex items-center px-2 py-1 text-xs font-medium bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 rounded">
<Image className="w-3 h-3 mr-1" />
{t('events.heroLogoHidden', 'Logo hidden')}
</span>
@@ -1450,14 +1450,14 @@ export const EventDetailsPage: React.FC = () => {
{/* Share Link */}
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.shareLink')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.shareLink')}</h2>
<div className="flex items-center gap-2">
<input
type="text"
value={event.share_link}
readOnly
className="flex-1 px-3 py-2 bg-neutral-50 border border-neutral-300 rounded-lg text-sm"
className="flex-1 px-3 py-2 bg-neutral-50 dark:bg-neutral-700 border border-neutral-300 dark:border-neutral-600 text-neutral-900 dark:text-neutral-100 rounded-lg text-sm"
/>
<Button
variant="outline"
@@ -1468,15 +1468,15 @@ export const EventDetailsPage: React.FC = () => {
{copiedLink ? t('events.copied') : t('events.copy')}
</Button>
</div>
<p className="text-sm text-neutral-600 mt-2">
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-2">
{isGalleryPublic(event.require_password)
? t('events.shareWithGuestsPublic', 'Anyone with this link can view the gallery. No password is required.')
: t('events.shareWithGuests')}
</p>
{!event.is_archived && (
<div className="mt-4 pt-4 border-t border-neutral-200 space-y-2">
<div className="mt-4 pt-4 border-t border-neutral-200 dark:border-neutral-700 space-y-2">
<Button
variant="outline"
size="sm"
@@ -1511,8 +1511,8 @@ export const EventDetailsPage: React.FC = () => {
{/* Actions */}
{!event.is_archived && (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.actions')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.actions')}</h2>
<div className="space-y-3">
<Button
variant="outline"
@@ -1527,8 +1527,8 @@ export const EventDetailsPage: React.FC = () => {
>
{t('events.archiveEvent')}
</Button>
<p className="text-xs text-neutral-500 text-center">
<p className="text-xs text-neutral-500 dark:text-neutral-400 text-center">
{t('events.archivingInfo')}
</p>
</div>
@@ -1540,44 +1540,44 @@ export const EventDetailsPage: React.FC = () => {
<div className="space-y-6">
{/* Photo Statistics */}
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.photoStatistics')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.photoStatistics')}</h2>
<div className="space-y-3">
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.totalPhotos')}</span>
<span className="text-sm font-medium">{event.photo_count || 0}</span>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.totalPhotos')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{event.photo_count || 0}</span>
</div>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.totalSize')}</span>
<span className="text-sm font-medium">
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.totalSize')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{event.total_size ? `${(event.total_size / (1024 * 1024)).toFixed(1)} MB` : '0 MB'}
</span>
</div>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.categories')}</span>
<span className="text-sm font-medium">{categories.length}</span>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.categories')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{categories.length}</span>
</div>
{event.total_views !== undefined && (
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.totalViews')}</span>
<span className="text-sm font-medium">{event.total_views || 0}</span>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.totalViews')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{event.total_views || 0}</span>
</div>
)}
{event.total_downloads !== undefined && (
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.totalDownloads')}</span>
<span className="text-sm font-medium">{event.total_downloads || 0}</span>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.totalDownloads')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{event.total_downloads || 0}</span>
</div>
)}
{event.unique_visitors !== undefined && (
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-lg">
<span className="text-sm text-neutral-600">{t('events.uniqueVisitors')}</span>
<span className="text-sm font-medium">{event.unique_visitors || 0}</span>
<div className="flex items-center justify-between py-2 px-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<span className="text-sm text-neutral-600 dark:text-neutral-400">{t('events.uniqueVisitors')}</span>
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">{event.unique_visitors || 0}</span>
</div>
)}
</div>
@@ -1598,7 +1598,7 @@ export const EventDetailsPage: React.FC = () => {
{/* Theme & Style */}
{isEditing && !event.is_archived && (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('branding.themeAndStyle')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('branding.themeAndStyle')}</h2>
<ThemeCustomizerEnhanced
value={currentTheme || GALLERY_THEME_PRESETS.default.config}
onChange={(theme) => {
@@ -1629,7 +1629,7 @@ export const EventDetailsPage: React.FC = () => {
{/* Theme Display (when not editing) */}
{!isEditing && !event.is_archived && (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.galleryTheme')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.galleryTheme')}</h2>
<ThemeDisplay
theme={event.color_theme || GALLERY_THEME_PRESETS.default.config}
presetName={event.color_theme && !event.color_theme.startsWith('{') ? event.color_theme : undefined}
@@ -1650,12 +1650,12 @@ export const EventDetailsPage: React.FC = () => {
{/* Archive Status */}
{event.is_archived ? (
<Card padding="md">
<h2 className="text-lg font-semibold text-neutral-900 mb-4">{t('events.archiveStatusTitle')}</h2>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4">{t('events.archiveStatusTitle')}</h2>
<div className="space-y-3">
<div>
<p className="text-sm font-medium text-neutral-500">{t('events.archivedOn')}</p>
<p className="text-sm text-neutral-900">
<p className="text-sm font-medium text-neutral-500 dark:text-neutral-400">{t('events.archivedOn')}</p>
<p className="text-sm text-neutral-900 dark:text-neutral-100">
{event.archived_at && format(safeParseDate(event.archived_at)!, 'PPp')}
</p>
</div>
@@ -1798,18 +1798,18 @@ export const EventDetailsPage: React.FC = () => {
<div>
<Card padding="md">
<div className="mb-6">
<h2 className="text-lg font-semibold text-neutral-900 mb-2">{t('events.photoCategories')}</h2>
<p className="text-sm text-neutral-600">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-2">{t('events.photoCategories')}</h2>
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('events.organizeCategoriesInfo')}
</p>
</div>
<EventCategoryManager
eventId={parseInt(id!)}
<EventCategoryManager
eventId={parseInt(id!)}
/>
<div className="mt-6 p-4 bg-blue-50 rounded-lg">
<p className="text-sm text-blue-800">
<div className="mt-6 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-300">
{t('events.categoriesTip')}
</p>
</div>
@@ -1834,16 +1834,16 @@ export const EventDetailsPage: React.FC = () => {
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<Card className="max-w-2xl w-full">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-neutral-900">{t('events.importExternal', 'Import from External Folder')}</h2>
<button onClick={() => setShowExternalImport(false)} className="text-neutral-400 hover:text-neutral-600">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">{t('events.importExternal', 'Import from External Folder')}</h2>
<button onClick={() => setShowExternalImport(false)} className="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-300">
<X className="w-5 h-5" />
</button>
</div>
<div className="mb-3 text-sm text-neutral-700">
<div className="mb-3 text-sm text-neutral-700 dark:text-neutral-300">
{t('events.externalImportInfo', 'All pictures from the selected folder will be imported.')}
</div>
<div className="mb-2 text-sm text-neutral-700">
<div className="mb-2 text-sm text-neutral-700 dark:text-neutral-300">
{t('events.selectExternalFolder', 'Select external folder under /external-media')}
</div>
<ExternalFolderPicker value={externalPath || event.external_path || ''} onChange={setExternalPath} />
+42 -42
View File
@@ -119,11 +119,11 @@ export const EventTypesPage: React.FC = () => {
<div className="mb-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-neutral-900 flex items-center gap-2">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100 flex items-center gap-2">
<Tags className="w-6 h-6" />
{t('eventTypes.title', 'Event Types')}
</h1>
<p className="text-neutral-600 mt-1">
<p className="text-neutral-600 dark:text-neutral-400 mt-1">
{t('eventTypes.subtitle', 'Customize event types and their default themes')}
</p>
</div>
@@ -153,9 +153,9 @@ export const EventTypesPage: React.FC = () => {
type="checkbox"
checked={showInactive}
onChange={(e) => setShowInactive(e.target.checked)}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm text-neutral-700">
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('eventTypes.showInactive', 'Show inactive')}
</span>
</label>
@@ -166,32 +166,32 @@ export const EventTypesPage: React.FC = () => {
<Card>
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-neutral-50 border-b border-neutral-200">
<thead className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase w-10">
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase w-10">
{/* Drag handle column */}
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase">
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase">
{t('eventTypes.table.type', 'Type')}
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase">
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase">
{t('eventTypes.table.slugPrefix', 'URL Prefix')}
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase">
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase">
{t('eventTypes.table.theme', 'Default Theme')}
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase">
<th className="px-4 py-3 text-left text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase">
{t('eventTypes.table.status', 'Status')}
</th>
<th className="px-4 py-3 text-right text-xs font-medium text-neutral-500 uppercase">
<th className="px-4 py-3 text-right text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase">
{t('eventTypes.table.actions', 'Actions')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-neutral-200">
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{filteredTypes.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-neutral-500">
<td colSpan={6} className="px-4 py-8 text-center text-neutral-500 dark:text-neutral-400">
{searchTerm
? t('eventTypes.noResults', 'No event types found')
: t('eventTypes.empty', 'No event types yet')}
@@ -199,7 +199,7 @@ export const EventTypesPage: React.FC = () => {
</tr>
) : (
filteredTypes.map((type) => (
<tr key={type.id} className={`hover:bg-neutral-50 ${!type.is_active ? 'opacity-60' : ''}`}>
<tr key={type.id} className={`hover:bg-neutral-50 dark:hover:bg-neutral-700/50 ${!type.is_active ? 'opacity-60' : ''}`}>
<td className="px-4 py-4">
<GripVertical className="w-4 h-4 text-neutral-400 cursor-grab" />
</td>
@@ -207,9 +207,9 @@ export const EventTypesPage: React.FC = () => {
<div className="flex items-center gap-3">
<span className="text-2xl">{type.emoji}</span>
<div>
<div className="font-medium text-neutral-900">{type.name}</div>
<div className="font-medium text-neutral-900 dark:text-neutral-100">{type.name}</div>
{type.is_system && (
<span className="text-xs text-neutral-500">
<span className="text-xs text-neutral-500 dark:text-neutral-400">
{t('eventTypes.system', 'System')}
</span>
)}
@@ -217,21 +217,21 @@ export const EventTypesPage: React.FC = () => {
</div>
</td>
<td className="px-4 py-4">
<code className="px-2 py-1 bg-neutral-100 rounded text-sm">
<code className="px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-900 dark:text-neutral-100 rounded text-sm">
{type.slug_prefix}
</code>
</td>
<td className="px-4 py-4 text-sm text-neutral-600">
<td className="px-4 py-4 text-sm text-neutral-600 dark:text-neutral-300">
{GALLERY_THEME_PRESETS[type.theme_preset]?.name || type.theme_preset || '-'}
</td>
<td className="px-4 py-4">
{type.is_active ? (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 text-green-700 rounded-full text-xs">
<span className="inline-flex items-center gap-1 px-2 py-1 bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 rounded-full text-xs">
<Eye className="w-3 h-3" />
{t('common.active', 'Active')}
</span>
) : (
<span className="inline-flex items-center gap-1 px-2 py-1 bg-neutral-100 text-neutral-600 rounded-full text-xs">
<span className="inline-flex items-center gap-1 px-2 py-1 bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400 rounded-full text-xs">
<EyeOff className="w-3 h-3" />
{t('common.inactive', 'Inactive')}
</span>
@@ -241,7 +241,7 @@ export const EventTypesPage: React.FC = () => {
<div className="flex items-center justify-end gap-2">
<button
onClick={() => setEditingType(type)}
className="p-2 hover:bg-neutral-100 rounded-lg text-neutral-600 hover:text-primary-600"
className="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg text-neutral-600 dark:text-neutral-400 hover:text-primary-600"
title={t('common.edit', 'Edit')}
>
<Edit className="w-4 h-4" />
@@ -249,7 +249,7 @@ export const EventTypesPage: React.FC = () => {
{!type.is_system && (
<button
onClick={() => setDeleteConfirm(type)}
className="p-2 hover:bg-red-50 rounded-lg text-neutral-600 hover:text-red-600"
className="p-2 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg text-neutral-600 dark:text-neutral-400 hover:text-red-600"
title={t('common.delete', 'Delete')}
>
<Trash2 className="w-4 h-4" />
@@ -266,8 +266,8 @@ export const EventTypesPage: React.FC = () => {
</Card>
{/* Slug Preview Info */}
<div className="mt-4 p-4 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-800">
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-lg border border-blue-200 dark:border-blue-800">
<p className="text-sm text-blue-800 dark:text-blue-200">
<strong>{t('eventTypes.slugInfo.title', 'URL Prefix Info:')}</strong>{' '}
{t('eventTypes.slugInfo.description', 'The URL prefix is used to generate gallery URLs. For example, an event type with prefix "family" will create URLs like: family-smith-family-2025-01-22')}
</p>
@@ -368,17 +368,17 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
<Card className="w-full max-w-lg">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-neutral-900">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{isEditing
? t('eventTypes.edit', 'Edit Event Type')
: t('eventTypes.createNew', 'New Event Type')}
</h2>
<button
onClick={onClose}
className="p-1 hover:bg-neutral-100 rounded-lg"
className="p-1 hover:bg-neutral-100 dark:hover:bg-neutral-700 rounded-lg"
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>
@@ -410,9 +410,9 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
error={errors.slug_prefix}
/>
{form.slug_prefix && (
<p className="mt-1 text-xs text-neutral-500">
<p className="mt-1 text-xs text-neutral-500 dark:text-neutral-400">
{t('eventTypes.form.slugPreview', 'Example URL:')}{' '}
<code className="bg-neutral-100 px-1 rounded">
<code className="bg-neutral-100 dark:bg-neutral-700 px-1 rounded">
{form.slug_prefix}-event-name-2025-01-22
</code>
</p>
@@ -421,7 +421,7 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
{/* Emoji */}
<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('eventTypes.form.emoji', 'Icon')}
</label>
<div className="flex flex-wrap gap-2">
@@ -432,8 +432,8 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
onClick={() => setForm({ ...form, emoji })}
className={`p-2 text-xl rounded-lg border-2 transition-all ${
form.emoji === emoji
? 'border-primary-600 bg-primary-50'
: 'border-neutral-200 hover:border-neutral-300'
? 'border-primary-600 bg-primary-50 dark:bg-primary-900/30'
: 'border-neutral-200 dark:border-neutral-600 hover:border-neutral-300 dark:hover:border-neutral-500'
}`}
>
{emoji}
@@ -444,13 +444,13 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
{/* Theme Preset */}
<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('eventTypes.form.themePreset', 'Default Theme')}
</label>
<select
value={form.theme_preset}
onChange={(e) => setForm({ ...form, theme_preset: 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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
>
{Object.entries(GALLERY_THEME_PRESETS).map(([key, preset]) => (
<option key={key} value={key}>
@@ -467,16 +467,16 @@ const EventTypeModal: React.FC<EventTypeModalProps> = ({
type="checkbox"
checked={eventType?.is_active}
onChange={(e) => onSubmit({ is_active: e.target.checked })}
className="rounded border-neutral-300 text-primary-600 focus:ring-primary-500"
className="rounded border-neutral-300 dark:border-neutral-600 text-primary-600 focus:ring-primary-500"
/>
<span className="text-sm text-neutral-700">
<span className="text-sm text-neutral-700 dark:text-neutral-300">
{t('eventTypes.form.isActive', 'Active (visible in event creation)')}
</span>
</label>
)}
</div>
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-neutral-200">
<div className="flex justify-end gap-3 mt-6 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<Button variant="outline" onClick={onClose} disabled={isLoading}>
{t('common.cancel', 'Cancel')}
</Button>
@@ -512,19 +512,19 @@ const DeleteConfirmModal: React.FC<DeleteConfirmModalProps> = ({
<Card className="w-full max-w-md">
<div className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-red-100 rounded-full">
<AlertTriangle className="w-6 h-6 text-red-600" />
<div className="p-2 bg-red-100 dark:bg-red-900/40 rounded-full">
<AlertTriangle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
<h2 className="text-xl font-semibold text-neutral-900">
<h2 className="text-xl font-semibold text-neutral-900 dark:text-neutral-100">
{t('eventTypes.deleteConfirm.title', 'Delete Event Type')}
</h2>
</div>
<p className="text-neutral-600 mb-4">
<p className="text-neutral-600 dark:text-neutral-400 mb-4">
{t('eventTypes.deleteConfirm.message', 'Are you sure you want to delete')} "{eventType.name}"?
</p>
<p className="text-sm text-neutral-500 bg-neutral-50 p-3 rounded-lg mb-6">
<p className="text-sm text-neutral-500 dark:text-neutral-400 bg-neutral-50 dark:bg-neutral-700 p-3 rounded-lg mb-6">
{t('eventTypes.deleteConfirm.warning', 'This action cannot be undone. Make sure no events are using this type.')}
</p>
+19 -6
View File
@@ -12,9 +12,10 @@ import {
AnalyticsTab,
ModerationTab,
StylingTab,
SEOTab,
} from '../../features/settings';
type TabType = 'general' | 'events' | 'status' | 'security' | 'imageSecurity' | 'categories' | 'analytics' | 'moderation' | 'styling';
type TabType = 'general' | 'events' | 'status' | 'security' | 'imageSecurity' | 'categories' | 'seo' | 'analytics' | 'moderation' | 'styling';
export const SettingsPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('general');
@@ -54,6 +55,9 @@ export const SettingsPage: React.FC = () => {
saveSecurityMutation,
saveAnalyticsMutation,
saveEventSettingsMutation,
seoSettings,
setSeoSettings,
saveSeoMutation,
} = useSettingsState();
if (isLoading) {
@@ -70,6 +74,7 @@ export const SettingsPage: React.FC = () => {
{ key: 'status', label: t('settings.systemStatus.title') },
{ key: 'security', label: t('settings.security.title') },
{ key: 'imageSecurity', label: t('settings.imageSecurity.title', 'Image Protection') },
{ key: 'seo', label: t('settings.seo.title', 'SEO & Robots') },
{ key: 'categories', label: t('settings.categories.title') },
{ key: 'analytics', label: t('settings.analytics.title') },
{ key: 'moderation', label: t('settings.moderation.title', 'Moderation') },
@@ -79,12 +84,12 @@ export const SettingsPage: React.FC = () => {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">{t('settings.title')}</h1>
<p className="text-neutral-600 mt-1">{t('settings.subtitle')}</p>
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">{t('settings.title')}</h1>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('settings.subtitle')}</p>
</div>
{/* Tab Navigation */}
<div className="border-b border-neutral-200 mb-6">
<div className="border-b border-neutral-200 dark:border-neutral-700 mb-6">
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => (
<button
@@ -92,8 +97,8 @@ export const SettingsPage: React.FC = () => {
onClick={() => setActiveTab(tab.key)}
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.key
? 'border-primary-600 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700'
? 'border-primary-600 text-primary-600 dark:text-primary-400'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-200'
}`}
>
{tab.label}
@@ -153,6 +158,14 @@ export const SettingsPage: React.FC = () => {
/>
)}
{activeTab === 'seo' && (
<SEOTab
seoSettings={seoSettings}
setSeoSettings={setSeoSettings}
saveSeoMutation={saveSeoMutation}
/>
)}
{activeTab === 'imageSecurity' && <ImageSecurityTab />}
{activeTab === 'categories' && <CategoriesTab />}
+74 -74
View File
@@ -29,14 +29,14 @@ type TabType = 'users' | 'invitations';
const getRoleBadgeColor = (roleName: string): string => {
switch (roleName?.toLowerCase()) {
case 'super_admin':
return 'bg-red-100 text-red-700 border-red-200';
return 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-300 border-red-200 dark:border-red-800';
case 'admin':
return 'bg-blue-100 text-blue-700 border-blue-200';
return 'bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 border-blue-200 dark:border-blue-800';
case 'editor':
return 'bg-green-100 text-green-700 border-green-200';
return 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800';
case 'viewer':
default:
return 'bg-neutral-100 text-neutral-700 border-neutral-200';
return 'bg-neutral-100 dark:bg-neutral-700 text-neutral-700 dark:text-neutral-300 border-neutral-200 dark:border-neutral-600';
}
};
@@ -97,22 +97,22 @@ const CreateInvitationModal: React.FC<CreateInvitationModalProps> = ({
<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('userManagement.createInvitation')}
</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>
<form onSubmit={handleSubmit}>
<div className="space-y-4">
<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('userManagement.email')}
</label>
<Input
@@ -131,7 +131,7 @@ const CreateInvitationModal: React.FC<CreateInvitationModalProps> = ({
</div>
<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('userManagement.role')}
</label>
<select
@@ -140,7 +140,7 @@ const CreateInvitationModal: React.FC<CreateInvitationModalProps> = ({
setRoleId(e.target.value ? Number(e.target.value) : '');
setErrors((prev) => ({ ...prev, role: undefined }));
}}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:outline-none 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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
disabled={isLoading}
>
<option value="">{t('userManagement.selectRole')}</option>
@@ -226,34 +226,34 @@ const EditUserModal: React.FC<EditUserModalProps> = ({
<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('userManagement.editUser')}
</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-4 p-3 bg-neutral-50 rounded-lg">
<p className="text-sm text-neutral-600">
<div className="mb-4 p-3 bg-neutral-50 dark:bg-neutral-700 rounded-lg">
<p className="text-sm text-neutral-600 dark:text-neutral-300">
{t('userManagement.editingUser')}: <strong>{user.username}</strong>
</p>
<p className="text-sm text-neutral-500">{user.email}</p>
<p className="text-sm text-neutral-500 dark:text-neutral-400">{user.email}</p>
</div>
<form onSubmit={handleSubmit}>
<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('userManagement.role')}
</label>
<select
value={roleId}
onChange={(e) => setRoleId(e.target.value ? Number(e.target.value) : '')}
className="w-full px-3 py-2 border border-neutral-300 rounded-lg focus:outline-none 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 bg-white dark:bg-neutral-800 text-neutral-900 dark:text-neutral-100 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
disabled={isLoading}
>
<option value="">{t('userManagement.selectRole')}</option>
@@ -323,18 +323,18 @@ const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
<div className="flex items-start gap-3 mb-4">
<div
className={`p-2 rounded-full ${
variant === 'danger' ? 'bg-red-100' : 'bg-amber-100'
variant === 'danger' ? 'bg-red-100 dark:bg-red-900/40' : 'bg-amber-100 dark:bg-amber-900/40'
}`}
>
<AlertTriangle
className={`w-5 h-5 ${
variant === 'danger' ? 'text-red-600' : 'text-amber-600'
variant === 'danger' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400'
}`}
/>
</div>
<div>
<h2 className="text-lg font-semibold text-neutral-900">{title}</h2>
<p className="text-sm text-neutral-600 mt-1">{message}</p>
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">{title}</h2>
<p className="text-sm text-neutral-600 dark:text-neutral-400 mt-1">{message}</p>
</div>
</div>
@@ -536,10 +536,10 @@ export const UserManagementPage: React.FC = () => {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('userManagement.title')}
</h1>
<p className="text-neutral-600 mt-1">{t('userManagement.subtitle')}</p>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('userManagement.subtitle')}</p>
</div>
<div className="flex items-center justify-center min-h-[400px]">
<Loading size="lg" text={t('userManagement.loading')} />
@@ -553,10 +553,10 @@ export const UserManagementPage: React.FC = () => {
return (
<div>
<div className="mb-6">
<h1 className="text-2xl font-bold text-neutral-900">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('userManagement.title')}
</h1>
<p className="text-neutral-600 mt-1">{t('userManagement.subtitle')}</p>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('userManagement.subtitle')}</p>
</div>
<div className="text-center py-12">
<p className="text-red-600">{t('userManagement.loadError')}</p>
@@ -582,10 +582,10 @@ export const UserManagementPage: React.FC = () => {
{/* Page Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h1 className="text-2xl font-bold text-neutral-900">
<h1 className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{t('userManagement.title')}
</h1>
<p className="text-neutral-600 mt-1">{t('userManagement.subtitle')}</p>
<p className="text-neutral-600 dark:text-neutral-400 mt-1">{t('userManagement.subtitle')}</p>
</div>
<Button
variant="primary"
@@ -601,10 +601,10 @@ export const UserManagementPage: React.FC = () => {
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('userManagement.stats.totalUsers')}
</p>
<p className="text-2xl font-bold text-neutral-900">
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{users?.length || 0}
</p>
</div>
@@ -615,10 +615,10 @@ export const UserManagementPage: React.FC = () => {
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('userManagement.stats.activeUsers')}
</p>
<p className="text-2xl font-bold text-neutral-900">
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{users?.filter((u) => u.isActive).length || 0}
</p>
</div>
@@ -629,10 +629,10 @@ export const UserManagementPage: React.FC = () => {
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('userManagement.stats.pendingInvitations')}
</p>
<p className="text-2xl font-bold text-neutral-900">
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{invitations?.length || 0}
</p>
</div>
@@ -643,10 +643,10 @@ export const UserManagementPage: React.FC = () => {
<Card padding="sm">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-neutral-600">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
{t('userManagement.stats.inactiveUsers')}
</p>
<p className="text-2xl font-bold text-neutral-900">
<p className="text-2xl font-bold text-neutral-900 dark:text-neutral-100">
{users?.filter((u) => !u.isActive).length || 0}
</p>
</div>
@@ -656,7 +656,7 @@ export const UserManagementPage: React.FC = () => {
</div>
{/* Tab Navigation */}
<div className="border-b border-neutral-200 mb-6">
<div className="border-b border-neutral-200 dark:border-neutral-700 mb-6">
<nav className="-mb-px flex gap-6">
{tabs.map((tab) => (
<button
@@ -665,15 +665,15 @@ export const UserManagementPage: React.FC = () => {
className={`py-2 px-1 border-b-2 font-medium text-sm transition-colors flex items-center gap-2 ${
activeTab === tab.key
? 'border-primary-600 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700'
: 'border-transparent text-neutral-500 dark:text-neutral-400 hover:text-neutral-700 dark:hover:text-neutral-300'
}`}
>
{tab.label}
<span
className={`px-2 py-0.5 text-xs rounded-full ${
activeTab === tab.key
? 'bg-primary-100 text-primary-700'
: 'bg-neutral-100 text-neutral-600'
? 'bg-primary-100 dark:bg-primary-900/40 text-primary-700 dark:text-primary-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-600 dark:text-neutral-400'
}`}
>
{tab.count}
@@ -707,29 +707,29 @@ export const UserManagementPage: React.FC = () => {
<Card className="overflow-visible">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-neutral-50 border-b border-neutral-200">
<thead className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.user')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.role')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.status')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.lastLogin')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-neutral-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('userManagement.table.actions')}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-neutral-200">
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{filteredUsers.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-neutral-500">
<td colSpan={5} className="px-6 py-12 text-center text-neutral-500 dark:text-neutral-400">
{searchTerm
? t('userManagement.noUsersFound')
: t('userManagement.noUsers')}
@@ -737,19 +737,19 @@ export const UserManagementPage: React.FC = () => {
</tr>
) : (
filteredUsers.map((user) => (
<tr key={user.id} className="hover:bg-neutral-50">
<tr key={user.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700/50">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-primary-100 flex items-center justify-center">
<span className="text-primary-700 font-medium text-sm">
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900/40 flex items-center justify-center">
<span className="text-primary-700 dark:text-primary-300 font-medium text-sm">
{user.username.charAt(0).toUpperCase()}
</span>
</div>
<div>
<p className="text-sm font-medium text-neutral-900">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{user.username}
</p>
<p className="text-xs text-neutral-500">{user.email}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400">{user.email}</p>
</div>
</div>
</td>
@@ -767,8 +767,8 @@ export const UserManagementPage: React.FC = () => {
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
user.isActive
? 'bg-green-100 text-green-700'
: 'bg-neutral-100 text-neutral-500'
? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300'
: 'bg-neutral-100 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400'
}`}
>
{user.isActive
@@ -778,14 +778,14 @@ export const UserManagementPage: React.FC = () => {
</td>
<td className="px-6 py-4">
{user.lastLogin ? (
<div className="flex items-center gap-1 text-sm text-neutral-600">
<div className="flex items-center gap-1 text-sm text-neutral-600 dark:text-neutral-300">
<Clock className="w-4 h-4" />
{formatDistanceToNow(parseISO(user.lastLogin), {
addSuffix: true,
})}
</div>
) : (
<span className="text-sm text-neutral-400">
<span className="text-sm text-neutral-400 dark:text-neutral-500">
{t('userManagement.neverLoggedIn')}
</span>
)}
@@ -794,7 +794,7 @@ export const UserManagementPage: React.FC = () => {
<div className="flex items-center justify-end gap-2">
<button
onClick={() => handleEditUser(user)}
className="p-1.5 text-neutral-400 hover:text-primary-600 hover:bg-primary-50 rounded-lg transition-colors"
className="p-1.5 text-neutral-400 hover:text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg transition-colors"
title={t('userManagement.editUser')}
>
<Edit className="w-4 h-4" />
@@ -802,7 +802,7 @@ export const UserManagementPage: React.FC = () => {
{user.isActive && (
<button
onClick={() => handleDeactivateUser(user)}
className="p-1.5 text-neutral-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
className="p-1.5 text-neutral-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
title={t('userManagement.deactivateUser')}
>
<UserX className="w-4 h-4" />
@@ -824,29 +824,29 @@ export const UserManagementPage: React.FC = () => {
<Card className="overflow-visible">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-neutral-50 border-b border-neutral-200">
<thead className="bg-neutral-50 dark:bg-neutral-800 border-b border-neutral-200 dark:border-neutral-700">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.email')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.role')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.invitedBy')}
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-neutral-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('userManagement.table.expires')}
</th>
<th className="px-6 py-3 text-right text-xs font-medium text-neutral-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('userManagement.table.actions')}
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-neutral-200">
<tbody className="bg-white dark:bg-neutral-800 divide-y divide-neutral-200 dark:divide-neutral-700">
{filteredInvitations.length === 0 ? (
<tr>
<td colSpan={5} className="px-6 py-12 text-center text-neutral-500">
<td colSpan={5} className="px-6 py-12 text-center text-neutral-500 dark:text-neutral-400">
{searchTerm
? t('userManagement.noInvitationsFound')
: t('userManagement.noInvitations')}
@@ -856,13 +856,13 @@ export const UserManagementPage: React.FC = () => {
filteredInvitations.map((invitation) => {
const isExpired = isPast(parseISO(invitation.expiresAt));
return (
<tr key={invitation.id} className="hover:bg-neutral-50">
<tr key={invitation.id} className="hover:bg-neutral-50 dark:hover:bg-neutral-700/50">
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center">
<Mail className="w-5 h-5 text-blue-600" />
<div className="w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900/40 flex items-center justify-center">
<Mail className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</div>
<p className="text-sm font-medium text-neutral-900">
<p className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{invitation.email}
</p>
</div>
@@ -877,13 +877,13 @@ export const UserManagementPage: React.FC = () => {
{invitation.roleName}
</span>
</td>
<td className="px-6 py-4 text-sm text-neutral-600">
<td className="px-6 py-4 text-sm text-neutral-600 dark:text-neutral-300">
{invitation.invitedBy || '-'}
</td>
<td className="px-6 py-4">
<span
className={`inline-flex items-center gap-1 text-sm ${
isExpired ? 'text-red-600' : 'text-neutral-600'
isExpired ? 'text-red-600 dark:text-red-400' : 'text-neutral-600 dark:text-neutral-300'
}`}
>
<Clock className="w-4 h-4" />
@@ -897,7 +897,7 @@ export const UserManagementPage: React.FC = () => {
<td className="px-6 py-4 text-right">
<button
onClick={() => handleCancelInvitation(invitation)}
className="p-1.5 text-neutral-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
className="p-1.5 text-neutral-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
title={t('userManagement.cancelInvitation')}
>
<Trash2 className="w-4 h-4" />