feat(vcard-api): modal Ver VCard + boton ver en tabla + animacion como select

- Tabla VCards: boton 'Ver' abre modal detalle con 3 tabs (Info, Redes, Apariencia)
- Modal Ver VCard: muestra todos los campos, colores con preview, redes configuradas
- Footer del modal: boton 'Ver VCard' abre https://vcard.u-site.app/{unico} en nueva pestaña
- Footer: boton 'Editar' abre modal de edicion desde el ver
- Edit VCard: campo animacion cambiado a select (Sin animacion, Fade, Slide, Zoom, Bounce, Flip)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lizandro Guarnizo
2026-05-20 10:23:11 -05:00
co-authored by Copilot
parent bf4fdd6db0
commit 5a1a8857a1
+171 -3
View File
@@ -176,8 +176,12 @@
</td>
<td class="py-2 px-3 text-xs text-gray-400" x-text="v.created_at ? v.created_at.substring(0,10) : '—'"></td>
<td class="py-2 px-3 text-right">
<button @click="openEditVcardModal(v)"
class="text-gray-600 hover:text-gray-800 text-xs px-2 py-1 border border-gray-300 rounded">Editar</button>
<div class="flex justify-end gap-2">
<button @click="openVerVcardModal(v)"
class="text-indigo-500 hover:text-indigo-700 text-xs px-2 py-1 border border-indigo-200 rounded">Ver</button>
<button @click="openEditVcardModal(v)"
class="text-gray-600 hover:text-gray-800 text-xs px-2 py-1 border border-gray-300 rounded">Editar</button>
</div>
</td>
</tr>
</template>
@@ -776,6 +780,149 @@
</div>
</div>
<!-- ─── Modal: Ver VCard ─────────────────────────────────────────────── -->
<div x-show="verVcardModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" style="display:none">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col">
<!-- Header -->
<div class="flex justify-between items-start p-5 border-b shrink-0">
<div>
<h2 class="text-lg font-semibold" x-text="verVcardData ? (verVcardData.nombre || '—') : 'Cargando...'"></h2>
<p class="text-xs text-gray-500 mt-0.5" x-text="verVcardData ? (verVcardData.cargo || '') : ''"></p>
<div class="flex flex-wrap gap-2 mt-1">
<template x-if="verVcardData">
<span :class="verVcardData.estado ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verVcardData.estado ? 'Activa' : 'Inactiva'"></span>
</template>
<template x-if="verVcardData && verVcardData.es_borrador">
<span class="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">Borrador</span>
</template>
<template x-if="verVcardData && verVcardData.privacidad">
<span :class="verVcardData.privacidad === 'publica' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-600'"
class="text-xs px-2 py-0.5 rounded-full" x-text="verVcardData.privacidad"></span>
</template>
</div>
</div>
<button @click="verVcardModal = false" class="text-gray-400 hover:text-gray-600 text-xl shrink-0 ml-3"></button>
</div>
<!-- Tabs -->
<div class="border-b px-5 flex gap-1 overflow-x-auto shrink-0">
<template x-for="vt in ['info','redes','apariencia']" :key="vt">
<button @click="verVcardTab = vt"
:class="verVcardTab === vt ? 'border-b-2 border-[#8eb02f] text-[#6d8c24] font-semibold' : 'text-gray-500 hover:text-gray-700'"
class="text-xs py-2 px-3 whitespace-nowrap capitalize"
x-text="vt === 'info' ? 'Información' : vt.charAt(0).toUpperCase()+vt.slice(1)"></button>
</template>
</div>
<!-- Content -->
<div class="overflow-y-auto flex-1 p-5">
<!-- Tab Información -->
<template x-if="verVcardTab === 'info' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm">
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Nombre</span><span class="font-medium" x-text="verVcardData.nombre || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Cargo</span><span x-text="verVcardData.cargo || '—'"></span></div>
<div class="sm:col-span-2"><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Descripción</span><p class="text-sm text-gray-600" x-text="verVcardData.descripcion || '—'"></p></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Empresa</span><span x-text="verVcardData.empresa || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Email</span><span x-text="verVcardData.email || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Teléfono</span>
<span x-text="(verVcardData.consecutivo_telefono||'') + ' ' + (verVcardData.telefono||'—')"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">WhatsApp</span>
<span x-text="(verVcardData.consecutivo_whatsapp||'') + ' ' + (verVcardData.whatsapp||'—')"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Dirección</span><span x-text="verVcardData.direccion || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Website</span>
<a :href="verVcardData.website" target="_blank" x-show="verVcardData.website"
class="text-blue-600 hover:underline text-xs" x-text="verVcardData.website"></a>
<span x-show="!verVcardData.website" class="text-gray-400"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">País</span><span x-text="verVcardData.pais || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Ciudad</span><span x-text="verVcardData.ciudad || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Usuario</span>
<span x-text="(verVcardData.user && verVcardData.user.name) ? verVcardData.user.name : '—'"></span><br>
<span class="text-xs text-gray-400" x-text="(verVcardData.user && verVcardData.user.email) ? verVcardData.user.email : ''"></span>
</div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Creada</span>
<span class="text-xs" x-text="verVcardData.created_at ? verVcardData.created_at.substring(0,10) : '—'"></span></div>
<div x-show="verVcardData.foto_perfil">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1">Foto perfil</span>
<img :src="verVcardData.foto_perfil" class="h-14 w-14 rounded-full object-cover border" onerror="this.style.display='none'" />
</div>
<div x-show="verVcardData.logo">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1">Logo</span>
<img :src="verVcardData.logo" class="h-14 rounded border object-contain" onerror="this.style.display='none'" />
</div>
<div class="sm:col-span-2"><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">ID</span>
<span class="text-xs font-mono text-gray-400" x-text="verVcardData._id || verVcardData.id || '—'"></span></div>
</div>
</template>
<!-- Tab Redes -->
<template x-if="verVcardTab === 'redes' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<template x-for="red in [
{key:'linkedin',label:'LinkedIn'},{key:'twitter',label:'Twitter / X'},
{key:'facebook',label:'Facebook'},{key:'instagram',label:'Instagram'},
{key:'tiktok',label:'TikTok'},{key:'github',label:'GitHub'},
{key:'behance',label:'Behance'},{key:'youtube',label:'YouTube'},
{key:'youtube_channel',label:'Canal YouTube'},{key:'telegram',label:'Telegram'},
{key:'messenger',label:'Messenger'},{key:'wechat',label:'WeChat'},
{key:'signal',label:'Signal'},{key:'line',label:'Line'}
]" :key="red.key">
<div x-show="verVcardData[red.key]">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5" x-text="red.label"></span>
<span class="text-sm" x-text="verVcardData[red.key]"></span>
</div>
</template>
<div x-show="!['linkedin','twitter','facebook','instagram','tiktok','github','behance','youtube','youtube_channel','telegram','messenger','wechat','signal','line'].some(k => verVcardData[k])"
class="sm:col-span-2 text-sm text-gray-400">Sin redes sociales configuradas.</div>
</div>
</template>
<!-- Tab Apariencia -->
<template x-if="verVcardTab === 'apariencia' && verVcardData">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
<template x-for="c in [
{key:'cabecera',label:'Cabecera'},{key:'fondo',label:'Fondo'},
{key:'color_cuerpo',label:'Cuerpo'},{key:'color_iconos',label:'Iconos'},
{key:'color_info',label:'Info'}
]" :key="c.key">
<div x-show="verVcardData[c.key]">
<span class="text-xs text-gray-400 uppercase tracking-wide block mb-1" x-text="c.label"></span>
<div class="flex items-center gap-2">
<div class="h-6 w-6 rounded border border-gray-200 shrink-0" :style="'background:' + verVcardData[c.key]"></div>
<span class="font-mono text-xs" x-text="verVcardData[c.key]"></span>
</div>
</div>
</template>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Fuente</span><span x-text="verVcardData.fuente || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Bordes</span><span x-text="verVcardData.bordes || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Estilo</span><span x-text="verVcardData.estilo_vcard || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Orden iconos</span><span x-text="verVcardData.orden_iconos || '—'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Animación</span><span x-text="verVcardData.animacion || 'Sin animación'"></span></div>
<div><span class="text-xs text-gray-400 uppercase tracking-wide block mb-0.5">Botón</span><span x-text="verVcardData.boton || '—'"></span></div>
</div>
</template>
</div>
<!-- Footer -->
<div class="flex justify-between items-center gap-3 px-5 py-4 border-t shrink-0">
<div class="flex gap-2">
<a :href="verVcardData ? (verVcardData.url || ('https://vcard.u-site.app/' + verVcardData.unico)) : '#'"
target="_blank"
class="inline-flex items-center gap-1 px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#3b82f6"
onmouseover="this.style.backgroundColor='#2563eb'"
onmouseout="this.style.backgroundColor='#3b82f6'">
🔗 Ver VCard
</a>
<button @click="openEditVcardModal(verVcardData); verVcardModal = false"
class="px-4 py-2 text-sm text-white rounded-lg"
style="background-color:#8eb02f"
onmouseover="this.style.backgroundColor='#6d8c24'"
onmouseout="this.style.backgroundColor='#8eb02f'">✏ Editar</button>
</div>
<button @click="verVcardModal = false" class="px-4 py-2 text-sm border rounded-lg hover:bg-gray-50">Cerrar</button>
</div>
</div>
</div>
<!-- ─── Modal: Editar VCard ───────────────────────────────────────────── -->
<div x-show="editVcardModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm" style="display:none">
<div class="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col">
@@ -981,7 +1128,14 @@
</div>
<div>
<label class="block text-xs font-medium text-gray-700 mb-1">Animación</label>
<input x-model="editVcardForm.animacion" type="text" placeholder="ej: fade, slide..." class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" />
<select x-model="editVcardForm.animacion" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
<option value="">Sin animación</option>
<option value="fade">Fade</option>
<option value="slide">Slide</option>
<option value="zoom">Zoom</option>
<option value="bounce">Bounce</option>
<option value="flip">Flip</option>
</select>
</div>
</div>
@@ -1109,6 +1263,11 @@ function vcardApiApp() {
editUsuarioError: '',
editUsuarioSuccess: false,
// Modal ver vcard (detalle completo)
verVcardModal: false,
verVcardData: null,
verVcardTab: 'info',
// Modal editar vcard
editVcardModal: false,
editVcardTab: 'datos',
@@ -1160,6 +1319,7 @@ function vcardApiApp() {
this.editUsuarioModal = false;
this.editVcardModal = false;
this.verModal = false;
this.verVcardModal = false;
},
async loadConfig() {
@@ -1374,6 +1534,14 @@ function vcardApiApp() {
this.saving = false;
},
// ─── Ver VCard ───────────────────────────────────────────────────────
openVerVcardModal(v) {
this.verVcardData = v;
this.verVcardTab = 'info';
this.verVcardModal = true;
},
// ─── Edit VCard ──────────────────────────────────────────────────────
openEditVcardModal(v) {