Show thumbnails on component list cards
Deploy to LXC / deploy (push) Successful in 30s

Component cards now display the first uploaded image as a thumbnail,
matching the device list card design. Falls back to a placeholder
icon when no image is uploaded.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 17:04:18 +07:00
parent 2695de25a3
commit 3a3781ba7a
2 changed files with 35 additions and 3 deletions
+22 -2
View File
@@ -1,7 +1,7 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db/index.js'; import { db } from '$lib/server/db/index.js';
import { components, componentInstances, devices } from '$lib/server/db/schema.js'; import { components, componentInstances, componentImages } from '$lib/server/db/schema.js';
import { eq, ilike, or, and, isNull, isNotNull, count, desc, sql } from 'drizzle-orm'; import { eq, ilike, or, and, count, sql } from 'drizzle-orm';
export const load: PageServerLoad = async ({ url }) => { export const load: PageServerLoad = async ({ url }) => {
const type = url.searchParams.get('type'); const type = url.searchParams.get('type');
@@ -60,9 +60,29 @@ export const load: PageServerLoad = async ({ url }) => {
} }
} }
// Get first image per component
let imageMap: Record<string, string> = {};
if (componentIds.length > 0) {
const images = await db
.select({
componentId: componentImages.componentId,
thumbnailPath: componentImages.thumbnailPath,
filePath: componentImages.filePath
})
.from(componentImages)
.where(sql`${componentImages.componentId} IN ${componentIds}`);
for (const img of images) {
if (!imageMap[img.componentId]) {
imageMap[img.componentId] = img.thumbnailPath ?? img.filePath;
}
}
}
return { return {
components: componentList.map((c) => ({ components: componentList.map((c) => ({
...c, ...c,
thumbnail: imageMap[c.id] ?? null,
total: instanceCounts[c.id]?.total ?? 0, total: instanceCounts[c.id]?.total ?? 0,
installed: instanceCounts[c.id]?.installed ?? 0, installed: instanceCounts[c.id]?.installed ?? 0,
available: (instanceCounts[c.id]?.total ?? 0) - (instanceCounts[c.id]?.installed ?? 0) available: (instanceCounts[c.id]?.total ?? 0) - (instanceCounts[c.id]?.installed ?? 0)
+13 -1
View File
@@ -61,7 +61,18 @@
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{#each data.components as comp} {#each data.components as comp}
<a href="/components/{comp.id}" <a href="/components/{comp.id}"
class="group rounded-lg border border-gray-200 bg-white p-4 transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"> class="group rounded-lg border border-gray-200 bg-white transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
<!-- Thumbnail -->
<div class="flex h-32 items-center justify-center bg-gray-100 dark:bg-gray-700">
{#if comp.thumbnail}
<img src={comp.thumbnail} alt={comp.title} class="h-full w-full object-cover" />
{:else}
<svg class="h-10 w-10 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
{/if}
</div>
<div class="p-4">
<div class="mb-2 flex items-start justify-between"> <div class="mb-2 flex items-start justify-between">
<h3 class="font-medium text-gray-900 group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400">{comp.title}</h3> <h3 class="font-medium text-gray-900 group-hover:text-blue-600 dark:text-white dark:group-hover:text-blue-400">{comp.title}</h3>
<span class="flex-shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400"> <span class="flex-shrink-0 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-400">
@@ -83,6 +94,7 @@
{comp.available} available {comp.available} available
</span> </span>
</div> </div>
</div>
</a> </a>
{/each} {/each}
</div> </div>