Initial commit — Bradly branding editor platform

This commit is contained in:
2026-06-02 03:27:03 -05:00
commit b135a70cc7
180 changed files with 43160 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import React, { useMemo, useCallback } from 'react';
import { ContentPiece, ContentPillar, Platform } from '../../types';
import { PlatformIcons } from './PlatformIcons';
import { StatusBadge } from './StatusBadge';
import { Image as ImageIcon, Video, Instagram } from 'lucide-react';
interface GridViewProps {
pieces: ContentPiece[];
pillars: ContentPillar[];
onPieceClick: (piece: ContentPiece) => void;
platform: Platform;
onPlatformChange: (platform: Platform) => void;
}
/**
* Visual grid view inspired by Later/Instagram feed planner.
* Shows content as a 3-column grid preview mimicking how it will look on a social feed.
*/
export const GridView: React.FC<GridViewProps> = ({
pieces,
pillars,
onPieceClick,
platform,
onPlatformChange,
}) => {
// Filter pieces that target the selected platform and are scheduled/published
const gridPieces = useMemo(() => {
return pieces
.filter(p => p.platforms.includes(platform))
.sort((a, b) => {
// Sort by scheduled date, most recent first
const dateA = a.scheduledDate || a.createdAt;
const dateB = b.scheduledDate || b.createdAt;
return dateB.localeCompare(dateA);
});
}, [pieces, platform]);
const columns = platform === 'tiktok' || platform === 'youtube' ? 2 : 3;
return (
<div className="flex flex-col h-full">
{/* Platform selector */}
<div className="flex items-center gap-3 pb-4">
<span className="text-[10px] font-semibold text-neutral-500 uppercase tracking-widest">
Vista de Feed:
</span>
<div className="flex gap-1">
{(['instagram', 'tiktok', 'facebook', 'linkedin'] as Platform[]).map(p => {
const isActive = platform === p;
return (
<button
key={p}
onClick={() => onPlatformChange(p)}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-all border ${
isActive
? 'bg-violet-600/15 border-violet-500/30 text-violet-300'
: 'bg-neutral-900/50 border-neutral-800 text-neutral-500 hover:text-neutral-300 hover:border-neutral-700'
}`}
title={`Vista de ${p}`}
>
{p === 'instagram' && '📸'}
{p === 'tiktok' && '🎵'}
{p === 'facebook' && '📘'}
{p === 'linkedin' && '💼'}
{p.charAt(0).toUpperCase() + p.slice(1)}
</button>
);
})}
</div>
</div>
{/* Feed preview container */}
<div className="flex-1 flex justify-center overflow-y-auto">
<div
className="bg-neutral-900/30 border border-neutral-800/50 rounded-2xl p-4 max-w-lg w-full"
style={{ maxWidth: columns === 2 ? '380px' : '480px' }}
>
{/* Fake profile header */}
<div className="flex items-center gap-3 mb-4 px-1">
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-violet-500 to-fuchsia-500" />
<div>
<p className="text-xs font-semibold text-white">@mi_marca</p>
<p className="text-[10px] text-neutral-500">{gridPieces.length} publicaciones</p>
</div>
</div>
{/* Grid */}
{gridPieces.length > 0 ? (
<div
className="grid gap-0.5"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
{gridPieces.map(piece => {
const pillar = pillars.find(p => p.id === piece.pillarId);
return (
<button
key={piece.id}
onClick={() => onPieceClick(piece)}
className="relative aspect-square bg-neutral-800 rounded-sm overflow-hidden group hover:opacity-90 transition-all"
>
{/* Thumbnail or placeholder */}
{piece.thumbnailUrl ? (
<img
src={piece.thumbnailUrl}
alt={piece.title}
className="w-full h-full object-cover"
/>
) : (
<div
className="w-full h-full flex flex-col items-center justify-center p-2"
style={{ backgroundColor: pillar ? `${pillar.color}15` : '#1a1a2e' }}
>
<div className="text-neutral-600 mb-1">
{piece.projectId ? <Video size={16} /> : <ImageIcon size={16} />}
</div>
<span className="text-[8px] text-neutral-500 text-center line-clamp-2 leading-tight">
{piece.title}
</span>
</div>
)}
{/* Hover overlay */}
<div className="absolute inset-0 bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity flex flex-col items-center justify-center gap-1.5 p-2">
<span className="text-[10px] font-semibold text-white text-center line-clamp-2">
{piece.title}
</span>
<StatusBadge status={piece.status} size="sm" />
{piece.scheduledDate && (
<span className="text-[9px] text-neutral-400 font-mono">
{new Date(piece.scheduledDate).toLocaleDateString('es-ES', {
day: 'numeric',
month: 'short',
})}
</span>
)}
</div>
{/* Pillar indicator */}
{pillar && (
<div
className="absolute top-1 right-1 w-2 h-2 rounded-full ring-1 ring-black/30"
style={{ backgroundColor: pillar.color }}
/>
)}
</button>
);
})}
</div>
) : (
<div className="flex flex-col items-center justify-center py-16 text-neutral-600">
<Instagram size={32} className="mb-3 opacity-30" />
<p className="text-xs font-medium">No hay contenido para {platform}</p>
<p className="text-[10px] text-neutral-700 mt-1">
Crea piezas de contenido y asígnalas a esta plataforma
</p>
</div>
)}
</div>
</div>
</div>
);
};