mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix: pre-release UI bug batch
- Budget table column alignment: the NAME data cell had `display: flex` directly on the <td>, 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 <div> 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
This commit is contained in:
@@ -900,7 +900,8 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
|
||||||
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
|
||||||
<td style={{ ...td, display: 'flex', alignItems: 'center', gap: 4 }}>
|
<td style={td}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<div draggable onDragStart={e => { e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }}
|
<div draggable onDragStart={e => { e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }}
|
||||||
onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }}
|
onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }}
|
||||||
@@ -910,7 +911,6 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
|
|||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<InlineEditCell value={item.name} onSave={v => 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} />
|
<InlineEditCell value={item.name} onSave={v => 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 && (
|
{hasMultipleMembers && (
|
||||||
<div className="sm:hidden" style={{ marginTop: 4 }}>
|
<div className="sm:hidden" style={{ marginTop: 4 }}>
|
||||||
<BudgetMemberChips
|
<BudgetMemberChips
|
||||||
@@ -924,6 +924,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td style={{ ...td, textAlign: 'center' }}>
|
<td style={{ ...td, textAlign: 'center' }}>
|
||||||
<InlineEditCell value={item.total_price} type="number" decimals={currencyDecimals(currency)} onSave={v => handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder={currencyDecimals(currency) === 0 ? '0' : '0,00'} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} />
|
<InlineEditCell value={item.total_price} type="number" decimals={currencyDecimals(currency)} onSave={v => handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder={currencyDecimals(currency) === 0 ? '0' : '0,00'} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} />
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import { Package } from 'lucide-react'
|
import { Package } from 'lucide-react'
|
||||||
import { adminApi, packingApi } from '../../api/client'
|
import { adminApi, packingApi } from '../../api/client'
|
||||||
|
import { useTripStore } from '../../store/tripStore'
|
||||||
import { useToast } from '../shared/Toast'
|
import { useToast } from '../shared/Toast'
|
||||||
import { useTranslation } from '../../i18n'
|
import { useTranslation } from '../../i18n'
|
||||||
|
|
||||||
@@ -43,9 +44,9 @@ export default function ApplyTemplateButton({ tripId, style, className }: ApplyT
|
|||||||
setApplying(true)
|
setApplying(true)
|
||||||
try {
|
try {
|
||||||
const data = await packingApi.applyTemplate(tripId, templateId)
|
const data = await packingApi.applyTemplate(tripId, templateId)
|
||||||
|
useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(data.items || [])] }))
|
||||||
toast.success(t('packing.templateApplied', { count: data.count }))
|
toast.success(t('packing.templateApplied', { count: data.count }))
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
window.location.reload()
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('packing.templateError'))
|
toast.error(t('packing.templateError'))
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -959,10 +959,9 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
|
|||||||
setApplyingTemplate(true)
|
setApplyingTemplate(true)
|
||||||
try {
|
try {
|
||||||
const data = await packingApi.applyTemplate(tripId, templateId)
|
const data = await packingApi.applyTemplate(tripId, templateId)
|
||||||
|
useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(data.items || [])] }))
|
||||||
toast.success(t('packing.templateApplied', { count: data.count }))
|
toast.success(t('packing.templateApplied', { count: data.count }))
|
||||||
setShowTemplateDropdown(false)
|
setShowTemplateDropdown(false)
|
||||||
// Reload packing items
|
|
||||||
window.location.reload()
|
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(t('packing.templateError'))
|
toast.error(t('packing.templateError'))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1020,10 +1019,10 @@ export default function PackingListPanel({ tripId, items, openImportSignal = 0,
|
|||||||
if (parsed.length === 0) { toast.error(t('packing.importEmpty')); return }
|
if (parsed.length === 0) { toast.error(t('packing.importEmpty')); return }
|
||||||
try {
|
try {
|
||||||
const result = await packingApi.bulkImport(tripId, parsed)
|
const result = await packingApi.bulkImport(tripId, parsed)
|
||||||
|
useTripStore.setState(s => ({ packingItems: [...s.packingItems, ...(result.items || [])] }))
|
||||||
toast.success(t('packing.importSuccess', { count: result.count }))
|
toast.success(t('packing.importSuccess', { count: result.count }))
|
||||||
setImportText('')
|
setImportText('')
|
||||||
setShowImportModal(false)
|
setShowImportModal(false)
|
||||||
window.location.reload()
|
|
||||||
} catch { toast.error(t('packing.importError')) }
|
} catch { toast.error(t('packing.importError')) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -595,7 +595,11 @@ export default function JourneyDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{entries.map((entry, idx) => {
|
{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) => {
|
const move = (direction: -1 | 1) => {
|
||||||
if (!current) return
|
if (!current) return
|
||||||
const target = idx + direction
|
const target = idx + direction
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ interface PublicPhoto {
|
|||||||
caption?: string | null
|
caption?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
function photoUrl(p: PublicPhoto, shareToken: string): string {
|
function photoUrl(p: PublicPhoto, shareToken: string, kind: 'thumbnail' | 'original' = 'original'): string {
|
||||||
return `/api/public/journey/${shareToken}/photos/${p.photo_id}/original`
|
return `/api/public/journey/${shareToken}/photos/${p.photo_id}/${kind}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(d: string): { weekday: string; month: string; day: number } {
|
function formatDate(d: string): { weekday: string; month: string; day: number } {
|
||||||
@@ -84,9 +84,20 @@ export default function JourneyPublicPage() {
|
|||||||
const journey = data?.journey || {}
|
const journey = data?.journey || {}
|
||||||
const stats = data?.stats || {}
|
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 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])
|
const allPhotos = useMemo(() => entries.flatMap(e => (e.photos || []).map(p => ({ photo: p, entry: e }))), [entries])
|
||||||
|
|
||||||
// Set default view based on permissions
|
// Set default view based on permissions
|
||||||
@@ -312,7 +323,7 @@ export default function JourneyPublicPage() {
|
|||||||
className="aspect-square rounded-lg overflow-hidden cursor-pointer"
|
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 })}
|
onClick={() => setLightbox({ photos: allPhotos.map(({ photo: p }) => ({ id: String(p.id), src: photoUrl(p, token!), caption: p.caption })), index: idx })}
|
||||||
>
|
>
|
||||||
<img src={photoUrl(photo, token!)} className="w-full h-full object-cover hover:scale-105 transition-transform" alt="" loading="lazy" />
|
<img src={photoUrl(photo, token!, 'thumbnail')} className="w-full h-full object-cover hover:scale-105 transition-transform" alt="" loading="lazy" />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -679,8 +679,10 @@ export async function streamSynologyAsset(
|
|||||||
|
|
||||||
//size: 'sm' 240px| 'm' 320px| 'xl' 1280px| 'preview' ?
|
//size: 'sm' 240px| 'm' 320px| 'xl' 1280px| 'preview' ?
|
||||||
// Use Thumbnail API for both thumbnail and original — avoids serving raw HEIC files
|
// 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)
|
// (original uses xl size to get a full-resolution JPEG-compatible render).
|
||||||
const resolvedSize = kind === 'original' ? 'xl' : (size || 'sm');
|
// 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({
|
const params = new URLSearchParams({
|
||||||
api: 'SYNO.Foto.Thumbnail',
|
api: 'SYNO.Foto.Thumbnail',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
|||||||
Reference in New Issue
Block a user