initial commit: rocket.new export of broadcastbeat
This commit is contained in:
574
src/app/contributor/page.tsx
Normal file
574
src/app/contributor/page.tsx
Normal file
@@ -0,0 +1,574 @@
|
||||
'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';
|
||||
|
||||
// ─── 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: 'Broadcast 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 Broadcast 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@broadcastbeat.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">
|
||||
{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 min-h-screen">
|
||||
{/* 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-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<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-[#1a1a1a] 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: 'Broadcast 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-[#1a1a1a] 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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user