2026-02-14 00:59:57 +01:00
|
|
|
/**
|
|
|
|
|
* Lightbox — fullscreen image viewer with navigation and EXIF info.
|
|
|
|
|
*
|
|
|
|
|
* Features:
|
|
|
|
|
* - Full-resolution image display
|
|
|
|
|
* - Previous/Next navigation (arrows + swipe)
|
|
|
|
|
* - Close (Esc, click outside, X button)
|
|
|
|
|
* - EXIF metadata panel (fetched on demand)
|
|
|
|
|
* - Preloads adjacent images
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
2026-03-08 16:16:15 +01:00
|
|
|
import { createPortal } from 'react-dom';
|
2026-02-14 00:59:57 +01:00
|
|
|
import type { PhotoItem } from '@/app/[...path]/PhotoGrid';
|
2026-03-08 18:55:45 +01:00
|
|
|
import { useExif } from '@/hooks/useExif';
|
|
|
|
|
import { useSwipe } from '@/hooks/useSwipe';
|
2026-02-14 11:47:26 +01:00
|
|
|
import styles from './Lightbox.module.css';
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
import { useProofing } from './ProofingContext';
|
|
|
|
|
|
|
|
|
|
export interface LightboxWatermark {
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
text?: string;
|
|
|
|
|
opacity?: number;
|
|
|
|
|
position?: 'bottom-right' | 'bottom-left' | 'center' | string;
|
|
|
|
|
}
|
2026-02-14 00:59:57 +01:00
|
|
|
|
|
|
|
|
interface LightboxProps {
|
2026-02-14 11:47:26 +01:00
|
|
|
assets: PhotoItem[];
|
|
|
|
|
currentIndex: number;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
onNext: () => void;
|
|
|
|
|
onPrev: () => void;
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
watermark?: LightboxWatermark;
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|
|
|
|
|
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev, watermark }: LightboxProps) {
|
2026-02-14 11:47:26 +01:00
|
|
|
const [showExif, setShowExif] = useState(false);
|
2026-03-08 18:55:45 +01:00
|
|
|
const { exifData, exifLoading, fetchExif, clearExif } = useExif();
|
2026-02-14 11:47:26 +01:00
|
|
|
const [imageLoaded, setImageLoaded] = useState(false);
|
|
|
|
|
const overlayRef = useRef<HTMLDivElement>(null);
|
2026-04-21 22:51:31 +02:00
|
|
|
const closeBtnRef = useRef<HTMLButtonElement>(null);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
const current = assets[currentIndex];
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
const proofing = useProofing();
|
|
|
|
|
const isFav = proofing && current ? proofing.isFavorite(current.id) : false;
|
2026-03-08 16:16:15 +01:00
|
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Mount guard — createPortal needs document.body (client-only)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
setMounted(true);
|
|
|
|
|
}, []);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-04-21 22:51:31 +02:00
|
|
|
// Auto-focus the close button when the modal mounts
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
closeBtnRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
}, [mounted]);
|
|
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
// Reset EXIF data when switching images; refetch if panel is open
|
|
|
|
|
useEffect(() => {
|
2026-03-08 18:55:45 +01:00
|
|
|
clearExif();
|
2026-02-14 11:47:26 +01:00
|
|
|
setImageLoaded(false);
|
|
|
|
|
if (showExif && current) {
|
|
|
|
|
fetchExif(current.exifUrl);
|
|
|
|
|
}
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
|
}, [currentIndex]);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
// When the toggle is turned on or the image changes while open, fetch
|
|
|
|
|
const handleExifToggle = useCallback(() => {
|
|
|
|
|
const next = !showExif;
|
|
|
|
|
setShowExif(next);
|
|
|
|
|
if (next && current) {
|
|
|
|
|
fetchExif(current.exifUrl);
|
|
|
|
|
}
|
|
|
|
|
}, [showExif, current, fetchExif]);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-05-29 17:05:12 +02:00
|
|
|
// Preload adjacent images (skip videos — they stream on demand)
|
2026-02-14 11:47:26 +01:00
|
|
|
useEffect(() => {
|
|
|
|
|
const preload = (index: number) => {
|
2026-05-29 17:05:12 +02:00
|
|
|
if (index >= 0 && index < assets.length && assets[index].type !== 'video') {
|
2026-02-14 11:47:26 +01:00
|
|
|
const img = new Image();
|
|
|
|
|
img.src = assets[index].previewUrl;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
preload(currentIndex + 1);
|
|
|
|
|
preload(currentIndex - 1);
|
|
|
|
|
}, [currentIndex, assets]);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-03-08 18:55:45 +01:00
|
|
|
const { handleTouchStart, handleTouchEnd } = useSwipe({
|
|
|
|
|
onSwipeLeft: onNext,
|
|
|
|
|
onSwipeRight: onPrev,
|
|
|
|
|
});
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-03-11 23:33:00 +01:00
|
|
|
// Listen for 'i' key to toggle EXIF
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'i' || e.key === 'I') {
|
|
|
|
|
handleExifToggle();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [handleExifToggle]);
|
|
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
// Click on overlay background → close
|
|
|
|
|
const handleOverlayClick = useCallback(
|
|
|
|
|
(e: React.MouseEvent) => {
|
|
|
|
|
if (e.target === overlayRef.current) {
|
|
|
|
|
onClose();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[onClose],
|
|
|
|
|
);
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-03-08 16:16:15 +01:00
|
|
|
const lightboxJsx = (
|
2026-02-14 11:47:26 +01:00
|
|
|
<div
|
|
|
|
|
className={styles.overlay}
|
|
|
|
|
ref={overlayRef}
|
|
|
|
|
onClick={handleOverlayClick}
|
|
|
|
|
onTouchStart={handleTouchStart}
|
|
|
|
|
onTouchEnd={handleTouchEnd}
|
2026-03-27 10:36:19 +01:00
|
|
|
role="dialog"
|
|
|
|
|
aria-modal="true"
|
|
|
|
|
aria-label="Image viewer"
|
2026-02-14 11:47:26 +01:00
|
|
|
>
|
|
|
|
|
{/* Close button */}
|
2026-04-21 22:51:31 +02:00
|
|
|
<button
|
|
|
|
|
ref={closeBtnRef}
|
|
|
|
|
className={styles.close}
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
aria-label="Close"
|
|
|
|
|
title="Close (Esc)"
|
|
|
|
|
>
|
2026-02-14 11:47:26 +01:00
|
|
|
<svg
|
2026-04-01 12:46:20 +02:00
|
|
|
aria-hidden="true"
|
2026-02-14 11:47:26 +01:00
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
2026-02-14 00:59:57 +01:00
|
|
|
>
|
2026-02-14 11:47:26 +01:00
|
|
|
<line x1="18" y1="6" x2="6" y2="18" />
|
|
|
|
|
<line x1="6" y1="6" x2="18" y2="18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
{/* Previous button */}
|
|
|
|
|
<button
|
|
|
|
|
className={`${styles.nav} ${styles.navPrev}`}
|
|
|
|
|
onClick={onPrev}
|
|
|
|
|
aria-label="Previous photo"
|
2026-03-11 23:33:00 +01:00
|
|
|
title="Previous photo (Left arrow)"
|
2026-02-14 11:47:26 +01:00
|
|
|
>
|
|
|
|
|
<svg
|
2026-04-01 12:46:20 +02:00
|
|
|
aria-hidden="true"
|
2026-02-14 11:47:26 +01:00
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<polyline points="15 18 9 12 15 6" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-05-29 17:05:12 +02:00
|
|
|
{/* Image or Video */}
|
2026-02-14 11:47:26 +01:00
|
|
|
<div
|
|
|
|
|
className={styles.imageContainer}
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: current.dominantColor || '#000',
|
2026-05-29 17:05:12 +02:00
|
|
|
backgroundImage:
|
|
|
|
|
current.type !== 'video' && current.blurDataURL
|
|
|
|
|
? `url(${current.blurDataURL})`
|
|
|
|
|
: undefined,
|
2026-02-14 11:47:26 +01:00
|
|
|
backgroundSize: 'cover',
|
|
|
|
|
backgroundPosition: 'center',
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-29 17:05:12 +02:00
|
|
|
{current.type === 'video' && current.videoUrl ? (
|
|
|
|
|
// eslint-disable-next-line jsx-a11y/media-has-caption
|
|
|
|
|
<video
|
|
|
|
|
className={`${styles.image}${imageLoaded ? ` ${styles.imageLoaded}` : ''}`}
|
|
|
|
|
src={current.videoUrl}
|
|
|
|
|
controls
|
|
|
|
|
autoPlay={false}
|
|
|
|
|
playsInline
|
|
|
|
|
onCanPlay={() => setImageLoaded(true)}
|
|
|
|
|
onError={() => console.error(`[Lightbox] Failed to load video: ${current.videoUrl}`)}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
// eslint-disable-next-line @next/next/no-img-element
|
|
|
|
|
<img
|
|
|
|
|
className={`${styles.image}${imageLoaded ? ` ${styles.imageLoaded}` : ''}`}
|
|
|
|
|
src={current.previewUrl}
|
|
|
|
|
alt=""
|
|
|
|
|
draggable={false}
|
|
|
|
|
onLoad={() => setImageLoaded(true)}
|
|
|
|
|
onError={() => console.error(`[Lightbox] Failed to load image: ${current.previewUrl}`)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
|
|
|
|
|
{watermark?.enabled && watermark.text && (
|
|
|
|
|
<div
|
|
|
|
|
className={`${styles.watermark} ${
|
|
|
|
|
watermark.position === 'bottom-left'
|
|
|
|
|
? styles.watermark_bottom_left
|
|
|
|
|
: watermark.position === 'center'
|
|
|
|
|
? styles.watermark_center
|
|
|
|
|
: styles.watermark_bottom_right
|
|
|
|
|
}`}
|
|
|
|
|
style={{ opacity: (watermark.opacity ?? 50) / 100 }}
|
|
|
|
|
>
|
|
|
|
|
{watermark.text}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-14 11:47:26 +01:00
|
|
|
</div>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
{/* Next button */}
|
|
|
|
|
<button
|
|
|
|
|
className={`${styles.nav} ${styles.navNext}`}
|
|
|
|
|
onClick={onNext}
|
|
|
|
|
aria-label="Next photo"
|
2026-03-11 23:33:00 +01:00
|
|
|
title="Next photo (Right arrow)"
|
2026-02-14 11:47:26 +01:00
|
|
|
>
|
|
|
|
|
<svg
|
2026-04-01 12:46:20 +02:00
|
|
|
aria-hidden="true"
|
2026-02-14 11:47:26 +01:00
|
|
|
viewBox="0 0 24 24"
|
|
|
|
|
fill="none"
|
|
|
|
|
stroke="currentColor"
|
|
|
|
|
strokeWidth="2"
|
|
|
|
|
strokeLinecap="round"
|
|
|
|
|
strokeLinejoin="round"
|
|
|
|
|
>
|
|
|
|
|
<polyline points="9 6 15 12 9 18" />
|
|
|
|
|
</svg>
|
|
|
|
|
</button>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
{/* Counter */}
|
2026-05-31 10:03:08 +02:00
|
|
|
<div className={styles.counter} aria-live="polite" aria-atomic="true">
|
|
|
|
|
<span className="sr-only">
|
|
|
|
|
Photo {currentIndex + 1} of {assets.length}
|
|
|
|
|
</span>
|
|
|
|
|
<span aria-hidden="true">
|
|
|
|
|
{currentIndex + 1} / {assets.length}
|
|
|
|
|
</span>
|
2026-02-14 11:47:26 +01:00
|
|
|
</div>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
security: add Trivy container scan to Docker publish workflow (#399)
* feat(admin): add backup manager UI with 1-click restore
* feat(admin): collapse older backup entries by default
* fix(theme): isolate admin layout, instant DOM theme updates, and add Light/Dark mode toggle
* feat(admin): replace text controls for Photo Frame and Hero Style with visual preview cards
* fix(admin): define missing CSS rules for button system (.btn, .preset-btn)
* feat(admin): redesign theme settings with segmented mode toggle, accent color swatches, and custom iOS-style toggle cards
* feat(admin): upgrade all settings tabs with visual grid previews, Google SEO snippet preview, and toggle cards
* feat(admin): overhaul Pages Builder UX with slide-over drawer, card hover badges, real-time search, and floating save bar
* fix(admin): remove leftover dangling syntax snippet in PageBuilder.tsx
* fix(admin): add position: relative to subpage-tile so hover overlay is contained inside the tile
* style(admin): replace all emojis with clean Leica-style vector SVG icons
* fix(types): extend IconProps with SVGProps to allow style prop on Leica SVG icons
* feat(admin): add subpage live-preview, privacy analytics, and asset protection settings
* fix(typecheck): fix AlbumEntry property access in subpage drawer preview mode
* fix(types): fix ImmichAlbumInfo property names thumbnailAssetId and albumName in PageBuilder.tsx
* fix(types): add protection and watermark optional properties to AppConfig interface
* feat(settings): add toggle for disabling analytics tracking (defaults to enabled)
* feat(ux): show status banner when analytics tracking is disabled + add live sync indicator
* feat(watermark): render customizable watermark overlay on Lightbox images + rename section to Portfolio Features & Modules
* fix(types): import type LightboxWatermark in PhotoGrid.tsx
* fix(types): destructure watermark prop in PhotoGrid function signature
* feat(seo): add configurable subpage title template and metadata descriptions
* feat(seo): add generateMetadata to about page for title template support
* feat(proofing): add client proofing, favorites selection, compact URL bitmasking, and export modal
* feat(essay): add Photo Essay storytelling mode with RSC markdown parser and fullbleed/pair layouts
* feat(subpages): add page enabled toggle to deactivate subpages without deleting them
* feat(admin): add visual essay block builder for photo essay storytelling pages
* security: add Trivy container scan to Docker publish workflow
Scans the built image for CRITICAL/HIGH CVEs on every release and
uploads results to the GitHub Security tab. Non-blocking for now
(exit-code 0) so releases aren't gated on findings. Also documents
the scan and existing Dependabot coverage in the README.
---------
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
2026-08-07 11:08:04 +02:00
|
|
|
{/* Proofing favorite button */}
|
|
|
|
|
{proofing && current && (
|
|
|
|
|
<button
|
|
|
|
|
className={styles.infoToggle}
|
|
|
|
|
style={{
|
|
|
|
|
right: '6.5rem',
|
|
|
|
|
color: isFav ? '#ff4d4f' : 'inherit',
|
|
|
|
|
fontWeight: isFav ? 600 : 400,
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => proofing.toggleFavorite(current.id)}
|
|
|
|
|
aria-label={isFav ? 'Remove from favorites' : 'Add to favorites'}
|
|
|
|
|
title={isFav ? 'Remove from favorites' : 'Add to favorites'}
|
|
|
|
|
>
|
|
|
|
|
{isFav ? '❤️ Saved' : '🤍 Favorite'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
{/* EXIF toggle */}
|
2026-02-15 11:23:51 +01:00
|
|
|
<button
|
|
|
|
|
className={styles.infoToggle}
|
|
|
|
|
onClick={handleExifToggle}
|
2026-03-27 10:36:19 +01:00
|
|
|
aria-expanded={showExif}
|
|
|
|
|
aria-controls="exif-panel"
|
2026-02-15 11:23:51 +01:00
|
|
|
aria-label="Toggle photo info"
|
2026-03-11 23:33:00 +01:00
|
|
|
title="Toggle photo info (i)"
|
2026-02-15 11:23:51 +01:00
|
|
|
>
|
2026-02-14 11:47:26 +01:00
|
|
|
{showExif ? 'Hide Info' : 'Info'}
|
|
|
|
|
</button>
|
2026-02-14 00:59:57 +01:00
|
|
|
|
2026-02-14 11:47:26 +01:00
|
|
|
{/* EXIF panel */}
|
|
|
|
|
{showExif && (
|
2026-03-27 10:36:19 +01:00
|
|
|
<div id="exif-panel" className={styles.exifPanel}>
|
2026-02-14 11:47:26 +01:00
|
|
|
{exifLoading ? (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Loading...</span>
|
|
|
|
|
</div>
|
|
|
|
|
) : exifData ? (
|
|
|
|
|
<>
|
|
|
|
|
{exifData.model && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Camera</span>
|
|
|
|
|
<span className={styles.exifValue}>
|
|
|
|
|
{[exifData.make, exifData.model].filter(Boolean).join(' ')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{exifData.lensModel && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Lens</span>
|
|
|
|
|
<span className={styles.exifValue}>{exifData.lensModel}</span>
|
2026-02-14 00:59:57 +01:00
|
|
|
</div>
|
2026-02-14 11:47:26 +01:00
|
|
|
)}
|
|
|
|
|
{exifData.focalLength && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Focal Length</span>
|
|
|
|
|
<span className={styles.exifValue}>{exifData.focalLength}mm</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{exifData.fNumber && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Aperture</span>
|
|
|
|
|
<span className={styles.exifValue}>ƒ/{exifData.fNumber}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{exifData.exposureTime && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Shutter</span>
|
|
|
|
|
<span className={styles.exifValue}>{exifData.exposureTime}s</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{exifData.iso && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>ISO</span>
|
|
|
|
|
<span className={styles.exifValue}>{exifData.iso}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{(exifData.city || exifData.country) && (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>Location</span>
|
|
|
|
|
<span className={styles.exifValue}>
|
|
|
|
|
{[exifData.city, exifData.country].filter(Boolean).join(', ')}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div className={styles.exifRow}>
|
|
|
|
|
<span className={styles.exifLabel}>No EXIF data available</span>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-14 00:59:57 +01:00
|
|
|
</div>
|
2026-02-14 11:47:26 +01:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
2026-03-08 16:16:15 +01:00
|
|
|
|
|
|
|
|
if (!mounted) return null;
|
|
|
|
|
return createPortal(lightboxJsx, document.body);
|
2026-02-14 00:59:57 +01:00
|
|
|
}
|