fix: resolve code quality issues and add missing i18n keys (#162, #163)

Add missing i18n translations for hero image focal point picker in both
EN and DE locales. Fix lint errors across touched files: remove unused
imports/variables, replace raw buttons with shared Button component,
eliminate inline styles, extract duplicated backend validation, and
remove dead heroImagePosition type.
This commit is contained in:
Paul Nothaft
2026-02-03 10:53:13 +01:00
parent 734868abc2
commit 329d224846
8 changed files with 48 additions and 42 deletions
+16 -24
View File
@@ -1,5 +1,5 @@
const express = require('express');
const { body, query, validationResult } = require('express-validator');
const { body, validationResult } = require('express-validator');
const { db, logActivity } = require('../database/db');
const { formatBoolean } = require('../utils/dbCompat');
const { adminAuth } = require('../middleware/auth');
@@ -17,10 +17,20 @@ const { escapeLikePattern } = require('../utils/sqlSecurity');
const { validatePasswordInContext, getBcryptRounds } = require('../utils/passwordValidation');
const logger = require('../utils/logger');
const { buildShareLinkVariants } = require('../services/shareLinkService');
const { parseBooleanInput, parseStringInput, parseJsonInput } = require('../utils/parsers');
const { parseBooleanInput, parseStringInput } = require('../utils/parsers');
const eventTypeService = require('../services/eventTypeService');
const { validateFileType } = require('../utils/fileSecurityUtils');
// Shared validator for hero_image_anchor accepts legacy keywords or "X% Y%" focal point
const validateHeroImageAnchor = (value) => {
if (['top', 'center', 'bottom'].includes(value)) return true;
if (typeof value === 'string' && /^\d{1,3}%\s+\d{1,3}%$/.test(value)) {
const [x, y] = value.split(/\s+/).map(v => parseInt(v));
if (x >= 0 && x <= 100 && y >= 0 && y <= 100) return true;
}
throw new Error('Must be top, center, bottom, or "X% Y%" (0-100)');
};
// Get storage path from environment or default
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
@@ -198,14 +208,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
body('header_style').optional().isIn(['hero', 'standard', 'minimal', 'none']),
body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']),
// Hero image anchor position (#162) accepts legacy keywords or "X% Y%" focal point
body('hero_image_anchor').optional().custom((value) => {
if (['top', 'center', 'bottom'].includes(value)) return true;
if (typeof value === 'string' && /^\d{1,3}%\s+\d{1,3}%$/.test(value)) {
const [x, y] = value.split(/\s+/).map(v => parseInt(v));
if (x >= 0 && x <= 100 && y >= 0 && y <= 100) return true;
}
throw new Error('Must be top, center, bottom, or "X% Y%" (0-100)');
})
body('hero_image_anchor').optional().custom(validateHeroImageAnchor)
], async (req, res) => {
try {
logger.debug('Create event request body', { body: req.body });
@@ -313,10 +316,6 @@ router.post('/', adminAuth, requirePermission('events.create'), [
}
}
// Get event type info for slug generation
const eventTypeInfo = await eventTypeService.getEventTypeForSlug(event_type);
const slugPrefix = eventTypeInfo.slug_prefix || event_type;
// Generate unique slug
const processedEventName = event_name
.toLowerCase()
@@ -337,7 +336,7 @@ router.post('/', adminAuth, requirePermission('events.create'), [
// Generate share link respecting configured format
const shareToken = crypto.randomBytes(16).toString('hex');
const { sharePath, shareUrl, shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
const { shareUrl, shareLinkToStore } = await buildShareLinkVariants({ slug, shareToken });
// Hash password with configurable rounds (random placeholder when not required)
const password_hash = requirePassword
@@ -665,7 +664,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
body('overlay_protection').optional().isBoolean(),
body('image_quality').optional().isInt({ min: 1, max: 100 }),
body('fragmentation_level').optional().isInt({ min: 1, max: 10 }),
body('password').optional().isString().custom((value, { req }) => {
body('password').optional().isString().custom((value) => {
if (value === undefined || value === null || value === '') {
return true;
}
@@ -683,14 +682,7 @@ router.put('/:id', adminAuth, requirePermission('events.edit'), [
body('header_style').optional().isIn(['hero', 'standard', 'minimal', 'none']),
body('hero_divider_style').optional().isIn(['wave', 'straight', 'angle', 'curve', 'none']),
// Hero image anchor position (#162) accepts legacy keywords or "X% Y%" focal point
body('hero_image_anchor').optional().custom((value) => {
if (['top', 'center', 'bottom'].includes(value)) return true;
if (typeof value === 'string' && /^\d{1,3}%\s+\d{1,3}%$/.test(value)) {
const [x, y] = value.split(/\s+/).map(v => parseInt(v));
if (x >= 0 && x <= 100 && y >= 0 && y <= 100) return true;
}
throw new Error('Must be top, center, bottom, or "X% Y%" (0-100)');
})
body('hero_image_anchor').optional().custom(validateHeroImageAnchor)
], async (req, res) => {
try {
const errors = validationResult(req);
+1 -1
View File
@@ -795,7 +795,7 @@ router.get('/:slug/photo/:photoId',
if (range) {
// Parse range header
const parts = range.replace(/bytes=/, "").split("-");
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = (end - start) + 1;