From 9fc984bea395f69d01ea2ba465d21308dfaaed7f Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Sun, 8 Feb 2026 12:56:50 -0800 Subject: [PATCH] Fix photo upload on mobile devices - Add HEIC/HEIF support for iPhone camera photos - Make frontend file type validation more permissive for mobile browsers - Add 'capture' attribute to prefer rear camera on mobile - Include actual file extension in error messages for debugging Co-Authored-By: Claude Opus 4.5 --- api/menu/uploadHeader.cfm | 11 ++++++++--- api/menu/uploadItemPhoto.cfm | 22 ++++++++++++++-------- portal/menu-builder.html | 21 ++++++++++++--------- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/api/menu/uploadHeader.cfm b/api/menu/uploadHeader.cfm index 3c9c605..980f259 100644 --- a/api/menu/uploadHeader.cfm +++ b/api/menu/uploadHeader.cfm @@ -64,14 +64,19 @@ if (bizId LTE 0) { - - + + - #serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })# + #serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic)" })# + + + + + diff --git a/api/menu/uploadItemPhoto.cfm b/api/menu/uploadItemPhoto.cfm index e17c340..3150fa0 100644 --- a/api/menu/uploadItemPhoto.cfm +++ b/api/menu/uploadItemPhoto.cfm @@ -26,17 +26,23 @@ if (itemId LTE 0) { - - - + + + + - #serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp)" })# + #serializeJSON({ "OK": false, "ERROR": "invalid_type", "MESSAGE": "Only image files are accepted (jpg, jpeg, gif, png, webp, heic). Got: #actualExt#" })# + + + + + -for (ext in listToArray(allowedExtensions)) { +for (ext in listToArray("jpg,jpeg,gif,png,webp,heic,heif")) { oldFile = "#itemsDir#/#itemId#.#ext#"; if (fileExists(oldFile)) { try { fileDelete(oldFile); } catch (any e) {} @@ -44,15 +50,15 @@ for (ext in listToArray(allowedExtensions)) { } - - + + #serializeJSON({ "OK": true, "ERROR": "", "MESSAGE": "Photo uploaded successfully", - "IMAGEURL": "/uploads/items/#itemId#.#uploadResult.ClientFileExt#" + "IMAGEURL": "/uploads/items/#itemId#.#actualExt#" })# diff --git a/portal/menu-builder.html b/portal/menu-builder.html index 5cc9652..a24ae32 100644 --- a/portal/menu-builder.html +++ b/portal/menu-builder.html @@ -2819,14 +2819,17 @@ const input = document.createElement('input'); input.type = 'file'; - input.accept = 'image/jpeg,image/png,image/gif,image/webp'; + input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif'; + input.capture = 'environment'; // Prefer rear camera on mobile input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; - const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if (!validTypes.includes(file.type)) { - this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error'); + // Be permissive with file types - server will validate + // Mobile cameras may report inconsistent MIME types + const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', '']; + if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) { + this.toast('Please select a valid image file', 'error'); return; } if (file.size > 5 * 1024 * 1024) { @@ -3115,15 +3118,15 @@ uploadHeader() { const input = document.createElement('input'); input.type = 'file'; - input.accept = 'image/jpeg,image/png,image/gif,image/webp'; + input.accept = 'image/jpeg,image/png,image/gif,image/webp,image/heic,image/heif,.heic,.heif'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; - // Validate file type - const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; - if (!validTypes.includes(file.type)) { - this.toast('Please select a valid image (JPG, PNG, GIF, or WebP)', 'error'); + // Validate file type (be permissive for mobile cameras) + const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/heic', 'image/heif', '']; + if (file.type && !validTypes.includes(file.type) && !file.type.startsWith('image/')) { + this.toast('Please select a valid image file', 'error'); return; }