fix: Social — solo posts IG y barras de evolución visibles con delta
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -277,23 +277,39 @@ function MiniBars({
|
|||||||
}
|
}
|
||||||
const max = Math.max(1, ...pts.map((p) => p.value));
|
const max = Math.max(1, ...pts.map((p) => p.value));
|
||||||
return (
|
return (
|
||||||
<div className="flex items-end gap-0.5 h-24 w-full">
|
<div className="flex items-end gap-0.5 h-28 w-full">
|
||||||
{pts.map((p) => (
|
{pts.map((p) => {
|
||||||
<div
|
const pct = Math.max(6, Math.round((p.value / max) * 100));
|
||||||
key={p.day}
|
return (
|
||||||
className="flex-1 min-w-0"
|
|
||||||
title={`${p.day}: ${n(p.value)}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-t ${color} hover:opacity-90`}
|
key={p.day}
|
||||||
style={{ height: `${Math.max(4, (p.value / max) * 100)}%` }}
|
className="flex-1 min-w-0 h-full flex flex-col justify-end"
|
||||||
/>
|
title={`${p.day}: ${n(p.value)}`}
|
||||||
</div>
|
>
|
||||||
))}
|
<div
|
||||||
|
className={`w-full rounded-t ${color}`}
|
||||||
|
style={{ height: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function seriesDelta(series?: DayPoint[]): {
|
||||||
|
abs: number;
|
||||||
|
pct: number | null;
|
||||||
|
up: boolean;
|
||||||
|
} | null {
|
||||||
|
if (!series || series.length < 2) return null;
|
||||||
|
const first = series[0].value;
|
||||||
|
const last = series[series.length - 1].value;
|
||||||
|
const abs = last - first;
|
||||||
|
const pct = first !== 0 ? (100 * abs) / Math.abs(first) : null;
|
||||||
|
return { abs, pct, up: abs >= 0 };
|
||||||
|
}
|
||||||
|
|
||||||
function ChartCard({
|
function ChartCard({
|
||||||
title,
|
title,
|
||||||
hint,
|
hint,
|
||||||
@@ -307,6 +323,7 @@ function ChartCard({
|
|||||||
total?: number | null;
|
total?: number | null;
|
||||||
color: string;
|
color: string;
|
||||||
}) {
|
}) {
|
||||||
|
const delta = seriesDelta(series);
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-4 space-y-2">
|
<div className="rounded-xl border border-gray-200 bg-white p-4 space-y-2">
|
||||||
<div className="flex items-start justify-between gap-2">
|
<div className="flex items-start justify-between gap-2">
|
||||||
@@ -316,7 +333,22 @@ function ChartCard({
|
|||||||
</p>
|
</p>
|
||||||
{hint ? <p className="text-[10px] text-gray-400 mt-0.5">{hint}</p> : null}
|
{hint ? <p className="text-[10px] text-gray-400 mt-0.5">{hint}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg font-bold text-gray-900 tabular-nums">{n(total)}</p>
|
<div className="text-right">
|
||||||
|
<p className="text-lg font-bold text-gray-900 tabular-nums">{n(total)}</p>
|
||||||
|
{delta ? (
|
||||||
|
<p
|
||||||
|
className={`text-[10px] font-semibold tabular-nums ${
|
||||||
|
delta.up ? 'text-emerald-600' : 'text-rose-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{delta.up ? '↑' : '↓'} {n(Math.abs(delta.abs))}
|
||||||
|
{delta.pct != null
|
||||||
|
? ` (${delta.pct > 0 ? '+' : ''}${delta.pct.toFixed(0)}%)`
|
||||||
|
: ''}
|
||||||
|
<span className="text-gray-400 font-normal"> inicio→fin</span>
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MiniBars series={series} color={color} />
|
<MiniBars series={series} color={color} />
|
||||||
{series && series.length > 1 ? (
|
{series && series.length > 1 ? (
|
||||||
@@ -345,9 +377,6 @@ export default function SocialDashboard({
|
|||||||
warning?: string;
|
warning?: string;
|
||||||
}>({});
|
}>({});
|
||||||
const [posts, setPosts] = useState<SocialPost[]>([]);
|
const [posts, setPosts] = useState<SocialPost[]>([]);
|
||||||
const [feedFilter, setFeedFilter] = useState<'all' | 'instagram' | 'facebook'>(
|
|
||||||
'all'
|
|
||||||
);
|
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -375,10 +404,10 @@ export default function SocialDashboard({
|
|||||||
const by = useMemo(() => new Map(integs.map((i) => [i.provider, i])), [integs]);
|
const by = useMemo(() => new Map(integs.map((i) => [i.provider, i])), [integs]);
|
||||||
const ig = metrics.instagram;
|
const ig = metrics.instagram;
|
||||||
const fb = metrics.facebook;
|
const fb = metrics.facebook;
|
||||||
const visiblePosts =
|
const visiblePosts = useMemo(
|
||||||
feedFilter === 'all'
|
() => posts.filter((p) => p.platform === 'instagram'),
|
||||||
? posts
|
[posts]
|
||||||
: posts.filter((p) => p.platform === feedFilter);
|
);
|
||||||
|
|
||||||
const kpiCards = [
|
const kpiCards = [
|
||||||
{
|
{
|
||||||
@@ -606,8 +635,8 @@ export default function SocialDashboard({
|
|||||||
Evolución diaria · {rangeLabel}
|
Evolución diaria · {rangeLabel}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-gray-500 mt-0.5">
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
Totales del periodo arriba a la derecha de cada gráfica. Globales
|
Barras = valor diario. Flecha = cambio del primer al último día
|
||||||
(seguidores/publicaciones) en las KPI.
|
del periodo. Totales arriba a la derecha.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||||
@@ -618,38 +647,18 @@ export default function SocialDashboard({
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="space-y-3">
|
<section className="space-y-3">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-900">
|
<h3 className="text-sm font-semibold text-gray-900">
|
||||||
Contenido publicado
|
Publicaciones Instagram
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1">
|
<p className="text-xs text-gray-500 mt-0.5">
|
||||||
{(
|
Solo IG (feed, reels y carruseles). Sin posts de Facebook.
|
||||||
[
|
</p>
|
||||||
['all', 'Todo'],
|
|
||||||
['instagram', 'IG'],
|
|
||||||
['facebook', 'FB'],
|
|
||||||
] as const
|
|
||||||
).map(([id, lbl]) => (
|
|
||||||
<button
|
|
||||||
key={id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setFeedFilter(id)}
|
|
||||||
className={`px-2.5 py-1 rounded-lg text-[11px] font-semibold ${
|
|
||||||
feedFilter === id
|
|
||||||
? 'bg-gray-900 text-white'
|
|
||||||
: 'bg-gray-100 text-gray-600'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{lbl}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{visiblePosts.length === 0 ? (
|
{visiblePosts.length === 0 ? (
|
||||||
<p className="text-sm text-gray-500 border border-dashed border-gray-200 rounded-xl px-4 py-10 text-center">
|
<p className="text-sm text-gray-500 border border-dashed border-gray-200 rounded-xl px-4 py-10 text-center">
|
||||||
Sin posts en este periodo (o el token Meta no puede leer la
|
Sin publicaciones de Instagram en este periodo.
|
||||||
Página/IG).
|
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
<ul className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user