Files
avbeat-com/src/app/contributor/page.tsx
Local Administrator 8042024c4a feat(brand): AV BEAT 2026 rebrand — blue/navy/white identity
- 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.
2026-06-03 12:07:18 +00:00

579 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}