mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
v3.0.19 Bug Fixes (#992)
* fix(mcp): replace relative oauth constent redirect by absolute redirect derived from APP_URL (#987) * feat(journey): convert HEIC/HEIF uploads to JPEG for cross-platform compatibility HEIC is an Apple-only format not recognised as an image by many browsers and platforms. heic-to (lazy-loaded) now converts HEIC/HEIF files to JPEG before upload in both the gallery and entry editor photo pickers. Embedded metadata (EXIF, GPS) may be lost during conversion — documented in the Journey Journal wiki page. * fix(journey): skip heic-to import for non-HEIC files to avoid test env failures * fix(notifications): prevent double-escaping HTML in password reset emails buildPasswordResetHtml passed a pre-built HTML block to buildEmailHtml, which then escaped it again — rendering raw tags as plain text in the email.
This commit is contained in:
Generated
+7
@@ -11,6 +11,7 @@
|
|||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dexie": "^4.4.2",
|
"dexie": "^4.4.2",
|
||||||
|
"heic-to": "^1.4.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"mapbox-gl": "^3.22.0",
|
"mapbox-gl": "^3.22.0",
|
||||||
@@ -5827,6 +5828,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/hsl-to-hex": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"@react-pdf/renderer": "^4.3.2",
|
"@react-pdf/renderer": "^4.3.2",
|
||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dexie": "^4.4.2",
|
"dexie": "^4.4.2",
|
||||||
|
"heic-to": "^1.4.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"lucide-react": "^0.344.0",
|
"lucide-react": "^0.344.0",
|
||||||
"mapbox-gl": "^3.22.0",
|
"mapbox-gl": "^3.22.0",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
import { useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||||
import { formatLocationName } from '../utils/formatters'
|
import { formatLocationName } from '../utils/formatters'
|
||||||
|
import { normalizeImageFiles } from '../utils/convertHeic'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { useJourneyStore } from '../store/journeyStore'
|
import { useJourneyStore } from '../store/journeyStore'
|
||||||
@@ -1027,8 +1028,9 @@ function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick,
|
|||||||
if (!files?.length) return
|
if (!files?.length) return
|
||||||
setGalleryUploading(true)
|
setGalleryUploading(true)
|
||||||
try {
|
try {
|
||||||
|
const normalized = await normalizeImageFiles(files)
|
||||||
const formData = new FormData()
|
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)
|
await journeyApi.uploadGalleryPhotos(journeyId, formData)
|
||||||
toast.success(t('journey.photosUploaded', { count: files.length }))
|
toast.success(t('journey.photosUploaded', { count: files.length }))
|
||||||
onRefresh()
|
onRefresh()
|
||||||
@@ -2265,7 +2267,8 @@ function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, onClose, onSa
|
|||||||
if (!files?.length) return
|
if (!files?.length) return
|
||||||
// Queue files locally until Save so cancel/close actually discards. This
|
// Queue files locally until Save so cancel/close actually discards. This
|
||||||
// keeps photo behavior consistent with text fields — no silent persistence.
|
// 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 (
|
return (
|
||||||
|
|||||||
@@ -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<File> {
|
||||||
|
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<File[]> {
|
||||||
|
return Promise.all(Array.from(files).map(normalizeImageFile))
|
||||||
|
}
|
||||||
@@ -147,7 +147,8 @@ export const trekOAuthProvider: OAuthServerProvider = {
|
|||||||
if (params.state) qs.set('state', params.state);
|
if (params.state) qs.set('state', params.state);
|
||||||
if (params.resource) qs.set('resource', params.resource.href);
|
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.
|
// Not called because skipLocalPkceValidation = true.
|
||||||
|
|||||||
@@ -316,12 +316,12 @@ export function getEventText(lang: string, event: NotifEventType, params: Record
|
|||||||
|
|
||||||
// ── Email HTML builder ─────────────────────────────────────────────────────
|
// ── 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 s = I18N[lang] || I18N.en;
|
||||||
const appUrl = getAppUrl();
|
const appUrl = getAppUrl();
|
||||||
const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || ''));
|
const ctaHref = escapeHtml(navigateTarget ? `${appUrl}${navigateTarget}` : (appUrl || ''));
|
||||||
const safeSubject = escapeHtml(subject);
|
const safeSubject = escapeHtml(subject);
|
||||||
const safeBody = escapeHtml(body);
|
const safeBody = rawBody ? body : escapeHtml(body);
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
@@ -396,7 +396,7 @@ function buildPasswordResetHtml(subject: string, strings: PasswordResetStrings,
|
|||||||
<p style="margin:0 0 10px 0; font-size:13px; color:#6B7280;">${safeExpiry}</p>
|
<p style="margin:0 0 10px 0; font-size:13px; color:#6B7280;">${safeExpiry}</p>
|
||||||
<p style="margin:0; font-size:13px; color:#6B7280;">${safeIgnore}</p>
|
<p style="margin:0; font-size:13px; color:#6B7280;">${safeIgnore}</p>
|
||||||
`;
|
`;
|
||||||
return buildEmailHtml(subject, block, lang);
|
return buildEmailHtml(subject, block, lang, undefined, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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.
|
- **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.
|
- **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.
|
- **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").
|
- **Tags** — free-form labels (e.g. "hidden gem", "best meal").
|
||||||
- **Location** — pin the entry to a map location.
|
- **Location** — pin the entry to a map location.
|
||||||
|
|||||||
Reference in New Issue
Block a user