Fix language setting not being saved to database on admin settings page

- Added default_language field to general settings state in SettingsPage
- Replaced LanguageSelector component with simple select dropdown on settings page
- Fixed public settings endpoint to read general_default_language from database
- Language setting now properly saved when clicking Save Settings button
- Setting is correctly used by gallery login page and legal pages

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-08 09:49:45 +02:00
parent cfa0b0da69
commit 2012b0bab9
91 changed files with 6183 additions and 577 deletions
+82 -209
View File
@@ -1,14 +1,16 @@
import React, { useState, useMemo, useEffect } from 'react';
import { Download, Grid, Square, LogOut, Calendar, Clock, Search, SortAsc } from 'lucide-react';
import { format, differenceInDays, parseISO } from 'date-fns';
import React, { useState, useMemo, useEffect, useRef } from 'react';
import { differenceInDays, parseISO } from 'date-fns';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Button, Input, SkeletonGalleryGrid, Skeleton } from '../common';
import { Button, SkeletonGalleryGrid, Skeleton } from '../common';
import { useGalleryAuth, useTheme } from '../../contexts';
import { useGalleryPhotos, useDownloadAllPhotos } from '../../hooks/useGallery';
import { PhotoGrid } from './PhotoGrid';
import { ExpirationBanner } from './ExpirationBanner';
import { CountdownTimer } from './CountdownTimer';
import { GalleryLayout } from './GalleryLayout';
import { PhotoFilterBar } from './PhotoFilterBar';
import { analyticsService } from '../../services/analytics.service';
import { api } from '../../config/api';
@@ -26,13 +28,14 @@ interface GalleryViewProps {
}
export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const { t } = useTranslation();
const { logout } = useGalleryAuth();
const { setTheme } = useTheme();
const [viewMode, setViewMode] = useState<'all' | 'collages' | 'individual'>('all');
const [selectedCategoryId, setSelectedCategoryId] = useState<number | null>(null);
const [searchTerm, setSearchTerm] = useState('');
const [sortBy, setSortBy] = useState<'date' | 'name' | 'size'>('date');
const [showSortMenu, setShowSortMenu] = useState(false);
const [brandingSettings, setBrandingSettings] = useState<any>(null);
const themeAppliedRef = useRef(false);
// Fetch photos
const { data, isLoading, error } = useGalleryPhotos(slug);
@@ -48,40 +51,56 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
// Apply theme and branding settings
// Apply branding settings
useEffect(() => {
if (settingsData) {
// Apply branding settings
setBrandingSettings({
company_name: settingsData.branding_company_name || '',
company_tagline: settingsData.branding_company_tagline || '',
support_email: settingsData.branding_support_email || '',
footer_text: settingsData.branding_footer_text || '© 2024 Your Company. All rights reserved.',
watermark_enabled: settingsData.branding_watermark_enabled || false,
logo_url: settingsData.branding_logo_url || null,
});
// Apply theme settings
if (settingsData.theme_config) {
setTheme(settingsData.theme_config);
}
}
}, [settingsData, setTheme]);
}, [settingsData]);
// Apply event-specific theme if available
// Apply theme only once when component mounts and settings are loaded
useEffect(() => {
if (event.color_theme) {
try {
const eventTheme = JSON.parse(event.color_theme);
console.log('Applying event-specific theme:', eventTheme);
setTheme(eventTheme);
} catch (e) {
console.error('Failed to parse event theme:', e);
if (!themeAppliedRef.current && settingsData) {
let themeToApply = null;
if (event.color_theme) {
try {
// Check if it's a valid JSON string
if (event.color_theme.startsWith('{')) {
const eventTheme = JSON.parse(event.color_theme);
themeToApply = eventTheme;
} else {
// Handle legacy theme names - use global theme
if (settingsData.theme_config) {
themeToApply = settingsData.theme_config;
}
}
} catch (e) {
console.error('Failed to parse event theme:', e);
// Fall back to global theme
if (settingsData.theme_config) {
themeToApply = settingsData.theme_config;
}
}
} else if (settingsData.theme_config) {
// No event theme, use global theme
themeToApply = settingsData.theme_config;
}
// Apply theme only once
if (themeToApply) {
themeAppliedRef.current = true;
setTheme(themeToApply);
}
} else if (settingsData?.theme_config) {
// Fall back to global theme if no event-specific theme
console.log('No event theme, using global theme');
}
}, [event.color_theme, setTheme, settingsData]);
}, [settingsData]); // Only depend on settingsData, not setTheme or event
// Calculate days until expiration
const daysUntilExpiration = differenceInDays(parseISO(event.expires_at), new Date());
@@ -93,11 +112,9 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
let photos = [...data.photos];
// Apply view mode filter
if (viewMode === 'collages') {
photos = photos.filter(photo => photo.type === 'collage');
} else if (viewMode === 'individual') {
photos = photos.filter(photo => photo.type === 'individual');
// Apply category filter
if (selectedCategoryId) {
photos = photos.filter(photo => photo.category_id === selectedCategoryId);
}
// Apply search filter
@@ -122,7 +139,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
});
return photos;
}, [data?.photos, viewMode, searchTerm, sortBy]);
}, [data?.photos, selectedCategoryId, searchTerm, sortBy]);
const handleDownloadAll = () => {
downloadAllMutation.mutate(slug);
@@ -185,9 +202,9 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<div className="text-center">
<p className="text-lg text-neutral-600">Failed to load photos</p>
<p className="text-lg text-neutral-600">{t('gallery.failedToLoad')}</p>
<Button onClick={() => window.location.reload()} className="mt-4">
Try Again
{t('gallery.tryAgain')}
</Button>
</div>
</div>
@@ -195,71 +212,28 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}
return (
<div className="min-h-screen bg-neutral-50">
<GalleryLayout
event={event}
brandingSettings={brandingSettings}
showLogout={true}
onLogout={logout}
showDownloadAll={true}
onDownloadAll={handleDownloadAll}
isDownloading={downloadAllMutation.isPending}
headerExtra={
daysUntilExpiration <= 1 && daysUntilExpiration > 0 ? (
<CountdownTimer expiresAt={event.expires_at} className="mr-4" />
) : null
}
>
{/* Expiration Banner */}
{showUrgentWarning && (
<ExpirationBanner daysRemaining={daysUntilExpiration} expiresAt={event.expires_at} />
)}
{/* Header */}
<header className="bg-white border-b border-neutral-200 sticky top-0 z-40">
<div className="container py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
{/* Company branding */}
{brandingSettings?.company_name && (
<div className="pr-4 border-r border-neutral-200">
<h2 className="text-lg font-semibold text-neutral-800">{brandingSettings.company_name}</h2>
{brandingSettings.company_tagline && (
<p className="text-xs text-neutral-600">{brandingSettings.company_tagline}</p>
)}
</div>
)}
<div>
<h1 className="text-2xl font-bold text-neutral-900">{event.event_name}</h1>
<div className="flex items-center gap-4 mt-1 text-sm text-neutral-600">
<span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" />
{format(parseISO(event.event_date), 'MMMM d, yyyy')}
</span>
<span className="flex items-center">
<Clock className="w-4 h-4 mr-1" />
Expires {format(parseISO(event.expires_at), 'MMM d')}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2">
{daysUntilExpiration <= 1 && daysUntilExpiration > 0 && (
<CountdownTimer expiresAt={event.expires_at} className="mr-4" />
)}
<Button
variant="primary"
size="md"
leftIcon={<Download className="w-4 h-4" />}
onClick={handleDownloadAll}
isLoading={downloadAllMutation.isPending}
className={showUrgentWarning ? 'animate-pulse' : ''}
>
Download All
</Button>
<Button
variant="outline"
size="md"
leftIcon={<LogOut className="w-4 h-4" />}
onClick={logout}
>
Logout
</Button>
</div>
</div>
</div>
</header>
{/* Welcome Message */}
{event.welcome_message && (
<div className="container mt-6">
<div className="mt-6">
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
<p className="text-primary-900">{event.welcome_message}</p>
</div>
@@ -267,125 +241,24 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
)}
{/* Search and Filters */}
<div className="container mt-6">
<div className="flex flex-col lg:flex-row gap-4 mb-6">
{/* Search Bar */}
<div className="flex-1">
<Input
type="text"
placeholder="Search photos by filename..."
leftIcon={<Search className="w-5 h-5 text-neutral-400" />}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* Sort Dropdown */}
<div className="relative">
<Button
variant="outline"
size="md"
leftIcon={<SortAsc className="w-4 h-4" />}
onClick={() => setShowSortMenu(!showSortMenu)}
>
Sort by {sortBy === 'date' ? 'Date' : sortBy === 'name' ? 'Name' : 'Size'}
</Button>
{showSortMenu && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-10">
<button
onClick={() => {
setSortBy('date');
setShowSortMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 ${sortBy === 'date' ? 'text-primary-600 bg-primary-50' : 'text-neutral-700'}`}
>
Sort by Date
</button>
<button
onClick={() => {
setSortBy('name');
setShowSortMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 ${sortBy === 'name' ? 'text-primary-600 bg-primary-50' : 'text-neutral-700'}`}
>
Sort by Name
</button>
<button
onClick={() => {
setSortBy('size');
setShowSortMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-neutral-50 ${sortBy === 'size' ? 'text-primary-600 bg-primary-50' : 'text-neutral-700'}`}
>
Sort by Size
</button>
</div>
)}
</div>
</div>
{/* View Mode Toggle */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Button
variant={viewMode === 'all' ? 'primary' : 'outline'}
size="sm"
onClick={() => setViewMode('all')}
leftIcon={<Grid className="w-4 h-4" />}
>
All Photos ({data.photos.length})
</Button>
<Button
variant={viewMode === 'collages' ? 'primary' : 'outline'}
size="sm"
onClick={() => setViewMode('collages')}
leftIcon={<Square className="w-4 h-4" />}
>
Collages ({data.photos.filter(p => p.type === 'collage').length})
</Button>
<Button
variant={viewMode === 'individual' ? 'primary' : 'outline'}
size="sm"
onClick={() => setViewMode('individual')}
>
Individual ({data.photos.filter(p => p.type === 'individual').length})
</Button>
</div>
<p className="text-sm text-neutral-600">
{filteredPhotos.length} {filteredPhotos.length === 1 ? 'photo' : 'photos'}
</p>
</div>
<div className="mt-6">
<PhotoFilterBar
categories={data.categories}
photos={data.photos}
selectedCategoryId={selectedCategoryId}
onCategoryChange={setSelectedCategoryId}
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
sortBy={sortBy}
onSortChange={setSortBy}
photoCount={filteredPhotos.length}
/>
{/* Photo Grid */}
<PhotoGrid photos={filteredPhotos} slug={slug} />
</div>
{/* Footer */}
<footer className="mt-12 py-8 border-t border-neutral-200">
<div className="container text-center">
{brandingSettings?.support_email && (
<p className="text-sm text-neutral-600 mb-2">
Need help? Contact us at{' '}
<a
href={`mailto:${brandingSettings.support_email}`}
className="text-primary-600 hover:text-primary-700"
>
{brandingSettings.support_email}
</a>
</p>
)}
<p className="text-sm text-neutral-500">
{brandingSettings?.footer_text || '© 2024 Your Company. All rights reserved.'}
</p>
{brandingSettings?.company_name && brandingSettings?.company_tagline && (
<p className="text-xs text-neutral-400 mt-2">
{brandingSettings.company_name} - {brandingSettings.company_tagline}
</p>
)}
<div className="mt-6">
<PhotoGrid photos={filteredPhotos} slug={slug} categoryId={selectedCategoryId} />
</div>
</footer>
</div>
</div>
</GalleryLayout>
);
};