Files
BRETT/app/components/UploadPopover.vue
FliesentischKingandDevin d95ccf9dc7 Shared Spinner component for button loading states
Adds a reusable shadcn-style Spinner (lucide loader-circle icon,
localized aria-label) and wires it into every button that has a
pending state: comment submit, delete, tag save, profile save,
admin saves, upload, login and register.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-04 03:05:48 +02:00

99 lines
3.1 KiB
Vue

<script setup lang="ts">
import FileDropzone from '@/components/FileDropzone.vue'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from '@/components/ui/popover'
import { Spinner } from '@/components/ui/spinner'
const { user } = useAuth()
const { t } = useI18n()
const open = ref(false)
const file = ref<File | null>(null)
const url = ref('')
const errorMessage = ref('')
const loading = ref(false)
async function onSubmit() {
if (loading.value || (!file.value && !url.value.trim())) return
loading.value = true
errorMessage.value = ''
try {
if (url.value.trim()) {
await $fetch('/api/uploads', {
method: 'POST',
body: { url: url.value.trim() },
})
}
else {
const formData = new FormData()
formData.append('file', file.value!)
await $fetch('/api/uploads', { method: 'POST', body: formData })
}
file.value = null
url.value = ''
open.value = false
await refreshNuxtData('uploads')
await navigateTo('/')
}
catch (err) {
const e = err as { data?: { statusMessage?: string, message?: string } }
errorMessage.value = e.data?.statusMessage ?? e.data?.message ?? t('upload.failed')
}
finally {
loading.value = false
}
}
</script>
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button variant="ghost">
<Icon name="ph:cloud-arrow-up-fill" class="size-4" />
{{ $t('upload.upload') }}
</Button>
</PopoverTrigger>
<PopoverContent align="start" class="w-160">
<template v-if="user">
<PopoverHeader>
<PopoverTitle>{{ $t('upload.upload') }}</PopoverTitle>
<PopoverDescription>{{ $t('upload.desc') }}</PopoverDescription>
</PopoverHeader>
<form class="space-y-4" @submit.prevent="onSubmit">
<div class="space-y-1.5">
<Label>{{ $t('upload.file') }}</Label>
<FileDropzone v-model="file" :disabled="!!url.trim()" />
</div>
<div class="space-y-1.5">
<Label for="upload-url">{{ $t('upload.orUrl') }}</Label>
<Input id="upload-url" v-model="url" type="url" placeholder="https://…" :disabled="!!file" />
<p class="text-xs text-muted-foreground">
{{ $t('upload.urlHint') }}
</p>
</div>
<p v-if="errorMessage" class="text-sm text-destructive">
{{ errorMessage }}
</p>
<Button type="submit" class="w-full" :disabled="loading || (!file && !url.trim())">
<Spinner v-if="loading" />
{{ loading ? $t('upload.uploading') : $t('upload.upload') }}
</Button>
</form>
</template>
<p v-else class="text-sm text-muted-foreground">
<NuxtLink to="/login" class="text-primary underline" @click="open = false">
{{ $t('common.loginLink') }}
</NuxtLink>{{ $t('upload.loginToUpload') }}
</p>
</PopoverContent>
</Popover>
</template>