feat: photography features & image optimizations

- Hero carousel: gallery.yaml hero accepts single UUID or array, crossfade component
- Full-bleed single albums: subpages with 1 album skip grid, render photos directly
- EXIF on hover: camera/lens/focal length overlay on grid items (toggle via exifOnHover)
- Customizable grid: columns/gap/aspectRatio/layout (masonry|uniform) via gallery.yaml
- Per-subpage grid overrides: each subpage can override global grid config
- True masonry: per-image aspect ratios from EXIF dimensions
- next/image migration: homepage hero, album covers, grid thumbnails, about portrait
- ThumbHash blur placeholders and dominant color backgrounds
- Custom next/image loader via image proxy with width/quality params
- Lightbox fade-in transition with dominant color background
- Password-protected subpages, health check, rate limiting, OG images
This commit is contained in:
ralksta
2026-02-14 10:30:27 +01:00
parent 3c69860a64
commit b7217c4a76
31 changed files with 1998 additions and 162 deletions

View File

@@ -1,13 +1,14 @@
/**
* Homepage — Split hero layout.
* Left panel: site title, subtitle, navigation links.
* Right panel: hero image from Immich.
* Right panel: hero image carousel from Immich.
*/
import Link from 'next/link';
import { immich } from '@/lib/immich';
import { getConfig } from '@/lib/config';
import { imageUrl } from '@/lib/urls';
import { imageUrl, assetPlaceholder } from '@/lib/urls';
import { HeroCarousel } from '@/components/HeroCarousel';
// Render at request time — requires live Immich connection
export const dynamic = 'force-dynamic';
@@ -19,6 +20,18 @@ export default async function HomePage() {
immich.getStandaloneAlbums(),
]);
// Fetch ThumbHash for all hero images
const heroData = await Promise.all(
config.heroImages.map(async (id) => {
const asset = await immich.getAssetInfo(id);
const ph = asset ? assetPlaceholder(asset) : null;
return {
src: imageUrl(id, 'preview'),
...(ph ? { blurDataURL: ph.blurDataURL } : {}),
};
}),
);
return (
<div className="hero">
{/* ── Left Panel ──────────────────────────────── */}
@@ -52,18 +65,11 @@ export default async function HomePage() {
</div>
</div>
{/* ── Right Panel (Hero Image) ────────────────── */}
{/* ── Right Panel (Hero Carousel) ─────────────── */}
<div className="hero__right">
{config.heroImage ? (
<img
src={imageUrl(config.heroImage, 'preview')}
alt=""
className="hero__image"
/>
) : (
<div className="hero__image-placeholder" />
)}
<HeroCarousel images={heroData} />
</div>
</div>
);
}