mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
feat(journey): photo provider buttons beside upload in the gallery header
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// FE-JRN-GALLERY-001 to FE-JRN-GALLERY-016
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { act } from 'react'
|
||||
import { act, useRef, useState } from 'react'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render'
|
||||
@@ -73,20 +73,41 @@ const entries: JourneyEntry[] = [
|
||||
},
|
||||
]
|
||||
|
||||
/** Mirrors the page wiring: the gallery reports its connected providers up,
|
||||
and the host renders one button per provider next to Upload. */
|
||||
function GalleryHarness({ gallery, onPhotoClick, onRefresh, onRegisterUpload }: {
|
||||
gallery: GalleryPhoto[]
|
||||
onPhotoClick: (photos: GalleryPhoto[], index: number) => void
|
||||
onRefresh: () => void
|
||||
onRegisterUpload?: (fn: () => void) => void
|
||||
}) {
|
||||
const [providers, setProviders] = useState<{ id: string; name: string }[]>([])
|
||||
const browseRef = useRef<((provider: string) => void) | null>(null)
|
||||
return (
|
||||
<>
|
||||
{providers.map(p => (
|
||||
<button type="button" key={p.id} onClick={() => browseRef.current?.(p.id)}>{p.name}</button>
|
||||
))}
|
||||
<GalleryView
|
||||
entries={entries}
|
||||
gallery={gallery}
|
||||
journeyId={JOURNEY_ID}
|
||||
userId={1}
|
||||
trips={trips}
|
||||
onPhotoClick={onPhotoClick}
|
||||
onRefresh={onRefresh}
|
||||
onRegisterUpload={onRegisterUpload}
|
||||
onRegisterProviders={(p, browse) => { setProviders(p); browseRef.current = browse }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function mountGallery(gallery: GalleryPhoto[], onRegisterUpload?: (fn: () => void) => void) {
|
||||
const onPhotoClick = vi.fn()
|
||||
const onRefresh = vi.fn()
|
||||
const utils = render(
|
||||
<GalleryView
|
||||
entries={entries}
|
||||
gallery={gallery}
|
||||
journeyId={JOURNEY_ID}
|
||||
userId={1}
|
||||
trips={trips}
|
||||
onPhotoClick={onPhotoClick}
|
||||
onRefresh={onRefresh}
|
||||
onRegisterUpload={onRegisterUpload}
|
||||
/>,
|
||||
<GalleryHarness gallery={gallery} onPhotoClick={onPhotoClick} onRefresh={onRefresh} onRegisterUpload={onRegisterUpload} />,
|
||||
)
|
||||
return { ...utils, onPhotoClick, onRefresh }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { RefreshCw, Camera, Image, X, Play } from 'lucide-react'
|
||||
import { RefreshCw, Camera, X, Play } from 'lucide-react'
|
||||
import { normalizeImageFiles } from '../../utils/convertHeic'
|
||||
import { isVideoFile } from '../../utils/videoPoster'
|
||||
import { useJourneyStore } from '../../store/journeyStore'
|
||||
@@ -13,7 +13,7 @@ import { ProviderPicker } from './JourneyDetailPageProviderPicker'
|
||||
import { ScrollTrigger } from './JourneyDetailPageScrollTrigger'
|
||||
import EmptyState from '../shared/EmptyState'
|
||||
|
||||
export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, onRefresh, onRegisterUpload }: {
|
||||
export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, onRefresh, onRegisterUpload, onRegisterProviders }: {
|
||||
entries: JourneyEntry[]
|
||||
gallery: GalleryPhoto[]
|
||||
journeyId: number
|
||||
@@ -22,16 +22,17 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot
|
||||
onPhotoClick: (photos: GalleryPhoto[], index: number) => void
|
||||
onRefresh: () => void
|
||||
onRegisterUpload?: (fn: () => void) => void
|
||||
onRegisterProviders?: (providers: { id: string; name: string }[], browse: (provider: string) => void) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [showPicker, setShowPicker] = useState(false)
|
||||
const [pickerProvider, setPickerProvider] = useState<string | null>(null)
|
||||
const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([])
|
||||
const [galleryProgress, setGalleryProgress] = useState<{ done: number; total: number } | null>(null)
|
||||
const galleryUploading = galleryProgress !== null
|
||||
const toast = useToast()
|
||||
|
||||
// check which providers are enabled AND connected for the current user
|
||||
// check which providers are enabled AND connected for the current user, then
|
||||
// hand them up: the page header renders the provider buttons next to Upload
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
@@ -46,7 +47,7 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot
|
||||
if (status.connected) connected.push({ id: p.id, name: p.name })
|
||||
} catch { }
|
||||
}
|
||||
setAvailableProviders(connected)
|
||||
onRegisterProviders?.(connected, browseProvider)
|
||||
} catch { }
|
||||
})()
|
||||
}, [])
|
||||
@@ -126,23 +127,11 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot
|
||||
<div>
|
||||
<input ref={galleryFileRef} type="file" accept="image/*,video/*" multiple onChange={handleGalleryUpload} className="hidden" />
|
||||
|
||||
{/* Header */}
|
||||
{/* Header — the provider buttons live in the page header next to Upload */}
|
||||
<div className="flex items-center justify-between mb-4 flex-wrap gap-2">
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-[0.07em]" style={{ background: 'var(--vg-surf2)', color: 'var(--vg-ink3)' }}>
|
||||
<Camera size={11} /> {allPhotos.length} {t('journey.detail.photos')}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{availableProviders.map(p => (
|
||||
<button type="button"
|
||||
key={p.id}
|
||||
onClick={() => browseProvider(p.id)}
|
||||
className="inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border border-zinc-200 dark:border-zinc-700 text-[11px] font-medium text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<Image size={12} />
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allPhotos.length === 0 ? (
|
||||
|
||||
@@ -10,7 +10,7 @@ import EmptyState from '../components/shared/EmptyState'
|
||||
import { Outlet } from 'react-router'
|
||||
import {
|
||||
ArrowLeft, MoreHorizontal, List, Grid, MapPin,
|
||||
Plus, ChevronUp, ChevronDown, Eye, EyeOff, BookOpen,
|
||||
Plus, ChevronUp, ChevronDown, Eye, EyeOff, BookOpen, Image,
|
||||
} from 'lucide-react'
|
||||
import MobileMapTimeline from '../components/Journey/MobileMapTimeline'
|
||||
import MobileEntryView from '../components/Journey/MobileEntryView'
|
||||
@@ -44,7 +44,8 @@ function JourneyDetailPageDesktop() {
|
||||
showInvite, setShowInvite, showAddTrip, setShowAddTrip,
|
||||
unlinkTrip, setUnlinkTrip, showSettings, setShowSettings,
|
||||
hideSkeletons, setHideSkeletons,
|
||||
mapRef, fullMapRef, galleryUploadRef, activeLocationId, handleMarkerClick, handleLocationClick,
|
||||
mapRef, fullMapRef, galleryUploadRef, galleryProviders, setGalleryProviders, galleryBrowseRef,
|
||||
activeLocationId, handleMarkerClick, handleLocationClick,
|
||||
mapEntries, sidebarMapItems, tripDates, isMobile, tracks,
|
||||
feedEdge, scrollFeedTo,
|
||||
loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto,
|
||||
@@ -346,14 +347,27 @@ function JourneyDetailPageDesktop() {
|
||||
</button>
|
||||
)}
|
||||
{canEditEntries && view === 'gallery' && (
|
||||
<button type="button"
|
||||
onClick={() => galleryUploadRef.current?.()}
|
||||
className="inline-flex items-center gap-1.5 h-9 px-4 rounded-full text-[13px] font-semibold transition-transform hover:-translate-y-0.5"
|
||||
style={{ background: 'var(--vg-ink)', color: 'var(--vg-bg)' }}
|
||||
>
|
||||
<Plus size={16} strokeWidth={2.4} />
|
||||
{t('common.upload')}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
{galleryProviders.map(p => (
|
||||
<button type="button"
|
||||
key={p.id}
|
||||
onClick={() => galleryBrowseRef.current?.(p.id)}
|
||||
className="inline-flex items-center gap-1.5 h-9 px-4 rounded-full text-[13px] font-semibold transition-transform hover:-translate-y-0.5"
|
||||
style={{ background: 'var(--vg-ink)', color: 'var(--vg-bg)' }}
|
||||
>
|
||||
<Image size={16} strokeWidth={2.4} />
|
||||
{p.name}
|
||||
</button>
|
||||
))}
|
||||
<button type="button"
|
||||
onClick={() => galleryUploadRef.current?.()}
|
||||
className="inline-flex items-center gap-1.5 h-9 px-4 rounded-full text-[13px] font-semibold transition-transform hover:-translate-y-0.5"
|
||||
style={{ background: 'var(--vg-ink)', color: 'var(--vg-bg)' }}
|
||||
>
|
||||
<Plus size={16} strokeWidth={2.4} />
|
||||
{t('common.upload')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -452,6 +466,7 @@ function JourneyDetailPageDesktop() {
|
||||
>
|
||||
<GalleryView
|
||||
onRegisterUpload={(fn) => { galleryUploadRef.current = fn }}
|
||||
onRegisterProviders={(providers, browse) => { setGalleryProviders(providers); galleryBrowseRef.current = browse }}
|
||||
entries={current.entries}
|
||||
gallery={current.gallery || []}
|
||||
journeyId={current.id}
|
||||
|
||||
@@ -31,6 +31,11 @@ export function useJourneyDetail() {
|
||||
const fullMapRef = useRef<JourneyMapHandle>(null)
|
||||
/** The gallery hands its file picker up here, so the hero button can open it. */
|
||||
const galleryUploadRef = useRef<(() => void) | null>(null)
|
||||
/** Same deal for photo providers: the gallery probes which ones are
|
||||
connected and reports them up, so the header can render an Immich/
|
||||
Synology button next to Upload. */
|
||||
const [galleryProviders, setGalleryProviders] = useState<{ id: string; name: string }[]>([])
|
||||
const galleryBrowseRef = useRef<((provider: string) => void) | null>(null)
|
||||
const [activeLocationId, setActiveLocationId] = useState<string | null>(null)
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
@@ -384,7 +389,8 @@ export function useJourneyDetail() {
|
||||
showInvite, setShowInvite, showAddTrip, setShowAddTrip,
|
||||
unlinkTrip, setUnlinkTrip, showSettings, setShowSettings,
|
||||
hideSkeletons, setHideSkeletons,
|
||||
mapRef, fullMapRef, galleryUploadRef, activeLocationId, handleMarkerClick, handleLocationClick,
|
||||
mapRef, fullMapRef, galleryUploadRef, galleryProviders, setGalleryProviders, galleryBrowseRef,
|
||||
activeLocationId, handleMarkerClick, handleLocationClick,
|
||||
mapEntries, sidebarMapItems, tripDates, isMobile, tracks,
|
||||
feedEdge, scrollFeedTo,
|
||||
loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto,
|
||||
|
||||
Reference in New Issue
Block a user