diff --git a/neode-ui/src/views/web5/Web5Identities.vue b/neode-ui/src/views/web5/Web5Identities.vue
index 30b09c74..9cc79ad6 100644
--- a/neode-ui/src/views/web5/Web5Identities.vue
+++ b/neode-ui/src/views/web5/Web5Identities.vue
@@ -321,12 +321,24 @@
@@ -395,6 +407,46 @@ const profileEditorIdentity = ref
(null)
const profileForm = ref({})
const profileSaving = ref(false)
const profilePublishing = ref(false)
+const avatarUploading = ref(false)
+const bannerUploading = ref(false)
+
+// Upload to local blob store + set the corresponding profile URL so
+// the kind:0 event (publish) includes a reachable picture/banner. The
+// returned `self_test_url` is a capability-signed /blob/?cap=…
+// path — works locally. For external nostr clients to see the image,
+// swap to a public image host later.
+async function uploadAsset(ev: Event, field: 'picture' | 'banner') {
+ const input = ev.target as HTMLInputElement
+ const file = input?.files?.[0]
+ if (!file) return
+ const flag = field === 'picture' ? avatarUploading : bannerUploading
+ flag.value = true
+ profileError.value = ''
+ try {
+ const buf = await file.arrayBuffer()
+ const resp = await fetch('/api/blob', {
+ method: 'POST',
+ credentials: 'include',
+ headers: {
+ 'Content-Type': 'application/octet-stream',
+ 'X-Blob-Mime': file.type || 'application/octet-stream',
+ 'X-Blob-Filename': file.name,
+ },
+ body: buf,
+ })
+ if (!resp.ok) throw new Error(`upload failed: HTTP ${resp.status}`)
+ const { self_test_url } = await resp.json() as { self_test_url?: string }
+ if (!self_test_url) throw new Error('blob API returned no URL')
+ // Assign and let the
in the header preview react.
+ profileForm.value[field] = self_test_url
+ } catch (e: unknown) {
+ profileError.value = e instanceof Error ? e.message : `${field} upload failed`
+ } finally {
+ flag.value = false
+ // Clear the input so selecting the same file again re-fires change.
+ if (input) input.value = ''
+ }
+}
const profileError = ref('')
const profileSuccess = ref('')