feat: add category hero/cover photo selection (#163)
Wire up the hero_photo_id column on photo_categories that was added in the migration but never connected. Backend routes now accept and persist hero_photo_id on category create/update, a dedicated PUT /:id/hero endpoint is added, and the gallery API returns hero_photo_id for each category. Frontend EventCategoryManager shows a clickable thumbnail per category that opens a photo picker modal. Includes EN/DE i18n keys.
This commit is contained in:
@@ -106,42 +106,53 @@ router.post('/', adminAuth, requirePermission('settings.edit'), [
|
||||
|
||||
// Update a category
|
||||
router.put('/:id', adminAuth, requirePermission('settings.edit'), [
|
||||
body('name').notEmpty().withMessage('Category name is required')
|
||||
body('name').notEmpty().withMessage('Category name is required'),
|
||||
body('hero_photo_id').optional({ nullable: true }).custom((value) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
return Number.isInteger(Number(value));
|
||||
}).withMessage('hero_photo_id must be an integer or null')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
|
||||
const { id } = req.params;
|
||||
const { name } = req.body;
|
||||
|
||||
const { name, hero_photo_id } = req.body;
|
||||
|
||||
const category = await db('photo_categories').where('id', id).first();
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
}
|
||||
|
||||
|
||||
const updateData = {
|
||||
name,
|
||||
slug: name.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim()
|
||||
};
|
||||
|
||||
// Update hero_photo_id if provided (including null to clear it)
|
||||
if (Object.prototype.hasOwnProperty.call(req.body, 'hero_photo_id')) {
|
||||
updateData.hero_photo_id = hero_photo_id || null;
|
||||
}
|
||||
|
||||
await db('photo_categories')
|
||||
.where('id', id)
|
||||
.update({
|
||||
name,
|
||||
slug: name.toLowerCase()
|
||||
.replace(/[^\w\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.trim()
|
||||
});
|
||||
|
||||
.update(updateData);
|
||||
|
||||
const updated = await db('photo_categories').where('id', id).first();
|
||||
|
||||
|
||||
// Log activity
|
||||
await logActivity('category_updated',
|
||||
{ categoryName: name },
|
||||
{ categoryName: name, heroPhotoId: hero_photo_id },
|
||||
category.event_id,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating category:', error);
|
||||
@@ -149,6 +160,55 @@ router.put('/:id', adminAuth, requirePermission('settings.edit'), [
|
||||
}
|
||||
});
|
||||
|
||||
// Set category hero photo (#163)
|
||||
router.put('/:id/hero', adminAuth, requirePermission('settings.edit'), [
|
||||
body('hero_photo_id').optional({ nullable: true }).custom((value) => {
|
||||
if (value === null || value === undefined) return true;
|
||||
return Number.isInteger(Number(value));
|
||||
}).withMessage('hero_photo_id must be an integer or null')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({ errors: errors.array() });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const { hero_photo_id } = req.body;
|
||||
|
||||
const category = await db('photo_categories').where('id', id).first();
|
||||
if (!category) {
|
||||
return res.status(404).json({ error: 'Category not found' });
|
||||
}
|
||||
|
||||
// If hero_photo_id is provided, verify it belongs to a photo in this category
|
||||
if (hero_photo_id) {
|
||||
const photo = await db('photos').where('id', hero_photo_id).first();
|
||||
if (!photo) {
|
||||
return res.status(404).json({ error: 'Photo not found' });
|
||||
}
|
||||
}
|
||||
|
||||
await db('photo_categories')
|
||||
.where('id', id)
|
||||
.update({ hero_photo_id: hero_photo_id || null });
|
||||
|
||||
const updated = await db('photo_categories').where('id', id).first();
|
||||
|
||||
// Log activity
|
||||
await logActivity('category_hero_updated',
|
||||
{ categoryName: category.name, heroPhotoId: hero_photo_id },
|
||||
category.event_id,
|
||||
{ type: 'admin', id: req.admin.id, name: req.admin.username }
|
||||
);
|
||||
|
||||
res.json(updated);
|
||||
} catch (error) {
|
||||
console.error('Error updating category hero:', error);
|
||||
res.status(500).json({ error: 'Failed to update category hero' });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete a category
|
||||
router.delete('/:id', adminAuth, requirePermission('settings.edit'), async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -632,7 +632,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
|
||||
body('event_name').optional().trim().notEmpty(),
|
||||
body('admin_email').optional().isEmail(),
|
||||
body('is_active').optional().isBoolean(),
|
||||
body('expires_at').optional().isISO8601(),
|
||||
body('expires_at').optional({ nullable: true, checkFalsy: true }).isISO8601(),
|
||||
body('welcome_message').optional({ nullable: true, checkFalsy: true }).trim(),
|
||||
body('color_theme').optional({ nullable: true }),
|
||||
body('allow_user_uploads').optional().isBoolean(),
|
||||
@@ -794,6 +794,17 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
|
||||
updates.password_hash = await bcrypt.hash(crypto.randomBytes(32).toString('hex'), getBcryptRounds());
|
||||
}
|
||||
|
||||
// Enforce expires_at requirement based on app settings
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'expires_at')) {
|
||||
if (!updates.expires_at) {
|
||||
const fieldReqs = await getEventFieldRequirements();
|
||||
if (fieldReqs.require_expiration) {
|
||||
return res.status(400).json({ error: 'Expiration date is required.' });
|
||||
}
|
||||
updates.expires_at = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Format hero logo settings if provided
|
||||
if (Object.prototype.hasOwnProperty.call(updates, 'hero_logo_visible')) {
|
||||
updates.hero_logo_visible = formatBoolean(updates.hero_logo_visible);
|
||||
|
||||
@@ -309,14 +309,15 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
||||
if (usedCategoryIds.length > 0) {
|
||||
const categoryDetails = await db('photo_categories')
|
||||
.whereIn('id', usedCategoryIds)
|
||||
.select('id', 'name', 'slug', 'is_global')
|
||||
.select('id', 'name', 'slug', 'is_global', 'hero_photo_id')
|
||||
.orderBy('name', 'asc');
|
||||
|
||||
categories = categoryDetails.map(cat => ({
|
||||
id: cat.id,
|
||||
name: cat.name,
|
||||
slug: cat.slug,
|
||||
is_global: cat.is_global
|
||||
is_global: cat.is_global,
|
||||
hero_photo_id: cat.hero_photo_id || null
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user