refactor: apply Clean Architecture principles

- Extract EXIF and Swipe logic into custom hooks for Lightbox
- Extract Map layout data formatting into mapService
- Extract AlbumDetailView and SubpageGridView components from page router
- Restructure monolithic config.ts into clean config/ directory
This commit is contained in:
ralksta
2026-03-08 18:55:45 +01:00
parent 89dfef6144
commit 23d8bf0425
14 changed files with 721 additions and 204 deletions

View File

@@ -14,20 +14,10 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { createPortal } from 'react-dom';
import type { PhotoItem } from '@/app/[...path]/PhotoGrid';
import { useExif } from '@/hooks/useExif';
import { useSwipe } from '@/hooks/useSwipe';
import styles from './Lightbox.module.css';
interface ExifData {
make?: string | null;
model?: string | null;
lensModel?: string | null;
focalLength?: number | null;
fNumber?: number | null;
exposureTime?: string | null;
iso?: number | null;
city?: string | null;
country?: string | null;
}
interface LightboxProps {
assets: PhotoItem[];
currentIndex: number;
@@ -38,12 +28,9 @@ interface LightboxProps {
export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: LightboxProps) {
const [showExif, setShowExif] = useState(false);
const [exifData, setExifData] = useState<ExifData | null>(null);
const [exifLoading, setExifLoading] = useState(false);
const { exifData, exifLoading, fetchExif, clearExif } = useExif();
const [imageLoaded, setImageLoaded] = useState(false);
const overlayRef = useRef<HTMLDivElement>(null);
const touchStartX = useRef<number>(0);
const touchStartY = useRef<number>(0);
const current = assets[currentIndex];
const [mounted, setMounted] = useState(false);
@@ -53,20 +40,9 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
setMounted(true);
}, []);
// Fetch EXIF data on demand
const fetchExif = useCallback((url: string) => {
setExifLoading(true);
setExifData(null);
fetch(url)
.then((res) => (res.ok ? res.json() : null))
.then((data) => setExifData(data))
.catch(() => setExifData(null))
.finally(() => setExifLoading(false));
}, []);
// Reset EXIF data when switching images; refetch if panel is open
useEffect(() => {
setExifData(null);
clearExif();
setImageLoaded(false);
if (showExif && current) {
fetchExif(current.exifUrl);
@@ -95,25 +71,10 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
preload(currentIndex - 1);
}, [currentIndex, assets]);
// Touch/swipe support
const handleTouchStart = useCallback((e: React.TouchEvent) => {
touchStartX.current = e.touches[0].clientX;
touchStartY.current = e.touches[0].clientY;
}, []);
const handleTouchEnd = useCallback(
(e: React.TouchEvent) => {
const dx = e.changedTouches[0].clientX - touchStartX.current;
const dy = e.changedTouches[0].clientY - touchStartY.current;
// Only register horizontal swipes (not vertical scrolling)
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy)) {
if (dx > 0) onPrev();
else onNext();
}
},
[onNext, onPrev],
);
const { handleTouchStart, handleTouchEnd } = useSwipe({
onSwipeLeft: onNext,
onSwipeRight: onPrev,
});
// Click on overlay background → close
const handleOverlayClick = useCallback(