feat(admin): add asset picker with favorites, drag-and-drop reordering, preview links, and keyboard shortcuts
- Asset Picker: Visual browser for hero images with Favorites tab (shown first) and All Photos tab. Includes infinite scroll and UUID fallback input for assets not easily found in the grid. - Drag & Drop: All lists (hero images, standalone albums, subpage albums, subpage tiles) are now reorderable via drag handles using @dnd-kit. - Preview Links: 'Preview Site' button in save bar, '↗ Site' in header, and per-subpage preview links with URL slug preview shown inline. - Keyboard Shortcuts: ⌘+S / Ctrl+S to save in both Pages and Settings tabs. - Unsaved Changes Guard: beforeunload warning prevents accidental data loss. - New API route /api/admin/assets for browsing Immich assets with pagination and favorites filter via search/metadata endpoint.
This commit is contained in:
87
app/api/admin/assets/route.ts
Normal file
87
app/api/admin/assets/route.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Admin API: Browse Immich assets for the hero picker.
|
||||
* Supports pagination and fetching favorites first.
|
||||
*/
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { isAdminAuthenticated, isAdminEnabled } from '@/lib/admin/auth';
|
||||
import { getConfig } from '@/lib/config';
|
||||
|
||||
interface ImmichSearchResult {
|
||||
assets: {
|
||||
items: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
originalFileName: string;
|
||||
thumbhash: string | null;
|
||||
fileCreatedAt: string;
|
||||
isFavorite: boolean;
|
||||
}>;
|
||||
nextPage: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
/** GET: Search/browse assets from Immich for hero image selection. */
|
||||
export async function GET(request: NextRequest) {
|
||||
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: 'Immich not configured' }, { status: 503 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const favoritesOnly = searchParams.get('favorites') === 'true';
|
||||
const pageSize = 50;
|
||||
|
||||
try {
|
||||
// Use Immich search/metadata endpoint to query assets
|
||||
const body: Record<string, unknown> = {
|
||||
type: 'IMAGE',
|
||||
size: pageSize,
|
||||
page,
|
||||
order: 'desc',
|
||||
isNotInAlbum: false,
|
||||
};
|
||||
|
||||
if (favoritesOnly) {
|
||||
body.isFavorite = true;
|
||||
}
|
||||
|
||||
const res = await fetch(`${config.immich.apiUrl}/search/metadata`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': config.immich.apiKey,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return NextResponse.json({ error: `Immich API returned ${res.status}` }, { status: 502 });
|
||||
}
|
||||
|
||||
const data = (await res.json()) as ImmichSearchResult;
|
||||
const assets = data.assets.items.map((asset) => ({
|
||||
id: asset.id,
|
||||
originalFileName: asset.originalFileName,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
isFavorite: asset.isFavorite,
|
||||
}));
|
||||
|
||||
return NextResponse.json({
|
||||
assets,
|
||||
nextPage: data.assets.nextPage ? page + 1 : null,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Admin] Failed to fetch assets from Immich:', err);
|
||||
return NextResponse.json({ error: 'Failed to connect to Immich' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user