chore: production readiness audit
- Move plans/ to dev-docs/, delete unused scaffold SVGs - Create .dockerignore, rename container to immich-folio - Extract BackLink component and toPhotoItems() helper in page.tsx - Add keyboard accessibility (role=button, tabIndex, onKeyDown) to PhotoGrid - Add aria-label to Lightbox EXIF toggle - Fix React lint: replace setState-in-effect with useState initializer - Add rate limiting to /api/auth (brute-force protection, 10 RPM) - Add HSTS and X-DNS-Prefetch-Control security headers - Add Secure flag to auth cookie in production - New unit tests: rate-limit.test.ts (5), auth.test.ts (12) - All 47 tests pass, 0 lint errors, build clean
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { Lightbox } from '@/components/Lightbox';
|
||||
import { FadeIn } from '@/components/FadeIn';
|
||||
@@ -50,19 +50,11 @@ function buildPhotoHash(index: number): string {
|
||||
}
|
||||
|
||||
export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridProps) {
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
|
||||
const isInitialized = useRef(false);
|
||||
|
||||
// ── On mount: check URL hash for a deep-link ──────────────────
|
||||
useEffect(() => {
|
||||
if (isInitialized.current) return;
|
||||
isInitialized.current = true;
|
||||
|
||||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(() => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const idx = parsePhotoHash(window.location.hash);
|
||||
if (idx !== null && idx < assets.length) {
|
||||
setLightboxIndex(idx);
|
||||
}
|
||||
}, [assets.length]);
|
||||
return idx !== null && idx < assets.length ? idx : null;
|
||||
});
|
||||
|
||||
// ── Sync URL hash when lightbox state changes ─────────────────
|
||||
useEffect(() => {
|
||||
@@ -90,14 +82,11 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
||||
return () => window.removeEventListener('hashchange', handleHashChange);
|
||||
}, [assets.length]);
|
||||
|
||||
const openLightbox = useCallback(
|
||||
(index: number) => {
|
||||
setLightboxIndex(index);
|
||||
// Push state (not replace) so the user can press Back to close
|
||||
window.history.pushState(null, '', buildPhotoHash(index));
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openLightbox = useCallback((index: number) => {
|
||||
setLightboxIndex(index);
|
||||
// Push state (not replace) so the user can press Back to close
|
||||
window.history.pushState(null, '', buildPhotoHash(index));
|
||||
}, []);
|
||||
|
||||
const closeLightbox = useCallback(() => {
|
||||
setLightboxIndex(null);
|
||||
@@ -149,6 +138,15 @@ export function PhotoGrid({ assets, layout = 'masonry', gridStyle }: PhotoGridPr
|
||||
<div
|
||||
className="photo-grid__item"
|
||||
onClick={() => openLightbox(index)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
openLightbox(index);
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`View photo ${index + 1}`}
|
||||
style={{
|
||||
...(asset.dominantColor ? { backgroundColor: asset.dominantColor } : {}),
|
||||
...(layout === 'masonry' && asset.aspectRatio
|
||||
|
||||
Reference in New Issue
Block a user