Add image uploads to components, fix sidebar count for disabled
Deploy to LXC / deploy (push) Successful in 19s
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:
@@ -1,7 +1,7 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
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';
|
||||
|
||||
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 [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
|
||||
.select({ value: count() })
|
||||
.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 { eq, desc } from 'drizzle-orm';
|
||||
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 }) => {
|
||||
const [component] = await db
|
||||
@@ -164,6 +164,42 @@ export const actions: Actions = {
|
||||
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 }) => {
|
||||
await db
|
||||
.update(components)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
let editingInstanceId = $state<string | null>(null);
|
||||
let showAddInstances = $state(false);
|
||||
let showUploadForm = $state(false);
|
||||
let showDocForm = $state(false);
|
||||
let showDeleteConfirm = $state(false);
|
||||
|
||||
@@ -85,6 +86,55 @@
|
||||
|
||||
<div class="grid gap-6 lg:grid-cols-3">
|
||||
<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 -->
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user