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 <[email protected]>
This commit is contained in:
2025-07-08 09:49:45 +02:00
co-authored by Claude
parent cfa0b0da69
commit 2012b0bab9
91 changed files with 6183 additions and 577 deletions
+153 -43
View File
@@ -1,23 +1,44 @@
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useParams, Link } from 'react-router-dom';
import { Camera, Calendar, AlertCircle, Clock } from 'lucide-react';
import { format, differenceInDays, parseISO } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import { Card, CardContent, Input, Button, Loading } from '../components/common';
import { useGalleryAuth } from '../contexts';
import { useGalleryInfo } from '../hooks/useGallery';
import { GalleryView } from '../components/gallery';
import { analyticsService } from '../services/analytics.service';
import { api } from '../config/api';
export const GalleryPage: React.FC = () => {
const { slug, token } = useParams<{ slug: string; token?: string }>();
const { isAuthenticated, login, event } = useGalleryAuth();
const { t, i18n } = useTranslation();
const [password, setPassword] = useState('');
const [isLoggingIn, setIsLoggingIn] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
// Fetch gallery info (public data)
const { data: galleryInfo, isLoading: isLoadingInfo, error: infoError } = useGalleryInfo(slug!, token);
// Fetch branding settings
const { data: settingsData } = useQuery({
queryKey: ['gallery-settings'],
queryFn: async () => {
const response = await api.get('/api/public/settings');
return response.data;
},
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
// Set language from admin settings when on login page
React.useEffect(() => {
if (!isAuthenticated && settingsData?.default_language) {
i18n.changeLanguage(settingsData.default_language);
}
}, [settingsData, isAuthenticated, i18n]);
// Calculate days until expiration
const daysUntilExpiration = galleryInfo
@@ -27,7 +48,7 @@ export const GalleryPage: React.FC = () => {
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!password.trim()) {
setLoginError('Please enter a password');
setLoginError(t('auth.pleaseEnterPassword'));
return;
}
@@ -42,7 +63,7 @@ export const GalleryPage: React.FC = () => {
success: true
});
} catch (error: any) {
setLoginError(error.response?.data?.error || 'Invalid password');
setLoginError(error.response?.data?.error || t('auth.invalidPassword'));
// Track failed password entry
analyticsService.trackGalleryEvent('password_entry', {
@@ -57,8 +78,10 @@ export const GalleryPage: React.FC = () => {
// Show loading state
if (isLoadingInfo) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<Loading size="lg" text="Loading gallery..." />
<div className="min-h-screen bg-neutral-50">
<div className="min-h-screen flex items-center justify-center">
<Loading size="lg" text={t('gallery.loading')} />
</div>
</div>
);
}
@@ -66,16 +89,50 @@ export const GalleryPage: React.FC = () => {
// Show error state
if (infoError) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<Card className="max-w-md w-full mx-4">
<CardContent className="text-center py-12">
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Gallery Not Found</h2>
<p className="text-neutral-600">
This gallery does not exist or has been removed.
</p>
</CardContent>
</Card>
<div className="min-h-screen bg-neutral-50">
<div className="min-h-screen flex flex-col">
{/* Logo at top */}
{settingsData?.branding_logo_url && (
<div className="p-8 text-center">
<img
src={settingsData.branding_logo_url}
alt={settingsData.branding_company_name || 'Company Logo'}
className="h-16 w-auto object-contain mx-auto"
/>
</div>
)}
<div className="flex-1 flex items-center justify-center">
<Card className="max-w-md w-full mx-4">
<CardContent className="text-center py-12">
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">{t('errors.galleryNotFound')}</h2>
<p className="text-neutral-600">
{t('errors.galleryNotFoundMessage')}
</p>
</CardContent>
</Card>
</div>
{/* Legal Links */}
<div className="p-8 text-center">
<div className="flex items-center justify-center gap-4">
<Link
to="/impressum"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.impressum')}
</Link>
<span className="text-xs text-neutral-400">|</span>
<Link
to="/datenschutz"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.datenschutz')}
</Link>
</div>
</div>
</div>
</div>
);
}
@@ -83,19 +140,53 @@ export const GalleryPage: React.FC = () => {
// Show expired state
if (galleryInfo?.is_expired) {
return (
<div className="min-h-screen bg-neutral-50 flex items-center justify-center">
<Card className="max-w-md w-full mx-4">
<CardContent className="text-center py-12">
<Clock className="w-16 h-16 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">Gallery Expired</h2>
<p className="text-neutral-600 mb-4">
This gallery expired on {format(parseISO(galleryInfo.expires_at), 'MMMM d, yyyy')}.
</p>
<p className="text-sm text-neutral-500">
Please contact the event organizer if you need access to these photos.
</p>
</CardContent>
</Card>
<div className="min-h-screen bg-neutral-50">
<div className="min-h-screen flex flex-col">
{/* Logo at top */}
{settingsData?.branding_logo_url && (
<div className="p-8 text-center">
<img
src={settingsData.branding_logo_url}
alt={settingsData.branding_company_name || 'Company Logo'}
className="h-16 w-auto object-contain mx-auto"
/>
</div>
)}
<div className="flex-1 flex items-center justify-center">
<Card className="max-w-md w-full mx-4">
<CardContent className="text-center py-12">
<Clock className="w-16 h-16 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold mb-2">{t('gallery.expired')}</h2>
<p className="text-neutral-600 mb-4">
{t('gallery.expiredOn', { date: format(parseISO(galleryInfo.expires_at), 'MMMM d, yyyy') })}
</p>
<p className="text-sm text-neutral-500">
{t('gallery.contactOrganizer')}
</p>
</CardContent>
</Card>
</div>
{/* Legal Links */}
<div className="p-8 text-center">
<div className="flex items-center justify-center gap-4">
<Link
to="/impressum"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.impressum')}
</Link>
<span className="text-xs text-neutral-400">|</span>
<Link
to="/datenschutz"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.datenschutz')}
</Link>
</div>
</div>
</div>
</div>
);
}
@@ -112,9 +203,17 @@ export const GalleryPage: React.FC = () => {
<div className="w-full max-w-md">
{/* Logo/Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-primary-600 rounded-2xl mb-4">
<Camera className="w-10 h-10 text-white" />
</div>
{settingsData?.branding_logo_url ? (
<img
src={settingsData.branding_logo_url}
alt={settingsData.branding_company_name || 'Company Logo'}
className="h-20 w-auto object-contain mx-auto mb-4"
/>
) : (
<div className="inline-flex items-center justify-center w-20 h-20 bg-primary-600 rounded-2xl mb-4">
<Camera className="w-10 h-10 text-white" />
</div>
)}
<h1 className="text-3xl font-bold text-neutral-900 mb-2">
{galleryInfo?.event_name}
</h1>
@@ -131,10 +230,10 @@ export const GalleryPage: React.FC = () => {
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5 mr-2 flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-800">
Gallery expires in {daysUntilExpiration} {daysUntilExpiration === 1 ? 'day' : 'days'}
{t('gallery.expiresIn', { count: daysUntilExpiration })}
</p>
<p className="text-xs text-amber-700 mt-1">
Download your photos before they're no longer available.
{t('gallery.downloadBefore')}
</p>
</div>
</div>
@@ -144,13 +243,13 @@ export const GalleryPage: React.FC = () => {
{/* Login Card */}
<Card>
<CardContent className="p-6">
<h2 className="text-xl font-semibold mb-6">Enter Gallery Password</h2>
<h2 className="text-xl font-semibold mb-6">{t('auth.enterPassword')}</h2>
<form onSubmit={handleLogin} className="space-y-4">
<Input
type="password"
label="Password"
placeholder="Enter the gallery password"
label={t('auth.password')}
placeholder={t('auth.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
error={loginError || undefined}
@@ -165,22 +264,33 @@ export const GalleryPage: React.FC = () => {
isLoading={isLoggingIn}
disabled={isLoggingIn}
>
View Gallery
{t('gallery.viewGallery')}
</Button>
</form>
<p className="text-xs text-neutral-500 text-center mt-6">
The password was provided by the event organizer.
Contact them if you don't have it.
{t('auth.passwordHint')}
</p>
</CardContent>
</Card>
{/* Event Type Badge */}
{/* Legal Links */}
<div className="text-center mt-6">
<span className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
{galleryInfo?.event_type}
</span>
<div className="flex items-center justify-center gap-4">
<a
href="/impressum"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.impressum')}
</a>
<span className="text-xs text-neutral-400">|</span>
<a
href="/datenschutz"
className="text-xs text-neutral-500 hover:text-neutral-700 transition-colors"
>
{t('legal.datenschutz')}
</a>
</div>
</div>
</div>
</div>