'use client'; import React, { useState, useEffect, useCallback } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; import { createClient } from '@/lib/supabase/client'; import Header from '@/components/Header'; import Footer from '@/components/Footer'; // ─── Types ──────────────────────────────────────────────────────────────────── interface Article { id: string; title: string; status: string; category: string | null; site_id: number; created_at: string; updated_at: string; scheduled_at: string | null; published_at: string | null; submitted_for_review_at: string | null; scope_flagged?: boolean; editor_note?: string | null; } interface UserProfile { full_name: string; email: string; bio: string | null; role: string; wp_username: string | null; display_name: string | null; } type DashboardView = 'overview' | 'new-post' | 'my-posts' | 'profile'; const STATUS_CONFIG: Record = { draft: { label: 'Draft', color: 'text-gray-400', bg: 'bg-gray-400/10' }, pending: { label: 'Pending Review', color: 'text-yellow-400', bg: 'bg-yellow-400/10' }, scope_flagged: { label: 'Scope Flagged', color: 'text-amber-400', bg: 'bg-amber-400/10' }, scheduled: { label: 'Scheduled', color: 'text-blue-400', bg: 'bg-blue-400/10' }, scheduled_pending_review: { label: 'Scheduled — Pending Review', color: 'text-orange-400', bg: 'bg-orange-400/10' }, published: { label: 'Published', color: 'text-green-400', bg: 'bg-green-400/10' }, archived: { label: 'Archived', color: 'text-gray-500', bg: 'bg-gray-500/10' }, rejected: { label: 'Rejected', color: 'text-red-400', bg: 'bg-red-400/10' }, }; const BB_CATEGORIES = [ 'Audio', 'Broadcast', 'Business', 'Camera', 'Cloud', 'Content Creation', 'Distribution', 'Education', 'Events', 'IP & Networking', 'Live Production', 'Media Management', 'News', 'OTT', 'Post Production', 'Radio', 'Sports', 'Streaming', 'Technology', 'Uncategorized', ]; const AV_CATEGORIES = [ 'AV Integration', 'Digital Signage', 'Unified Communications', 'Streaming Technology', 'Conferencing & Collaboration', 'Houses of Worship', 'Education AV', 'Control Systems', 'Display Technology', 'Sound & Audio', 'Trade Shows & Events', 'Product News', 'Industry News', ]; const SITE_CONFIG = { 1: { name: 'AV Beat', badge: 'BB', color: '#0ea5e9', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]', borderClass: 'border-[#0ea5e9]/30' }, 2: { name: 'AV Beat', badge: 'AV', color: '#f97316', bgClass: 'bg-[#f97316]/10', textClass: 'text-[#f97316]', borderClass: 'border-[#f97316]/30' }, }; function SiteBadge({ siteId }: { siteId: number }) { const cfg = SITE_CONFIG[siteId as 1 | 2] || SITE_CONFIG[1]; return ( {cfg.badge} ); } export default function ContributorDashboard() { const { user, loading } = useAuth(); const router = useRouter(); const supabase = createClient(); const [view, setView] = useState('overview'); const [profile, setProfile] = useState(null); const [articles, setArticles] = useState([]); const [loadingData, setLoadingData] = useState(true); const [notification, setNotification] = useState<{ type: 'success' | 'error'; message: string } | null>(null); // New post form state const [postTitle, setPostTitle] = useState(''); const [postContent, setPostContent] = useState(''); const [postExcerpt, setPostExcerpt] = useState(''); const [postCategory, setPostCategory] = useState('Uncategorized'); const [postScheduledAt, setPostScheduledAt] = useState(''); const [postSiteId, setPostSiteId] = useState(1); const [submittingPost, setSubmittingPost] = useState(false); const [scopeWarning, setScopeWarning] = useState(null); const [checkingScope, setCheckingScope] = useState(false); // Profile edit state const [editName, setEditName] = useState(''); const [editBio, setEditBio] = useState(''); const [savingProfile, setSavingProfile] = useState(false); const showNotification = (type: 'success' | 'error', message: string) => { setNotification({ type, message }); setTimeout(() => setNotification(null), 5000); }; const fetchData = useCallback(async () => { if (!user) return; setLoadingData(true); try { const [profileRes, articlesRes] = await Promise.all([ supabase.from('user_profiles').select('full_name, email, bio, role, wp_username, display_name').eq('id', user.id).single(), fetch('/api/contributor/posts'), ]); if (profileRes.data) { setProfile(profileRes.data); setEditName(profileRes.data.full_name || ''); setEditBio(profileRes.data.bio || ''); } if (articlesRes.ok) { const data = await articlesRes.json(); setArticles(data.articles || []); } } catch (err) { console.error('Failed to fetch contributor data:', err); } finally { setLoadingData(false); } }, [user]); useEffect(() => { if (!loading && !user) { router.push('/login'); return; } if (user) fetchData(); }, [user, loading, fetchData, router]); useEffect(() => { if (profile && !['contributor', 'author'].includes(profile.role)) { router.push('/admin'); } }, [profile, router]); // When site changes, reset category to appropriate default useEffect(() => { setPostCategory(postSiteId === 1 ? 'Uncategorized' : 'AV Integration'); setScopeWarning(null); }, [postSiteId]); // AV Beat scope check const checkAVScope = useCallback(async () => { if (postSiteId !== 2 || !postTitle.trim()) return; setCheckingScope(true); try { const res = await fetch('/api/contributor/scope-check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: postTitle, content: postContent, siteId: 2 }), }); const data = await res.json(); if (data.scope === 'broadcast' && data.confidence > 0.75) { setScopeWarning('This content may be better suited for AV Beat. AV Beat covers professional AV integration. Your post has been flagged for editorial review.'); } else { setScopeWarning(null); } } catch { setScopeWarning(null); } finally { setCheckingScope(false); } }, [postSiteId, postTitle, postContent]); const handleSubmitPost = async (e: React.FormEvent) => { e.preventDefault(); if (!postTitle.trim()) { showNotification('error', 'Post title is required.'); return; } setSubmittingPost(true); try { const res = await fetch('/api/contributor/posts', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: postTitle, content: postContent, excerpt: postExcerpt, category: postCategory, scheduled_at: postScheduledAt || null, site_id: postSiteId, scope_flagged: !!scopeWarning, }), }); const data = await res.json(); if (!res.ok) { if (data.blocked) { showNotification('error', 'This post could not be submitted. Please contact the editorial team at editor@avbeat.com for assistance.'); } else { showNotification('error', data.error || 'Failed to submit post.'); } } else { const statusLabel = STATUS_CONFIG[data.status]?.label || data.status; showNotification('success', `Post submitted! Status: ${statusLabel}`); setPostTitle(''); setPostContent(''); setPostExcerpt(''); setPostCategory('Uncategorized'); setPostScheduledAt(''); setPostSiteId(1); setScopeWarning(null); setView('my-posts'); fetchData(); } } catch { showNotification('error', 'Failed to submit post. Please try again.'); } finally { setSubmittingPost(false); } }; const handleSaveProfile = async (e: React.FormEvent) => { e.preventDefault(); if (!user) return; setSavingProfile(true); try { const { error } = await supabase .from('user_profiles') .update({ full_name: editName, bio: editBio, updated_at: new Date().toISOString() }) .eq('id', user.id); if (error) { showNotification('error', error.message); } else { showNotification('success', 'Profile updated successfully.'); fetchData(); } } catch { showNotification('error', 'Failed to save profile.'); } finally { setSavingProfile(false); } }; const formatDate = (d: string | null) => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; const formatDateTime = (d: string | null) => d ? new Date(d).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '—'; const getMinDateTime = () => { const n = new Date(); n.setMinutes(n.getMinutes() + 5); return n.toISOString().slice(0, 16); }; const pendingCount = articles.filter(a => ['pending','scheduled_pending_review'].includes(a.status)).length; const publishedCount = articles.filter(a => a.status === 'published').length; const scheduledCount = articles.filter(a => ['scheduled','scheduled_pending_review'].includes(a.status)).length; const currentCategories = postSiteId === 1 ? BB_CATEGORIES : AV_CATEGORIES; if (loading || loadingData) { return (
); } return (
{notification && (
{notification.message}
)}
{/* Sidebar */} {/* Main Content */}
{/* Overview */} {view === 'overview' && (

Welcome back, {profile?.full_name?.split(' ')[0] || 'Contributor'}

Here's a summary of your submissions across both publications.

{[ { label: 'Total Posts', value: articles.length, color: 'text-white' }, { label: 'Pending Review', value: pendingCount, color: 'text-yellow-400' }, { label: 'Published', value: publishedCount, color: 'text-green-400' }, ].map(stat => (
{stat.value}
{stat.label}
))}
{scheduledCount > 0 && (
📅 You have {scheduledCount} scheduled post{scheduledCount > 1 ? 's' : ''} awaiting publication
)}
)} {/* New Post */} {view === 'new-post' && (

New Post

{profile?.role === 'contributor' ? 'Your post will be submitted for editorial review before publishing.' : 'Your post will be published directly.'}

{/* Publish To — site selector */}
Publish To
{[ { id: 1, name: 'AV Beat', badge: 'BB', color: '#0ea5e9' }, { id: 2, name: 'AV Beat', badge: 'AV', color: '#f97316' }, ].map(site => ( ))}
{/* Scope warning */} {scopeWarning && (

{scopeWarning}

)}
setPostTitle(e.target.value)} onBlur={() => { if (postSiteId === 2) checkAVScope(); }} placeholder="Enter post title..." className="w-full bg-[#111] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#444] focus:outline-none focus:border-[#c9a84c] text-sm" required />