All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection - Detectar si ya existe un webhook apuntando al receptor de debug - Permitir eliminar el webhook de debug - Incluir todos los eventos disponibles al crear el webhook - También incluye mejoras previas de manejo de media y mensajes
173 lines
5.1 KiB
Vue
173 lines
5.1 KiB
Vue
<template>
|
|
<div class="space-y-2">
|
|
<!-- Files grid -->
|
|
<div class="flex flex-wrap gap-2">
|
|
<div
|
|
v-for="(file, index) in files"
|
|
:key="index"
|
|
class="relative group"
|
|
>
|
|
<!-- Image preview -->
|
|
<div
|
|
v-if="isImage(file)"
|
|
class="relative"
|
|
>
|
|
<img
|
|
:src="getPreviewUrl(file)"
|
|
class="h-20 w-20 object-cover rounded-lg"
|
|
/>
|
|
<button
|
|
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
@click="$emit('remove', index)"
|
|
>
|
|
<UIcon name="i-lucide-x" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Video preview -->
|
|
<div
|
|
v-else-if="isVideo(file)"
|
|
class="relative"
|
|
>
|
|
<div class="h-20 w-20 bg-gray-800 rounded-lg flex items-center justify-center">
|
|
<video
|
|
:src="getPreviewUrl(file)"
|
|
class="h-full w-full object-cover rounded-lg"
|
|
/>
|
|
<div class="absolute inset-0 flex items-center justify-center">
|
|
<UIcon name="i-lucide-play-circle" class="w-8 h-8 text-white/80" />
|
|
</div>
|
|
</div>
|
|
<button
|
|
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
|
@click="$emit('remove', index)"
|
|
>
|
|
<UIcon name="i-lucide-x" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Audio preview -->
|
|
<div
|
|
v-else-if="isAudio(file)"
|
|
class="relative flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg"
|
|
>
|
|
<UIcon name="i-lucide-music" class="w-5 h-5 text-[var(--wa-green-light)]" />
|
|
<span class="text-sm text-white truncate max-w-[100px]">{{ file.name }}</span>
|
|
<button
|
|
class="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center"
|
|
@click="$emit('remove', index)"
|
|
>
|
|
<UIcon name="i-lucide-x" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Document preview -->
|
|
<div
|
|
v-else
|
|
class="relative flex items-center gap-2 px-3 py-2 bg-gray-800 rounded-lg"
|
|
>
|
|
<UIcon :name="getDocIcon(file)" class="w-5 h-5 text-[var(--wa-blue)]" />
|
|
<div class="flex flex-col min-w-0">
|
|
<span class="text-sm text-white truncate max-w-[100px]">{{ file.name }}</span>
|
|
<span class="text-xs text-gray-400">{{ formatSize(file.size) }}</span>
|
|
</div>
|
|
<button
|
|
class="w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center flex-shrink-0"
|
|
@click="$emit('remove', index)"
|
|
>
|
|
<UIcon name="i-lucide-x" class="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Caption input -->
|
|
<div v-if="showCaption && files.length > 0">
|
|
<UInput
|
|
:model-value="caption"
|
|
placeholder="Agregar descripción..."
|
|
size="sm"
|
|
class="bg-[var(--wa-bg-light)]"
|
|
@update:model-value="$emit('update:caption', $event)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Props {
|
|
files: File[]
|
|
caption?: string
|
|
showCaption?: boolean
|
|
}
|
|
|
|
withDefaults(defineProps<Props>(), {
|
|
caption: '',
|
|
showCaption: true
|
|
})
|
|
|
|
defineEmits<{
|
|
remove: [index: number]
|
|
'update:caption': [value: string]
|
|
}>()
|
|
|
|
// Preview URL cache
|
|
const previewUrls = new Map<File, string>()
|
|
|
|
const getPreviewUrl = (file: File): string => {
|
|
if (!previewUrls.has(file)) {
|
|
previewUrls.set(file, URL.createObjectURL(file))
|
|
}
|
|
return previewUrls.get(file)!
|
|
}
|
|
|
|
// Cleanup URLs on unmount
|
|
onUnmounted(() => {
|
|
previewUrls.forEach(url => URL.revokeObjectURL(url))
|
|
previewUrls.clear()
|
|
})
|
|
|
|
const isImage = (file: File): boolean => {
|
|
return file.type.startsWith('image/')
|
|
}
|
|
|
|
const isVideo = (file: File): boolean => {
|
|
return file.type.startsWith('video/')
|
|
}
|
|
|
|
const isAudio = (file: File): boolean => {
|
|
return file.type.startsWith('audio/')
|
|
}
|
|
|
|
const getDocIcon = (file: File): string => {
|
|
const type = file.type
|
|
const name = file.name.toLowerCase()
|
|
|
|
if (type.includes('pdf') || name.endsWith('.pdf')) {
|
|
return 'i-lucide-file-text'
|
|
}
|
|
if (type.includes('spreadsheet') || name.match(/\.(xlsx?|csv)$/)) {
|
|
return 'i-lucide-file-spreadsheet'
|
|
}
|
|
if (type.includes('word') || name.match(/\.(docx?)$/)) {
|
|
return 'i-lucide-file-text'
|
|
}
|
|
if (type.includes('presentation') || name.match(/\.(pptx?)$/)) {
|
|
return 'i-lucide-file-presentation'
|
|
}
|
|
if (type.includes('zip') || name.match(/\.(zip|rar|7z)$/)) {
|
|
return 'i-lucide-file-archive'
|
|
}
|
|
|
|
return 'i-lucide-file'
|
|
}
|
|
|
|
const formatSize = (bytes: number): string => {
|
|
if (bytes === 0) return '0 B'
|
|
const k = 1024
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
|
|
}
|
|
</script>
|