feat(panel-cliente): soporte de filtrado mensual, histórico instagram ene-sep 2026 y enlaces a publicaciones

This commit is contained in:
Marcos Fargas
2026-10-04 19:31:11 +02:00
parent a3a2e4b62b
commit 2f6e50d5a7
4 changed files with 799 additions and 36 deletions

View File

@@ -1,7 +1,20 @@
'use client';
import React, { useEffect, useMemo, useState } from 'react';
import { ImagePlus, Plus, Trash2, Upload } from 'lucide-react';
import {
ImagePlus,
Plus,
Trash2,
Upload,
Instagram,
ExternalLink,
X,
Eye,
Layers,
ChevronLeft,
ChevronRight,
Filter,
} from 'lucide-react';
import { PageHeader, Surface } from '@/components/ui';
type PipelineItem = {
@@ -31,12 +44,19 @@ const STATUS_LABEL: Record<string, string> = {
};
const STATUS_COLOR: Record<string, string> = {
propuesta: 'bg-gray-100 text-gray-700',
en_diseno: 'bg-blue-100 text-blue-700',
revision_interna: 'bg-amber-100 text-amber-800',
aprobado: 'bg-emerald-100 text-emerald-700',
rechazado: 'bg-red-100 text-red-700',
publicado: 'bg-purple-100 text-purple-700',
propuesta: 'bg-gray-100 text-gray-700 border-gray-200',
en_diseno: 'bg-blue-100 text-blue-700 border-blue-200',
revision_interna: 'bg-amber-100 text-amber-800 border-amber-200',
aprobado: 'bg-emerald-100 text-emerald-700 border-emerald-200',
rechazado: 'bg-red-100 text-red-700 border-red-200',
publicado: 'bg-purple-100 text-purple-700 border-purple-200',
};
const FORMAT_CONFIG: Record<string, { label: string; badgeCls: string }> = {
post: { label: 'Post', badgeCls: 'bg-blue-50 text-blue-700 border-blue-200' },
carrusel: { label: 'Carrusel', badgeCls: 'bg-indigo-50 text-indigo-700 border-indigo-200' },
reel: { label: 'Reel', badgeCls: 'bg-pink-50 text-pink-700 border-pink-200' },
story: { label: 'Story', badgeCls: 'bg-amber-50 text-amber-700 border-amber-200' },
};
const AGENCY_STATUS_ORDER = [
@@ -60,6 +80,20 @@ function monthLabel(m: string | null) {
return d.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' });
}
function monthShortLabel(m: string | null) {
if (!m) return 'Sin mes';
const [y, mm] = m.split('-');
const d = new Date(Number(y), Number(mm) - 1, 1);
const name = d.toLocaleDateString('es-ES', { month: 'short' });
return `${name} ${y.slice(2)}`;
}
function getInstagramUrl(notes: string | null): string | null {
if (!notes) return null;
const match = notes.match(/https?:\/\/(?:www\.)?instagram\.com\S+/i);
return match ? match[0] : null;
}
export default function CreativePipelineBoard() {
const [items, setItems] = useState<PipelineItem[]>([]);
const [isAgency, setIsAgency] = useState(false);
@@ -71,6 +105,9 @@ export default function CreativePipelineBoard() {
const [newTitle, setNewTitle] = useState('');
const [newFormat, setNewFormat] = useState<PipelineItem['format']>('post');
const [newMonth, setNewMonth] = useState('');
const [selectedMonth, setSelectedMonth] = useState<string>('all');
const [selectedFormat, setSelectedFormat] = useState<string>('all');
const [previewItem, setPreviewItem] = useState<PipelineItem | null>(null);
async function load() {
setLoading(true);
@@ -89,15 +126,40 @@ export default function CreativePipelineBoard() {
void load();
}, []);
const monthCounts = useMemo(() => {
const map = new Map<string, number>();
for (const it of items) {
const key = it.month || 'sin-mes';
map.set(key, (map.get(key) || 0) + 1);
}
return map;
}, [items]);
const months = useMemo(() => {
const set = new Set<string>();
for (const it of items) {
if (it.month) set.add(it.month);
}
return Array.from(set).sort();
}, [items]);
const filteredItems = useMemo(() => {
return items.filter((it) => {
if (selectedMonth !== 'all' && (it.month || 'sin-mes') !== selectedMonth) return false;
if (selectedFormat !== 'all' && it.format !== selectedFormat) return false;
return true;
});
}, [items, selectedMonth, selectedFormat]);
const grouped = useMemo(() => {
const map = new Map<string, PipelineItem[]>();
for (const it of items) {
for (const it of filteredItems) {
const key = it.month || 'sin-mes';
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(it);
}
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
}, [items]);
}, [filteredItems]);
async function createItem(e: React.FormEvent) {
e.preventDefault();
@@ -133,6 +195,9 @@ export default function CreativePipelineBoard() {
setErr(data.error || 'Error al cambiar estado');
return;
}
if (previewItem && previewItem.id === id) {
setPreviewItem({ ...previewItem, status });
}
await load();
} finally {
setBusyId(null);
@@ -143,6 +208,7 @@ export default function CreativePipelineBoard() {
setBusyId(id);
try {
await fetch(`/api/creative-pipeline?id=${encodeURIComponent(id)}`, { method: 'DELETE' });
if (previewItem?.id === id) setPreviewItem(null);
await load();
} finally {
setBusyId(null);
@@ -180,7 +246,7 @@ export default function CreativePipelineBoard() {
<PageHeader
eyebrow="Producción editorial"
title="Panel de Creatividades"
description="Propuesta → Diseño → Revisión interna → Aprobación del cliente."
description="Planificación, diseño y archivo histórico sincronizado con Instagram."
actions={
isAgency ? (
<button
@@ -204,6 +270,77 @@ export default function CreativePipelineBoard() {
</div>
) : null}
{/* Barra de Filtros por Mes y Formato */}
<div className="mb-6 space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold uppercase tracking-wider text-gray-500">
Meses ({items.length} piezas):
</span>
</div>
<div className="flex items-center gap-1.5 text-xs text-gray-500">
<Filter className="h-3.5 w-3.5" />
Mostrando <strong>{filteredItems.length}</strong> de {items.length}
</div>
</div>
{/* Meses pills */}
<div className="flex flex-wrap gap-1.5">
<button
onClick={() => setSelectedMonth('all')}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
selectedMonth === 'all'
? 'bg-gray-900 text-white shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Todos ({items.length})
</button>
{months.map((m) => {
const count = monthCounts.get(m) || 0;
const active = selectedMonth === m;
return (
<button
key={m}
onClick={() => setSelectedMonth(m)}
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
active
? 'bg-brand-600 text-white shadow-sm'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
{monthShortLabel(m)} ({count})
</button>
);
})}
</div>
{/* Formatos pills */}
<div className="flex flex-wrap items-center gap-1.5 pt-1">
<span className="text-xs text-gray-400 mr-1">Formato:</span>
{['all', 'carrusel', 'reel', 'post', 'story'].map((f) => {
const active = selectedFormat === f;
const label =
f === 'all'
? 'Todos los formatos'
: FORMAT_CONFIG[f]?.label || f;
return (
<button
key={f}
onClick={() => setSelectedFormat(f)}
className={`rounded-md px-2.5 py-0.5 text-xs font-medium transition-colors ${
active
? 'bg-gray-800 text-white'
: 'bg-white border border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
{label}
</button>
);
})}
</div>
</div>
{showForm && isAgency ? (
<Surface className="mb-6">
<form onSubmit={createItem} className="flex flex-wrap items-end gap-3">
@@ -240,7 +377,7 @@ export default function CreativePipelineBoard() {
</div>
<button
type="submit"
className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white"
className="rounded-lg bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
>
Crear
</button>
@@ -249,18 +386,29 @@ export default function CreativePipelineBoard() {
) : null}
{loading ? (
<p className="text-sm text-gray-500">Cargando…</p>
<p className="text-sm text-gray-500">Cargando creatividades…</p>
) : items.length === 0 ? (
<Surface>
<p className="text-sm text-gray-500">Todavía no hay piezas en el pipeline.</p>
</Surface>
) : filteredItems.length === 0 ? (
<Surface>
<p className="text-sm text-gray-500">
No hay piezas con los filtros seleccionados (Mes: {selectedMonth}, Formato: {selectedFormat}).
</p>
</Surface>
) : (
<div className="space-y-8">
{grouped.map(([month, monthItems]) => (
<div key={month}>
<h2 className="mb-3 text-sm font-semibold uppercase tracking-wide text-gray-500">
{monthLabel(month === 'sin-mes' ? null : month)}
</h2>
<div className="mb-3 flex items-center justify-between border-b border-gray-200 pb-2">
<h2 className="text-sm font-bold uppercase tracking-wider text-gray-700">
{monthLabel(month === 'sin-mes' ? null : month)}
</h2>
<span className="text-xs text-gray-500 font-medium">
{monthItems.length} {monthItems.length === 1 ? 'pieza' : 'piezas'}
</span>
</div>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{monthItems.map((it) => (
<PieceCard
@@ -271,6 +419,7 @@ export default function CreativePipelineBoard() {
onStatus={(s) => setStatus(it.id, s)}
onDelete={() => removeItem(it.id)}
onUpload={(file, slot, index) => uploadImage(it.id, file, slot, index)}
onPreview={() => setPreviewItem(it)}
/>
))}
</div>
@@ -278,6 +427,17 @@ export default function CreativePipelineBoard() {
))}
</div>
)}
{/* Modal de Detalle / Preview */}
{previewItem && (
<DetailModal
item={previewItem}
isAgency={isAgency}
busy={busyId === previewItem.id}
onClose={() => setPreviewItem(null)}
onStatus={(s) => setStatus(previewItem.id, s)}
/>
)}
</div>
);
}
@@ -289,6 +449,7 @@ function PieceCard({
onStatus,
onDelete,
onUpload,
onPreview,
}: {
item: PipelineItem;
isAgency: boolean;
@@ -296,26 +457,70 @@ function PieceCard({
onStatus: (status: string) => void;
onDelete: () => void;
onUpload: (file: File, slot: 'cover' | 'slide', index: number) => void;
onPreview: () => void;
}) {
const cover = mediaUrl(item.cover_path);
const slides = item.slide_paths || [];
const isCarrusel = item.format === 'carrusel';
const formatInfo = FORMAT_CONFIG[item.format] || {
label: item.format,
badgeCls: 'bg-gray-100 text-gray-700 border-gray-200',
};
const igUrl = getInstagramUrl(item.notes);
const clientCanDecide = !isAgency && item.status === 'revision_interna';
return (
<Surface padded={false} className="overflow-hidden flex flex-col">
<div className="relative aspect-[4/5] bg-gray-100">
<Surface padded={false} className="overflow-hidden flex flex-col group transition-shadow hover:shadow-md">
{/* Portada / Imagen */}
<div className="relative aspect-[4/5] bg-gray-100 overflow-hidden cursor-pointer" onClick={onPreview}>
{cover ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={cover} alt={item.title} className="h-full w-full object-cover" />
<img
src={cover}
alt={item.title}
className="h-full w-full object-cover transition-transform duration-200 group-hover:scale-105"
/>
) : (
<div className="flex h-full items-center justify-center text-gray-300">
<ImagePlus className="h-10 w-10" />
</div>
)}
{/* Badges superiores */}
<div className="absolute top-2 left-2 flex items-center gap-1.5 pointer-events-none">
<span
className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider border shadow-sm ${formatInfo.badgeCls}`}
>
{isCarrusel && <Layers className="h-2.5 w-2.5" />}
{formatInfo.label}
</span>
{isCarrusel && slides.length > 0 && (
<span className="rounded-md bg-black/60 px-1.5 py-0.5 text-[10px] font-medium text-white shadow-sm">
{slides.length} slides
</span>
)}
</div>
{/* Botón de vista rápida */}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onPreview();
}}
className="absolute top-2 right-2 rounded-full bg-black/50 p-1.5 text-white hover:bg-black/75 transition-opacity"
title="Ver detalle"
>
<Eye className="h-3.5 w-3.5" />
</button>
{/* Botón subir imagen agencia */}
{isAgency ? (
<label className="absolute bottom-2 right-2 cursor-pointer rounded-full bg-black/60 p-2 text-white hover:bg-black/80">
<label
onClick={(e) => e.stopPropagation()}
className="absolute bottom-2 right-2 cursor-pointer rounded-full bg-black/60 p-2 text-white hover:bg-black/80"
>
<Upload className="h-3.5 w-3.5" />
<input
type="file"
@@ -330,21 +535,26 @@ function PieceCard({
) : null}
</div>
{/* Tira de slides para carruseles */}
{isCarrusel ? (
<div className="flex gap-1 overflow-x-auto p-2 bg-gray-50">
{Array.from({ length: 4 }).map((_, i) => {
<div className="flex gap-1 overflow-x-auto p-2 bg-gray-50 border-t border-gray-100">
{Array.from({ length: Math.min(Math.max(slides.length, 1), 4) }).map((_, i) => {
const url = mediaUrl(slides[i] || null);
return (
<div
key={i}
className="relative h-14 w-11 shrink-0 overflow-hidden rounded bg-gray-200"
onClick={onPreview}
className="relative h-14 w-11 shrink-0 overflow-hidden rounded bg-gray-200 cursor-pointer border border-gray-200"
>
{url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={url} alt={`slide ${i + 1}`} className="h-full w-full object-cover" />
) : null}
{isAgency ? (
<label className="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/0 hover:bg-black/40 text-transparent hover:text-white text-[10px]">
<label
onClick={(e) => e.stopPropagation()}
className="absolute inset-0 flex cursor-pointer items-center justify-center bg-black/0 hover:bg-black/40 text-transparent hover:text-white text-[10px]"
>
+
<input
type="file"
@@ -363,26 +573,49 @@ function PieceCard({
</div>
) : null}
{/* Contenido de la tarjeta */}
<div className="flex flex-1 flex-col gap-2 p-3">
<div className="flex items-start justify-between gap-2">
<p className="text-sm font-medium text-gray-900 line-clamp-2">{item.title}</p>
<p
onClick={onPreview}
className="text-sm font-semibold text-gray-900 line-clamp-2 cursor-pointer hover:text-brand-600"
>
{item.title}
</p>
{isAgency ? (
<button
onClick={onDelete}
disabled={busy}
className="shrink-0 text-gray-400 hover:text-red-500"
title="Eliminar"
>
<Trash2 className="h-4 w-4" />
</button>
) : null}
</div>
<span
className={`inline-flex w-fit rounded-full px-2 py-0.5 text-xs font-medium ${
STATUS_COLOR[item.status] || 'bg-gray-100 text-gray-700'
}`}
>
{STATUS_LABEL[item.status] || item.status}
</span>
<div className="flex flex-wrap items-center gap-1.5">
<span
className={`inline-flex w-fit rounded-full px-2 py-0.5 text-xs font-semibold border ${
STATUS_COLOR[item.status] || 'bg-gray-100 text-gray-700 border-gray-200'
}`}
>
{STATUS_LABEL[item.status] || item.status}
</span>
{igUrl && (
<a
href={igUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-full bg-gradient-to-r from-pink-500 via-red-500 to-yellow-500 px-2 py-0.5 text-[10px] font-bold text-white shadow-sm hover:opacity-90"
title="Abrir publicación original en Instagram"
>
<Instagram className="h-2.5 w-2.5" /> IG
<ExternalLink className="h-2 w-2" />
</a>
)}
</div>
<div className="mt-auto pt-2">
{isAgency ? (
@@ -390,7 +623,7 @@ function PieceCard({
value={item.status}
disabled={busy}
onChange={(e) => onStatus(e.target.value)}
className="w-full rounded-lg border border-gray-200 px-2 py-1.5 text-xs"
className="w-full rounded-lg border border-gray-200 px-2 py-1.5 text-xs font-medium"
>
{AGENCY_STATUS_ORDER.map((s) => (
<option key={s} value={s}>
@@ -419,7 +652,9 @@ function PieceCard({
<p className="text-xs text-gray-400">
{item.status === 'aprobado'
? 'Aprobada por ti'
: 'Pendiente del equipo de 4Ventures'}
: item.status === 'publicado'
? 'Publicado en redes'
: 'Pendiente del equipo de 4Ventures'}
</p>
)}
</div>
@@ -427,3 +662,215 @@ function PieceCard({
</Surface>
);
}
function DetailModal({
item,
isAgency,
busy,
onClose,
onStatus,
}: {
item: PipelineItem;
isAgency: boolean;
busy: boolean;
onClose: () => void;
onStatus: (status: string) => void;
}) {
const slides = item.slide_paths || [];
const allImages = [item.cover_path, ...slides].filter(Boolean) as string[];
const [activeIdx, setActiveIdx] = useState(0);
const activePath = allImages[activeIdx] || item.cover_path;
const currentUrl = mediaUrl(activePath);
const igUrl = getInstagramUrl(item.notes);
const formatInfo = FORMAT_CONFIG[item.format] || {
label: item.format,
badgeCls: 'bg-gray-100 text-gray-700 border-gray-200',
};
const clientCanDecide = !isAgency && item.status === 'revision_interna';
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/75 p-4 backdrop-blur-sm"
onClick={onClose}
>
<div
className="flex max-h-[92vh] w-full max-w-4xl flex-col overflow-hidden rounded-2xl bg-white shadow-2xl md:flex-row"
onClick={(e) => e.stopPropagation()}
>
{/* Lado izquierdo: visor de imagen / slider */}
<div className="flex flex-1 flex-col bg-gray-900 p-4">
<div className="relative flex flex-1 items-center justify-center overflow-hidden rounded-lg bg-black/40 min-h-[300px]">
{currentUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={currentUrl}
alt={item.title}
className="max-h-[60vh] max-w-full object-contain"
/>
) : (
<div className="flex flex-col items-center gap-2 text-gray-500">
<ImagePlus className="h-12 w-12" />
<span className="text-xs">Sin imagen cargada</span>
</div>
)}
{allImages.length > 1 && (
<>
<button
type="button"
onClick={() => setActiveIdx((prev) => (prev > 0 ? prev - 1 : allImages.length - 1))}
className="absolute left-2 top-1/2 -translate-y-1/2 rounded-full bg-black/60 p-1.5 text-white hover:bg-black/90"
>
<ChevronLeft className="h-5 w-5" />
</button>
<button
type="button"
onClick={() => setActiveIdx((prev) => (prev < allImages.length - 1 ? prev + 1 : 0))}
className="absolute right-2 top-1/2 -translate-y-1/2 rounded-full bg-black/60 p-1.5 text-white hover:bg-black/90"
>
<ChevronRight className="h-5 w-5" />
</button>
<div className="absolute bottom-2 left-1/2 -translate-x-1/2 rounded-full bg-black/60 px-2 py-0.5 text-xs text-white">
{activeIdx + 1} / {allImages.length}
</div>
</>
)}
</div>
{/* Miniaturas de slides */}
{allImages.length > 1 && (
<div className="mt-3 flex gap-2 overflow-x-auto py-1">
{allImages.map((path, idx) => {
const url = mediaUrl(path);
const isSelected = idx === activeIdx;
return (
<button
key={idx}
type="button"
onClick={() => setActiveIdx(idx)}
className={`relative h-16 w-12 shrink-0 overflow-hidden rounded border-2 transition-all ${
isSelected ? 'border-brand-500 scale-105' : 'border-transparent opacity-60 hover:opacity-100'
}`}
>
{url && (
// eslint-disable-next-line @next/next/no-img-element
<img src={url} alt={`Slide ${idx + 1}`} className="h-full w-full object-cover" />
)}
</button>
);
})}
</div>
)}
</div>
{/* Lado derecho: información, caption y acciones */}
<div className="flex w-full flex-col justify-between p-6 md:w-96 border-t md:border-t-0 md:border-l border-gray-100 overflow-y-auto">
<div className="space-y-4">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-wrap items-center gap-2">
<span
className={`inline-flex items-center gap-1 rounded-md px-2 py-0.5 text-xs font-bold uppercase tracking-wider border ${formatInfo.badgeCls}`}
>
{formatInfo.label}
</span>
<span
className={`inline-flex rounded-full px-2 py-0.5 text-xs font-semibold border ${
STATUS_COLOR[item.status] || 'bg-gray-100 text-gray-700'
}`}
>
{STATUS_LABEL[item.status] || item.status}
</span>
</div>
<button
type="button"
onClick={onClose}
className="rounded-full p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-700"
>
<X className="h-5 w-5" />
</button>
</div>
<div>
<h3 className="text-base font-bold text-gray-900 leading-snug">{item.title}</h3>
<p className="mt-1 text-xs text-gray-500">
{monthLabel(item.month)} {item.external_ref ? `· Ref: ${item.external_ref}` : ''}
</p>
</div>
{igUrl && (
<a
href={igUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-xl bg-gradient-to-r from-purple-600 via-pink-600 to-amber-500 px-3 py-2 text-xs font-bold text-white shadow-sm hover:opacity-95"
>
<Instagram className="h-4 w-4" />
<span>Ver publicación original en Instagram</span>
<ExternalLink className="h-3 w-3 ml-auto" />
</a>
)}
{item.caption && (
<div className="space-y-1">
<p className="text-xs font-semibold text-gray-700 uppercase tracking-wider">Texto / Copy</p>
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-xs text-gray-800 whitespace-pre-wrap leading-relaxed max-h-48 overflow-y-auto">
{item.caption}
</div>
</div>
)}
</div>
<div className="mt-6 border-t border-gray-100 pt-4">
{isAgency ? (
<div className="space-y-2">
<label className="block text-xs font-semibold text-gray-600">Cambiar estado (Agencia):</label>
<select
value={item.status}
disabled={busy}
onChange={(e) => onStatus(e.target.value)}
className="w-full rounded-lg border border-gray-200 px-3 py-2 text-xs font-medium"
>
{AGENCY_STATUS_ORDER.map((s) => (
<option key={s} value={s}>
{STATUS_LABEL[s]}
</option>
))}
</select>
</div>
) : clientCanDecide ? (
<div className="space-y-2">
<p className="text-xs font-semibold text-gray-600">Tu decisión como cliente:</p>
<div className="flex gap-2">
<button
disabled={busy}
onClick={() => onStatus('aprobado')}
className="flex-1 rounded-lg bg-emerald-600 px-3 py-2 text-xs font-bold text-white hover:bg-emerald-700"
>
Aprobar pieza
</button>
<button
disabled={busy}
onClick={() => onStatus('rechazado')}
className="flex-1 rounded-lg bg-red-600 px-3 py-2 text-xs font-bold text-white hover:bg-red-700"
>
Solicitar cambios
</button>
</div>
</div>
) : (
<p className="text-center text-xs text-gray-500">
{item.status === 'aprobado'
? '✓ Esta pieza fue aprobada por el cliente.'
: item.status === 'publicado'
? '✓ Esta pieza está publicada y archivada.'
: 'En proceso con el equipo de 4Ventures.'}
</p>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -10,6 +10,7 @@ import {
MessageSquare,
ShieldCheck,
X,
ExternalLink,
} from 'lucide-react';
export type RrssPiece = {
@@ -544,6 +545,17 @@ export default function RrssMonthView({
<span className="text-[11px] text-gray-500 font-mono">
{p.date} · {p.format}
</span>
{p.meta_line?.includes('http') && (
<a
href={p.meta_line.replace(/^Instagram:\s*/i, '').trim()}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className="inline-flex items-center gap-1 rounded bg-pink-50 border border-pink-200 px-1.5 py-0.5 text-[10px] font-semibold text-pink-700 hover:bg-pink-100"
>
<Instagram className="w-2.5 h-2.5" /> Ver en IG
</a>
)}
<DualBadge dual={dualMap[p.id]} />
</div>
<p className="text-sm font-bold text-gray-900">{p.title}</p>
@@ -578,7 +590,18 @@ export default function RrssMonthView({
<div className="p-5 overflow-y-auto space-y-3 text-sm">
<DualBadge dual={dualMap[selected.id]} />
{selected.meta_line && (
<p className="text-xs text-gray-400">{selected.meta_line}</p>
selected.meta_line.includes('http') ? (
<a
href={selected.meta_line.replace(/^Instagram:\s*/i, '').trim()}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 rounded-lg bg-gradient-to-r from-purple-600 via-pink-600 to-amber-500 px-3 py-1.5 text-xs font-bold text-white shadow-sm hover:opacity-95"
>
<Instagram className="w-3.5 h-3.5" /> Abrir en Instagram <ExternalLink className="w-3 h-3 ml-auto" />
</a>
) : (
<p className="text-xs text-gray-400">{selected.meta_line}</p>
)
)}
<div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-amber-200 text-sm">
{selected.hook}