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
+63 -28
View File
@@ -1,28 +1,48 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Download, Maximize2, Check, Package } from 'lucide-react';
import { useInView } from 'react-intersection-observer';
import { toast } from 'react-toastify';
import { toast as toastify } from 'react-toastify';
import { useTranslation } from 'react-i18next';
import type { Photo } from '../../types';
import { useDownloadPhoto } from '../../hooks/useGallery';
import { PhotoLightbox } from './PhotoLightbox';
import { Button } from '../common';
import { Button, AuthenticatedImage } from '../common';
import { galleryService } from '../../services/gallery.service';
import { analyticsService } from '../../services/analytics.service';
interface PhotoGridProps {
photos: Photo[];
slug: string;
categoryId?: number | null;
}
export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug, categoryId }) => {
const { t } = useTranslation();
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null);
const [selectedPhotos, setSelectedPhotos] = useState<Set<number>>(new Set());
const [isSelectionMode, setIsSelectionMode] = useState(false);
const downloadPhotoMutation = useDownloadPhoto();
const handlePhotoClick = (index: number) => {
if (isSelectionMode) {
// Clear selection when category changes
useEffect(() => {
setSelectedPhotos(new Set());
}, [categoryId]);
const handlePhotoClick = (index: number, e?: React.MouseEvent) => {
// Check for ctrl/cmd+click for quick selection
if (e && (e.ctrlKey || e.metaKey)) {
if (!isSelectionMode) {
setIsSelectionMode(true);
}
const newSelected = new Set(selectedPhotos);
if (newSelected.has(photos[index].id)) {
newSelected.delete(photos[index].id);
} else {
newSelected.add(photos[index].id);
}
setSelectedPhotos(newSelected);
} else if (isSelectionMode) {
const newSelected = new Set(selectedPhotos);
if (newSelected.has(photos[index].id)) {
newSelected.delete(photos[index].id);
@@ -66,7 +86,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
const selectedPhotosList = photos.filter(p => selectedPhotos.has(p.id));
toast.info(`Downloading ${selectedPhotos.size} photos...`);
toastify.info(t('gallery.downloading', { count: selectedPhotos.size }));
// Download each selected photo
const downloadPromises = selectedPhotosList.map(photo =>
@@ -79,7 +99,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
try {
await Promise.all(downloadPromises);
toast.success(`Downloaded ${selectedPhotos.size} photos!`);
toastify.success(t('gallery.downloadedPhotos', { count: selectedPhotos.size }));
// Track bulk download
analyticsService.trackGalleryEvent('bulk_download', {
@@ -91,14 +111,14 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
setSelectedPhotos(new Set());
setIsSelectionMode(false);
} catch (error) {
toast.error('Some photos failed to download');
toastify.error(t('gallery.downloadError'));
}
};
if (photos.length === 0) {
return (
<div className="text-center py-12">
<p className="text-neutral-600">No photos found</p>
<p className="text-neutral-600">{t('gallery.noPhotosFound')}</p>
</div>
);
}
@@ -108,24 +128,39 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
{/* Selection Mode Controls */}
{photos.length > 1 && (
<div className="mb-4 flex items-center justify-between">
<Button
variant="outline"
size="sm"
onClick={toggleSelectionMode}
>
{isSelectionMode ? 'Cancel Selection' : 'Select Photos'}
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={toggleSelectionMode}
title={t('gallery.selectPhotosHint')}
>
{isSelectionMode ? t('gallery.cancelSelection') : t('gallery.selectPhotos')}
</Button>
{!isSelectionMode && (
<Button
variant="ghost"
size="sm"
onClick={() => {
setIsSelectionMode(true);
selectAll();
}}
>
{t('gallery.selectAll')}
</Button>
)}
</div>
{isSelectionMode && (
<div className="flex items-center gap-2">
<span className="text-sm text-neutral-600">
{selectedPhotos.size} selected
{t('gallery.photosSelected', { count: selectedPhotos.size })}
</span>
<Button variant="ghost" size="sm" onClick={selectAll}>
Select All
{t('gallery.selectAll')}
</Button>
<Button variant="ghost" size="sm" onClick={deselectAll}>
Deselect All
{t('gallery.deselectAll')}
</Button>
{selectedPhotos.size > 0 && (
<Button
@@ -134,7 +169,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
leftIcon={<Package className="w-4 h-4" />}
onClick={handleDownloadSelected}
>
Download {selectedPhotos.size} Selected
{t('gallery.downloadSelected', { count: selectedPhotos.size })}
</Button>
)}
</div>
@@ -150,7 +185,7 @@ export const PhotoGrid: React.FC<PhotoGridProps> = ({ photos, slug }) => {
photo={photo}
isSelected={selectedPhotos.has(photo.id)}
isSelectionMode={isSelectionMode}
onClick={() => handlePhotoClick(index)}
onClick={(e) => handlePhotoClick(index, e)}
onDownload={(e) => handleDownload(photo, e)}
/>
))}
@@ -173,7 +208,7 @@ interface PhotoThumbnailProps {
photo: Photo;
isSelected: boolean;
isSelectionMode: boolean;
onClick: () => void;
onClick: (e: React.MouseEvent) => void;
onDownload: (e: React.MouseEvent) => void;
}
@@ -192,12 +227,12 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
return (
<div
ref={ref}
className="relative group cursor-pointer"
onClick={onClick}
className="relative group cursor-pointer aspect-square"
onClick={(e) => onClick(e)}
>
{inView ? (
<>
<img
<AuthenticatedImage
src={photo.thumbnail_url || photo.url}
alt={photo.filename}
className="w-full h-full object-cover rounded-lg transition-transform duration-200 group-hover:scale-105"
@@ -212,7 +247,7 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
onClick={(e) => {
e.stopPropagation();
onClick();
onClick(e);
}}
aria-label="View full size"
>
@@ -232,7 +267,7 @@ const PhotoThumbnail: React.FC<PhotoThumbnailProps> = ({
{/* Selection checkbox */}
{isSelectionMode && (
<div className={`absolute top-2 right-2 ${isSelected ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}>
<div className={`w-6 h-6 rounded-full border-2 ${isSelected ? 'bg-primary-600 border-primary-600' : 'bg-white/80 border-white'} flex items-center justify-center`}>
<div className={`w-6 h-6 rounded-full border-2 ${isSelected ? 'bg-primary-600 border-primary-600' : 'bg-white/80 border-white'} flex items-center justify-center transition-colors`}>
{isSelected && <Check className="w-4 h-4 text-white" />}
</div>
</div>