Upload Images

Add portfolio images with tags and optional descriptions

Drop or Select Files HEIC, AVIF, WebP, PNG, JPEG and more. Maximum 20MB per file

Drop images here or click to browse

HEIC, AVIF, WebP, PNG, JPEG, GIF & more — max 20MB per file

{#if uploadFiles.length > 0}

Ready to Upload

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

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

{f.name}

{formatFileSize(f.size)} {#if oversized} — exceeds 20MB limit {/if}

{#if uploading && uploadStatus[f.name]} {uploadStatus[f.name]} {/if}
{/each}
{/if} {#if uploadResults.length > 0}

Upload Results

{#each uploadResults as result (result.url || result.name)}
{#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}
selectSuggestion(s)} onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && selectSuggestion(s)} role="button" tabindex="0" > {s}
{/each}
{/if}
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)} {tag} {/each}
{/if}
Cancel Upload Now