diff --git a/client/package-lock.json b/client/package-lock.json index f829027e..7ddbaaa2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", @@ -5827,6 +5828,12 @@ "dev": true, "license": "MIT" }, + "node_modules/heic-to": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/heic-to/-/heic-to-1.4.2.tgz", + "integrity": "sha512-y69thwxfNcEm2Vk8lbOD/cMabnvMJyOREfJYiCHcXCDqlfcPyJoBhyRc8+iDe1B95LRfpbTOpzxzY1xbRkdwBA==", + "license": "LGPL-3.0" + }, "node_modules/hsl-to-hex": { "version": "1.0.0", "license": "MIT", diff --git a/client/package.json b/client/package.json index ab8585b7..80def49d 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@react-pdf/renderer": "^4.3.2", "axios": "^1.6.7", "dexie": "^4.4.2", + "heic-to": "^1.4.2", "leaflet": "^1.9.4", "lucide-react": "^0.344.0", "mapbox-gl": "^3.22.0", diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index e7aa51f9..360247e7 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState, useRef, useCallback, useMemo } from 'react' import { formatLocationName } from '../utils/formatters' +import { normalizeImageFiles } from '../utils/convertHeic' import { createPortal } from 'react-dom' import { useParams, useNavigate } from 'react-router-dom' import { useJourneyStore } from '../store/journeyStore' @@ -1027,8 +1028,9 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, if (!files?.length) return setGalleryUploading(true) try { + const normalized = await normalizeImageFiles(files) const formData = new FormData() - for (const f of files) formData.append('photos', f) + for (const f of normalized) formData.append('photos', f) await journeyApi.uploadGalleryPhotos(journeyId, formData) toast.success(t('journey.photosUploaded', { count: files.length })) onRefresh() @@ -2265,7 +2267,8 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa if (!files?.length) return // Queue files locally until Save so cancel/close actually discards. This // keeps photo behavior consistent with text fields — no silent persistence. - setPendingFiles(prev => [...prev, ...Array.from(files)]) + const normalized = await normalizeImageFiles(files) + setPendingFiles(prev => [...prev, ...normalized]) } return ( diff --git a/client/src/utils/convertHeic.ts b/client/src/utils/convertHeic.ts new file mode 100644 index 00000000..6f757792 --- /dev/null +++ b/client/src/utils/convertHeic.ts @@ -0,0 +1,17 @@ +function looksLikeHeic(file: File): boolean { + const ext = file.name.split('.').pop()?.toLowerCase() ?? '' + return ext === 'heic' || ext === 'heif' || file.type === 'image/heic' || file.type === 'image/heif' +} + +export async function normalizeImageFile(file: File): Promise { + if (!looksLikeHeic(file)) return file + const { isHeic, heicTo } = await import('heic-to') + if (!(await isHeic(file))) return file + const blob = await heicTo({ blob: file, type: 'image/jpeg', quality: 0.92 }) + const jpegName = file.name.replace(/\.(heic|heif)$/i, '.jpg') + return new File([blob], jpegName, { type: 'image/jpeg' }) +} + +export async function normalizeImageFiles(files: FileList | File[]): Promise { + return Promise.all(Array.from(files).map(normalizeImageFile)) +} diff --git a/server/src/mcp/oauthProvider.ts b/server/src/mcp/oauthProvider.ts index 943a515f..592fd282 100644 --- a/server/src/mcp/oauthProvider.ts +++ b/server/src/mcp/oauthProvider.ts @@ -147,7 +147,8 @@ export const trekOAuthProvider: OAuthServerProvider = { if (params.state) qs.set('state', params.state); if (params.resource) qs.set('resource', params.resource.href); - res.redirect(302, `/oauth/consent?${qs.toString()}`); + const base = getMcpSafeUrl().replace(/\/+$/, ''); + res.redirect(302, `${base}/oauth/consent?${qs.toString()}`); }, // Not called because skipLocalPkceValidation = true. diff --git a/server/src/services/notifications.ts b/server/src/services/notifications.ts index ecc9a3bc..94b6b34c 100644 --- a/server/src/services/notifications.ts +++ b/server/src/services/notifications.ts @@ -316,12 +316,12 @@ export function getEventText(lang: string, event: NotifEventType, params: Record // ── Email HTML builder ───────────────────────────────────────────────────── -export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string): string { +export function buildEmailHtml(subject: string, body: string, lang: string, navigateTarget?: string, rawBody = false): string { const s = I18N[lang] || I18N.en; const appUrl = getAppUrl(); const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || '')); const safeSubject = escapeHtml(subject); - const safeBody = escapeHtml(body); + const safeBody = rawBody ? body : escapeHtml(body); return ` @@ -396,7 +396,7 @@ function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings,

${safeExpiry}

${safeIgnore}

`; - return buildEmailHtml(subject, block, lang); + return buildEmailHtml(subject, block, lang, undefined, true); } /** diff --git a/wiki/Journey-Journal.md b/wiki/Journey-Journal.md index 5dd39145..ceea0ca7 100644 --- a/wiki/Journey-Journal.md +++ b/wiki/Journey-Journal.md @@ -37,6 +37,7 @@ Each entry corresponds to a day in your journey. The entry editor provides: - **Weather** — choose one of six values: Sunny, Partly cloudy, Cloudy, Rainy, Stormy, Cold. - **Photos** — attach photos to the entry. The first photo becomes the card thumbnail in list views. + > **Note on HEIC files:** HEIC is an Apple-only format that many browsers and platforms do not recognise as an image. To ensure broad compatibility, HEIC/HEIF files are automatically converted to JPEG before upload. This conversion may result in the loss of embedded metadata (EXIF data such as GPS coordinates, camera information, etc.). - **Pros / Cons** — optional verdict cards. Add items to a **Pros** list (thumbs-up) or a **Cons** list (thumbs-down) to summarise what you loved or what could have been better. These are stored in the `pros_cons.pros` and `pros_cons.cons` arrays on the entry. - **Tags** — free-form labels (e.g. "hidden gem", "best meal"). - **Location** — pin the entry to a map location.