feat: media picker and image removal

This commit is contained in:
fsylvain committed 2026-10-01 05:04:58 +02:00
1 parent 008b0dfbb9
commit b3793d5dbb
54 files changed
+3769 -1412

No files matched your search

+46 -17
View File
@@ -1,4 +1,14 @@
import { ref } from 'vue'
import { redirectToLogin, redirectUnverified } from '@/composables/useApi'
import type { MediaItem } from '@/composables/useMedia'
const UPLOAD_URL = '/v1/media/'
const HTTP_CREATED = 201
const HTTP_BAD_REQUEST = 400
const HTTP_UNAUTHORIZED = 401
const HTTP_FORBIDDEN = 403
const HTTP_TOO_LARGE = 413
const HTTP_UNSUPPORTED = 415
export interface UploadProgress {
loaded: number
@@ -6,10 +16,41 @@ export interface UploadProgress {
}
export type UploadOutcome =
| { kind: 'success'; location: string; id: number }
| { kind: 'rejected'; body: string }
| { kind: 'success'; item: MediaItem }
| { kind: 'rejected'; reason: 'size' | 'type' }
| { kind: 'handled' }
| { kind: 'failed' }
function readItem(responseText: string): MediaItem | null {
try {
return JSON.parse(responseText) as MediaItem
} catch {
return null
}
}
function outcomeOf(request: XMLHttpRequest): UploadOutcome {
switch (request.status) {
case HTTP_CREATED: {
const item = readItem(request.responseText)
return item ? { kind: 'success', item } : { kind: 'failed' }
}
case HTTP_TOO_LARGE:
return { kind: 'rejected', reason: 'size' }
case HTTP_UNSUPPORTED:
case HTTP_BAD_REQUEST:
return { kind: 'rejected', reason: 'type' }
case HTTP_UNAUTHORIZED:
redirectToLogin()
return { kind: 'handled' }
case HTTP_FORBIDDEN:
redirectUnverified()
return { kind: 'handled' }
default:
return { kind: 'failed' }
}
}
export function useImageUpload() {
const progress = ref<UploadProgress>({ loaded: 0, total: 0 })
let activeRequest: XMLHttpRequest | null = null
@@ -19,31 +60,19 @@ export function useImageUpload() {
activeRequest = null
}
function start(articleId: number, file: File): Promise<UploadOutcome> {
function start(file: File): Promise<UploadOutcome> {
progress.value = { loaded: 0, total: file.size }
return new Promise((resolve) => {
const request = new XMLHttpRequest()
activeRequest = request
request.open('POST', `/v1/article/${articleId}/image`)
request.open('POST', UPLOAD_URL)
request.withCredentials = true
request.upload.addEventListener('progress', (event) => {
progress.value = { loaded: event.loaded, total: event.total }
})
request.addEventListener('load', () => {
activeRequest = null
if (request.status === 201) {
const body = JSON.parse(request.responseText) as {
id: number
location: string
}
resolve({ kind: 'success', location: body.location, id: body.id })
return
}
if (request.status === 400) {
resolve({ kind: 'rejected', body: request.responseText })
return
}
resolve({ kind: 'failed' })
resolve(outcomeOf(request))
})
request.addEventListener('error', () => {
activeRequest = null