fix: Social — métricas IG total_value, posts en formato original y tamaño

Rellena views/perfil/clics/interacciones con metric_type=total_value y series día a día; carruseles/vídeos/reels con aspect ratio y px originales.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-28 14:31:12 +02:00
parent 530b5ac5ae
commit aa41594e1e
3 changed files with 356 additions and 149 deletions

View File

@@ -85,13 +85,147 @@ type SocialPost = {
reach?: number | null;
impressions?: number | null;
mediaType?: string | null;
mediaProductType?: string | null;
children?: {
id?: string;
mediaType?: string | null;
mediaUrl?: string | null;
thumbnailUrl?: string | null;
}[];
};
function isVideoPost(p: SocialPost): boolean {
function formatLabel(p: SocialPost): string {
const t = (p.mediaType || '').toUpperCase();
const prod = (p.mediaProductType || '').toUpperCase();
if (prod === 'REELS' || t === 'REELS' || t === 'REEL') return 'Reel';
if (t.includes('CAROUSEL') || t.includes('ALBUM')) return 'Carrusel';
if (t.includes('VIDEO')) return 'Vídeo';
if (t.includes('IMAGE') || t === 'PHOTO') return 'Imagen';
if (t === 'STORY' || prod === 'STORY') return 'Story';
return t || 'Post';
}
function isVideoSlide(mediaType?: string | null, url?: string | null): boolean {
const t = (mediaType || '').toUpperCase();
if (t.includes('VIDEO') || t === 'REELS' || t === 'REEL') return true;
const url = p.mediaUrl || '';
return /\.mp4($|\?)/i.test(url) || /video/i.test(url);
return /\.mp4($|\?)/i.test(url || '') || /video/i.test(url || '');
}
function proxiedSocialUrl(raw?: string | null): string {
if (!raw) return '';
if (raw.startsWith('/api/')) return raw;
return `/api/ad-tracker/media?url=${encodeURIComponent(raw)}`;
}
function PostMedia({ post }: { post: SocialPost }) {
const slides =
post.children && post.children.length > 0
? post.children
: [
{
id: post.id,
mediaType: post.mediaType,
mediaUrl: post.mediaUrl,
thumbnailUrl: post.thumbnailUrl,
},
];
const [idx, setIdx] = useState(0);
const [dims, setDims] = useState<{ w: number; h: number } | null>(null);
const slide = slides[Math.min(idx, slides.length - 1)] || slides[0];
const video = isVideoSlide(slide?.mediaType, slide?.mediaUrl);
const src = proxiedSocialUrl(slide?.mediaUrl || slide?.thumbnailUrl);
const poster = proxiedSocialUrl(slide?.thumbnailUrl);
const aspect =
dims && dims.w > 0 && dims.h > 0 ? `${dims.w} / ${dims.h}` : video ? '9 / 16' : '1 / 1';
return (
<div className="space-y-1.5">
<div
className="relative w-full bg-gray-950 overflow-hidden rounded-t-2xl"
style={{ aspectRatio: aspect, maxHeight: 520 }}
>
{src && video ? (
<video
key={src}
src={src}
poster={poster || undefined}
className="absolute inset-0 w-full h-full object-contain bg-black"
controls
playsInline
preload="metadata"
onLoadedMetadata={(e) => {
const el = e.currentTarget;
if (el.videoWidth && el.videoHeight) {
setDims({ w: el.videoWidth, h: el.videoHeight });
}
}}
/>
) : src ? (
// eslint-disable-next-line @next/next/no-img-element
<img
key={src}
src={src}
alt=""
className="absolute inset-0 w-full h-full object-contain bg-black"
onLoad={(e) => {
const el = e.currentTarget;
if (el.naturalWidth && el.naturalHeight) {
setDims({ w: el.naturalWidth, h: el.naturalHeight });
}
}}
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-xs text-gray-400 px-4 text-center">
{post.message?.slice(0, 120) || 'Sin media'}
</div>
)}
<span className="absolute top-2 left-2 text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded bg-black/70 text-white">
{post.platform === 'instagram' ? 'IG' : 'FB'} · {formatLabel(post)}
{slides.length > 1 ? ` · ${idx + 1}/${slides.length}` : ''}
</span>
{dims ? (
<span className="absolute top-2 right-2 text-[10px] font-semibold tabular-nums px-2 py-0.5 rounded bg-black/70 text-white">
{dims.w}×{dims.h}
{dims.w && dims.h
? ` · ${(dims.w / dims.h).toFixed(2)}:1`
: ''}
</span>
) : null}
{slides.length > 1 ? (
<>
<button
type="button"
aria-label="Anterior"
className="absolute left-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/60 text-white text-sm"
onClick={() => {
setDims(null);
setIdx((i) => (i - 1 + slides.length) % slides.length);
}}
>
‹
</button>
<button
type="button"
aria-label="Siguiente"
className="absolute right-2 top-1/2 -translate-y-1/2 w-8 h-8 rounded-full bg-black/60 text-white text-sm"
onClick={() => {
setDims(null);
setIdx((i) => (i + 1) % slides.length);
}}
>
›
</button>
</>
) : null}
</div>
{dims ? (
<p className="px-3 text-[10px] text-gray-500 tabular-nums">
Tamaño original: {dims.w} × {dims.h}px
{video ? ' (vídeo)' : slides.length > 1 ? ` · slide ${idx + 1}` : ''}
</p>
) : null}
</div>
);
}
const CHANNELS: {
@@ -263,7 +397,7 @@ export default function SocialDashboard({
icon: Eye,
},
{
label: 'Impresiones/vistas IG',
label: 'Visualizaciones IG',
value: n(ig?.igInsights?.impressions),
icon: Eye,
},
@@ -324,8 +458,8 @@ export default function SocialDashboard({
color: 'bg-pink-400/80',
},
{
title: 'IG · Impresiones/vistas',
hint: 'Views / impressions',
title: 'IG · Visualizaciones',
hint: 'views (metric_type=total_value)',
series: ig?.series?.impressions,
total: ig?.igInsights?.impressions,
color: 'bg-fuchsia-400/80',
@@ -524,36 +658,13 @@ export default function SocialDashboard({
key={p.id}
className="rounded-2xl border border-gray-200 bg-white overflow-hidden flex flex-col"
>
<div className="relative aspect-square bg-gray-100">
{p.mediaUrl && isVideoPost(p) ? (
<video
src={p.mediaUrl}
poster={p.thumbnailUrl || undefined}
className="absolute inset-0 w-full h-full object-cover"
controls
playsInline
preload="metadata"
/>
) : p.mediaUrl || p.thumbnailUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={p.thumbnailUrl || p.mediaUrl || ''}
alt=""
className="absolute inset-0 w-full h-full object-cover"
/>
) : (
<div className="absolute inset-0 flex items-center justify-center text-xs text-gray-400 px-4 text-center">
{p.message?.slice(0, 120) || 'Sin media'}
</div>
)}
<span className="absolute top-2 left-2 text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded bg-black/70 text-white">
{p.platform === 'instagram' ? 'IG' : 'FB'}
{p.mediaType ? ` · ${p.mediaType}` : ''}
</span>
</div>
<PostMedia post={p} />
<div className="p-3 space-y-2 flex-1 flex flex-col">
<p className="text-[11px] text-gray-500">
{fmtDate(p.createdAt)}
{p.mediaProductType
? ` · ${p.mediaProductType}`
: ''}
</p>
{p.message ? (
<p className="text-xs text-gray-800 line-clamp-3 whitespace-pre-wrap">
@@ -575,6 +686,11 @@ export default function SocialDashboard({
<Eye className="w-3 h-3" /> {n(p.reach)} alc.
</span>
)}
{p.impressions != null && (
<span className="inline-flex items-center gap-1">
<Eye className="w-3 h-3" /> {n(p.impressions)} views
</span>
)}
</div>
{p.permalink && (
<a