chore(release): v0.10.0 (#400)
Photo Essay mode, client proofing, studio-modern preset, admin panel overhaul, watermark, cookieless analytics, subpage toggle, SEO title templates. Analytics API now admin-only, Trivy scan on publish. Album order mirrors the Immich timeline; map theme, admin status panel, lightbox counter and Docker health check fixed. See CHANGELOG.md for the full list and the health-check upgrade note.
This commit is contained in:
133
components/AdminDiagnosticBanner.tsx
Normal file
133
components/AdminDiagnosticBanner.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
interface AdminDiagnosticBannerProps {
|
||||
slug: string;
|
||||
subpageName?: string;
|
||||
configuredAlbumCount?: number;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function AdminDiagnosticBanner({
|
||||
slug,
|
||||
subpageName,
|
||||
configuredAlbumCount = 0,
|
||||
reason,
|
||||
}: AdminDiagnosticBannerProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '680px',
|
||||
margin: '60px auto',
|
||||
padding: '24px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: '#1c1c1e',
|
||||
border: '1px solid #333336',
|
||||
color: '#f2f2f7',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
boxShadow: '0 8px 24px rgba(0,0,0,0.4)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', marginBottom: '16px' }}>
|
||||
<span style={{ fontSize: '24px' }}>⚠️</span>
|
||||
<h2 style={{ margin: 0, fontSize: '18px', fontWeight: 600, color: '#ffd60a' }}>
|
||||
Admin Diagnostic: Subpage loaded with 0 photos
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p style={{ fontSize: '14px', lineHeight: 1.6, color: '#aeaeb2', margin: '0 0 16px 0' }}>
|
||||
The subpage <strong>/{slug}</strong> {subpageName ? `("${subpageName}")` : ''} is active in{' '}
|
||||
<code style={{ background: '#2c2c2e', padding: '2px 6px', borderRadius: '4px' }}>
|
||||
gallery.yaml
|
||||
</code>
|
||||
{configuredAlbumCount > 0
|
||||
? `, and configured with ${configuredAlbumCount} album(s)`
|
||||
: ''}
|
||||
, but Immich returned no accessible albums or assets.
|
||||
</p>
|
||||
|
||||
{reason && (
|
||||
<div
|
||||
style={{
|
||||
background: '#2c2c2e',
|
||||
padding: '12px',
|
||||
borderRadius: '6px',
|
||||
fontSize: '13px',
|
||||
color: '#ff9f0a',
|
||||
marginBottom: '16px',
|
||||
}}
|
||||
>
|
||||
{reason}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#f2f2f7' }}>
|
||||
Possible Causes & Checklist:
|
||||
</h4>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '13px', color: '#d1d1d6', lineHeight: 1.8 }}>
|
||||
<li>
|
||||
<strong>Album Shared Status in Immich:</strong> Ensure the album in Immich is marked as{' '}
|
||||
<strong>Shared (Geteilt)</strong>. Unshared albums are excluded by Immich Folio for security.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Cache Lag:</strong> If you just added the album or changed permissions, click{' '}
|
||||
<em>Force Reload Fresh</em> below.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Album UUID:</strong> Check if the album UUID in your Page Builder matches the exact Immich Album ID.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
|
||||
<a
|
||||
href={`/${slug}?fresh=1`}
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#0a84ff',
|
||||
color: '#ffffff',
|
||||
textDecoration: 'none',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
🔄 Force Reload Fresh (?fresh=1)
|
||||
</a>
|
||||
<Link
|
||||
href="/admin"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: '#2c2c2e',
|
||||
color: '#f2f2f7',
|
||||
border: '1px solid #3a3a3c',
|
||||
textDecoration: 'none',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
}}
|
||||
>
|
||||
⚙️ Open Page Builder (/admin)
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
style={{
|
||||
padding: '8px 16px',
|
||||
borderRadius: '6px',
|
||||
color: '#8e8e93',
|
||||
textDecoration: 'none',
|
||||
fontSize: '13px',
|
||||
}}
|
||||
>
|
||||
Back to Gallery
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback, useLayoutEffect } from 'react';
|
||||
|
||||
const PRESETS = ['studio', 'minimal', 'editorial', 'classic', 'noir', 'monograph'];
|
||||
const PRESETS = ['studio', 'studio-modern', 'minimal', 'editorial', 'classic', 'noir', 'monograph'];
|
||||
const FRAMES = ['none', 'passepartout', 'shadow'];
|
||||
const HERO_STYLES = ['split', 'fullbleed', 'minimal', 'stacked', 'typographic', 'mosaic'];
|
||||
const GRID_LAYOUTS = ['masonry', 'uniform', 'showcase', 'filmstrip', 'editorial-flow'];
|
||||
@@ -27,6 +27,7 @@ type Tab = 'theme' | 'grid' | 'server';
|
||||
// Font stacks for each preset (must match THEME_PRESETS in config.ts)
|
||||
const PRESET_FONTS: Record<string, { heading: string; body: string; caption: string }> = {
|
||||
studio: { heading: 'Playfair Display', body: 'DM Sans', caption: 'EB Garamond' },
|
||||
'studio-modern': { heading: 'Archivo', body: 'Archivo', caption: 'IBM Plex Mono' },
|
||||
minimal: { heading: 'Geist', body: 'Geist', caption: 'IBM Plex Mono' },
|
||||
editorial: { heading: 'Bodoni Moda', body: 'Newsreader', caption: 'Spectral' },
|
||||
classic: { heading: 'Cinzel', body: 'Crimson Pro', caption: 'Crimson Pro' },
|
||||
@@ -36,6 +37,7 @@ const PRESET_FONTS: Record<string, { heading: string; body: string; caption: str
|
||||
|
||||
const PRESET_ACCENTS: Record<string, string> = {
|
||||
studio: 'var(--accent-studio, #e60012)',
|
||||
'studio-modern': 'var(--accent-studio-modern, #e60012)',
|
||||
minimal: 'var(--accent-minimal, #000000)',
|
||||
editorial: 'var(--accent-editorial, #8B2500)',
|
||||
classic: 'var(--accent-classic, #c49a3c)',
|
||||
|
||||
@@ -13,6 +13,8 @@ import Image from 'next/image';
|
||||
interface HeroImage {
|
||||
src: string;
|
||||
blurDataURL?: string;
|
||||
/** Pre-formatted camera line, shown as a chip by presets that use it. */
|
||||
exif?: string;
|
||||
}
|
||||
|
||||
interface HeroCarouselProps {
|
||||
@@ -42,15 +44,20 @@ export function HeroCarousel({ images }: HeroCarouselProps) {
|
||||
if (images.length === 1) {
|
||||
const img = images[0];
|
||||
return (
|
||||
<Image
|
||||
src={img.src}
|
||||
alt=""
|
||||
className="hero__image"
|
||||
fill
|
||||
sizes="50vw"
|
||||
priority
|
||||
{...(img.blurDataURL ? { placeholder: 'blur' as const, blurDataURL: img.blurDataURL } : {})}
|
||||
/>
|
||||
<>
|
||||
<Image
|
||||
src={img.src}
|
||||
alt=""
|
||||
className="hero__image"
|
||||
fill
|
||||
sizes="50vw"
|
||||
priority
|
||||
{...(img.blurDataURL
|
||||
? { placeholder: 'blur' as const, blurDataURL: img.blurDataURL }
|
||||
: {})}
|
||||
/>
|
||||
<HeroExifChip text={img.exif} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,6 +77,17 @@ export function HeroCarousel({ images }: HeroCarouselProps) {
|
||||
: {})}
|
||||
/>
|
||||
))}
|
||||
<HeroExifChip text={images[currentIndex]?.exif} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/** Camera line over the hero image. Hidden unless the preset shows it. */
|
||||
function HeroExifChip({ text }: { text?: string }) {
|
||||
if (!text) return null;
|
||||
return (
|
||||
<span className="hero__exif-chip" aria-hidden="true">
|
||||
{text}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,11 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Proofing favourite button — sits left of the info toggle. */
|
||||
.favToggle {
|
||||
right: 6.5rem;
|
||||
}
|
||||
|
||||
.exifPanel {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
@@ -216,6 +221,12 @@
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Marks the ISO row so a preset can keep its label — "200" alone reads
|
||||
as nothing once the labels are hidden. */
|
||||
.exifRowIso {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* ── Responsive ──────────────────────────────────── */
|
||||
@media (max-width: 640px) {
|
||||
.nav {
|
||||
@@ -300,3 +311,194 @@
|
||||
.image {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* ══ Studio Modern ═══════════════════════════════════════════════
|
||||
Topbar with a mono counter, square outlined controls and a
|
||||
film-edge EXIF strip along the bottom instead of a popup panel. */
|
||||
|
||||
:global([data-preset='studio-modern']) .overlay {
|
||||
background: rgba(10, 10, 10, 0.92);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .imageContainer,
|
||||
:global([data-preset='studio-modern']) .image {
|
||||
max-width: 88vw;
|
||||
max-height: 78vh;
|
||||
}
|
||||
|
||||
/* Counter → top left, monospace */
|
||||
:global([data-preset='studio-modern']) .counter {
|
||||
top: 20px;
|
||||
bottom: auto;
|
||||
left: 32px;
|
||||
transform: none;
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.15em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .close {
|
||||
top: 20px;
|
||||
right: 32px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .close:hover {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .close svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
/* Square outlined navigation */
|
||||
:global([data-preset='studio-modern']) .nav {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
transition:
|
||||
border-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .nav:hover {
|
||||
background: none;
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .navPrev {
|
||||
left: 24px;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .navNext {
|
||||
right: 24px;
|
||||
}
|
||||
|
||||
/* Info + favourite toggles move up into the topbar */
|
||||
:global([data-preset='studio-modern']) .infoToggle {
|
||||
top: 20px;
|
||||
bottom: auto;
|
||||
right: 76px;
|
||||
background: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 0;
|
||||
padding: 8px 14px;
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.625rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .infoToggle:hover {
|
||||
background: none;
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .favToggle {
|
||||
right: 176px;
|
||||
}
|
||||
|
||||
/* Film-edge strip replaces the popup panel */
|
||||
:global([data-preset='studio-modern']) .exifPanel {
|
||||
top: auto;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0 12px;
|
||||
min-width: 0;
|
||||
padding: 18px 32px;
|
||||
background: rgba(10, 10, 10, 0.9);
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: none;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifPanel::before {
|
||||
content: '';
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
flex: 0 0 auto;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifRow {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifRow + .exifRow::before {
|
||||
content: '·';
|
||||
margin-right: 12px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifLabel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifRowIso .exifLabel {
|
||||
display: inline;
|
||||
margin-right: 6px;
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .exifValue {
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
/* The "Loading…" / "no EXIF" fallback keeps its own label visible */
|
||||
:global([data-preset='studio-modern']) .exifRow:only-child .exifLabel {
|
||||
display: inline;
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.06em;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
/* No room next to the info toggle — park it in the opposite corner. */
|
||||
:global([data-preset='studio-modern']) .favToggle {
|
||||
top: auto;
|
||||
bottom: 76px;
|
||||
right: auto;
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .navPrev {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
:global([data-preset='studio-modern']) .navNext {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,9 +254,8 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev, waterm
|
||||
{/* Proofing favorite button */}
|
||||
{proofing && current && (
|
||||
<button
|
||||
className={styles.infoToggle}
|
||||
className={`${styles.infoToggle} ${styles.favToggle}`}
|
||||
style={{
|
||||
right: '6.5rem',
|
||||
color: isFav ? '#ff4d4f' : 'inherit',
|
||||
fontWeight: isFav ? 600 : 400,
|
||||
}}
|
||||
@@ -322,7 +321,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev, waterm
|
||||
</div>
|
||||
)}
|
||||
{exifData.iso && (
|
||||
<div className={styles.exifRow}>
|
||||
<div className={`${styles.exifRow} ${styles.exifRowIso}`}>
|
||||
<span className={styles.exifLabel}>ISO</span>
|
||||
<span className={styles.exifValue}>{exifData.iso}</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user