- New AvBeatLogo inline-SVG component (rounded-square emblem with forward-leaning AV monogram + horizontal beam detail) at src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon). - Header.tsx now uses the responsive AV BEAT logo lockup in the top-left, links to /, full lockup on desktop (emblem+wordmark+tagline), emblem+wordmark on tablet, emblem-only on mobile. - Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip); the news ticker / glow chrome does not match the new aesthetic and the forum row was the explicit removal target. - Tailwind theme + globals.css now expose the AV BEAT 2026 palette as semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8), --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border, --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the new tokens so any inline-styled component re-themes for free. - Site-wide hex sweep migrates 2,769 hardcoded color literals across 144 files from the old dark-broadcast palette to the new tokens (orange -> blue, dark-brown -> white surface / navy text, cream -> navy). - Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text' so the dark glow chrome no longer leaks through the new light theme.
579 lines
28 KiB
TypeScript
579 lines
28 KiB
TypeScript
'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<string, { label: string; color: string; bg: string }> = {
|
||
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 (
|
||
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-xs font-bold ${cfg.bgClass} ${cfg.textClass}`}>
|
||
{cfg.badge}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
export default function ContributorDashboard() {
|
||
const { user, loading } = useAuth();
|
||
const router = useRouter();
|
||
const supabase = createClient();
|
||
|
||
const [view, setView] = useState<DashboardView>('overview');
|
||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||
const [articles, setArticles] = useState<Article[]>([]);
|
||
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<string | null>(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 (
|
||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||
<div className="w-8 h-8 border-2 border-[#c9a84c] border-t-transparent rounded-full animate-spin" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||
<Header />
|
||
{notification && (
|
||
<div className={`fixed top-4 right-4 z-50 max-w-sm px-5 py-3 rounded-lg text-sm font-medium shadow-lg ${
|
||
notification.type === 'success' ? 'bg-green-500/20 border border-green-500/40 text-green-300' : 'bg-red-500/20 border border-red-500/40 text-red-300'
|
||
}`}>
|
||
{notification.message}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex">
|
||
{/* Sidebar */}
|
||
<aside className="w-60 bg-[#111] border-r border-[#222] flex flex-col">
|
||
<div className="p-5 border-b border-[#222]">
|
||
<div className="text-xs font-bold text-[#555] uppercase tracking-widest mb-1">Relevant Media Properties</div>
|
||
<div className="text-white font-bold text-sm">Publisher Dashboard</div>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-[#0ea5e9]/10 text-[#0ea5e9] font-bold">BB</span>
|
||
<span className="text-[#444] text-xs">·</span>
|
||
<span className="text-xs px-1.5 py-0.5 rounded bg-[#f97316]/10 text-[#f97316] font-bold">AV</span>
|
||
</div>
|
||
</div>
|
||
|
||
<nav className="flex-1 p-3 space-y-1">
|
||
{[
|
||
{ id: 'overview', label: 'Overview', icon: '⊞' },
|
||
{ id: 'new-post', label: 'New Post', icon: '+' },
|
||
{ id: 'my-posts', label: 'My Posts', icon: '≡' },
|
||
{ id: 'profile', label: 'My Profile', icon: '◯' },
|
||
].map(item => (
|
||
<button
|
||
key={item.id}
|
||
onClick={() => setView(item.id as DashboardView)}
|
||
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${
|
||
view === item.id
|
||
? 'bg-[#c9a84c]/10 text-[#c9a84c] border border-[#c9a84c]/20'
|
||
: 'text-[#888] hover:text-white hover:bg-[#FFFFFF]'
|
||
}`}
|
||
>
|
||
<span className="text-base w-5 text-center">{item.icon}</span>
|
||
{item.label}
|
||
</button>
|
||
))}
|
||
</nav>
|
||
|
||
<div className="p-4 border-t border-[#222]">
|
||
<div className="text-xs text-[#555] mb-1">Signed in as</div>
|
||
<div className="text-sm text-[#888] truncate">{profile?.full_name || user?.email}</div>
|
||
<div className="mt-1">
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-400/10 text-blue-400 capitalize">
|
||
{profile?.role || 'contributor'}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* Main Content */}
|
||
<main className="flex-1 overflow-auto">
|
||
|
||
{/* Overview */}
|
||
{view === 'overview' && (
|
||
<div className="p-8">
|
||
<h1 className="text-2xl font-bold text-white mb-1">
|
||
Welcome back, {profile?.full_name?.split(' ')[0] || 'Contributor'}
|
||
</h1>
|
||
<p className="text-[#555] text-sm mb-8">Here's a summary of your submissions across both publications.</p>
|
||
|
||
<div className="grid grid-cols-3 gap-4 mb-8">
|
||
{[
|
||
{ 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 => (
|
||
<div key={stat.label} className="bg-[#111] border border-[#222] rounded-xl p-5">
|
||
<div className={`text-3xl font-bold ${stat.color}`}>{stat.value}</div>
|
||
<div className="text-[#555] text-sm mt-1">{stat.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{scheduledCount > 0 && (
|
||
<div className="bg-blue-500/5 border border-blue-500/20 rounded-xl p-4 mb-6">
|
||
<div className="text-blue-400 font-medium text-sm">
|
||
📅 You have {scheduledCount} scheduled post{scheduledCount > 1 ? 's' : ''} awaiting publication
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex gap-3">
|
||
<button onClick={() => setView('new-post')} className="px-5 py-2.5 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors">
|
||
+ New Post
|
||
</button>
|
||
<button onClick={() => setView('my-posts')} className="px-5 py-2.5 bg-[#FFFFFF] border border-[#333] text-white rounded-lg text-sm hover:bg-[#222] transition-colors">
|
||
View My Posts
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* New Post */}
|
||
{view === 'new-post' && (
|
||
<div className="p-8 max-w-3xl">
|
||
<h1 className="text-2xl font-bold text-white mb-1">New Post</h1>
|
||
<p className="text-[#555] text-sm mb-8">
|
||
{profile?.role === 'contributor' ? 'Your post will be submitted for editorial review before publishing.' : 'Your post will be published directly.'}
|
||
</p>
|
||
|
||
<form onSubmit={handleSubmitPost} className="space-y-5">
|
||
|
||
{/* Publish To — site selector */}
|
||
<div className="bg-[#111] border border-[#333] rounded-xl p-5">
|
||
<div className="text-sm font-semibold text-white mb-3">Publish To</div>
|
||
<div className="flex gap-3">
|
||
{[
|
||
{ id: 1, name: 'AV Beat', badge: 'BB', color: '#0ea5e9' },
|
||
{ id: 2, name: 'AV Beat', badge: 'AV', color: '#f97316' },
|
||
].map(site => (
|
||
<button
|
||
key={site.id}
|
||
type="button"
|
||
onClick={() => setPostSiteId(site.id)}
|
||
className={`flex-1 flex items-center gap-3 px-4 py-3 rounded-lg border-2 transition-all ${
|
||
postSiteId === site.id
|
||
? 'border-current bg-current/5' :'border-[#333] hover:border-[#444]'
|
||
}`}
|
||
style={{ color: postSiteId === site.id ? site.color : '#888' }}
|
||
>
|
||
<span className="w-5 h-5 rounded-full border-2 flex items-center justify-center flex-shrink-0" style={{ borderColor: 'currentColor' }}>
|
||
{postSiteId === site.id && <span className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: site.color }} />}
|
||
</span>
|
||
<div className="text-left">
|
||
<div className="text-sm font-semibold">{site.name}</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Scope warning */}
|
||
{scopeWarning && (
|
||
<div className="bg-yellow-500/10 border border-yellow-500/30 rounded-lg px-4 py-3 flex items-start gap-3">
|
||
<svg className="w-4 h-4 text-yellow-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||
</svg>
|
||
<p className="text-yellow-300 text-sm">{scopeWarning}</p>
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Title *</label>
|
||
<input
|
||
type="text"
|
||
value={postTitle}
|
||
onChange={e => 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
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Excerpt</label>
|
||
<textarea value={postExcerpt} onChange={e => setPostExcerpt(e.target.value)} placeholder="Brief summary..." rows={2}
|
||
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 resize-none" />
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Content</label>
|
||
<textarea value={postContent} onChange={e => setPostContent(e.target.value)} placeholder="Write your post content here..." rows={12}
|
||
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 resize-none font-mono" />
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Category</label>
|
||
<select value={postCategory} onChange={e => setPostCategory(e.target.value)}
|
||
className="w-full bg-[#111] border border-[#333] rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#c9a84c] text-sm">
|
||
{currentCategories.map(cat => (
|
||
<option key={cat} value={cat}>{cat}</option>
|
||
))}
|
||
</select>
|
||
<p className="text-[#444] text-xs mt-1">Note: The Featured category is reserved for editorial use only.</p>
|
||
</div>
|
||
|
||
{/* Scheduling */}
|
||
<div className="bg-[#111] border border-[#333] rounded-xl p-5">
|
||
<div className="flex items-center gap-2 mb-3">
|
||
<span className="text-[#c9a84c]">📅</span>
|
||
<span className="text-sm font-medium text-white">Schedule Publication</span>
|
||
<span className="text-xs text-[#555]">(optional)</span>
|
||
</div>
|
||
<input type="datetime-local" value={postScheduledAt} onChange={e => setPostScheduledAt(e.target.value)} min={getMinDateTime()}
|
||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#c9a84c] text-sm" />
|
||
{postScheduledAt && (
|
||
<p className="text-[#555] text-xs mt-2">
|
||
{profile?.role === 'contributor' ? '⚠️ Scheduled posts still require editorial approval before going live.' : '✓ Post will automatically publish at the scheduled time.'}
|
||
</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex gap-3 pt-2">
|
||
<button type="submit" disabled={submittingPost || checkingScope}
|
||
className="px-6 py-2.5 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
|
||
{submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'}
|
||
</button>
|
||
<button type="button" onClick={() => setView('overview')}
|
||
className="px-5 py-2.5 bg-[#FFFFFF] border border-[#333] text-[#888] rounded-lg text-sm hover:text-white hover:bg-[#222] transition-colors">
|
||
Cancel
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
)}
|
||
|
||
{/* My Posts */}
|
||
{view === 'my-posts' && (
|
||
<div className="p-8">
|
||
<div className="flex items-center justify-between mb-6">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white">My Posts</h1>
|
||
<p className="text-[#555] text-sm mt-1">{articles.length} total submission{articles.length !== 1 ? 's' : ''} across both publications</p>
|
||
</div>
|
||
<button onClick={() => setView('new-post')} className="px-4 py-2 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors">
|
||
+ New Post
|
||
</button>
|
||
</div>
|
||
|
||
{articles.length === 0 ? (
|
||
<div className="bg-[#111] border border-[#222] rounded-xl p-12 text-center">
|
||
<div className="text-4xl mb-3">📝</div>
|
||
<div className="text-[#555] text-sm">No posts yet. Create your first post!</div>
|
||
<button onClick={() => setView('new-post')} className="mt-4 px-5 py-2 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors">
|
||
Write a Post
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="space-y-3">
|
||
{articles.map(article => {
|
||
const statusCfg = STATUS_CONFIG[article.status] || { label: article.status, color: 'text-gray-400', bg: 'bg-gray-400/10' };
|
||
return (
|
||
<div key={article.id} className="bg-[#111] border border-[#222] rounded-xl p-5 hover:border-[#333] transition-colors">
|
||
<div className="flex items-start justify-between gap-4">
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<SiteBadge siteId={article.site_id || 1} />
|
||
<h3 className="text-white font-medium text-sm truncate">{article.title}</h3>
|
||
</div>
|
||
<div className="flex items-center gap-3 mt-1 flex-wrap">
|
||
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${statusCfg.color} ${statusCfg.bg}`}>
|
||
{statusCfg.label}
|
||
</span>
|
||
{article.category && <span className="text-xs text-[#555]">{article.category}</span>}
|
||
{article.scope_flagged && (
|
||
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-400/10 text-yellow-400">Scope Review</span>
|
||
)}
|
||
</div>
|
||
{article.editor_note && article.status === 'rejected' && (
|
||
<div className="mt-2 text-xs text-red-400 bg-red-400/5 border border-red-400/20 rounded px-3 py-2">
|
||
Editor note: {article.editor_note}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="text-right text-xs text-[#444] shrink-0">
|
||
{article.status === 'published' && article.published_at ? (
|
||
<div>Published {formatDate(article.published_at)}</div>
|
||
) : article.scheduled_at ? (
|
||
<div className="text-blue-400">Scheduled: {formatDateTime(article.scheduled_at)}</div>
|
||
) : article.submitted_for_review_at ? (
|
||
<div>Submitted {formatDate(article.submitted_for_review_at)}</div>
|
||
) : (
|
||
<div>Created {formatDate(article.created_at)}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Profile */}
|
||
{view === 'profile' && (
|
||
<div className="p-8 max-w-xl">
|
||
<h1 className="text-2xl font-bold text-white mb-1">My Profile</h1>
|
||
<p className="text-[#555] text-sm mb-8">Update your display name and bio.</p>
|
||
|
||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Display Name</label>
|
||
<input type="text" value={editName} onChange={e => setEditName(e.target.value)}
|
||
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" />
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Email</label>
|
||
<input type="email" value={user?.email || ''} disabled
|
||
className="w-full bg-[#0a0a0a] border border-[#222] rounded-lg px-4 py-3 text-[#555] text-sm cursor-not-allowed" />
|
||
<p className="text-[#444] text-xs mt-1">Email cannot be changed here.</p>
|
||
</div>
|
||
|
||
{profile?.wp_username && (
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">WordPress Username</label>
|
||
<input type="text" value={profile.wp_username} disabled
|
||
className="w-full bg-[#0a0a0a] border border-[#222] rounded-lg px-4 py-3 text-[#555] text-sm cursor-not-allowed" />
|
||
</div>
|
||
)}
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-[#888] mb-2">Bio</label>
|
||
<textarea value={editBio} onChange={e => setEditBio(e.target.value)} placeholder="Tell readers about yourself..." rows={4}
|
||
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 resize-none" />
|
||
</div>
|
||
|
||
<button type="submit" disabled={savingProfile}
|
||
className="px-6 py-2.5 bg-[#c9a84c] text-black font-semibold rounded-lg text-sm hover:bg-[#b8973b] transition-colors disabled:opacity-50">
|
||
{savingProfile ? 'Saving...' : 'Save Profile'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
)}
|
||
</main>
|
||
</div>
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|