Add image uploads to components, fix sidebar count for disabled
Deploy to LXC / deploy (push) Successful in 19s

- Image upload/delete on component detail page (same pattern as devices)
- Images section with grid, hover-to-delete, 50MB client-side limit
- Sidebar component count now excludes disabled components by joining
  componentInstances with components and filtering disabled=false

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-09 15:31:56 +07:00
parent 9a59213da0
commit 29d2aa943c
3 changed files with 93 additions and 3 deletions
+6 -2
View File
@@ -1,7 +1,7 @@
import type { LayoutServerLoad } from './$types'; import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { db } from '$lib/server/db/index.js'; import { db } from '$lib/server/db/index.js';
import { devices, componentInstances } from '$lib/server/db/schema.js'; import { devices, components, componentInstances } from '$lib/server/db/schema.js';
import { count, eq, or, and } from 'drizzle-orm'; import { count, eq, or, and } from 'drizzle-orm';
export const load: LayoutServerLoad = async ({ locals }) => { export const load: LayoutServerLoad = async ({ locals }) => {
@@ -10,7 +10,11 @@ export const load: LayoutServerLoad = async ({ locals }) => {
} }
const [deviceCount] = await db.select({ value: count() }).from(devices).where(eq(devices.disabled, false)); const [deviceCount] = await db.select({ value: count() }).from(devices).where(eq(devices.disabled, false));
const [componentCount] = await db.select({ value: count() }).from(componentInstances); const [componentCount] = await db
.select({ value: count() })
.from(componentInstances)
.innerJoin(components, eq(componentInstances.componentId, components.id))
.where(eq(components.disabled, false));
const [repairCount] = await db const [repairCount] = await db
.select({ value: count() }) .select({ value: count() })
.from(devices) .from(devices)
@@ -3,7 +3,7 @@ import { db } from '$lib/server/db/index.js';
import { components, componentInstances, devices, locations, installationLog, componentImages, componentDocuments } from '$lib/server/db/schema.js'; import { components, componentInstances, devices, locations, installationLog, componentImages, componentDocuments } from '$lib/server/db/schema.js';
import { eq, desc } from 'drizzle-orm'; import { eq, desc } from 'drizzle-orm';
import { error, fail, redirect } from '@sveltejs/kit'; import { error, fail, redirect } from '@sveltejs/kit';
import { saveDocument, deleteFile } from '$lib/server/uploads.js'; import { saveImage, saveDocument, deleteFile } from '$lib/server/uploads.js';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params }) => {
const [component] = await db const [component] = await db
@@ -164,6 +164,42 @@ export const actions: Actions = {
return { documentDeleted: true }; return { documentDeleted: true };
}, },
uploadImage: async ({ request, params }) => {
const formData = await request.formData();
const file = formData.get('image') as File;
if (!file || file.size === 0) return fail(400, { error: 'No file selected' });
try {
const { filePath, thumbnailPath } = await saveImage(file, 'components');
const caption = formData.get('caption') as string;
await db.insert(componentImages).values({
componentId: params.id,
filePath,
thumbnailPath,
caption: caption || null
});
return { imageUploaded: true };
} catch (err) {
return fail(400, { error: `Upload failed: ${err instanceof Error ? err.message : 'Unknown error'}` });
}
},
deleteImage: async ({ request }) => {
const formData = await request.formData();
const imageId = formData.get('imageId') as string;
const [img] = await db.select().from(componentImages).where(eq(componentImages.id, imageId));
if (img) {
await deleteFile(img.filePath);
if (img.thumbnailPath) await deleteFile(img.thumbnailPath);
await db.delete(componentImages).where(eq(componentImages.id, imageId));
}
return { imageDeleted: true };
},
disable: async ({ params }) => { disable: async ({ params }) => {
await db await db
.update(components) .update(components)
@@ -8,6 +8,7 @@
let editingInstanceId = $state<string | null>(null); let editingInstanceId = $state<string | null>(null);
let showAddInstances = $state(false); let showAddInstances = $state(false);
let showUploadForm = $state(false);
let showDocForm = $state(false); let showDocForm = $state(false);
let showDeleteConfirm = $state(false); let showDeleteConfirm = $state(false);
@@ -85,6 +86,55 @@
<div class="grid gap-6 lg:grid-cols-3"> <div class="grid gap-6 lg:grid-cols-3">
<div class="space-y-6 lg:col-span-2"> <div class="space-y-6 lg:col-span-2">
<!-- Images -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">Images</h2>
<button type="button" onclick={() => (showUploadForm = !showUploadForm)}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400">
{showUploadForm ? 'Cancel' : 'Upload'}
</button>
</div>
{#if showUploadForm}
<form method="POST" action="?/uploadImage" enctype="multipart/form-data" use:enhance class="mb-4 flex flex-wrap items-end gap-3">
<input type="file" name="image" accept="image/*" required
onchange={(e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file && file.size > 50 * 1024 * 1024) {
alert('File too large. Maximum size is 50MB.');
(e.target as HTMLInputElement).value = '';
}
}}
class="text-sm text-gray-600 dark:text-gray-400" />
<input type="text" name="caption" placeholder="Caption (optional)"
class="rounded-md border border-gray-300 px-3 py-1.5 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white" />
<button type="submit" class="rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-blue-700">Upload</button>
</form>
{/if}
{#if data.images.length === 0}
<p class="text-sm text-gray-500 dark:text-gray-400">No images yet.</p>
{:else}
<div class="grid gap-2 sm:grid-cols-3">
{#each data.images as img}
<div class="group relative overflow-hidden rounded-md">
<img src={img.filePath} alt={img.caption ?? c.title} class="h-32 w-full object-cover" />
<form method="POST" action="?/deleteImage" use:enhance
class="absolute top-1 right-1 hidden group-hover:block">
<input type="hidden" name="imageId" value={img.id} />
<button type="submit" class="rounded bg-red-600 p-1 text-xs text-white hover:bg-red-700" title="Delete">
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</form>
</div>
{/each}
</div>
{/if}
</div>
<!-- Instances --> <!-- Instances -->
<div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800"> <div class="rounded-lg border border-gray-200 bg-white p-5 dark:border-gray-700 dark:bg-gray-800">
<div class="mb-3 flex items-center justify-between"> <div class="mb-3 flex items-center justify-between">