diff --git a/frontend/src/components/gallery/UserPhotoUpload.tsx b/frontend/src/components/gallery/UserPhotoUpload.tsx index 0f4ffef9..4d57f787 100644 --- a/frontend/src/components/gallery/UserPhotoUpload.tsx +++ b/frontend/src/components/gallery/UserPhotoUpload.tsx @@ -5,7 +5,7 @@ import { toast } from 'react-toastify'; import { Button } from '../common'; import { api } from '../../config/api'; import { usePublicSettings } from '../../hooks/usePublicSettings'; -import { extensionsToMimeTypes, extensionsToAcceptString } from '../../utils/fileTypes'; +import { extensionsToMimeTypes, extensionsToAcceptString, getSupportedExtensions } from '../../utils/fileTypes'; interface UserPhotoUploadProps { eventId: number; @@ -61,6 +61,11 @@ export const UserPhotoUpload: React.FC = ({ [publicSettings?.allowed_file_types] ); + const allowedExtensions = useMemo( + () => getSupportedExtensions(publicSettings?.allowed_file_types), + [publicSettings?.allowed_file_types] + ); + // Shared filter pipeline for both change and drag-and-drop (#504). const addFiles = (incoming: File[]) => { const validFiles = incoming.filter((file) => { @@ -233,10 +238,11 @@ export const UserPhotoUpload: React.FC = ({ {t('upload.clickToUpload')}

- {/* #613 — pass { limit } so `{{limit}}` interpolates - with the real number from settings instead of - rendering literally. */} - {t('upload.fileRequirements', { limit: maxFilesPerUpload, sizeLimit: maxFileSizeMb })} + {t('upload.fileRequirements', { + types: allowedExtensions.map(extension => extension.toUpperCase()).join(', '), + limit: maxFilesPerUpload, + sizeLimit: maxFileSizeMb, + })}

{ + it('keeps configured supported extensions for the upload hint', () => { + expect(getSupportedExtensions('jpg, mov, mp4, .webp')).toEqual([ + 'jpg', + 'mov', + 'mp4', + 'webp', + ]); + }); + + it('uses the same configured types for validation and file selection', () => { + expect(extensionsToMimeTypes('jpg,jpeg,mov,mp4')).toEqual([ + 'image/jpeg', + 'video/quicktime', + 'video/mp4', + ]); + expect(extensionsToAcceptString('jpg,jpeg,mov,mp4')).toBe( + 'image/jpeg,video/quicktime,video/mp4' + ); + }); + + it('falls back to the default formats when no configured types are supported', () => { + expect(getSupportedExtensions('dng,unknown')).toEqual([ + 'jpg', + 'jpeg', + 'png', + 'webp', + ]); + }); +}); diff --git a/frontend/src/utils/fileTypes.ts b/frontend/src/utils/fileTypes.ts index ba5514c9..46091dd8 100644 --- a/frontend/src/utils/fileTypes.ts +++ b/frontend/src/utils/fileTypes.ts @@ -16,27 +16,30 @@ const EXTENSION_TO_MIME: Record = { const DEFAULT_ALLOWED = 'jpg,jpeg,png,webp'; +/** + * Return the configured supported extensions, excluding values the upload + * pipeline cannot validate. Falls back to the default list when none match. + */ +export function getSupportedExtensions(extString?: string | null): string[] { + const input = extString?.trim() || DEFAULT_ALLOWED; + const extensions = input + .split(',') + .map(ext => ext.trim().toLowerCase().replace(/^\./, '')) + .filter(ext => Boolean(EXTENSION_TO_MIME[ext])); + + return extensions.length > 0 + ? Array.from(new Set(extensions)) + : getSupportedExtensions(DEFAULT_ALLOWED); +} + /** * Convert a comma-separated extension string (e.g. "jpg,png,mp4") to an * array of unique MIME types. */ export function extensionsToMimeTypes(extString?: string | null): string[] { - const input = extString?.trim() || DEFAULT_ALLOWED; - const mimeSet = new Set(); - - input.split(',').forEach(ext => { - const cleaned = ext.trim().toLowerCase().replace(/^\./, ''); - const mime = EXTENSION_TO_MIME[cleaned]; - if (mime) { - mimeSet.add(mime); - } - }); - - if (mimeSet.size === 0) { - return extensionsToMimeTypes(DEFAULT_ALLOWED); - } - - return Array.from(mimeSet); + return Array.from(new Set( + getSupportedExtensions(extString).map(extension => EXTENSION_TO_MIME[extension]) + )); } /**