release: v0.12.0 (#520)

Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
This commit is contained in:
Ralksta
2026-08-23 23:18:14 +02:00
committed by GitHub
parent 2f546f440d
commit 10ec68f493
119 changed files with 6888 additions and 758 deletions

View File

@@ -19,6 +19,7 @@ import type { PhotoItem } from '@/app/[...path]/PhotoGrid';
import type { ImmichAsset } from '@/lib/immich';
import PasswordGate from '@/components/PasswordGate';
import { BackLink } from '@/components/BackLink';
import { getServerDictionary } from '@/lib/i18n/server';
import crypto from 'crypto';
export const dynamic = 'force-dynamic';
@@ -30,11 +31,12 @@ interface JournalDetailPageProps {
export async function generateMetadata({ params }: JournalDetailPageProps): Promise<Metadata> {
const { slug } = await params;
const entry = await readJournalEntry(slug);
if (!entry) return { title: 'Not Found' };
const t = getServerDictionary();
if (!entry) return { title: t.journal.notFound };
const { frontmatter } = entry.parsed;
const title = frontmatter.title || slug;
const description = frontmatter.subtitle || 'Journal entry on Immich Folio';
const description = frontmatter.subtitle || t.journal.entryDescription;
const ogImages = frontmatter.coverAssetId
? [{ url: imageUrl(frontmatter.coverAssetId, 'preview') }]
@@ -171,7 +173,10 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
.filter((a) => a.type === 'IMAGE' || a.type === 'VIDEO')
.map((a) => {
const ph = assetPlaceholder(a);
const exif = config.exifOnHover && a.type === 'IMAGE' ? assetExifSummary(a) : undefined;
const exif =
config.exif.onHover && config.exif.camera && a.type === 'IMAGE'
? assetExifSummary(a)
: undefined;
const isVideo = a.type === 'VIDEO';
return {
id: encodeAssetId(a.id),
@@ -188,7 +193,7 @@ export default async function JournalDetailPage({ params }: JournalDetailPagePro
return (
<div style={{ paddingTop: '2rem' }}>
<div style={{ maxWidth: '1100px', margin: '0 auto', padding: '0 1.5rem 1rem' }}>
<BackLink href="/journal" label="Back to Journal" />
<BackLink href="/journal" label={getServerDictionary().common.backToJournal} />
</div>
<EssayView
essay={essayForClient}

View File

@@ -8,14 +8,15 @@ import { immich } from '@/lib/immich';
import { imageUrl, assetPlaceholder } from '@/lib/urls';
import { BackLink } from '@/components/BackLink';
import { IconBook } from '@/components/Icons';
import { getServerDictionary } from '@/lib/i18n/server';
import './journal.css';
export const dynamic = 'force-dynamic';
export const metadata: Metadata = {
title: 'Journal',
description: 'Photo essays, travel stories, and behind-the-scenes journals.',
};
export function generateMetadata(): Metadata {
const t = getServerDictionary();
return { title: t.journal.title, description: t.journal.description };
}
interface EnrichedJournalEntry extends JournalEntrySummary {
coverUrl?: string;
@@ -24,6 +25,7 @@ interface EnrichedJournalEntry extends JournalEntrySummary {
}
export default async function JournalIndexPage() {
const t = getServerDictionary();
const isAuthedAdmin = await isAdminAuthenticated();
const allEntries = await listJournalEntries();
@@ -60,25 +62,23 @@ export default async function JournalIndexPage() {
return (
<div className="journal-index-container">
<div className="journal-index-header">
<BackLink href="/" label="Back to Gallery" />
<BackLink href="/" label={t.common.backToGallery} />
<p className="journal-index-header__kicker" aria-hidden="true">
Stories &amp; Essays
</p>
<h1 className="journal-index-header__title">Journal</h1>
<p className="journal-index-header__subtitle">
Photo essays, visual stories, and field notes.
{t.journal.kicker}
</p>
<h1 className="journal-index-header__title">{t.journal.title}</h1>
<p className="journal-index-header__subtitle">{t.journal.subtitle}</p>
</div>
{enrichedEntries.length === 0 ? (
<div className="journal-empty-state">
<p>No journal entries published yet.</p>
<p>{t.journal.empty}</p>
</div>
) : (
<div className="journal-grid">
{enrichedEntries.map((entry) => {
const dateStr = entry.frontmatter.date
? new Date(entry.frontmatter.date).toLocaleDateString('en-US', {
? new Date(entry.frontmatter.date).toLocaleDateString(t.dateLocale, {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -113,9 +113,11 @@ export default async function JournalIndexPage() {
<div className="journal-card__meta">
{dateStr && <span>{dateStr}</span>}
{dateStr && entry.readingTimeMinutes && <span>•</span>}
{entry.readingTimeMinutes && <span>{entry.readingTimeMinutes} min read</span>}
{entry.readingTimeMinutes && (
<span>{t.journal.minRead(entry.readingTimeMinutes)}</span>
)}
{entry.frontmatter.draft && (
<span className="journal-card__draft-badge">Draft</span>
<span className="journal-card__draft-badge">{t.journal.draft}</span>
)}
</div>
@@ -134,9 +136,11 @@ export default async function JournalIndexPage() {
)}
<div className="journal-card__footer">
<span className="journal-card__read-more">Read Story →</span>
<span className="journal-card__read-more">{t.journal.readStory}</span>
{entry.frontmatter.author && (
<span style={{ opacity: 0.65 }}>by {entry.frontmatter.author}</span>
<span style={{ opacity: 0.65 }}>
{t.journal.by(entry.frontmatter.author)}
</span>
)}
</div>
</div>