mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
refactor: remove EXIF metadata from photo lightbox
EXIF was only available for Immich photos and inconsistent for local uploads. Removed entirely for now — cleaner lightbox with just photo, nav, counter, and caption. Nav buttons now show on hover (desktop) and always on mobile.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { ChevronLeft, ChevronRight, X, Camera, Aperture } from 'lucide-react'
|
||||
import apiClient from '../../api/client'
|
||||
import { ChevronLeft, ChevronRight, X } from 'lucide-react'
|
||||
|
||||
interface LightboxPhoto {
|
||||
id: string
|
||||
@@ -11,16 +10,6 @@ interface LightboxPhoto {
|
||||
owner_id?: number | null
|
||||
}
|
||||
|
||||
interface ExifData {
|
||||
camera?: string
|
||||
lens?: string
|
||||
focalLength?: string
|
||||
aperture?: string
|
||||
shutter?: string
|
||||
iso?: number
|
||||
fileName?: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
photos: LightboxPhoto[]
|
||||
startIndex?: number
|
||||
@@ -29,8 +18,6 @@ interface Props {
|
||||
|
||||
export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props) {
|
||||
const [idx, setIdx] = useState(startIndex)
|
||||
const [exif, setExif] = useState<ExifData | null>(null)
|
||||
const [exifLoading, setExifLoading] = useState(false)
|
||||
const touchStart = useRef<{ x: number; y: number } | null>(null)
|
||||
|
||||
const photo = photos[idx]
|
||||
@@ -50,32 +37,6 @@ export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [prev, next, onClose])
|
||||
|
||||
// Fetch EXIF data for Immich photos
|
||||
useEffect(() => {
|
||||
setExif(null)
|
||||
if (!photo || photo.provider !== 'immich' || !photo.asset_id || !photo.owner_id) return
|
||||
let cancelled = false
|
||||
setExifLoading(true)
|
||||
apiClient.get(`/integrations/memories/immich/assets/0/${photo.asset_id}/${photo.owner_id}/info`)
|
||||
.then(r => {
|
||||
if (!cancelled && r.data) {
|
||||
const d = r.data
|
||||
const parts: Partial<ExifData> = {}
|
||||
if (d.camera && d.camera.trim() && d.camera !== 'undefined undefined') parts.camera = d.camera
|
||||
if (d.lens) parts.lens = d.lens
|
||||
if (d.focalLength) parts.focalLength = d.focalLength
|
||||
if (d.aperture) parts.aperture = d.aperture
|
||||
if (d.shutter) parts.shutter = d.shutter
|
||||
if (d.iso) parts.iso = d.iso
|
||||
if (d.fileName) parts.fileName = d.fileName
|
||||
if (Object.keys(parts).length > 0) setExif(parts)
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { if (!cancelled) setExifLoading(false) })
|
||||
return () => { cancelled = true }
|
||||
}, [photo])
|
||||
|
||||
const onTouchStart = (e: React.TouchEvent) => {
|
||||
const t = e.touches[0]
|
||||
touchStart.current = { x: t.clientX, y: t.clientY }
|
||||
@@ -112,99 +73,77 @@ export default function PhotoLightbox({ photos, startIndex = 0, onClose }: Props
|
||||
onTouchStart={onTouchStart}
|
||||
onTouchEnd={onTouchEnd}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', flexShrink: 0 }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13 }}>
|
||||
{idx + 1} / {photos.length}
|
||||
</span>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%',
|
||||
width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', cursor: 'pointer',
|
||||
}}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{/* Photo area — centered with nav overlays */}
|
||||
<div
|
||||
className="group/lightbox"
|
||||
style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}
|
||||
>
|
||||
{/* Top bar */}
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, zIndex: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px 20px' }}>
|
||||
<span style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13, fontWeight: 500 }}>
|
||||
{idx + 1} / {photos.length}
|
||||
</span>
|
||||
<button onClick={onClose} style={{
|
||||
background: 'rgba(255,255,255,0.1)', border: 'none', borderRadius: '50%',
|
||||
width: 36, height: 36, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', cursor: 'pointer',
|
||||
}}>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Photo */}
|
||||
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* Prev button — visible on hover (desktop), always visible (mobile) */}
|
||||
{hasPrev && (
|
||||
<button onClick={prev} className="hidden sm:flex" style={{
|
||||
position: 'absolute', left: 12, zIndex: 2,
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.1)', border: 'none',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
<button onClick={prev} className="flex sm:opacity-0 sm:group-hover/lightbox:opacity-100 transition-opacity" style={{
|
||||
position: 'absolute', left: 16, zIndex: 5,
|
||||
width: 44, height: 44, borderRadius: '50%',
|
||||
background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', cursor: 'pointer',
|
||||
}}>
|
||||
<ChevronLeft size={20} />
|
||||
<ChevronLeft size={22} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div style={{ position: 'relative', display: 'inline-flex' }}>
|
||||
<img
|
||||
key={photo.id}
|
||||
src={photo.src}
|
||||
alt={photo.caption || ''}
|
||||
style={{
|
||||
maxWidth: '90vw', maxHeight: 'calc(100vh - 140px)',
|
||||
objectFit: 'contain', borderRadius: 4,
|
||||
animation: 'fadeIn 0.15s ease',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* EXIF metadata overlay */}
|
||||
{exif && !exifLoading && (
|
||||
<div style={{
|
||||
position: 'absolute', bottom: 12, right: 12,
|
||||
background: 'rgba(0,0,0,0.45)', backdropFilter: 'blur(16px)',
|
||||
borderRadius: 12, padding: '10px 14px',
|
||||
color: 'rgba(255,255,255,0.85)', fontSize: 11,
|
||||
display: 'flex', flexDirection: 'column', gap: 4,
|
||||
maxWidth: 220, border: '1px solid rgba(255,255,255,0.08)',
|
||||
}}>
|
||||
{exif.camera && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Camera size={11} style={{ opacity: 0.6, flexShrink: 0 }} />
|
||||
<span style={{ fontWeight: 500 }}>{exif.camera}</span>
|
||||
</div>
|
||||
)}
|
||||
{exif.lens && (
|
||||
<div style={{ fontSize: 10, color: 'rgba(255,255,255,0.55)', paddingLeft: 17 }}>{exif.lens}</div>
|
||||
)}
|
||||
{(exif.focalLength || exif.aperture || exif.shutter || exif.iso) && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 2 }}>
|
||||
<Aperture size={11} style={{ opacity: 0.6, flexShrink: 0 }} />
|
||||
<span style={{ fontWeight: 400, letterSpacing: '0.02em' }}>
|
||||
{[exif.focalLength, exif.aperture, exif.shutter, exif.iso ? `ISO ${exif.iso}` : ''].filter(Boolean).join(' · ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Photo */}
|
||||
<img
|
||||
key={photo.id}
|
||||
src={photo.src}
|
||||
alt={photo.caption || ''}
|
||||
style={{
|
||||
maxWidth: '92vw', maxHeight: '92vh',
|
||||
objectFit: 'contain', borderRadius: 4,
|
||||
animation: 'fadeIn 0.15s ease',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Next button */}
|
||||
{hasNext && (
|
||||
<button onClick={next} className="hidden sm:flex" style={{
|
||||
position: 'absolute', right: 12, zIndex: 2,
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
background: 'rgba(255,255,255,0.1)', border: 'none',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
<button onClick={next} className="flex sm:opacity-0 sm:group-hover/lightbox:opacity-100 transition-opacity" style={{
|
||||
position: 'absolute', right: 16, zIndex: 5,
|
||||
width: 44, height: 44, borderRadius: '50%',
|
||||
background: 'rgba(0,0,0,0.4)', backdropFilter: 'blur(8px)',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
alignItems: 'center', justifyContent: 'center',
|
||||
color: '#fff', cursor: 'pointer',
|
||||
}}>
|
||||
<ChevronRight size={20} />
|
||||
<ChevronRight size={22} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Caption — bottom center overlay */}
|
||||
{photo.caption && (
|
||||
<div style={{ position: 'absolute', bottom: 20, left: '50%', transform: 'translateX(-50%)', zIndex: 5, maxWidth: '70%', textAlign: 'center' }}>
|
||||
<p style={{
|
||||
fontSize: 14, fontStyle: 'italic',
|
||||
color: 'rgba(255,255,255,0.75)', margin: 0, lineHeight: 1.5,
|
||||
background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(8px)',
|
||||
padding: '6px 14px', borderRadius: 10,
|
||||
}}>{photo.caption}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Caption */}
|
||||
{photo.caption && (
|
||||
<div style={{ textAlign: 'center', padding: '12px 24px 20px', flexShrink: 0 }}>
|
||||
<p style={{
|
||||
fontFamily: 'var(--font-system)', fontSize: 14, fontStyle: 'italic',
|
||||
color: 'rgba(255,255,255,0.7)', margin: 0, lineHeight: 1.5,
|
||||
}}>{photo.caption}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user