feat(admin): add gallery multi-image upload to AdminBusinessEditor with live preview thumbnails and carousel
This commit is contained in:
@ -15,6 +15,10 @@ const showMessage = ref({ text: '', type: '' });
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const selectedFileName = ref('');
|
||||
|
||||
// Gallery de imágenes
|
||||
const galleryFiles = ref<File[]>([]); // archivos nuevos seleccionados
|
||||
const galleryPreviews = ref<string[]>([]); // previews locales URL.createObjectURL
|
||||
|
||||
// Business state
|
||||
const businessForm = ref<Partial<Business>>({
|
||||
name: '',
|
||||
@ -46,6 +50,10 @@ onMounted(async () => {
|
||||
if (data.image_url) {
|
||||
previewImageUrl.value = data.image_url;
|
||||
}
|
||||
// Cargar URLs existentes de galeria como previews
|
||||
if (data.gallery_images?.length) {
|
||||
galleryPreviews.value = [...data.gallery_images];
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showMessage.value = { text: 'Error cargando negocio', type: 'error' };
|
||||
@ -61,16 +69,38 @@ function handleImageChange(event: Event) {
|
||||
const file = input.files[0];
|
||||
selectedFile.value = file;
|
||||
selectedFileName.value = file.name;
|
||||
|
||||
// Preview logic
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
previewImageUrl.value = e.target?.result as string;
|
||||
};
|
||||
reader.onload = (e) => { previewImageUrl.value = e.target?.result as string; };
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
function handleGalleryFiles(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (!input.files) return;
|
||||
const newFiles = Array.from(input.files);
|
||||
newFiles.forEach(file => {
|
||||
galleryFiles.value.push(file);
|
||||
galleryPreviews.value.push(URL.createObjectURL(file));
|
||||
});
|
||||
// Reset input so same file can be re-picked
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
function removeGalleryItem(index: number) {
|
||||
const preview = galleryPreviews.value[index];
|
||||
if (!preview) return;
|
||||
if (preview.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(preview);
|
||||
const existingCount = (businessForm.value.gallery_images ?? []).length;
|
||||
const fileIndex = index - existingCount;
|
||||
if (fileIndex >= 0) galleryFiles.value.splice(fileIndex, 1);
|
||||
} else {
|
||||
businessForm.value.gallery_images = (businessForm.value.gallery_images ?? []).filter((_, i) => i !== index);
|
||||
}
|
||||
galleryPreviews.value.splice(index, 1);
|
||||
}
|
||||
|
||||
async function saveBusiness() {
|
||||
isLoading.value = true;
|
||||
showMessage.value = { text: '', type: '' };
|
||||
@ -93,6 +123,18 @@ async function saveBusiness() {
|
||||
formData.append('gallery_images', JSON.stringify(businessForm.value.gallery_images));
|
||||
}
|
||||
|
||||
// Subir archivos nuevos de galeria a Supabase Storage
|
||||
if (galleryFiles.value.length > 0) {
|
||||
const uploadedUrls: string[] = [];
|
||||
for (const gFile of galleryFiles.value) {
|
||||
const url = await businessService.uploadImage(gFile);
|
||||
uploadedUrls.push(url);
|
||||
}
|
||||
// Combinar las URL existentes ya serializadas + las nuevas subidas
|
||||
const existing: string[] = businessForm.value.gallery_images ?? [];
|
||||
formData.set('gallery_images', JSON.stringify([...existing, ...uploadedUrls]));
|
||||
}
|
||||
|
||||
if (selectedFile.value) {
|
||||
formData.append('image', selectedFile.value);
|
||||
}
|
||||
@ -229,6 +271,42 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
||||
<input v-model="businessForm.website" type="text" placeholder="https://">
|
||||
</div>
|
||||
|
||||
<!-- GALERIA DE IMAGENES -->
|
||||
<div class="form-section-label mt-4">📸 Galería de Imágenes</div>
|
||||
<div class="form-group">
|
||||
<label>Fotos adicionales (menú, ambiente, experiencias)</label>
|
||||
<!-- Drop zone -->
|
||||
<div class="gallery-upload-zone">
|
||||
<input
|
||||
type="file"
|
||||
id="gallery-input"
|
||||
accept="image/*"
|
||||
multiple
|
||||
@change="handleGalleryFiles"
|
||||
>
|
||||
<label for="gallery-input" class="gallery-upload-label">
|
||||
<span class="material-icons" style="font-size:36px">add_photo_alternate</span>
|
||||
<span>Agregar fotos a la galería</span>
|
||||
<span style="font-size:0.7rem;opacity:0.6">PNG, JPG hasta 5 MB cada una</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Thumbnails row -->
|
||||
<div v-if="galleryPreviews.length > 0" class="gallery-thumbs">
|
||||
<div
|
||||
v-for="(src, idx) in galleryPreviews"
|
||||
:key="idx"
|
||||
class="gallery-thumb"
|
||||
>
|
||||
<img :src="src" alt="Foto galeria" />
|
||||
<button class="thumb-remove" @click="removeGalleryItem(idx)" type="button">
|
||||
<span class="material-icons">close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="gallery-empty-hint">Sin fotos aún. Agrega imágenes para el carrusel del perfil.</p>
|
||||
</div>
|
||||
|
||||
<button class="deploy-btn" :disabled="isLoading" @click="saveBusiness">
|
||||
<span class="material-icons">{{ isLoading ? 'sync' : 'save' }}</span>
|
||||
{{ isLoading ? 'GUARDANDO...' : 'GUARDAR NEGOCIO' }}
|
||||
@ -285,6 +363,23 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Gallery carousel preview -->
|
||||
<section v-if="galleryPreviews.length > 0" class="biz-section">
|
||||
<div class="section-header">
|
||||
<span class="section-accent"></span>
|
||||
<h2 class="section-title">📸 Galería</h2>
|
||||
</div>
|
||||
<div class="gallery-mini-carousel">
|
||||
<img
|
||||
v-for="(src, i) in galleryPreviews"
|
||||
:key="i"
|
||||
:src="src"
|
||||
class="gallery-mini-img"
|
||||
alt="Foto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sticky CTA -->
|
||||
<div class="cta-bar">
|
||||
<button class="cta-map"><span class="material-icons">near_me</span> Ver en el Mapa</button>
|
||||
@ -638,4 +733,102 @@ const catEmoji = computed(() => CATEGORY_EMOJI[businessForm.value.category || ''
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* ── GALLERY UPLOAD ── */
|
||||
.gallery-upload-zone {
|
||||
position: relative;
|
||||
}
|
||||
.gallery-upload-zone input[type="file"] {
|
||||
position: absolute;
|
||||
width: 0.1px;
|
||||
height: 0.1px;
|
||||
opacity: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
.gallery-upload-label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 28px;
|
||||
border: 2px dashed rgba(254, 231, 21, 0.4);
|
||||
border-radius: 16px;
|
||||
background: rgba(254, 231, 21, 0.05);
|
||||
color: #fee715;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
transition: all 0.25s;
|
||||
text-align: center;
|
||||
}
|
||||
.gallery-upload-label:hover {
|
||||
border-color: #fee715;
|
||||
background: rgba(254, 231, 21, 0.1);
|
||||
}
|
||||
|
||||
.gallery-thumbs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.gallery-thumb {
|
||||
position: relative;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 2px solid rgba(255,255,255,0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.gallery-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumb-remove {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
background: rgba(239,68,68,0.9);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.thumb-remove .material-icons { font-size: 14px; }
|
||||
|
||||
.gallery-empty-hint {
|
||||
margin-top: 10px;
|
||||
font-size: 0.78rem;
|
||||
color: #64748b;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ── GALLERY PREVIEW CAROUSEL (mini) ── */
|
||||
.gallery-mini-carousel {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
.gallery-mini-carousel::-webkit-scrollbar { display: none; }
|
||||
.gallery-mini-img {
|
||||
width: 110px;
|
||||
height: 80px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user