forked from BRETT/BRETT
Initial Commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<script setup lang="ts">
|
||||
import { useDropZone, useEventListener } from '@vueuse/core'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { isMediaFile, isMediaItemType } from '~~/shared/utils/media'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
const model = defineModel<File | null>()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
disabled?: boolean
|
||||
accept?: string
|
||||
hint?: string
|
||||
}>(), {
|
||||
accept: 'image/*,video/*,audio/*,.mp3,.flac',
|
||||
hint: 'Bilder, Videos & Audio, max. 200 MB',
|
||||
})
|
||||
|
||||
const dropzoneRef = ref<HTMLElement>()
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const previewUrl = ref<string | null>(null)
|
||||
|
||||
const { isOverDropZone } = useDropZone(dropzoneRef, {
|
||||
dataTypes: types => types.some(isMediaItemType),
|
||||
onDrop(files) {
|
||||
if (props.disabled) return
|
||||
const file = files?.find(isMediaFile)
|
||||
if (file) select(file)
|
||||
else if (files?.length) toast.error('Nur Bilder, Videos und Audiodateien sind erlaubt')
|
||||
},
|
||||
})
|
||||
|
||||
// Drag-Events nicht bis zur seitenweiten Dropzone (document.body) durchreichen —
|
||||
// sonst würde ein Drop hier zusätzlich den globalen Upload auslösen
|
||||
useEventListener(dropzoneRef, ['dragenter', 'dragover', 'dragleave', 'drop'], e => e.stopPropagation())
|
||||
|
||||
function select(file: File | null) {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = file?.type.startsWith('image/') ? URL.createObjectURL(file) : null
|
||||
model.value = file
|
||||
if (inputRef.value) inputRef.value.value = ''
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (!props.disabled) inputRef.value?.click()
|
||||
}
|
||||
|
||||
function formatSize(bytes: number) {
|
||||
return bytes >= 1024 * 1024
|
||||
? `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||
: `${Math.max(1, Math.round(bytes / 1024))} KB`
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="dropzoneRef"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
:aria-disabled="disabled"
|
||||
class="flex min-h-32 cursor-pointer flex-col items-center justify-center gap-2 rounded-md border-2 border-dashed p-4 text-center text-sm transition-colors"
|
||||
:class="[
|
||||
isOverDropZone ? 'border-primary bg-accent' : 'border-primary/40 hover:border-primary',
|
||||
disabled && 'pointer-events-none opacity-50',
|
||||
]"
|
||||
@click="openPicker"
|
||||
@keydown.enter.prevent="openPicker"
|
||||
@keydown.space.prevent="openPicker"
|
||||
>
|
||||
<input
|
||||
ref="inputRef"
|
||||
type="file"
|
||||
:accept="accept"
|
||||
class="hidden"
|
||||
@change="select(($event.target as HTMLInputElement).files?.[0] ?? null)"
|
||||
>
|
||||
|
||||
<template v-if="model">
|
||||
<img
|
||||
v-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
:alt="model.name"
|
||||
class="max-h-32 rounded-md"
|
||||
>
|
||||
<span class="max-w-full truncate font-medium">{{ model.name }}</span>
|
||||
<span class="text-xs text-muted-foreground">{{ formatSize(model.size) }}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click.stop="select(null)"
|
||||
>
|
||||
<Icon name="ph:x-fill" class="size-4" />
|
||||
Entfernen
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<Icon name="ph:cloud-arrow-up-fill" class="size-8 text-muted-foreground" />
|
||||
<span>Datei hierher ziehen oder klicken</span>
|
||||
<span class="text-xs text-muted-foreground">{{ hint }}</span>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user