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
+109 -22
View File
@@ -5,6 +5,7 @@ const fs = require('fs').promises;
const { db, logActivity } = require('../database/db');
const { adminAuth } = require('../middleware/auth');
const { generateThumbnail } = require('../services/imageProcessor');
const { generatePhotoFilename } = require('../utils/filenameSanitizer');
const router = express.Router();
// Get storage path from environment or default
@@ -14,7 +15,6 @@ const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '.
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
const { eventId } = req.params;
const { type = 'individual' } = req.body;
try {
// Get event details
@@ -23,9 +23,11 @@ const storage = multer.diskStorage({
return cb(new Error('Event not found'));
}
// Create destination path
const photoType = type === 'collage' ? 'collages' : 'individual';
const destPath = path.join(getStoragePath(), 'events/active', event.slug, photoType);
// Store event in request for use in filename generation
req.eventData = event;
// Create destination path - now just event folder, no type subfolder
const destPath = path.join(getStoragePath(), 'events/active', event.slug);
// Ensure directory exists
await fs.mkdir(destPath, { recursive: true });
@@ -35,12 +37,14 @@ const storage = multer.diskStorage({
cb(error);
}
},
filename: (req, file, cb) => {
// Generate unique filename
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
const name = path.basename(file.originalname, ext);
cb(null, `${name}-${uniqueSuffix}${ext}`);
filename: async (req, file, cb) => {
try {
// Use temporary filename for now, will rename after getting category info
const tempName = `temp_${Date.now()}_${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
cb(null, tempName);
} catch (error) {
cb(error);
}
}
});
@@ -67,7 +71,12 @@ const upload = multer({
router.post('/:eventId/upload', adminAuth, upload.array('photos', 20), async (req, res) => {
try {
const { eventId } = req.params;
const { type = 'individual' } = req.body;
const { category_id } = req.body;
console.log('Upload request received:');
console.log('Body:', req.body);
console.log('Files:', req.files ? req.files.length : 'none');
console.log('Headers:', req.headers);
// Verify event exists and admin has access
const event = await db('events').where({ id: eventId }).first();
@@ -76,40 +85,103 @@ router.post('/:eventId/upload', adminAuth, upload.array('photos', 20), async (re
}
if (!req.files || req.files.length === 0) {
console.log('No files in request. req.files:', req.files);
return res.status(400).json({ error: 'No files uploaded' });
}
// Get category details if provided
let category = null;
if (category_id) {
category = await db('photo_categories').where({ id: category_id }).first();
if (!category) {
return res.status(400).json({ error: 'Invalid category' });
}
}
const uploadedPhotos = [];
// Process each uploaded file
for (const file of req.files) {
let trx;
try {
// Generate thumbnail
// Start transaction for atomic counter update
trx = await db.transaction();
// Get and increment the counter for this category
let counter = 1;
if (category) {
// Lock the category row and get current counter
const categoryData = await trx('photo_categories')
.where({ id: category_id })
.forUpdate()
.first();
counter = (categoryData.photo_counter || 0) + 1;
// Update counter
await trx('photo_categories')
.where({ id: category_id })
.update({ photo_counter: counter });
} else {
// For uncategorized photos, count existing uncategorized photos
const uncategorizedCount = await trx('photos')
.where({ event_id: eventId })
.whereNull('category_id')
.count('id as count')
.first();
counter = (uncategorizedCount.count || 0) + 1;
}
// Generate new filename
const extension = path.extname(file.originalname);
const newFilename = generatePhotoFilename(
event.event_name,
category ? category.name : 'uncategorized',
counter,
extension
);
// Rename the file
const oldPath = file.path;
const newPath = path.join(path.dirname(oldPath), newFilename);
await fs.rename(oldPath, newPath);
// Update file object
file.filename = newFilename;
file.path = newPath;
// Generate thumbnail with new filename
const thumbnailPath = await generateThumbnail(file.path);
// Calculate relative paths
const storagePath = getStoragePath();
const relativePath = path.relative(path.join(storagePath, 'events/active'), file.path);
const relativeThumbPath = thumbnailPath ? path.relative(path.join(storagePath, 'events/active'), thumbnailPath) : null;
const relativeThumbPath = thumbnailPath; // thumbnailPath is already relative to storage root
// Add to database
const [photoId] = await db('photos').insert({
const [photoId] = await trx('photos').insert({
event_id: eventId,
filename: file.filename,
path: relativePath,
thumbnail_path: relativeThumbPath,
type: type === 'collage' ? 'collage' : 'individual',
category_id: category_id || null,
type: 'individual', // Keep for backwards compatibility
size_bytes: file.size
});
// Commit transaction
await trx.commit();
uploadedPhotos.push({
id: photoId,
filename: file.filename,
size: file.size,
type
category_id: category_id || null
});
} catch (error) {
console.error(`Error processing file ${file.filename}:`, error);
if (trx) await trx.rollback();
// Continue with other files
}
}
@@ -187,23 +259,38 @@ router.delete('/:eventId/photos/:photoId', adminAuth, async (req, res) => {
router.get('/:eventId/photos', adminAuth, async (req, res) => {
try {
const { eventId } = req.params;
const { type } = req.query;
const { category_id, type } = req.query;
let query = db('photos').where({ event_id: eventId });
let query = db('photos')
.leftJoin('photo_categories', 'photos.category_id', 'photo_categories.id')
.where({ 'photos.event_id': eventId })
.select(
'photos.*',
'photo_categories.name as category_name',
'photo_categories.slug as category_slug'
);
if (type) {
query = query.where({ type });
if (category_id) {
query = query.where({ 'photos.category_id': category_id });
}
const photos = await query.orderBy('uploaded_at', 'desc');
// Keep type filter for backwards compatibility
if (type) {
query = query.where({ 'photos.type': type });
}
const photos = await query.orderBy('photos.uploaded_at', 'desc');
res.json({
photos: photos.map(photo => ({
id: photo.id,
filename: photo.filename,
url: `/photos/${photo.path}`,
thumbnail_url: photo.thumbnail_path ? `/photos/${photo.thumbnail_path}` : null,
thumbnail_url: photo.thumbnail_path ? `/thumbnails/${photo.thumbnail_path}` : null,
type: photo.type,
category_id: photo.category_id,
category_name: photo.category_name,
category_slug: photo.category_slug,
size: photo.size_bytes,
uploaded_at: photo.uploaded_at
}))