Ir al contenido

Carrusel de testimonios en video

Sección de testimonios verticales. Cada tarjeta puede usar una imagen o un video, mostrar el nombre y la descripción del testimonio, y enlazar al producto. El comerciante carga hasta 10 testimonios por sección, más un título y una descripción de la sección. El storefront de la aplicación la presenta como un carrusel horizontal tipo coverflow, con la tarjeta activa al centro y las tarjetas laterales parcialmente superpuestas.

En el paneltipo en el JSONToken de la clave
Carrusel de testimonios en videoVIDEO_REVIEWSvideo_reviews

Este tipo usa el sobre genérico de los tipos generados: los campos editables de cada testimonio viajan dentro de campos.

{
titulo: string | null; // título de la sección; null si el comerciante no lo escribió
nombre_personalizado?: string; // nombre interno del panel (opcional)
items: Array<{
orden: number;
campos: {
mediaUrl: string | null; // imagen o video; null si no hay archivo o no está READY
mediaMimeType: string | null; // "video/mp4", "video/webm", "image/webp"; null sin archivo
mediaDurationSeconds?: number; // solo videos READY con duración registrada; si no, la clave no viene
heading: string | null; // nombre o título del testimonio
body: string | null; // texto del testimonio
productLinkUrl: string | null; // URL absoluta https:// o null
productLinkLabel: string | null;
};
}>;
campos: {
descripcion: string | null; // descripción de la sección
};
estado: { lifecycle: string; storefrontVisible: boolean };
orden: number;
indice?: number; // solo cuando hay más de una sección del mismo tipo
}

Los items vienen sin filtrar: un testimonio cuyo archivo todavía no está READY (o fue eliminado) conserva su orden pero trae mediaUrl y mediaMimeType en null. Un testimonio sin archivo no tiene nada que mostrar: omítelo.

{
"sections": {
"video_reviews-product-344443275-cmu7axqfa0001uhc8b8hdznmy": {
"titulo": "Historias reales",
"items": [
{
"orden": 0,
"campos": {
"mediaUrl": "https://cdn.example.com/ann.mp4",
"mediaMimeType": "video/mp4",
"mediaDurationSeconds": 42,
"heading": "Ann F.",
"body": "Pine Stripe Roll Bed",
"productLinkUrl": "https://example.com/products/pine-stripe",
"productLinkLabel": "Ver producto →"
}
}
],
"campos": { "descripcion": "Mira lo que dicen nuestros clientes" },
"estado": { "lifecycle": "active", "storefrontVisible": true },
"orden": 0
}
}
}

La regla de render y el orden son los compartidos. Lo propio de este tipo es resolver campos, omitir los testimonios sin archivo y decidir entre <img> y <video> según mediaMimeType:

const res = await fetch(
`https://sectionfy.codefy-apps.com/api/public/content/${TOKEN}/product/${PRODUCT_ID}`,
);
const { sections } = await res.json();
const carruseles = Object.entries(sections)
.filter(([, s]) => s.tipo === 'VIDEO_REVIEWS')
.filter(([, s]) => s.estado.lifecycle === 'active' && s.estado.storefrontVisible)
.sort(([, a], [, b]) => a.orden - b.orden);
for (const [key, seccion] of carruseles) {
const tarjetas = seccion.items
.slice()
.sort((a, b) => a.orden - b.orden)
.filter((item) => item.campos.mediaUrl)
.map(
(item) => `
<article class="sectionfy-vr__card" data-item="${item.orden}">
${
item.campos.mediaMimeType?.startsWith('video/')
? `<video src="${item.campos.mediaUrl}" muted loop playsinline></video>`
: `<img src="${item.campos.mediaUrl}" alt="${escapar(item.campos.heading ?? '')}"
loading="lazy">`
}
${item.campos.heading ? `<h3>${escapar(item.campos.heading)}</h3>` : ''}
${item.campos.body ? `<p>${escapar(item.campos.body)}</p>` : ''}
${
item.campos.productLinkUrl
? `<a href="${escapar(item.campos.productLinkUrl)}">
${escapar(item.campos.productLinkLabel ?? 'Ver producto')}
</a>`
: ''
}
</article>
`,
)
.join('');
if (!tarjetas) continue;
contenedor.insertAdjacentHTML(
'beforeend',
`<section class="sectionfy-vr" data-block="${key}">
${seccion.titulo ? `<h2>${escapar(seccion.titulo)}</h2>` : ''}
${seccion.campos.descripcion ? `<p>${escapar(seccion.campos.descripcion)}</p>` : ''}
<div class="sectionfy-vr__track">${tarjetas}</div>
</section>`,
);
}

El renderer NubeSDK incluido en la aplicación carga el widget video-reviews.html y le entrega los datos mediante postMessage. El widget se encarga de la navegación, la superposición visual, el botón de reproducción y el enlace al producto.

Si se integra la respuesta manualmente, se recomienda mantener la relación vertical aproximada 9:16 y usar object-fit: cover.

  • titulo y campos.descripcion. Son el título y la descripción de la sección. Pueden venir en null; el renderer NubeSDK usa Carrusel de testimonios en video como título de respaldo, pero la API no lo inventa.
  • Máximo 10 testimonios. El panel bloquea el undécimo; no asumas más de diez items.
  • productLinkUrl es siempre una URL absoluta https:// (o null). El panel y la API rechazan http://, javascript:, data: y rutas relativas; un enlace vacío se guarda como null. productLinkLabel permite personalizar el texto; la interfaz del renderer NubeSDK usa Ver producto → como respaldo visual.
  • Placements home, category y product. En el endpoint propio category y product piden target_id (si falta, 400) y home no lo admite; las secciones generales de category/product llegan por el bundle (/{page} o /{page}/{id}).
  • mediaUrl es null mientras el archivo no esté READY. El archivo del testimonio es un video (video/mp4, video/webm) o una imagen image/webp de la biblioteca de la tienda; un PDF no es válido. Decide <img> o <video> por mediaMimeType, no por la extensión de la URL. mediaDurationSeconds es aditivo: solo llega para videos.