feat: upload creativos, PMax formats, portada IG/FB y seguimiento con notas
Permite subir media sin URL; previews Google PMax por ratio; calendario anual estilo Miia con filtros IG/FB y LinkedIn; seguimiento con selector de fechas, KPIs y cuadro de notas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
218
src/components/RrssArchiveYear.tsx
Normal file
218
src/components/RrssArchiveYear.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
import type { RrssMonthPlan, RrssPiece } from '@/components/RrssMonthView';
|
||||
|
||||
const MONTH_NAMES = [
|
||||
'Enero',
|
||||
'Febrero',
|
||||
'Marzo',
|
||||
'Abril',
|
||||
'Mayo',
|
||||
'Junio',
|
||||
'Julio',
|
||||
'Agosto',
|
||||
'Septiembre',
|
||||
'Octubre',
|
||||
'Noviembre',
|
||||
'Diciembre',
|
||||
];
|
||||
|
||||
function colorFor(piece: RrssPiece): string {
|
||||
if (piece.color) return piece.color;
|
||||
if (piece.platform === 'linkedin') return '#7C66B8';
|
||||
if ((piece.format || '').toLowerCase().includes('stor')) return '#374151';
|
||||
const hay = `${piece.pillar || ''} ${piece.meta_line || ''} ${piece.title}`.toLowerCase();
|
||||
if (hay.includes('familia') || hay.includes('bienestar')) return '#E08E2E';
|
||||
if (hay.includes('sostenib')) return '#0F9D8F';
|
||||
if (hay.includes('b2b') || hay.includes('hosteler')) return '#7C66B8';
|
||||
if (hay.includes('prueba') || hay.includes('reseña')) return '#8A93A6';
|
||||
return '#2071cf';
|
||||
}
|
||||
|
||||
function MiniMonth({
|
||||
year,
|
||||
monthIndex,
|
||||
plan,
|
||||
}: {
|
||||
year: number;
|
||||
monthIndex: number; // 0-11
|
||||
plan?: RrssMonthPlan;
|
||||
}) {
|
||||
const monthKey = `${year}-${String(monthIndex + 1).padStart(2, '0')}`;
|
||||
const first = new Date(Date.UTC(year, monthIndex, 1));
|
||||
const daysInMonth = new Date(Date.UTC(year, monthIndex + 1, 0)).getUTCDate();
|
||||
const startPad = (first.getUTCDay() + 6) % 7;
|
||||
const live = Boolean(plan);
|
||||
const byDate = new Map<string, RrssPiece[]>();
|
||||
for (const p of plan?.pieces || []) {
|
||||
const k = p.date.slice(0, 10);
|
||||
(byDate.get(k) || byDate.set(k, []).get(k)!).push(p);
|
||||
}
|
||||
|
||||
const headerCls = live
|
||||
? 'flex items-center justify-between gap-2 bg-gradient-to-r from-[#1a2a63] to-[#2071CF] text-white px-3.5 py-2.5'
|
||||
: 'flex items-center justify-between gap-2 bg-[#9aa6c0] text-white px-3.5 py-2.5';
|
||||
|
||||
const body = (
|
||||
<>
|
||||
<div className={headerCls}>
|
||||
<span className="text-[13px] font-extrabold tracking-wider uppercase">
|
||||
{MONTH_NAMES[monthIndex]}
|
||||
</span>
|
||||
<em className="not-italic text-[10px] font-bold tracking-wider uppercase px-2 py-1 rounded-full bg-white/20">
|
||||
{live ? 'Publicado' : 'Pronto'}
|
||||
</em>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-0.5 px-2.5 pt-3">
|
||||
{['L', 'M', 'X', 'J', 'V', 'S', 'D'].map((d, i) => (
|
||||
<span
|
||||
key={d}
|
||||
className={`text-center text-[10px] font-bold ${
|
||||
i >= 5 ? 'text-sky-400' : 'text-gray-500'
|
||||
}`}
|
||||
>
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-7 gap-0.5 px-2.5 pb-2.5 pt-1">
|
||||
{Array.from({ length: startPad }).map((_, i) => (
|
||||
<span key={`e-${i}`} className="aspect-square invisible" />
|
||||
))}
|
||||
{Array.from({ length: daysInMonth }).map((_, i) => {
|
||||
const day = i + 1;
|
||||
const key = `${monthKey}-${String(day).padStart(2, '0')}`;
|
||||
const hits = byDate.get(key) || [];
|
||||
const weekend =
|
||||
new Date(Date.UTC(year, monthIndex, day)).getUTCDay() % 6 === 0;
|
||||
const bg = hits[0] ? colorFor(hits[0]) : undefined;
|
||||
return (
|
||||
<span
|
||||
key={key}
|
||||
title={hits.map((h) => h.title).join(' · ') || undefined}
|
||||
className={`aspect-square flex items-center justify-center text-[11px] font-bold rounded-full ${
|
||||
weekend && !bg ? 'text-sky-400' : 'text-gray-300'
|
||||
}`}
|
||||
style={
|
||||
bg
|
||||
? {
|
||||
background: bg,
|
||||
color: '#fff',
|
||||
boxShadow: 'inset 0 0 0 1px rgba(255,255,255,.35)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{day}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-auto px-3.5 py-3 text-xs border-t border-gray-800 text-gray-500">
|
||||
{live
|
||||
? `${plan!.pieces.length} piezas · abrir mes`
|
||||
: 'Pendiente de publicar'}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
const className = `rounded-2xl border overflow-hidden flex flex-col min-h-full transition-all ${
|
||||
live
|
||||
? 'border-gray-700 bg-[#111827] hover:border-sky-500/50 hover:-translate-y-0.5 shadow-lg shadow-sky-900/10'
|
||||
: 'border-gray-800/80 bg-[#0D121F] opacity-80'
|
||||
}`;
|
||||
|
||||
if (live) {
|
||||
return (
|
||||
<Link
|
||||
href={`/dashboard/calendario?view=month&month=${monthKey}`}
|
||||
className={className}
|
||||
>
|
||||
{body}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} aria-disabled>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Year archive cover like /agua-miia/rrss/ig-fb/ */
|
||||
export default function RrssArchiveYear({
|
||||
year = 2026,
|
||||
plans,
|
||||
tenantLabel,
|
||||
agencyPublish,
|
||||
}: {
|
||||
year?: number;
|
||||
plans: RrssMonthPlan[];
|
||||
tenantLabel: string;
|
||||
agencyPublish?: React.ReactNode;
|
||||
}) {
|
||||
const byMonth = new Map(plans.map((p) => [p.month, p]));
|
||||
const legend = plans[0]?.legend || [
|
||||
{ label: 'Educativo / serie', color: '#2071cf' },
|
||||
{ label: 'Familia / bienestar', color: '#E08E2E' },
|
||||
{ label: 'Sostenibilidad', color: '#0F9D8F' },
|
||||
{ label: 'B2B / LinkedIn', color: '#7C66B8' },
|
||||
{ label: 'Stories', color: '#374151' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-7">
|
||||
<div className="flex flex-wrap items-end justify-between gap-4">
|
||||
<div>
|
||||
<Link
|
||||
href="/dashboard/calendario?view=hub"
|
||||
className="text-xs text-gray-400 hover:text-white"
|
||||
>
|
||||
← Hub RRSS
|
||||
</Link>
|
||||
<h2 className="text-3xl font-extrabold text-white tracking-tight mt-2">
|
||||
Calendario IG / FB {year}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mt-2 max-w-xl leading-relaxed">
|
||||
Archivo mensual de contenidos. Los días redondeados y coloreados
|
||||
marcan el tipo de publicación (misma paleta que en cada mes).
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-gray-700 bg-[#111827] px-4 py-2.5 text-sm font-bold text-white">
|
||||
<span className="w-2 h-2 rounded-full bg-sky-400" />
|
||||
Año {year} · {tenantLabel} × 4Ventures
|
||||
</div>
|
||||
{agencyPublish}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Array.from({ length: 12 }).map((_, i) => {
|
||||
const key = `${year}-${String(i + 1).padStart(2, '0')}`;
|
||||
return (
|
||||
<MiniMonth
|
||||
key={key}
|
||||
year={year}
|
||||
monthIndex={i}
|
||||
plan={byMonth.get(key)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 text-xs text-gray-400">
|
||||
{legend.map((l) => (
|
||||
<span key={l.label} className="inline-flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ background: l.color }}
|
||||
/>
|
||||
{l.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user