From 4a0b57b9320483d4d4e4eedd1c521c8ba80869c9 Mon Sep 17 00:00:00 2001 From: ralksta Date: Fri, 29 May 2026 17:14:58 +0200 Subject: [PATCH] feat: add thumbnail previews to admin panel for albums and hero images - Add /api/admin/thumbnail/[id] proxy endpoint for serving Immich thumbnails - Show album cover thumbnails in AlbumCard components (subpages + standalone) - Show actual image thumbnails in AlbumPicker modal instead of emoji placeholders - Show small thumbnail previews for hero image entries - Add corresponding CSS styles for all thumbnail elements --- app/admin/admin.css | 32 ++++++++++++++ app/admin/components/AlbumPicker.tsx | 7 ++- app/admin/components/PageBuilder.tsx | 26 ++++++++++- app/api/admin/thumbnail/[id]/route.ts | 62 +++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 app/api/admin/thumbnail/[id]/route.ts diff --git a/app/admin/admin.css b/app/admin/admin.css index dce3557..f2bd358 100644 --- a/app/admin/admin.css +++ b/app/admin/admin.css @@ -425,6 +425,14 @@ border-radius: 6px; } +.hero-item-thumb { + width: 32px; + height: 32px; + object-fit: cover; + border-radius: 4px; + background: #222; +} + .hero-uuid { font-family: monospace; font-size: 0.75rem; @@ -455,6 +463,22 @@ align-items: center; justify-content: space-between; padding: 0.6rem 0.75rem; + gap: 0.75rem; +} + +.album-card-thumb { + flex-shrink: 0; + width: 48px; + height: 48px; + border-radius: 6px; + overflow: hidden; + background: #222; +} + +.album-card-thumb img { + width: 100%; + height: 100%; + object-fit: cover; } .album-card-info { @@ -735,6 +759,14 @@ font-size: 1.1rem; } +.picker-thumb-img { + width: 40px; + height: 40px; + object-fit: cover; + border-radius: 6px; + background: #222; +} + .picker-item-info { flex: 1; min-width: 0; diff --git a/app/admin/components/AlbumPicker.tsx b/app/admin/components/AlbumPicker.tsx index 3c1ff06..dd86d57 100644 --- a/app/admin/components/AlbumPicker.tsx +++ b/app/admin/components/AlbumPicker.tsx @@ -68,7 +68,12 @@ export default function AlbumPicker({ albums, onSelect, onClose, usedAlbumIds }: >
{album.thumbnailAssetId ? ( -
📷
+ ) : (
📁
)} diff --git a/app/admin/components/PageBuilder.tsx b/app/admin/components/PageBuilder.tsx index 6b26f2c..8ffb9f4 100644 --- a/app/admin/components/PageBuilder.tsx +++ b/app/admin/components/PageBuilder.tsx @@ -419,6 +419,11 @@ export default function PageBuilder() { return found?.assetCount || 0; } + function getAlbumThumbnailId(id: string): string | null { + const found = immichAlbums.find((a) => a.id === id); + return found?.thumbnailAssetId || null; + } + // ── Render ─────────────────────────────────────────────────── if (loading) { return ( @@ -467,6 +472,12 @@ export default function PageBuilder() { )} {gallery.hero.map((id, i) => (
+ {id.slice(0, 8)}...{id.slice(-4)} @@ -499,6 +510,7 @@ export default function PageBuilder() { album={album} name={getAlbumName(album.id)} count={getAlbumCount(album.id)} + thumbnailId={getAlbumThumbnailId(album.id)} onRemove={() => removeStandaloneAlbum(i)} onUpdate={(updates) => { setGallery((g) => { @@ -624,6 +636,7 @@ export default function PageBuilder() { album={album} name={getAlbumName(album.id)} count={getAlbumCount(album.id)} + thumbnailId={getAlbumThumbnailId(album.id)} onRemove={() => removeSubpageAlbum(spIndex, aIndex)} onUpdate={(updates) => { setGallery((g) => { @@ -684,6 +697,7 @@ export default function PageBuilder() { album={album} name={getAlbumName(album.id)} count={getAlbumCount(album.id)} + thumbnailId={getAlbumThumbnailId(album.id)} onRemove={() => removeSectionAlbum(spIndex, secIndex, aIndex)} onUpdate={(updates) => { setGallery((g) => { @@ -767,16 +781,26 @@ interface AlbumCardProps { album: AlbumEntry; name: string; count: number; + thumbnailId: string | null; onRemove: () => void; onUpdate: (updates: Partial) => void; } -function AlbumCard({ album, name, count, onRemove, onUpdate }: AlbumCardProps) { +function AlbumCard({ album, name, count, thumbnailId, onRemove, onUpdate }: AlbumCardProps) { const [expanded, setExpanded] = useState(false); return (
+ {thumbnailId && ( +
+ +
+ )}
{album.title || name} {count} photos diff --git a/app/api/admin/thumbnail/[id]/route.ts b/app/api/admin/thumbnail/[id]/route.ts new file mode 100644 index 0000000..0d1842e --- /dev/null +++ b/app/api/admin/thumbnail/[id]/route.ts @@ -0,0 +1,62 @@ +/** + * Admin-only thumbnail proxy — serves small thumbnails for the admin panel + * using raw Immich asset UUIDs (no token encoding needed since admin is authenticated). + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { isAdminAuthenticated, isAdminEnabled } from '@/lib/admin/auth'; +import { getConfig } from '@/lib/config'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + if (!isAdminEnabled()) { + return NextResponse.json({ error: 'Admin not enabled' }, { status: 403 }); + } + if (!(await isAdminAuthenticated())) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + const config = getConfig(); + if (config.needsSetup) { + return NextResponse.json({ error: 'Not configured' }, { status: 503 }); + } + + const { id: assetId } = await params; + + // Validate it looks like a UUID + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(assetId)) { + return NextResponse.json({ error: 'Invalid asset ID' }, { status: 400 }); + } + + try { + const res = await fetch( + `${config.immich.apiUrl}/assets/${assetId}/thumbnail?size=thumbnail`, + { + headers: { + 'x-api-key': config.immich.apiKey, + Accept: 'application/octet-stream', + }, + }, + ); + + if (!res.ok) { + return NextResponse.json({ error: 'Asset not found' }, { status: 404 }); + } + + const contentType = res.headers.get('content-type') || 'image/jpeg'; + const body = res.body; + + return new NextResponse(body, { + headers: { + 'Content-Type': contentType, + 'Cache-Control': 'private, max-age=3600', + }, + }); + } catch (err) { + console.error('[Admin Thumbnail] Failed to fetch:', err); + return NextResponse.json({ error: 'Failed to fetch thumbnail' }, { status: 502 }); + } +}