feat: add noir, monograph, botanica presets + stacked/typographic/mosaic hero styles + showcase/filmstrip/editorial-flow gallery layouts
- 3 new theme presets: noir (darkroom analog), monograph (typographic book), botanica (organic nature) - 3 new hero styles: stacked (fullbleed + thumbnail strip), typographic (text-only), mosaic (multi-image grid) - 3 new gallery layouts: showcase (featured + grid), filmstrip (horizontal scroll), editorial-flow (alternating pairs) - Extended config types, validation, and CSS tokens for all new options - Updated theming docs with new presets table, descriptions, and example configs
This commit is contained in:
165
app/page.tsx
165
app/page.tsx
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Homepage — Split hero layout.
|
||||
* Left panel: site title, subtitle, navigation links.
|
||||
* Right panel: hero image carousel from Immich.
|
||||
* Homepage — renders different hero layouts based on theme config.
|
||||
*
|
||||
* Styles: split, fullbleed, minimal, stacked, typographic, mosaic.
|
||||
*/
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { immich } from '@/lib/immich';
|
||||
import { getConfig } from '@/lib/config';
|
||||
import { imageUrl, assetPlaceholder } from '@/lib/urls';
|
||||
@@ -14,6 +15,30 @@ import { FadeIn } from '@/components/FadeIn';
|
||||
// Render at request time — requires live Immich connection
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
/** Shared nav links for hero sections. */
|
||||
function HeroNavLinks({
|
||||
subpages,
|
||||
albums,
|
||||
}: {
|
||||
subpages: { slug: string; name: string }[];
|
||||
albums: { id: string; slug: string; albumName: string }[];
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
{subpages.map((sp) => (
|
||||
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
|
||||
{sp.name}
|
||||
</Link>
|
||||
))}
|
||||
{albums.map((album) => (
|
||||
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
|
||||
{album.albumName}
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const config = getConfig();
|
||||
const [subpages, albums] = await Promise.all([
|
||||
@@ -33,6 +58,129 @@ export default async function HomePage() {
|
||||
}),
|
||||
);
|
||||
|
||||
const heroStyle = config.theme.heroStyle;
|
||||
|
||||
// ── Typographic: no image, pure text ───────────────────────────
|
||||
if (heroStyle === 'typographic') {
|
||||
return (
|
||||
<div className="hero hero--typographic">
|
||||
<div className="hero__content">
|
||||
<FadeIn delay={0}>
|
||||
<h1 className="hero__title">{config.siteTitle}</h1>
|
||||
</FadeIn>
|
||||
{config.siteSubtitle && (
|
||||
<FadeIn delay={100}>
|
||||
<p className="hero__subtitle">{config.siteSubtitle}</p>
|
||||
</FadeIn>
|
||||
)}
|
||||
<FadeIn delay={200}>
|
||||
<div className="hero__divider" />
|
||||
</FadeIn>
|
||||
<FadeIn delay={300}>
|
||||
<nav className="hero__nav hero__nav--indexed">
|
||||
{subpages.map((sp, i) => (
|
||||
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
|
||||
<span className="hero__nav-index">
|
||||
{String(i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{sp.name}
|
||||
</Link>
|
||||
))}
|
||||
{albums.map((album, i) => (
|
||||
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
|
||||
<span className="hero__nav-index">
|
||||
{String(subpages.length + i + 1).padStart(2, '0')}
|
||||
</span>
|
||||
{album.albumName}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Stacked: fullbleed image + text at bottom + thumbnail strip ─
|
||||
if (heroStyle === 'stacked') {
|
||||
// Fetch album thumbnails for the strip
|
||||
const allEntries = [
|
||||
...subpages.map((sp) => ({ slug: sp.slug, name: sp.name })),
|
||||
...albums.map((a) => ({ slug: a.slug, name: a.albumName })),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="hero hero--stacked">
|
||||
<div className="hero__stacked-image">
|
||||
<HeroCarousel images={heroData} />
|
||||
<div className="hero__stacked-overlay">
|
||||
<FadeIn delay={0}>
|
||||
<h1 className="hero__title">{config.siteTitle}</h1>
|
||||
</FadeIn>
|
||||
{config.siteSubtitle && (
|
||||
<FadeIn delay={100}>
|
||||
<p className="hero__subtitle">{config.siteSubtitle}</p>
|
||||
</FadeIn>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FadeIn delay={200}>
|
||||
<nav className="hero__thumbnail-strip">
|
||||
{allEntries.map((entry) => (
|
||||
<Link
|
||||
key={entry.slug}
|
||||
href={`/${entry.slug}`}
|
||||
className="hero__thumbnail-item"
|
||||
>
|
||||
<span className="hero__thumbnail-label">{entry.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</FadeIn>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Mosaic: multi-image grid with frosted title overlay ────────
|
||||
if (heroStyle === 'mosaic') {
|
||||
return (
|
||||
<div className="hero hero--mosaic">
|
||||
<div className="hero__mosaic-grid">
|
||||
{heroData.slice(0, 6).map((img, i) => (
|
||||
<div key={i} className={`hero__mosaic-cell hero__mosaic-cell--${i + 1}`}>
|
||||
<Image
|
||||
src={img.src}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, 50vw"
|
||||
priority={i < 2}
|
||||
{...(img.blurDataURL
|
||||
? { placeholder: 'blur' as const, blurDataURL: img.blurDataURL }
|
||||
: {})}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="hero__mosaic-overlay">
|
||||
<FadeIn delay={0}>
|
||||
<h1 className="hero__title">{config.siteTitle}</h1>
|
||||
</FadeIn>
|
||||
{config.siteSubtitle && (
|
||||
<FadeIn delay={100}>
|
||||
<p className="hero__subtitle">{config.siteSubtitle}</p>
|
||||
</FadeIn>
|
||||
)}
|
||||
<FadeIn delay={200}>
|
||||
<nav className="hero__nav">
|
||||
<HeroNavLinks subpages={subpages} albums={albums} />
|
||||
</nav>
|
||||
</FadeIn>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Default: split / fullbleed / minimal (existing layouts) ────
|
||||
return (
|
||||
<div className="hero">
|
||||
{/* ── Left Panel ──────────────────────────────── */}
|
||||
@@ -49,16 +197,7 @@ export default async function HomePage() {
|
||||
|
||||
<FadeIn delay={200}>
|
||||
<nav className="hero__nav">
|
||||
{subpages.map((sp) => (
|
||||
<Link key={sp.slug} href={`/${sp.slug}`} className="hero__nav-link">
|
||||
{sp.name}
|
||||
</Link>
|
||||
))}
|
||||
{albums.map((album) => (
|
||||
<Link key={album.id} href={`/${album.slug}`} className="hero__nav-link">
|
||||
{album.albumName}
|
||||
</Link>
|
||||
))}
|
||||
<HeroNavLinks subpages={subpages} albums={albums} />
|
||||
</nav>
|
||||
</FadeIn>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user