diff --git a/frontend/src/lib/components/admin/ImageUpload.svelte b/frontend/src/lib/components/admin/ImageUpload.svelte index c6cf080..8fab99f 100644 --- a/frontend/src/lib/components/admin/ImageUpload.svelte +++ b/frontend/src/lib/components/admin/ImageUpload.svelte @@ -1,6 +1,7 @@ - - -
-
- Image Upload - Upload images for the portfolio or other uses. -
- -
-
- - -
-

Drop files here, or click to open the file picker

-
-
+
+ +
+

Upload Images

+

+ Add portfolio images with tags and optional descriptions +

+
-
-
Selected files ({uploadFiles.length})
-
- {#each uploadFiles as f (f.name)} -
-
{f.name} • {Math.round(f.size / 1024)}KB
- -
- {/each} + + + +
+
+ Drop or Select Files + Supported: JPEG, PNG, WebP, and other image formats +
- {#if uploadResults.length > 0} -
Upload Results
-
- {#each uploadResults as result (result.url || result.name)} -
- {result.name}: {result.error ? `Failed: ${result.error}` : `Success: `}{result.url} + + + + + +
+ + + + + +

Drop images here or click to browse

+

Supports AVIF, PNG, JPG, WebP and other formats

+
+
+ + + {#if uploadFiles.length > 0} +
+
+
+

Ready to Upload

+

+ {uploadFiles.length} + {uploadFiles.length === 1 ? 'file' : 'files'} selected +

- {/each} + + {uploadFiles.length} pending + +
+ + +
+ {#each uploadFiles as f (f.name)} + {@const preview = filePreviews.find((p) => p.file === f)?.preview} +
+
+
+ {#if preview} + {f.name} + {:else} +
+ + + + + +
+ {/if} +
+
+

{f.name}

+
+

{(f.size / 1024).toFixed(1)} KB

+ {#if uploading && uploadStatus[f.name]} + + + + + + {uploadStatus[f.name]} + + {/if} +
+
+
+ +
+ {/each} +
{/if} -
-
- - -
-
- {#each tags as tag (tag)} - - {tag} - - - - {/each} - - +
+
+ {#if result.error} + + + + + + {:else} + + + + + {/if} +
+
+

+ {result.name} +

+ {#if result.error} +

{result.error}

+ {:else} + + View uploaded image → + + {/if} +
+
+
+ {/each} +
+ {/if} + + +
+ +

+ Searchable tags help organize images. Use category:value format. +

+ +
+
+
+ {#each tags as tag (tag)} + + {tag} + + + {/each} + + +
{#if input.length && suggestions.length} -
+
{#each suggestions as s, i (s)} {@const isHighlighted = isMobile ? i === 0 : i === selectedSuggestionIndex}
{/if} -
+
+
-

- Add searchable tags here such as `scooby doo` or filterable categories like - `style:french` - or - `colour:green` -

-
-
- -
- - + +
+
+ +
+
+
+ - - - - Upload with {tags.length} tag{tags.length === 1 ? '' : 's'}? - - You have {tags.length} tag{tags.length === 1 ? '' : 's'} selected: - {tags.join(', ')}. - Ready to upload {uploadFiles.length} file{uploadFiles.length === 1 ? '' : 's'}? - - - - Cancel - Upload - - - + + + + + Confirm Upload + + You're about to upload {uploadFiles.length} + {uploadFiles.length === 1 ? 'file' : 'files'} with {tags.length} + {tags.length === 1 ? 'tag' : 'tags'}. + {#if tags.length > 0} +
+ {#each tags as tag} + + {tag} + + {/each} +
+ {/if} +
+
+ + Cancel + Upload Now + +
+
+
diff --git a/frontend/src/routes/portfolio/+page.svelte b/frontend/src/routes/portfolio/+page.svelte index 5e86d0c..cb1eada 100644 --- a/frontend/src/routes/portfolio/+page.svelte +++ b/frontend/src/routes/portfolio/+page.svelte @@ -275,6 +275,20 @@ function clearFilters() { selectedFilters = {}; + offset = 0; + + // Build URL with reactive page state + const url = new URL(page.url); + const keysToDelete = Array.from(url.searchParams.keys()).filter((k) => k.startsWith('filter[')); + keysToDelete.forEach((k) => url.searchParams.delete(k)); + // eslint-disable-next-line svelte/no-navigation-without-resolve + goto(url.pathname + url.search, { replaceState: true, noScroll: true }); + + fetchImages(false); + fetchFilters(); + } + + function clearSearch() { selectedTag = ''; selectedTags = []; searchQuery = ''; @@ -284,13 +298,10 @@ const url = new URL(page.url); url.searchParams.delete('tag'); url.searchParams.delete('tags'); - const keysToDelete = Array.from(url.searchParams.keys()).filter((k) => k.startsWith('filter[')); - keysToDelete.forEach((k) => url.searchParams.delete(k)); // eslint-disable-next-line svelte/no-navigation-without-resolve goto(url.pathname + url.search, { replaceState: true, noScroll: true }); fetchImages(false); - fetchFilters(); } let sentinelRef: HTMLDivElement | undefined = $state(undefined); @@ -614,6 +625,27 @@ + + {#if selectedTag || selectedTags.length > 0} + + {/if}