// fp-feed.jsx — Feed Planner with Supabase // Exports: FeedPlanner const DEFAULT_CATEGORIES = ["Lifestyle","Producto","Campaña","Educativo","Detrás de cámara","Colab","UGC"]; function FeedPlanner({ brand, onBack }) { const { profile } = useAuth(); const isClient = profile?.role === 'client'; const canEdit = !isClient; const [posts, setPosts] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); // Form state const [form, setForm] = useState({ imageUrl: null, imageFile: null, date: new Date().toISOString().slice(0,10), category: '', type: 'imagen', copy: '' }); const [dropOver, setDropOver] = useState(false); const [uploading, setUploading] = useState(false); const fileRef = useRef(); // Categories (local per brand, stored in brand row) const [categories, setCategories] = useState(brand.categories || DEFAULT_CATEGORIES); const [newCat, setNewCat] = useState(''); // Brand settings (editable inline) const [brandData, setBrandData] = useState({ ...brand }); const [editingBrand, setEditingBrand] = useState(false); // Brand avatar upload const brandAvatarRef = useRef(); const [uploadingAvatar, setUploadingAvatar] = useState(false); const uploadBrandAvatar = async (file) => { if (!file || !file.type.startsWith('image/')) return; setUploadingAvatar(true); const ext = file.name.split('.').pop(); const path = `brand-avatars/${brand.id}.${ext}`; const { error } = await sbClient.storage.from('post-images').upload(path, file, { upsert: true }); if (error) { alert('Error subiendo imagen: ' + error.message); setUploadingAvatar(false); return; } const { data } = sbClient.storage.from('post-images').getPublicUrl(path); const avatarUrl = data.publicUrl + '?t=' + Date.now(); await saveBrandSettings(avatarUrl); setUploadingAvatar(false); }; // Detail / edit modal const [detail, setDetail] = useState(null); // post object const [editing, setEditing] = useState(false); const [editForm, setEditForm] = useState({}); const [detailNotes, setDetailNotes] = useState(''); const [detailApproved, setDetailApproved] = useState(false); const [editImageFile, setEditImageFile] = useState(null); // Drag const [dragSrc, setDragSrc] = useState(null); const [dragTarget, setDragTarget] = useState(null); // ── Load posts + brand (to get saved categories) ───────── useEffect(() => { loadPosts(); sbClient.from('brands').select('categories, avatar_url').eq('id', brand.id).single().then(({ data }) => { if (data) { if (Array.isArray(data.categories) && data.categories.length) { setCategories(data.categories); } else { // First time: persist the defaults so future loads work sbClient.from('brands').update({ categories: brand.categories || DEFAULT_CATEGORIES }).eq('id', brand.id); } if (data.avatar_url !== undefined) setBrandData(d => ({ ...d, avatar_url: data.avatar_url })); } }); }, [brand.id]); const loadPosts = async () => { setLoading(true); const { data } = await sbClient.from('posts').select('*').eq('brand_id', brand.id).order('order_index').order('created_at', { ascending: false }); setPosts(data || []); setLoading(false); }; // ── Upload image to Supabase Storage ─────────────────── const uploadImage = async (file) => { const ext = file.name.split('.').pop(); const path = `${brand.id}/${Date.now()}.${ext}`; const { error } = await sbClient.storage.from('post-images').upload(path, file); if (error) { alert('Error subiendo imagen: ' + error.message); return null; } const { data } = sbClient.storage.from('post-images').getPublicUrl(path); return { url: data.publicUrl, path }; }; const handleFile = (file) => { if (!file || !file.type.startsWith('image/')) return; setForm(f => ({ ...f, imageFile: file, imageUrl: URL.createObjectURL(file) })); }; // ── Add post ─────────────────────────────────────────── const addPost = async () => { if (!form.imageFile) return; setUploading(true); const img = await uploadImage(form.imageFile); if (!img) { setUploading(false); return; } const maxOrder = posts.length ? Math.max(...posts.map(p => p.order_index || 0)) : 0; const { data, error } = await sbClient.from('posts').insert({ brand_id: brand.id, image_url: img.url, storage_path: img.path, date: form.date, category: form.category || 'Sin categoría', type: form.type, copy: form.copy, order_index: maxOrder + 1, }).select().single(); if (!error && data) setPosts(p => [data, ...p]); setForm(f => ({ ...f, imageFile: null, imageUrl: null, category: '', copy: '', type: 'imagen' })); setUploading(false); }; // ── Delete post ───────────────────────────────────────── const deletePost = async (post, e) => { e?.stopPropagation(); if (!confirm('¿Eliminar esta publicación?')) return; await sbClient.from('posts').delete().eq('id', post.id); if (post.storage_path) await sbClient.storage.from('post-images').remove([post.storage_path]); setPosts(p => p.filter(x => x.id !== post.id)); if (detail?.id === post.id) setDetail(null); }; // ── Open detail modal ─────────────────────────────────── const openDetail = (post) => { setDetail(post); setEditing(false); setEditForm({ ...post }); setDetailNotes(post.notes || ''); setDetailApproved(post.approved || false); setEditImageFile(null); }; // ── Save notes/approval (client + editor) ────────────── const saveNotes = async () => { setSaving(true); const updates = { notes: detailNotes, approved: detailApproved }; await sbClient.from('posts').update(updates).eq('id', detail.id); setPosts(ps => ps.map(p => p.id === detail.id ? { ...p, ...updates } : p)); setDetail(d => ({ ...d, ...updates })); setSaving(false); }; // ── Save full post edit ───────────────────────────────── const saveEdit = async () => { setSaving(true); let imageUrl = editForm.image_url; let storagePath = editForm.storage_path; if (editImageFile) { const img = await uploadImage(editImageFile); if (img) { imageUrl = img.url; storagePath = img.path; } } const updates = { ...editForm, image_url: imageUrl, storage_path: storagePath, notes: detailNotes, approved: detailApproved }; await sbClient.from('posts').update(updates).eq('id', detail.id); setPosts(ps => ps.map(p => p.id === detail.id ? updates : p)); setDetail(updates); setEditing(false); setSaving(false); }; // ── Save brand settings ──────────────────────────────── const saveBrandSettings = async (avatarUrl) => { const updates = { ...brandData, categories }; if (avatarUrl !== undefined) updates.avatar_url = avatarUrl; await sbClient.from('brands').update(updates).eq('id', brand.id); if (avatarUrl !== undefined) setBrandData(d => ({ ...d, avatar_url: avatarUrl })); setEditingBrand(false); }; // ── Drag reorder ─────────────────────────────────────── const handleDropGrid = async (targetId) => { if (!dragSrc || dragSrc === targetId) return; const arr = [...posts]; const fi = arr.findIndex(p => p.id === dragSrc); const ti = arr.findIndex(p => p.id === targetId); const [item] = arr.splice(fi, 1); arr.splice(ti, 0, item); setPosts(arr); setDragSrc(null); setDragTarget(null); // persist new order await Promise.all(arr.map((p, i) => sbClient.from('posts').update({ order_index: i }).eq('id', p.id))); }; // ── Categories ───────────────────────────────────────── const addCategory = () => { const c = newCat.trim(); if (c && !categories.includes(c)) setCategories(cs => [...cs, c]); setNewCat(''); }; const delCategory = (cat, e) => { e.stopPropagation(); setCategories(cs => cs.filter(c => c !== cat)); if (form.category === cat) setForm(f => ({ ...f, category: '' })); }; // ── Group by month ───────────────────────────────────── const groupedPosts = React.useMemo(() => { const groups = {}; posts.forEach(p => { const k = monthKey(p.date); if (!groups[k]) groups[k] = []; groups[k].push(p); }); return Object.entries(groups).sort((a, b) => b[0].localeCompare(a[0])); }, [posts]); const approvedCount = posts.filter(p => p.approved).length; return (
{/* ── SIDEBAR ── */} {canEdit && (
{brandData.name}
{/* Upload */}
{ e.preventDefault(); setDropOver(true); }} onDragLeave={() => setDropOver(false)} onDrop={e => { e.preventDefault(); setDropOver(false); handleFile(e.dataTransfer.files[0]); }} onClick={() => !form.imageUrl && fileRef.current.click()} > handleFile(e.target.files[0])} /> {form.imageUrl ? ( <> preview ) : ( <>

Sube una imagen
Arrastra o haz clic · JPG, PNG, WEBP

)}
{/* Type */}
Tipo de publicación
{POST_TYPES.map(t => (
setForm(f=>({...f,type:t.id}))}> {t.label}
))}
Fecha de publicación setForm(f=>({...f,date:e.target.value}))} />
Categoría ✕ elimina · Enter añade
{categories.map(c => (
setForm(f=>({...f,category:f.category===c?'':c}))}> {c}delCategory(c,e)}>✕
))} setNewCat(e.target.value)} onKeyDown={e => e.key==='Enter' && addCategory()} onBlur={addCategory}/>
Copy / Caption