Files
whatsappNucleo/app/components/messages/MediaPreview.vue
josedario87 ec40cd6826
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m28s
Unificar endpoint de envío y agregar soporte para stickers
- Consolidar send.post.ts y send-media.post.ts en un único endpoint /send
- Agregar servicio sticker-processor.ts para convertir imágenes a WebP 512x512
- Agregar toggle Imagen/Sticker en MediaPreview para enviar imágenes como stickers
- Actualizar MessageInput y página de mensajes para usar endpoint unificado
- Instalar dependencia sharp para procesamiento de imágenes
2025-12-04 09:33:03 -06:00

219 lines
6.9 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 with sticker toggle -->
<div
v-if="isImage(file)"
class="relative"
>
<img
:src="getPreviewUrl(file)"
class="h-20 w-20 object-cover rounded-lg transition-all"
:class="{ 'ring-2 ring-[var(--wa-green)]': stickerModes[index] }"
/>
<!-- Sticker toggle button -->
<button
class="absolute bottom-0 left-0 right-0 py-0.5 text-[10px] font-medium transition-colors"
:class="stickerModes[index]
? 'bg-[var(--wa-green)] text-white rounded-b-lg'
: 'bg-black/60 text-white/80 rounded-b-lg hover:bg-black/80'"
@click="toggleStickerMode(index)"
>
{{ stickerModes[index] ? 'Sticker' : 'Imagen' }}
</button>
<!-- Remove button -->
<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 (hide if all files are stickers) -->
<div v-if="showCaption && files.length > 0 && !allStickers">
<UInput
:model-value="caption"
placeholder="Agregar descripcion..."
size="sm"
class="bg-[var(--wa-bg-light)]"
@update:model-value="$emit('update:caption', $event)"
/>
</div>
<!-- Info text for stickers -->
<p v-if="hasStickers" class="text-xs text-[var(--wa-text-muted)]">
Los stickers se convertiran a formato 512x512 WebP
</p>
</div>
</template>
<script setup lang="ts">
interface Props {
files: File[]
caption?: string
showCaption?: boolean
}
const props = withDefaults(defineProps<Props>(), {
caption: '',
showCaption: true
})
const emit = defineEmits<{
remove: [index: number]
'update:caption': [value: string]
'update:stickerModes': [modes: boolean[]]
}>()
// Track which images should be sent as stickers
const stickerModes = ref<boolean[]>([])
// Initialize sticker modes when files change
watch(() => props.files, (files) => {
// Preserve existing modes and add false for new files
const newModes = files.map((_, i) => stickerModes.value[i] ?? false)
stickerModes.value = newModes
emit('update:stickerModes', [...newModes])
}, { immediate: true })
const toggleStickerMode = (index: number) => {
stickerModes.value[index] = !stickerModes.value[index]
emit('update:stickerModes', [...stickerModes.value])
}
// Check if all files are stickers (images marked as sticker)
const allStickers = computed(() => {
return props.files.length > 0 && props.files.every((f, i) =>
isImage(f) && stickerModes.value[i]
)
})
// Check if any file is marked as sticker
const hasStickers = computed(() => {
return props.files.some((f, i) => isImage(f) && stickerModes.value[i])
})
// 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>