From b20db1428d3c1c75541fc3be4edcbe03eff7a8d2 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 20 Apr 2026 21:53:45 +0200 Subject: [PATCH] fix: pre-release UI bug batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Budget table column alignment: the NAME data cell had `display: flex` directly on the , which pulled it out of the table-layout and desynced the column widths between data rows and the AddItemRow. Moved the flex wrapper into a
inside the cell. Closes #759 - Packing list: template-apply and bulk-import handlers called `window.location.reload()` to refresh the list, which re-rendered the whole trip loading screen. Both flows now merge the returned items into the trip store instead. Closes #760 - Journey timeline: move-up / move-down arrows were rendered on skeleton suggestions — skeletons are places from the linked trip and don't participate in sort order. Skip canReorder when entry.type === 'skeleton'. Closes #763 - Journey public view: the synthetic `[Trip Photos]` and `Gallery` entries produced by syncTripPhotos were leaking into the public timeline and map. The owner view already strips these in JourneyDetailPage — apply the same filter on JourneyPublicPage. Gallery photos still come from every entry so a shared gallery keeps showing the trip-synced photos. Closes #764 - Journey thumbnails: public gallery grid was loading the original asset for every tile. `photoUrl()` now takes an optional kind and the grid requests `thumbnail`; the lightbox still opens the original. Synology thumbnail default bumped from `sm` (240px) to `m` (320px) because `sm` looked pixelated on retina. Closes #761 --- client/src/components/Budget/BudgetPanel.tsx | 45 ++++++++++--------- .../Packing/ApplyTemplateButton.tsx | 3 +- .../components/Packing/PackingListPanel.tsx | 5 +-- client/src/pages/JourneyDetailPage.tsx | 6 ++- client/src/pages/JourneyPublicPage.tsx | 21 ++++++--- .../src/services/memories/synologyService.ts | 6 ++- 6 files changed, 52 insertions(+), 34 deletions(-) diff --git a/client/src/components/Budget/BudgetPanel.tsx b/client/src/components/Budget/BudgetPanel.tsx index 1d9616ec..e442a0ff 100644 --- a/client/src/components/Budget/BudgetPanel.tsx +++ b/client/src/components/Budget/BudgetPanel.tsx @@ -900,29 +900,30 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro }} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> - - {canEdit && ( -
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }} - onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }} - style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0 }}> - + +
+ {canEdit && ( +
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }} + onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }} + style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0 }}> + +
+ )} +
+ handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} /> + {hasMultipleMembers && ( +
+ setBudgetItemMembers(tripId, item.id, userIds)} + onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} + compact={false} + readOnly={!canEdit} + /> +
+ )}
- )} -
- handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} /> - {/* Mobile: larger chips under name since Persons column is hidden */} - {hasMultipleMembers && ( -
- setBudgetItemMembers(tripId, item.id, userIds)} - onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} - compact={false} - readOnly={!canEdit} - /> -
- )}
diff --git a/client/src/components/Packing/ApplyTemplateButton.tsx b/client/src/components/Packing/ApplyTemplateButton.tsx index d8466d2c..de0d1289 100644 --- a/client/src/components/Packing/ApplyTemplateButton.tsx +++ b/client/src/components/Packing/ApplyTemplateButton.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react' import { Package } from 'lucide-react' import { adminApi, packingApi } from '../../api/client' +import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' @@ -43,9 +44,9 @@ export default function ApplyTemplateButton({ tripId, style, className }: ApplyT setApplying(true) try { const data = await packingApi.applyTemplate(tripId, templateId) + useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(data.items || [])] })) toast.success(t('packing.templateApplied', { count: data.count })) setOpen(false) - window.location.reload() } catch { toast.error(t('packing.templateError')) } finally { diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx index 9909191b..d1bdb03d 100644 --- a/client/src/components/Packing/PackingListPanel.tsx +++ b/client/src/components/Packing/PackingListPanel.tsx @@ -959,10 +959,9 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, setApplyingTemplate(true) try { const data = await packingApi.applyTemplate(tripId, templateId) + useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(data.items || [])] })) toast.success(t('packing.templateApplied', { count: data.count })) setShowTemplateDropdown(false) - // Reload packing items - window.location.reload() } catch { toast.error(t('packing.templateError')) } finally { @@ -1020,10 +1019,10 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0, if (parsed.length === 0) { toast.error(t('packing.importEmpty')); return } try { const result = await packingApi.bulkImport(tripId, parsed) + useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(result.items || [])] })) toast.success(t('packing.importSuccess', { count: result.count })) setImportText('') setShowImportModal(false) - window.location.reload() } catch { toast.error(t('packing.importError')) } } diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index ba69cd79..c0619a5c 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -595,7 +595,11 @@ export default function JourneyDetailPage() {
{entries.map((entry, idx) => { - const canReorder = !isMobile && canEditEntries && entries.length > 1 + // Skeletons are just "suggested" places pulled + // from the linked trip — they aren't real + // journey entries until the user edits them, + // so reordering them does not make sense. + const canReorder = !isMobile && canEditEntries && entries.length > 1 && entry.type !== 'skeleton' const move = (direction: -1 | 1) => { if (!current) return const target = idx + direction diff --git a/client/src/pages/JourneyPublicPage.tsx b/client/src/pages/JourneyPublicPage.tsx index f096905e..667c6388 100644 --- a/client/src/pages/JourneyPublicPage.tsx +++ b/client/src/pages/JourneyPublicPage.tsx @@ -36,8 +36,8 @@ interface PublicPhoto { caption?: string | null } -function photoUrl(p: PublicPhoto, shareToken: string): string { - return `/api/public/journey/${shareToken}/photos/${p.photo_id}/original` +function photoUrl(p: PublicPhoto, shareToken: string, kind: 'thumbnail' | 'original' = 'original'): string { + return `/api/public/journey/${shareToken}/photos/${p.photo_id}/${kind}` } function formatDate(d: string): { weekday: string; month: string; day: number } { @@ -84,9 +84,20 @@ export default function JourneyPublicPage() { const journey = data?.journey || {} const stats = data?.stats || {} - const groupedEntries = useMemo(() => groupByDate(entries), [entries]) + // `[Trip Photos]` and `Gallery` are synthetic photo-only containers + // produced by the trip→journey sync. They have no story and no + // location, and the owner view strips them from the timeline the + // same way (JourneyDetailPage.tsx). Gallery keeps their photos. + const timelineEntries = useMemo( + () => entries.filter(e => e.title !== '[Trip Photos]' && e.title !== 'Gallery'), + [entries], + ) + const groupedEntries = useMemo(() => groupByDate(timelineEntries), [timelineEntries]) const sortedDates = useMemo(() => [...groupedEntries.keys()].sort(), [groupedEntries]) - const mapEntries = useMemo(() => entries.filter(e => e.location_lat && e.location_lng), [entries]) + const mapEntries = useMemo( + () => timelineEntries.filter(e => e.location_lat && e.location_lng), + [timelineEntries], + ) const allPhotos = useMemo(() => entries.flatMap(e => (e.photos || []).map(p => ({ photo: p, entry: e }))), [entries]) // Set default view based on permissions @@ -312,7 +323,7 @@ export default function JourneyPublicPage() { className="aspect-square rounded-lg overflow-hidden cursor-pointer" onClick={() => setLightbox({ photos: allPhotos.map(({ photo: p }) => ({ id: String(p.id), src: photoUrl(p, token!), caption: p.caption })), index: idx })} > - +
))}
diff --git a/server/src/services/memories/synologyService.ts b/server/src/services/memories/synologyService.ts index f65a2013..efb54547 100644 --- a/server/src/services/memories/synologyService.ts +++ b/server/src/services/memories/synologyService.ts @@ -679,8 +679,10 @@ export async function streamSynologyAsset( //size: 'sm' 240px| 'm' 320px| 'xl' 1280px| 'preview' ? // Use Thumbnail API for both thumbnail and original — avoids serving raw HEIC files - // (original uses xl size to get a full-resolution JPEG-compatible render) - const resolvedSize = kind === 'original' ? 'xl' : (size || 'sm'); + // (original uses xl size to get a full-resolution JPEG-compatible render). + // Thumbnail default is 'm' (~320px) — 'sm' (240px) looked pixelated on + // the journey grid on retina screens. + const resolvedSize = kind === 'original' ? 'xl' : (size || 'm'); const params = new URLSearchParams({ api: 'SYNO.Foto.Thumbnail', method: 'get',