Processing responsive images...
-
-
-
+
+
Drag and drop images here, or choose files below.
+
+
Saving image changes...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
No images uploaded yet.
+
+
@@ -76,7 +101,8 @@ function handleFileChange(event) {
.admin-image-manager__header, .admin-image-manager__row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-3); }
.admin-image-manager__header { justify-content: space-between; }
.admin-image-manager__header h2 { font-size: var(--text-xl); }
-.admin-image-manager__controls { display: grid; gap: var(--space-4); }
-.admin-image-manager__upload { display: grid; gap: var(--space-2); }
-.admin-image-manager__row--order { align-items: stretch; }
+.admin-image-manager__dropzone { display: grid; gap: var(--space-2); border: var(--border-thin) dashed var(--color-border-strong); border-radius: var(--radius-lg); padding: var(--space-4); text-align: center; }
+.admin-image-manager__dropzone--active { border-color: var(--color-interactive); background: var(--color-surface-muted, var(--color-surface)); }
+.admin-image-manager__list { display: grid; gap: var(--space-4); list-style: none; padding: 0; margin: 0; }
+.admin-image-manager__item { display: grid; gap: var(--space-3); border: var(--border-thin) solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-4); }
diff --git a/apps/web/src/composables/useAdminImages.js b/apps/web/src/composables/useAdminImages.js
index 1791306..565dd34 100644
--- a/apps/web/src/composables/useAdminImages.js
+++ b/apps/web/src/composables/useAdminImages.js
@@ -36,23 +36,48 @@ export function useAdminImages(inventoryItemId) {
finally { isSaving.value = false; }
}
- async function upload(file) {
+ async function uploadOne(file, isPrimary, token) {
const objectUrl = URL.createObjectURL(file);
const image = new Image();
image.src = objectUrl;
try {
await image.decode();
- await apply((token) => uploadAdminImage(inventoryItemId, file, {
+ await uploadAdminImage(inventoryItemId, file, {
width: image.naturalWidth,
height: image.naturalHeight,
altText: file.name,
- isPrimary: images.value.length === 0,
- }, token));
+ isPrimary,
+ }, token);
} finally {
URL.revokeObjectURL(objectUrl);
}
}
+ async function upload(files) {
+ const fileList = Array.from(files ?? []);
+ if (fileList.length === 0) return;
+ await apply(async (token) => {
+ let hasExistingImages = images.value.length > 0;
+ for (const file of fileList) {
+ await uploadOne(file, !hasExistingImages, token);
+ hasExistingImages = true;
+ }
+ });
+ }
+
+ async function moveImage(imageId, direction) {
+ const ordered = [...images.value].sort((left, right) => left.sortOrder - right.sortOrder);
+ const index = ordered.findIndex((image) => image.id === imageId);
+ const neighborIndex = direction === 'up' ? index - 1 : index + 1;
+ if (index === -1 || neighborIndex < 0 || neighborIndex >= ordered.length) return;
+ const current = ordered[index];
+ const neighbor = ordered[neighborIndex];
+ await apply(async (token) => {
+ await reorderImage(current.id, neighbor.sortOrder, token);
+ await reorderImage(neighbor.id, current.sortOrder, token);
+ });
+ }
+
return {
images,
isLoading,
@@ -60,7 +85,7 @@ export function useAdminImages(inventoryItemId) {
error,
load,
upload,
- reorder: (imageId, sortOrder) => apply((token) => reorderImage(imageId, sortOrder, token)),
+ move: moveImage,
setPrimary: (imageId) => apply((token) => setPrimaryImage(imageId, token)),
updateAltText: (imageId, altText) => apply((token) => updateImageAltText(imageId, altText, token)),
retry: (imageId) => apply((token) => retryImageProcessing(imageId, token)),
diff --git a/apps/web/src/pages/AdminImagePage.vue b/apps/web/src/pages/AdminImagePage.vue
index a680f86..9172061 100644
--- a/apps/web/src/pages/AdminImagePage.vue
+++ b/apps/web/src/pages/AdminImagePage.vue
@@ -7,7 +7,7 @@ import { useAdminImages } from '@/composables/useAdminImages.js';
const route = useRoute();
const inventoryItemId = route.params.id;
-const { images, isLoading, isSaving, error, load, upload, reorder, setPrimary, updateAltText, retry, remove } = useAdminImages(inventoryItemId);
+const { images, isLoading, isSaving, error, load, upload, move, setPrimary, updateAltText, retry, remove } = useAdminImages(inventoryItemId);
onMounted(load);
@@ -22,12 +22,12 @@ onMounted(load);
move(id, direction)"
@primary="setPrimary"
- @alt-text="updateAltText(images[0]?.id, $event)"
- @retry="retry(images[0]?.id)"
+ @alt-text="({ id, altText }) => updateAltText(id, altText)"
+ @retry="retry"
@delete="remove"
/>
diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md
index b25deef..932fa00 100644
--- a/docs/IMPLEMENTATION-STATUS.md
+++ b/docs/IMPLEMENTATION-STATUS.md
@@ -177,6 +177,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
- Public catalog sorting now supports latest manual/calculated market value low-to-high and high-to-low, with null values placed last.
- Public listing detail now shows market-value freshness and suppresses asking price for sold listings, with Storybook coverage for sold and valued states.
- Public catalog filters now collapse behind an accessible mobile disclosure, show an active-filter count, and provide a reset action while preserving URL synchronization.
+- The admin image manager now supports multi-file upload with a drag-and-drop dropzone, per-image alt-text editing, primary selection, up/down reordering, and per-image retry/delete actions targeted at the correct image instead of the first uploaded image.
## Local Commands