AV: precise recolor to spec — warm dark backgrounds (#1c1815/#231d18/#26201a/#15110e) + gold #F0A623 / orange #D85A30 accents + warm text (#FBEFE0/#C9BBA8/#8a7e6e). 138 component files + 36 token swaps.

This commit is contained in:
2026-06-02 23:46:00 +00:00
parent 01c1abf99d
commit 2876ac6ecd
141 changed files with 2020 additions and 2020 deletions

View File

@@ -51,7 +51,7 @@ export default function ContactPage() {
<div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]"> <div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main> <main>
<header className="mb-8 border-b border-[#252525] pb-6"> <header className="mb-8 border-b border-[#3a322b] pb-6">
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2"> <div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
AV Beat AV Beat
</div> </div>
@@ -63,7 +63,7 @@ export default function ContactPage() {
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4"> <ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
{CONTACTS.map((c) => ( {CONTACTS.map((c) => (
<li key={c.title} className="rounded border border-[#252525] bg-[#0b0f17] p-5 hover:border-[var(--color-text-info,#60a5fa)]/60 transition-colors"> <li key={c.title} className="rounded border border-[#3a322b] bg-[#0b0f17] p-5 hover:border-[var(--color-text-info,#60a5fa)]/60 transition-colors">
<div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[var(--color-text-info,#60a5fa)] mb-2"> <div className="text-[10px] font-mono uppercase tracking-[0.18em] text-[var(--color-text-info,#60a5fa)] mb-2">
{c.title} {c.title}
</div> </div>

View File

@@ -14,7 +14,7 @@ export default function AboutPage() {
<div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]"> <div className="max-w-container mx-auto px-4 py-12 text-[#e5e7eb]">
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10"> <div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
<main> <main>
<header className="mb-8 border-b border-[#252525] pb-6"> <header className="mb-8 border-b border-[#3a322b] pb-6">
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2"> <div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
AV Beat AV Beat
</div> </div>
@@ -79,7 +79,7 @@ export default function AboutPage() {
function Stat({ label, value }: { label: string; value: string }) { function Stat({ label, value }: { label: string; value: string }) {
return ( return (
<div className="rounded border border-[#252525] bg-[#0b0f17] p-4"> <div className="rounded border border-[#3a322b] bg-[#0b0f17] p-4">
<div className="font-serif text-3xl text-white">{value}</div> <div className="font-serif text-3xl text-white">{value}</div>
<div className="mt-1 text-[10px] font-mono uppercase tracking-[0.16em] text-[#6b7280]"> <div className="mt-1 text-[10px] font-mono uppercase tracking-[0.16em] text-[#6b7280]">
{label} {label}

View File

@@ -101,7 +101,7 @@ export default async function TeamPage() {
<div className="absolute inset-0 opacity-[0.04] bg-[radial-gradient(ellipse_at_top_right,#d60701,transparent_60%)]" aria-hidden /> <div className="absolute inset-0 opacity-[0.04] bg-[radial-gradient(ellipse_at_top_right,#d60701,transparent_60%)]" aria-hidden />
<div className="relative px-6 py-10 sm:px-10 sm:py-14"> <div className="relative px-6 py-10 sm:px-10 sm:py-14">
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-[#ff5a55] mb-3"> <div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-[#ff5a55] mb-3">
<span className="w-6 h-px bg-[#d60701]" /> <span className="w-6 h-px bg-[#D85A30]" />
AV Beat &middot; editorial AV Beat &middot; editorial
</div> </div>
<h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight"> <h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
@@ -129,10 +129,10 @@ export default async function TeamPage() {
return ( return (
<li <li
key={p.slug} key={p.slug}
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#d60701]/50 hover:shadow-[0_0_0_1px_rgba(214,7,1,0.15)] transition-all" className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#D85A30]/50 hover:shadow-[0_0_0_1px_rgba(214,7,1,0.15)] transition-all"
> >
{/* Top accent */} {/* Top accent */}
<div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#d60701]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden /> <div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#D85A30]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden />
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
{p.avatar_url ? ( {p.avatar_url ? (
@@ -142,7 +142,7 @@ export default async function TeamPage() {
alt={p.name} alt={p.name}
width={96} width={96}
height={96} height={96}
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#d60701]/40 transition-colors object-cover" className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#D85A30]/40 transition-colors object-cover"
/> />
</a> </a>
) : ( ) : (
@@ -155,7 +155,7 @@ export default async function TeamPage() {
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3> <h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3>
</a> </a>
{(p.beat || p.slug === "ryan-salazar") && ( {(p.beat || p.slug === "ryan-salazar") && (
<div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#d60701]/10 border border-[#d60701]/25 text-[10px] font-mono uppercase tracking-[0.12em] text-[#ff6b66]"> <div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#D85A30]/10 border border-[#D85A30]/25 text-[10px] font-mono uppercase tracking-[0.12em] text-[#ff6b66]">
{displayBeat(p)} {displayBeat(p)}
</div> </div>
)} )}
@@ -209,7 +209,7 @@ export default async function TeamPage() {
{ops.map((p) => ( {ops.map((p) => (
<li <li
key={p.id} key={p.id}
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#d60701]/40 transition-colors" className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#D85A30]/40 transition-colors"
> >
{p.avatar_url ? ( {p.avatar_url ? (
<img <img
@@ -226,7 +226,7 @@ export default async function TeamPage() {
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="font-serif text-lg font-bold leading-tight">{p.display_name}</h3> <h3 className="font-serif text-lg font-bold leading-tight">{p.display_name}</h3>
<div className="inline-block mt-1 px-2 py-0.5 rounded-sm bg-[#1a1208] border border-[#ffb800]/30 text-[10px] font-mono uppercase tracking-[0.12em] text-[#7ba0ff]"> <div className="inline-block mt-1 px-2 py-0.5 rounded-sm bg-[#231d18] border border-[#F0A623]/30 text-[10px] font-mono uppercase tracking-[0.12em] text-[#7ba0ff]">
{p.role} {p.role}
</div> </div>
<div className="text-xs text-[#aab1bd] mt-3 space-y-1"> <div className="text-xs text-[#aab1bd] mt-3 space-y-1">

View File

@@ -284,7 +284,7 @@ export default function AccountPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#111111]"> <div className="min-h-screen bg-[#1c1815]">
<Header /> <Header />
<div className="max-w-container mx-auto px-4 py-12"> <div className="max-w-container mx-auto px-4 py-12">
<div className="max-w-3xl mx-auto space-y-4"> <div className="max-w-3xl mx-auto space-y-4">
@@ -298,21 +298,21 @@ export default function AccountPage() {
} }
return ( return (
<div className="min-h-screen bg-[#111111]"> <div className="min-h-screen bg-[#1c1815]">
<Header /> <Header />
{/* Toast */} {/* Toast */}
{toast && ( {toast && (
<div <div
className={`fixed bottom-6 left-1/2 -translate-x-1/2 z-50 border font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2 animate-fade-in ${ className={`fixed bottom-6 left-1/2 -translate-x-1/2 z-50 border font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2 animate-fade-in ${
toast.type === "success" ?"bg-[#1a1208] border-[#ffb800] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#d60701] text-[#e0e0e0]" toast.type === "success" ?"bg-[#231d18] border-[#F0A623] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#D85A30] text-[#e0e0e0]"
}`}> }`}>
{toast.type === "success" ? ( {toast.type === "success" ? (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ffb800" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#F0A623" strokeWidth="2.5" aria-hidden="true">
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
) : ( ) : (
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#d60701" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#D85A30" strokeWidth="2.5" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" /> <circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
</svg> </svg>
)} )}
@@ -321,10 +321,10 @@ export default function AccountPage() {
)} )}
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]"> <div className="bg-[#1c1815] border-b border-[#1e1e1e]">
<div className="max-w-container mx-auto px-4 py-2.5"> <div className="max-w-container mx-auto px-4 py-2.5">
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]"> <nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#ffb800] transition-colors">Home</Link> <Link href="/home-page" className="hover:text-[#F0A623] transition-colors">Home</Link>
<span aria-hidden="true">/</span> <span aria-hidden="true">/</span>
<span className="text-[#888]">Account</span> <span className="text-[#888]">Account</span>
</nav> </nav>
@@ -336,25 +336,25 @@ export default function AccountPage() {
{/* Page Header */} {/* Page Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h1 className="font-heading text-2xl font-bold text-[#e8e8e8]">Account Settings</h1> <h1 className="font-heading text-2xl font-bold text-[#FBEFE0]">Account Settings</h1>
<p className="font-body text-sm text-[#666] mt-1">{user?.email}</p> <p className="font-body text-sm text-[#666] mt-1">{user?.email}</p>
</div> </div>
<button <button
onClick={handleSignOut} onClick={handleSignOut}
className="font-body text-xs font-bold uppercase tracking-wide text-[#888] hover:text-[#d60701] border border-[#2a2a2a] hover:border-[#d60701] px-4 py-2 rounded-sm transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#888] hover:text-[#D85A30] border border-[#3a322b] hover:border-[#D85A30] px-4 py-2 rounded-sm transition-colors">
Sign Out Sign Out
</button> </button>
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex border-b border-[#2a2a2a] mb-6 overflow-x-auto scrollbar-none"> <div className="flex border-b border-[#3a322b] mb-6 overflow-x-auto scrollbar-none">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`font-body text-sm font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${ className={`font-body text-sm font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors whitespace-nowrap ${
activeTab === tab.id activeTab === tab.id
? "border-[#ffb800] text-[#ffb800]" ? "border-[#F0A623] text-[#F0A623]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`}> }`}>
{tab.label} {tab.label}
@@ -365,8 +365,8 @@ export default function AccountPage() {
{/* Profile Tab */} {/* Profile Tab */}
{activeTab === "profile" && ( {activeTab === "profile" && (
<form onSubmit={handleSaveProfile} className="space-y-5"> <form onSubmit={handleSaveProfile} className="space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
Public Profile Public Profile
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -379,7 +379,7 @@ export default function AccountPage() {
value={fullName} value={fullName}
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
placeholder="Your full name" placeholder="Your full name"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444]" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -391,7 +391,7 @@ export default function AccountPage() {
onChange={(e) => setBio(e.target.value)} onChange={(e) => setBio(e.target.value)}
placeholder="Tell us a bit about yourself..." placeholder="Tell us a bit about yourself..."
rows={3} rows={3}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444] resize-none" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444] resize-none"
/> />
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
@@ -404,7 +404,7 @@ export default function AccountPage() {
value={website} value={website}
onChange={(e) => setWebsite(e.target.value)} onChange={(e) => setWebsite(e.target.value)}
placeholder="https://yoursite.com" placeholder="https://yoursite.com"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444]" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -416,15 +416,15 @@ export default function AccountPage() {
value={location} value={location}
onChange={(e) => setLocation(e.target.value)} onChange={(e) => setLocation(e.target.value)}
placeholder="City, Country" placeholder="City, Country"
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444]" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#3a322b]">
Account Info Account Info
</h2> </h2>
<div> <div>
@@ -435,7 +435,7 @@ export default function AccountPage() {
type="email" type="email"
value={user?.email || ""} value={user?.email || ""}
disabled disabled
className="w-full bg-[#0d0d0d] border border-[#222] text-[#555] font-body text-sm px-3 py-2.5 rounded-sm cursor-not-allowed" className="w-full bg-[#1c1815] border border-[#222] text-[#555] font-body text-sm px-3 py-2.5 rounded-sm cursor-not-allowed"
/> />
<p className="font-body text-xs text-[#555] mt-1.5">Email cannot be changed here.</p> <p className="font-body text-xs text-[#555] mt-1.5">Email cannot be changed here.</p>
</div> </div>
@@ -445,7 +445,7 @@ export default function AccountPage() {
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="bg-[#ffb800] hover:bg-[#d99700] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]"> className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
{saving ? "Saving..." : "Save Profile"} {saving ? "Saving..." : "Save Profile"}
</button> </button>
</div> </div>
@@ -456,8 +456,8 @@ export default function AccountPage() {
{activeTab === "security" && ( {activeTab === "security" && (
<div className="space-y-5"> <div className="space-y-5">
<form onSubmit={handleChangePassword}> <form onSubmit={handleChangePassword}>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
Change Password Change Password
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -471,7 +471,7 @@ export default function AccountPage() {
onChange={(e) => setNewPassword(e.target.value)} onChange={(e) => setNewPassword(e.target.value)}
placeholder="Enter new password" placeholder="Enter new password"
minLength={6} minLength={6}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444]" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -484,7 +484,7 @@ export default function AccountPage() {
onChange={(e) => setConfirmPassword(e.target.value)} onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirm new password" placeholder="Confirm new password"
minLength={6} minLength={6}
className="w-full bg-[#111111] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444]" className="w-full bg-[#1c1815] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444]"
/> />
</div> </div>
</div> </div>
@@ -492,7 +492,7 @@ export default function AccountPage() {
<button <button
type="submit" type="submit"
disabled={changingPassword || !newPassword || !confirmPassword} disabled={changingPassword || !newPassword || !confirmPassword}
className="bg-[#ffb800] hover:bg-[#d99700] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]"> className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-6 py-2.5 rounded-sm transition-colors disabled:opacity-60 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
{changingPassword ? "Updating..." : "Update Password"} {changingPassword ? "Updating..." : "Update Password"}
</button> </button>
</div> </div>
@@ -506,7 +506,7 @@ export default function AccountPage() {
</p> </p>
<button <button
onClick={handleSignOut} onClick={handleSignOut}
className="font-body text-xs font-bold uppercase tracking-wide text-[#d60701] border border-[#d60701]/40 hover:border-[#d60701] hover:bg-[#d60701]/10 px-5 py-2 rounded-sm transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#D85A30] border border-[#D85A30]/40 hover:border-[#D85A30] hover:bg-[#D85A30]/10 px-5 py-2 rounded-sm transition-colors">
Sign Out of All Sessions Sign Out of All Sessions
</button> </button>
</div> </div>
@@ -520,12 +520,12 @@ export default function AccountPage() {
<h2 className="font-heading text-base font-bold text-[#e0e0e0]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0]">
Saved Articles Saved Articles
{bookmarks.length > 0 && ( {bookmarks.length > 0 && (
<span className="ml-2 font-body text-xs text-[#666] bg-[#1a1a1a] border border-[#2a2a2a] px-2 py-0.5 rounded-sm"> <span className="ml-2 font-body text-xs text-[#666] bg-[#231d18] border border-[#3a322b] px-2 py-0.5 rounded-sm">
{bookmarks.length} {bookmarks.length}
</span> </span>
)} )}
</h2> </h2>
<Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> <Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Full List Full List
</Link> </Link>
</div> </div>
@@ -533,37 +533,37 @@ export default function AccountPage() {
{bookmarksLoading ? ( {bookmarksLoading ? (
<div className="space-y-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse"> <div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-20 h-14 bg-[#252525] rounded-sm flex-shrink-0" /> <div className="w-20 h-14 bg-[#3a322b] rounded-sm flex-shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 bg-[#252525] rounded w-1/4" /> <div className="h-3 bg-[#3a322b] rounded w-1/4" />
<div className="h-4 bg-[#252525] rounded w-3/4" /> <div className="h-4 bg-[#3a322b] rounded w-3/4" />
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : bookmarks.length === 0 ? ( ) : bookmarks.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" /> <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No saved articles yet.</p> <p className="font-body text-sm text-[#666]">No saved articles yet.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{bookmarks.map((item) => ( {bookmarks.map((item) => (
<div key={item.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm hover:border-[#3a3a3a] transition-colors group"> <div key={item.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm hover:border-[#3a3a3a] transition-colors group">
<div className="flex gap-3 p-4"> <div className="flex gap-3 p-4">
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm"> <Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
{item.article_image ? ( {item.article_image ? (
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" /> <AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
) : ( ) : (
<div className="w-full h-full bg-[#252525] flex items-center justify-center"> <div className="w-full h-full bg-[#3a322b] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -572,10 +572,10 @@ export default function AccountPage() {
</Link> </Link>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{item.article_category && ( {item.article_category && (
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#ffb800]">{item.article_category}</span> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#F0A623]">{item.article_category}</span>
)} )}
<Link href={`/articles/${item.article_slug}`}> <Link href={`/articles/${item.article_slug}`}>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#ffb800] transition-colors line-clamp-2 leading-snug mt-0.5"> <h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug mt-0.5">
{item.article_title} {item.article_title}
</h3> </h3>
</Link> </Link>
@@ -605,7 +605,7 @@ export default function AccountPage() {
<h2 className="font-heading text-base font-bold text-[#e0e0e0]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0]">
Reading History Reading History
{history.length > 0 && ( {history.length > 0 && (
<span className="ml-2 font-body text-xs text-[#666] bg-[#1a1a1a] border border-[#2a2a2a] px-2 py-0.5 rounded-sm"> <span className="ml-2 font-body text-xs text-[#666] bg-[#231d18] border border-[#3a322b] px-2 py-0.5 rounded-sm">
{history.length} {history.length}
</span> </span>
)} )}
@@ -622,37 +622,37 @@ export default function AccountPage() {
{historyLoading ? ( {historyLoading ? (
<div className="space-y-3"> <div className="space-y-3">
{[1, 2, 3].map((i) => ( {[1, 2, 3].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse"> <div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-20 h-14 bg-[#252525] rounded-sm flex-shrink-0" /> <div className="w-20 h-14 bg-[#3a322b] rounded-sm flex-shrink-0" />
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
<div className="h-3 bg-[#252525] rounded w-1/4" /> <div className="h-3 bg-[#3a322b] rounded w-1/4" />
<div className="h-4 bg-[#252525] rounded w-3/4" /> <div className="h-4 bg-[#3a322b] rounded w-3/4" />
</div> </div>
</div> </div>
</div> </div>
))} ))}
</div> </div>
) : history.length === 0 ? ( ) : history.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /> <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No reading history yet.</p> <p className="font-body text-sm text-[#666]">No reading history yet.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{history.map((item) => ( {history.map((item) => (
<div key={item.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm hover:border-[#3a3a3a] transition-colors group"> <div key={item.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm hover:border-[#3a3a3a] transition-colors group">
<div className="flex gap-3 p-4"> <div className="flex gap-3 p-4">
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm"> <Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
{item.article_image ? ( {item.article_image ? (
<AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" /> <AppImage src={item.article_image} alt={item.article_image_alt || item.article_title} width={80} height={56} className="w-full h-full object-cover" />
) : ( ) : (
<div className="w-full h-full bg-[#252525] flex items-center justify-center"> <div className="w-full h-full bg-[#3a322b] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -661,10 +661,10 @@ export default function AccountPage() {
</Link> </Link>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{item.article_category && ( {item.article_category && (
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#ffb800]">{item.article_category}</span> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#F0A623]">{item.article_category}</span>
)} )}
<Link href={`/articles/${item.article_slug}`}> <Link href={`/articles/${item.article_slug}`}>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#ffb800] transition-colors line-clamp-2 leading-snug mt-0.5"> <h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug mt-0.5">
{item.article_title} {item.article_title}
</h3> </h3>
</Link> </Link>
@@ -693,17 +693,17 @@ export default function AccountPage() {
{topicsLoading ? ( {topicsLoading ? (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{[1, 2, 3, 4, 5, 6].map((i) => ( {[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 animate-pulse h-16" /> <div key={i} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 animate-pulse h-16" />
))} ))}
</div> </div>
) : topics.length === 0 ? ( ) : topics.length === 0 ? (
<div className="text-center py-12 bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm"> <div className="text-center py-12 bg-[#231d18] border border-[#3a322b] rounded-sm">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#444" strokeWidth="1.5" className="mx-auto mb-3" aria-hidden="true">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /><line x1="7" y1="7" x2="7.01" y2="7" /> <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" /><line x1="7" y1="7" x2="7.01" y2="7" />
</svg> </svg>
<p className="font-body text-sm text-[#666]">No topic preferences tracked yet.</p> <p className="font-body text-sm text-[#666]">No topic preferences tracked yet.</p>
<p className="font-body text-xs text-[#555] mt-1">Read articles to build your interest profile.</p> <p className="font-body text-xs text-[#555] mt-1">Read articles to build your interest profile.</p>
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> <Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Browse News Browse News
</Link> </Link>
</div> </div>
@@ -714,18 +714,18 @@ export default function AccountPage() {
const maxCount = topics[0]?.view_count || 1; const maxCount = topics[0]?.view_count || 1;
const pct = Math.round((t.view_count / maxCount) * 100); const pct = Math.round((t.view_count / maxCount) * 100);
return ( return (
<div key={t.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 hover:border-[#3a3a3a] transition-colors"> <div key={t.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 hover:border-[#3a3a3a] transition-colors">
<div className="flex items-start justify-between mb-2"> <div className="flex items-start justify-between mb-2">
<span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight"> <span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight">
{t.topic} {t.topic}
</span> </span>
<span className="font-body text-[10px] text-[#ffb800] font-bold ml-2 flex-shrink-0"> <span className="font-body text-[10px] text-[#F0A623] font-bold ml-2 flex-shrink-0">
{t.view_count}× {t.view_count}×
</span> </span>
</div> </div>
<div className="w-full bg-[#252525] rounded-full h-1"> <div className="w-full bg-[#3a322b] rounded-full h-1">
<div <div
className="bg-[#ffb800] h-1 rounded-full transition-all" className="bg-[#F0A623] h-1 rounded-full transition-all"
style={{ width: `${pct}%` }} style={{ width: `${pct}%` }}
/> />
</div> </div>
@@ -744,13 +744,13 @@ export default function AccountPage() {
{/* Preferences Tab */} {/* Preferences Tab */}
{activeTab === "preferences" && ( {activeTab === "preferences" && (
<div className="space-y-5"> <div className="space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
Forum Reputation Forum Reputation
</h2> </h2>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="flex flex-col items-center justify-center bg-[#111] border border-[#2a2a2a] rounded-sm px-6 py-4 min-w-[100px]"> <div className="flex flex-col items-center justify-center bg-[#111] border border-[#3a322b] rounded-sm px-6 py-4 min-w-[100px]">
<span className="font-heading text-3xl font-bold text-[#ffb800]"> <span className="font-heading text-3xl font-bold text-[#F0A623]">
{profile?.reputation ?? 0} {profile?.reputation ?? 0}
</span> </span>
<span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span> <span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span>
@@ -759,15 +759,15 @@ export default function AccountPage() {
<p className="font-body text-sm text-[#aaa] leading-relaxed"> <p className="font-body text-sm text-[#aaa] leading-relaxed">
Earn reputation by posting helpful threads and replies in the community forum. Earn reputation by posting helpful threads and replies in the community forum.
</p> </p>
<Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> <Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Go to Forum Go to Forum
</Link> </Link>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]"> <h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#3a322b]">
Reading Preferences Reading Preferences
</h2> </h2>
<div className="space-y-4"> <div className="space-y-4">
@@ -778,7 +778,7 @@ export default function AccountPage() {
</div> </div>
<Link <Link
href="/home-page" href="/home-page"
className="font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
Manage Manage
</Link> </Link>
</div> </div>
@@ -789,7 +789,7 @@ export default function AccountPage() {
</div> </div>
<Link <Link
href="/reading-list" href="/reading-list"
className="font-body text-xs font-bold uppercase tracking-wide text-[#ffb800] hover:text-[#d99700] transition-colors"> className="font-body text-xs font-bold uppercase tracking-wide text-[#F0A623] hover:text-[#BA7517] transition-colors">
View List View List
</Link> </Link>
</div> </div>
@@ -808,8 +808,8 @@ export default function AccountPage() {
onClick={() => handleToggleDigest(!weeklyDigest)} onClick={() => handleToggleDigest(!weeklyDigest)}
aria-pressed={weeklyDigest} aria-pressed={weeklyDigest}
aria-label="Toggle weekly digest email" aria-label="Toggle weekly digest email"
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] disabled:opacity-50 ${ className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] disabled:opacity-50 ${
weeklyDigest ? "bg-[#ffb800] border-[#ffb800]" : "bg-[#2a2a2a] border-[#333]" weeklyDigest ? "bg-[#F0A623] border-[#F0A623]" : "bg-[#3a322b] border-[#333]"
}`}> }`}>
<span <span
className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out mt-0.5 ${ className={`pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow transition duration-200 ease-in-out mt-0.5 ${

View File

@@ -91,7 +91,7 @@ export default function ClientsPage() {
setForm({ company_name: '', contact_name: '', contact_email: '', contact_phone: '', billing_address: '', notes: '' }); setForm({ company_name: '', contact_name: '', contact_email: '', contact_phone: '', billing_address: '', notes: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -102,7 +102,7 @@ export default function ClientsPage() {
<h1 className="text-xl font-bold">Clients</h1> <h1 className="text-xl font-bold">Clients</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -116,14 +116,14 @@ export default function ClientsPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{['active','inactive','all'].map(s => ( {['active','inactive','all'].map(s => (
<button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s}</button> <button key={s} onClick={() => setFilter(s)} className={`px-3 py-1 rounded text-xs capitalize ${filter === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888] hover:text-white'}`}>{s}</button>
))} ))}
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" /> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search clients…" className="ml-auto px-3 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div> </div>
{/* New Client Form */} {/* New Client Form */}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Client</h3> <h3 className="text-sm font-semibold">New Client</h3>
{errors._form && ( {errors._form && (
@@ -148,7 +148,7 @@ export default function ClientsPage() {
type={f.key === 'contact_email' ? 'email' : 'text'} type={f.key === 'contact_email' ? 'email' : 'text'}
value={(form as any)[f.key]} value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)} onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors[f.key] ? 'border-red-500/60' : 'border-[#3a322b]'}`}
/> />
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>} {errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div> </div>
@@ -156,26 +156,26 @@ export default function ClientsPage() {
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Billing Address</label> <label className="text-xs text-[#888] block mb-1">Billing Address</label>
<input value={form.billing_address} onChange={e => handleChange('billing_address', e.target.value)} <input value={form.billing_address} onChange={e => handleChange('billing_address', e.target.value)}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} <textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2}
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Company','Contact','Email','Active Orders','Total Revenue','Status',''].map(h => ( {['Company','Contact','Email','Active Orders','Total Revenue','Status',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -187,17 +187,17 @@ export default function ClientsPage() {
) : clients.length === 0 ? ( ) : clients.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr>
) : clients.map(c => ( ) : clients.map(c => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white font-medium">{c.company_name}</td> <td className="px-4 py-2 text-white font-medium">{c.company_name}</td>
<td className="px-4 py-2 text-[#ccc]">{c.contact_name}</td> <td className="px-4 py-2 text-[#ccc]">{c.contact_name}</td>
<td className="px-4 py-2 text-[#888]">{c.contact_email}</td> <td className="px-4 py-2 text-[#888]">{c.contact_email}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td> <td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.total_revenue ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(c.total_revenue ?? 0)}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{c.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${c.status === 'active' ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{c.status}</span>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link> <Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -41,7 +41,7 @@ export default function CommissionsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -53,10 +53,10 @@ export default function CommissionsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={year} onChange={e => setYear(Number(e.target.value))} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
{[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)} {[2024, 2025, 2026, 2027].map(y => <option key={y} value={y}>{y}</option>)}
</select> </select>
<select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStaff} onChange={e => setFilterStaff(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Staff</option> <option value="">All Staff</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
@@ -68,7 +68,7 @@ export default function CommissionsPage() {
{summary.map((s: any) => { {summary.map((s: any) => {
const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100; const pct = s.next_threshold_cents ? Math.min(100, Math.round((s.ytd_sales_cents / s.next_threshold_cents) * 100)) : 100;
return ( return (
<div key={s.staff_id} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-2"> <div key={s.staff_id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm font-semibold text-white">{s.full_name}</p> <p className="text-sm font-semibold text-white">{s.full_name}</p>
<span className="text-xs text-[#888]">{s.current_tier}</span> <span className="text-xs text-[#888]">{s.current_tier}</span>
@@ -83,8 +83,8 @@ export default function CommissionsPage() {
<div className="flex justify-between text-xs text-[#555] mb-1"> <div className="flex justify-between text-xs text-[#555] mb-1">
<span>Tier progress</span><span>{pct}%</span> <span>Tier progress</span><span>{pct}%</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden"> <div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div className="h-full bg-[#ffb800] rounded-full transition-all" style={{ width: `${pct}%` }} /> <div className="h-full bg-[#F0A623] rounded-full transition-all" style={{ width: `${pct}%` }} />
</div> </div>
</div> </div>
)} )}
@@ -94,10 +94,10 @@ export default function CommissionsPage() {
</div> </div>
{/* Detail table */} {/* Detail table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => ( {['Staff','Invoice','Sale Amount','Rate','Commission','YTD at Time','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -109,9 +109,9 @@ export default function CommissionsPage() {
) : commissions.length === 0 ? ( ) : commissions.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr> <tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr>
) : commissions.map((c: any) => ( ) : commissions.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ffb800] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td> <td className="px-3 py-2 text-[#F0A623] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{fmt(c.sale_amount_cents ?? 0)}</td>
<td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td> <td className="px-3 py-2 text-[#888] text-xs">{((c.commission_rate ?? 0) * 100).toFixed(1)}%</td>
<td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td> <td className="px-3 py-2 text-green-400 text-xs">{fmt(c.commission_amount_cents ?? 0)}</td>

View File

@@ -43,7 +43,7 @@ export default function DocumentsPage() {
const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i); const years = Array.from({ length: 5 }, (_, i) => new Date().getFullYear() - i);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -54,70 +54,70 @@ export default function DocumentsPage() {
<h1 className="text-xl font-bold">Document Vault</h1> <h1 className="text-xl font-bold">Document Vault</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
</div> </div>
{/* Year tabs */} {/* Year tabs */}
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
<button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>All Years</button> <button onClick={() => setFilterYear('')} className={`px-3 py-1 rounded text-xs ${!filterYear ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>All Years</button>
{years.map(y => ( {years.map(y => (
<button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{y}</button> <button key={y} onClick={() => setFilterYear(String(y))} className={`px-3 py-1 rounded text-xs ${filterYear === String(y) ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>{y}</button>
))} ))}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option> <option value="">All Types</option>
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Upload Document</h3> <h3 className="text-sm font-semibold">Upload Document</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.document_type} onChange={e => setForm(p => ({ ...p, document_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Title *</label> <label className="text-xs text-[#888] block mb-1">Title *</label>
<input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={form.title} onChange={e => setForm(p => ({ ...p, title: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
{form.document_type === 'sales_agreement' && ( {form.document_type === 'sales_agreement' && (
<> <>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Person *</label> <label className="text-xs text-[#888] block mb-1">Person *</label>
<input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={form.person} onChange={e => setForm(p => ({ ...p, person: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Year *</label> <label className="text-xs text-[#888] block mb-1">Year *</label>
<input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input type="number" required value={form.year} onChange={e => setForm(p => ({ ...p, year: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</> </>
)} )}
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">File URL</label> <label className="text-xs text-[#888] block mb-1">File URL</label>
<input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.file_url} onChange={e => setForm(p => ({ ...p, file_url: e.target.value }))} placeholder="https://…" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Type','Title','Person','Year','Date',''].map(h => ( {['Type','Title','Person','Year','Date',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -129,14 +129,14 @@ export default function DocumentsPage() {
) : docs.length === 0 ? ( ) : docs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr> <tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr>
) : docs.map((d: any) => ( ) : docs.map((d: any) => (
<tr key={d.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={d.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-[#888] text-xs capitalize">{d.document_type?.replace(/_/g, ' ')}</td> <td className="px-4 py-2 text-[#888] text-xs capitalize">{d.document_type?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-white text-xs">{d.title}</td> <td className="px-4 py-2 text-white text-xs">{d.title}</td>
<td className="px-4 py-2 text-[#ccc] text-xs">{d.person ?? '—'}</td> <td className="px-4 py-2 text-[#ccc] text-xs">{d.person ?? '—'}</td>
<td className="px-4 py-2 text-[#888] text-xs">{d.year ?? '—'}</td> <td className="px-4 py-2 text-[#888] text-xs">{d.year ?? '—'}</td>
<td className="px-4 py-2 text-[#555] text-xs">{d.uploaded_at ? new Date(d.uploaded_at).toLocaleDateString() : '—'}</td> <td className="px-4 py-2 text-[#555] text-xs">{d.uploaded_at ? new Date(d.uploaded_at).toLocaleDateString() : '—'}</td>
<td className="px-4 py-2 flex gap-2"> <td className="px-4 py-2 flex gap-2">
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Download</a>} {d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#F0A623] hover:underline">Download</a>}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -84,7 +84,7 @@ export default function ExpensesPage() {
setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' }); setForm({ vendor: '', amount: '', category: 'other', description: '', expense_date: new Date().toISOString().split('T')[0], receipt_source: 'manual' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -95,7 +95,7 @@ export default function ExpensesPage() {
<h1 className="text-xl font-bold">Expenses</h1> <h1 className="text-xl font-bold">Expenses</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -107,11 +107,11 @@ export default function ExpensesPage() {
)} )}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterCat} onChange={e => setFilterCat(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Categories</option> <option value="">All Categories</option>
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)} {CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select> </select>
<select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterReconciled} onChange={e => setFilterReconciled(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All</option> <option value="">All</option>
<option value="true">Reconciled</option> <option value="true">Reconciled</option>
<option value="false">Unreconciled</option> <option value="false">Unreconciled</option>
@@ -119,7 +119,7 @@ export default function ExpensesPage() {
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Log Expense</h3> <h3 className="text-sm font-semibold">Log Expense</h3>
{errors._form && ( {errors._form && (
@@ -132,40 +132,40 @@ export default function ExpensesPage() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Vendor</label> <label className="text-xs text-[#888] block mb-1">Vendor</label>
<input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input type="text" value={form.vendor} onChange={e => handleChange('vendor', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Amount ($) <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Amount ($) <span className="text-red-400">*</span></label>
<input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.amount ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.01" value={form.amount} onChange={e => handleChange('amount', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.amount ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.amount && <p className="mt-1 text-xs text-red-400">{errors.amount}</p>} {errors.amount && <p className="mt-1 text-xs text-red-400">{errors.amount}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Date <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Date <span className="text-red-400">*</span></label>
<input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.expense_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={form.expense_date} onChange={e => handleChange('expense_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.expense_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>} {errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Category</label> <label className="text-xs text-[#888] block mb-1">Category</label>
<select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.category} onChange={e => handleChange('category', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)} {CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Description</label> <label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.description} onChange={e => handleChange('description', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => ( {['Date','Vendor','Amount','Category','Source','Reconciled','Description'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -177,7 +177,7 @@ export default function ExpensesPage() {
) : expenses.length === 0 ? ( ) : expenses.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr>
) : expenses.map((ex: any) => ( ) : expenses.map((ex: any) => (
<tr key={ex.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={ex.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{ex.expense_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{ex.expense_date}</td>
<td className="px-3 py-2 text-white text-xs">{ex.vendor ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{ex.vendor ?? '—'}</td>
<td className="px-3 py-2 text-red-400 text-xs">{ex.amount_cents ? fmt(ex.amount_cents) : '—'}</td> <td className="px-3 py-2 text-red-400 text-xs">{ex.amount_cents ? fmt(ex.amount_cents) : '—'}</td>

View File

@@ -10,7 +10,7 @@ const STATUS_COLORS: Record<string, string> = {
paid: 'bg-green-400/10 text-green-400', paid: 'bg-green-400/10 text-green-400',
pending: 'bg-yellow-400/10 text-yellow-400', pending: 'bg-yellow-400/10 text-yellow-400',
overdue: 'bg-red-400/10 text-red-400', overdue: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]', cancelled: 'bg-[#231d18] text-[#555]',
}; };
function InvoicesContent() { function InvoicesContent() {
@@ -83,7 +83,7 @@ function InvoicesContent() {
setPayForm({ method: 'check', paid_date: new Date().toISOString().split('T')[0], wire_reference: '' }); setPayForm({ method: 'check', paid_date: new Date().toISOString().split('T')[0], wire_reference: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -94,7 +94,7 @@ function InvoicesContent() {
<h1 className="text-xl font-bold">Invoices</h1> <h1 className="text-xl font-bold">Invoices</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link> <Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -108,14 +108,14 @@ function InvoicesContent() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{['','pending','paid','overdue','cancelled'].map(s => ( {['','pending','paid','overdue','cancelled'].map(s => (
<button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s || 'All'}</button> <button key={s} onClick={() => setFilterStatus(s)} className={`px-3 py-1 rounded text-xs capitalize ${filterStatus === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
))} ))}
</div> </div>
{/* Mark Paid Modal */} {/* Mark Paid Modal */}
{markPaidId && ( {markPaidId && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-5 w-80 space-y-3"> <form onSubmit={handleMarkPaid} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold">Mark Invoice as Paid</h3> <h3 className="text-sm font-semibold">Mark Invoice as Paid</h3>
{payErrors._form && ( {payErrors._form && (
@@ -127,35 +127,35 @@ function InvoicesContent() {
<div> <div>
<label className="text-xs text-[#888] block mb-1">Payment Method</label> <label className="text-xs text-[#888] block mb-1">Payment Method</label>
<select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={payForm.method} onChange={e => handlePayChange('method', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['stripe','wire','check','other'].map(m => <option key={m} value={m}>{m}</option>)} {['stripe','wire','check','other'].map(m => <option key={m} value={m}>{m}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Paid Date <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Paid Date <span className="text-red-400">*</span></label>
<input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={payForm.paid_date} onChange={e => handlePayChange('paid_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.paid_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{payErrors.paid_date && <p className="mt-1 text-xs text-red-400">{payErrors.paid_date}</p>} {payErrors.paid_date && <p className="mt-1 text-xs text-red-400">{payErrors.paid_date}</p>}
</div> </div>
{payForm.method === 'wire' && ( {payForm.method === 'wire' && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Wire Reference <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Wire Reference <span className="text-red-400">*</span></label>
<input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input value={payForm.wire_reference} onChange={e => handlePayChange('wire_reference', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${payErrors.wire_reference ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{payErrors.wire_reference && <p className="mt-1 text-xs text-red-400">{payErrors.wire_reference}</p>} {payErrors.wire_reference && <p className="mt-1 text-xs text-red-400">{payErrors.wire_reference}</p>}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-green-600 rounded text-xs font-medium hover:bg-green-500 disabled:opacity-50">{saving ? 'Saving…' : 'Mark Paid'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-green-600 rounded text-xs font-medium hover:bg-green-500 disabled:opacity-50">{saving ? 'Saving…' : 'Mark Paid'}</button>
<button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCloseModal} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Invoice#','Client','Site','Order#','Amount','Status','Due Date','Salesperson',''].map(h => ( {['Invoice#','Client','Site','Order#','Amount','Status','Due Date','Salesperson',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -167,14 +167,14 @@ function InvoicesContent() {
) : invoices.length === 0 ? ( ) : invoices.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr>
) : invoices.map((inv: any) => ( ) : invoices.map((inv: any) => (
<tr key={inv.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={inv.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{inv.invoice_number}</td> <td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{inv.invoice_number}</td>
<td className="px-3 py-2 text-white text-xs">{inv.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{inv.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{inv.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{inv.site}</td>
<td className="px-3 py-2 text-[#888] text-xs font-mono">{inv.rmp_orders?.internal_order_number ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs font-mono">{inv.rmp_orders?.internal_order_number ?? '—'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td> <td className="px-3 py-2 text-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#1a1a1a] text-[#555]'}`}>{inv.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${STATUS_COLORS[inv.status] ?? 'bg-[#231d18] text-[#555]'}`}>{inv.status}</span>
</td> </td>
<td className="px-3 py-2 text-[#888] text-xs">{inv.due_date ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{inv.due_date ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{inv.rmp_sales_staff?.full_name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{inv.rmp_sales_staff?.full_name ?? '—'}</td>
@@ -195,7 +195,7 @@ function InvoicesContent() {
export default function InvoicesPage() { export default function InvoicesPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<InvoicesContent /> <InvoicesContent />
</Suspense> </Suspense>
); );

View File

@@ -104,7 +104,7 @@ function OrdersContent() {
setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' }); setForm({ client_id: '', site: '', product_id: '', ad_unit: '', description: '', start_date: '', end_date: '', total_amount_cents: '', currency: 'USD', invoicing_schedule: 'single', next_invoice_date: '', staff_id: '', po_number: '', notes: '' });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -115,7 +115,7 @@ function OrdersContent() {
<h1 className="text-xl font-bold">Orders</h1> <h1 className="text-xl font-bold">Orders</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -128,20 +128,20 @@ function OrdersContent() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap items-center"> <div className="flex gap-2 flex-wrap items-center">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)} {STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" /> <input value={filterClient} onChange={e => setFilterClient(e.target.value)} placeholder="Filter by client…" className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div> </div>
{/* New Order Form */} {/* New Order Form */}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Order</h3> <h3 className="text-sm font-semibold">New Order</h3>
{errors._form && ( {errors._form && (
@@ -154,7 +154,7 @@ function OrdersContent() {
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Client <span className="text-red-400">*</span></label>
<select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}> <select value={form.client_id} onChange={e => handleChange('client_id', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.client_id ? 'border-red-500/60' : 'border-[#3a322b]'}`}>
<option value="">Select client</option> <option value="">Select client</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)} {clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select> </select>
@@ -162,7 +162,7 @@ function OrdersContent() {
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label> <label className="text-xs text-[#888] block mb-1">Site <span className="text-red-400">*</span></label>
<select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}> <select value={form.site} onChange={e => handleChange('site', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.site ? 'border-red-500/60' : 'border-[#3a322b]'}`}>
<option value="">Select site</option> <option value="">Select site</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
@@ -170,67 +170,67 @@ function OrdersContent() {
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Product</label> <label className="text-xs text-[#888] block mb-1">Product</label>
<select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]"> <select value={form.product_id} onChange={e => handleChange('product_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
<option value="">Select product</option> <option value="">Select product</option>
{products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)} {products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Ad Unit</label> <label className="text-xs text-[#888] block mb-1">Ad Unit</label>
<input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.ad_unit} onChange={e => handleChange('ad_unit', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Start Date</label> <label className="text-xs text-[#888] block mb-1">Start Date</label>
<input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input type="date" value={form.start_date} onChange={e => handleChange('start_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">End Date</label> <label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="date" value={form.end_date} onChange={e => handleChange('end_date', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.end_date ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>} {errors.end_date && <p className="mt-1 text-xs text-red-400">{errors.end_date}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Total Amount ($)</label> <label className="text-xs text-[#888] block mb-1">Total Amount ($)</label>
<input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.01" value={form.total_amount_cents} onChange={e => handleChange('total_amount_cents', e.target.value)} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>} {errors.total_amount_cents && <p className="mt-1 text-xs text-red-400">{errors.total_amount_cents}</p>}
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label> <label className="text-xs text-[#888] block mb-1">Invoicing Schedule</label>
<select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]"> <select value={form.invoicing_schedule} onChange={e => handleChange('invoicing_schedule', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
{SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)} {SCHEDULES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Next Invoice Date</label> <label className="text-xs text-[#888] block mb-1">Next Invoice Date</label>
<input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input type="date" value={form.next_invoice_date} onChange={e => handleChange('next_invoice_date', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label> <label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]"> <select value={form.staff_id} onChange={e => handleChange('staff_id', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]">
<option value="">None</option> <option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">PO Number</label> <label className="text-xs text-[#888] block mb-1">PO Number</label>
<input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.po_number} onChange={e => handleChange('po_number', e.target.value)} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <textarea value={form.notes} onChange={e => handleChange('notes', e.target.value)} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => ( {['Client','PO#','Order#','Site','Product','Ad Unit','Dates','Amount','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -242,20 +242,20 @@ function OrdersContent() {
) : orders.length === 0 ? ( ) : orders.length === 0 ? (
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr> <tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr>
) : orders.map((o: any) => ( ) : orders.map((o: any) => (
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={o.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{o.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{o.po_number ?? '—'}</td>
<td className="px-3 py-2 text-[#ffb800] text-xs font-mono">{o.internal_order_number}</td> <td className="px-3 py-2 text-[#F0A623] text-xs font-mono">{o.internal_order_number}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{o.site}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{o.rmp_products?.name ?? o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{o.ad_unit ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td> <td className="px-3 py-2 text-[#888] text-xs whitespace-nowrap">{o.start_date} {o.end_date ?? '∞'}</td>
<td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td> <td className="px-3 py-2 text-green-400 text-xs">{o.total_amount_cents ? fmt(o.total_amount_cents) : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{o.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${o.status === 'active' ? 'bg-green-400/10 text-green-400' : o.status === 'cancelled' ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>{o.status}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link> <Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}
@@ -269,7 +269,7 @@ function OrdersContent() {
export default function OrdersPage() { export default function OrdersPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<OrdersContent /> <OrdersContent />
</Suspense> </Suspense>
); );

View File

@@ -44,7 +44,7 @@ export default function AccountingDashboard() {
if (loading || loadingData) return ( if (loading || loadingData) return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
if (!user) return null; if (!user) return null;
@@ -59,18 +59,18 @@ export default function AccountingDashboard() {
<p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p> <p className="text-[#888] text-sm mt-1">Relevant Media Properties, LLC</p>
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link> <Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link> <Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link> <Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link> <Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link> <Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
</div> </div>
</div> </div>
{/* Stat Cards */} {/* Stat Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
{STAT_CARDS.map(c => ( {STAT_CARDS.map(c => (
<Link key={c.key} href={c.href} className="bg-[#111] border border-[#252525] rounded-lg p-4 hover:border-[#333] transition-colors"> <Link key={c.key} href={c.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 hover:border-[#333] transition-colors">
<p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p> <p className={`text-lg font-bold ${c.color}`}>{fmt(stats[c.key] ?? 0)}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</Link> </Link>
@@ -78,13 +78,13 @@ export default function AccountingDashboard() {
</div> </div>
{/* Site Revenue Table */} {/* Site Revenue Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold text-white">Revenue by Site</h2> <h2 className="text-sm font-semibold text-white">Revenue by Site</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => ( {['Site','Revenue MTD','Revenue YTD','Outstanding','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -94,7 +94,7 @@ export default function AccountingDashboard() {
{siteRevenue.length === 0 ? ( {siteRevenue.length === 0 ? (
<tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr> <tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : siteRevenue.map((row: any) => ( ) : siteRevenue.map((row: any) => (
<tr key={row.site} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={row.site} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td> <td className="px-4 py-2 text-white font-medium capitalize">{row.site}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(row.revenue_mtd ?? 0)}</td>
<td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(row.revenue_ytd ?? 0)}</td>
@@ -107,13 +107,13 @@ export default function AccountingDashboard() {
</div> </div>
{/* Top Clients */} {/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2> <h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Client','Revenue YTD','Active Orders'].map(h => ( {['Client','Revenue YTD','Active Orders'].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -123,7 +123,7 @@ export default function AccountingDashboard() {
{topClients.length === 0 ? ( {topClients.length === 0 ? (
<tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr> <tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
) : topClients.map((c: any) => ( ) : topClients.map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={c.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white">{c.company_name}</td> <td className="px-4 py-2 text-white">{c.company_name}</td>
<td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400">{fmt(c.revenue_ytd ?? 0)}</td>
<td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td> <td className="px-4 py-2 text-[#ccc]">{c.active_orders ?? 0}</td>
@@ -146,7 +146,7 @@ export default function AccountingDashboard() {
{ label: 'Reports', href: '/admin/accounting/reports' }, { label: 'Reports', href: '/admin/accounting/reports' },
{ label: 'Documents', href: '/admin/accounting/documents' }, { label: 'Documents', href: '/admin/accounting/documents' },
].map(l => ( ].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#ffb800] transition-colors"> <Link key={l.href} href={l.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#F0A623] transition-colors">
{l.label} {l.label}
</Link> </Link>
))} ))}

View File

@@ -48,7 +48,7 @@ export default function ReconciliationPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -59,7 +59,7 @@ export default function ReconciliationPage() {
<h1 className="text-xl font-bold">Bank Reconciliation</h1> <h1 className="text-xl font-bold">Bank Reconciliation</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<label className={`px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}> <label className={`px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
{uploading ? 'Uploading…' : 'Upload Bank CSV'} {uploading ? 'Uploading…' : 'Upload Bank CSV'}
<input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} /> <input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} />
</label> </label>
@@ -78,10 +78,10 @@ export default function ReconciliationPage() {
</div> </div>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Date','Description','Amount','Type','Matched To','Reconciled','Action'].map(h => ( {['Date','Description','Amount','Type','Matched To','Reconciled','Action'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -93,7 +93,7 @@ export default function ReconciliationPage() {
) : transactions.length === 0 ? ( ) : transactions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No transactions. Upload a CSV to begin.</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No transactions. Upload a CSV to begin.</td></tr>
) : transactions.map((tx: any) => ( ) : transactions.map((tx: any) => (
<tr key={tx.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={tx.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{tx.transaction_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{tx.transaction_date}</td>
<td className="px-3 py-2 text-white text-xs max-w-xs truncate">{tx.description}</td> <td className="px-3 py-2 text-white text-xs max-w-xs truncate">{tx.description}</td>
<td className={`px-3 py-2 text-xs font-medium ${tx.type === 'credit' ? 'text-green-400' : 'text-red-400'}`}>{fmt(Math.abs(tx.amount_cents ?? 0))}</td> <td className={`px-3 py-2 text-xs font-medium ${tx.type === 'credit' ? 'text-green-400' : 'text-red-400'}`}>{fmt(Math.abs(tx.amount_cents ?? 0))}</td>

View File

@@ -28,7 +28,7 @@ export default function ReportsPage() {
useEffect(() => { if (user) loadReport(); }, [user]); useEffect(() => { if (user) loadReport(); }, [user]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -40,41 +40,41 @@ export default function ReportsPage() {
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<span className="text-[#555] text-xs">to</span> <span className="text-[#555] text-xs">to</span>
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button> <button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
</div> </div>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div> <div className="flex items-center justify-center py-20"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>
) : report ? ( ) : report ? (
<div className="space-y-4"> <div className="space-y-4">
{/* P&L Summary */} {/* P&L Summary */}
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Revenue</p> <p className="text-xs text-[#555]">Total Revenue</p>
<p className="text-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p> <p className="text-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Total Expenses</p> <p className="text-xs text-[#555]">Total Expenses</p>
<p className="text-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p> <p className="text-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className="text-xs text-[#555]">Net Profit</p> <p className="text-xs text-[#555]">Net Profit</p>
<p className={`text-2xl font-bold mt-1 ${(report.net_profit_cents ?? 0) >= 0 ? 'text-blue-400' : 'text-red-400'}`}>{fmt(report.net_profit_cents ?? 0)}</p> <p className={`text-2xl font-bold mt-1 ${(report.net_profit_cents ?? 0) >= 0 ? 'text-blue-400' : 'text-red-400'}`}>{fmt(report.net_profit_cents ?? 0)}</p>
</div> </div>
</div> </div>
{/* Revenue by Site */} {/* Revenue by Site */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div> <div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Revenue by Site</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#231d18]">{['Site','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.by_site ?? []).map((r: any) => ( {(report.by_site ?? []).map((r: any) => (
<tr key={r.site} className="border-b border-[#1a1a1a]"> <tr key={r.site} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs capitalize">{r.site}</td> <td className="px-4 py-2 text-white text-xs capitalize">{r.site}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td>
</tr> </tr>
@@ -84,13 +84,13 @@ export default function ReportsPage() {
</div> </div>
{/* Top Clients */} {/* Top Clients */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div> <div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Top 10 Clients</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#231d18]">{['Client','Revenue'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.top_clients ?? []).map((c: any) => ( {(report.top_clients ?? []).map((c: any) => (
<tr key={c.id} className="border-b border-[#1a1a1a]"> <tr key={c.id} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{c.company_name}</td> <td className="px-4 py-2 text-white text-xs">{c.company_name}</td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td>
</tr> </tr>
@@ -100,13 +100,13 @@ export default function ReportsPage() {
</div> </div>
{/* Expenses by Category */} {/* Expenses by Category */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div> <div className="px-4 py-3 border-b border-[#3a322b]"><h2 className="text-sm font-semibold">Expenses by Category</h2></div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#231d18]">{['Category','Amount'].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{(report.by_category ?? []).map((c: any) => ( {(report.by_category ?? []).map((c: any) => (
<tr key={c.category} className="border-b border-[#1a1a1a]"> <tr key={c.category} className="border-b border-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{c.category?.replace(/_/g, ' ')}</td> <td className="px-4 py-2 text-white text-xs">{c.category?.replace(/_/g, ' ')}</td>
<td className="px-4 py-2 text-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td> <td className="px-4 py-2 text-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td>
</tr> </tr>

View File

@@ -97,7 +97,7 @@ export default function StaffPage() {
if (errors[errKey]) setErrors(p => { const n = { ...p }; delete n[errKey]; return n; }); if (errors[errKey]) setErrors(p => { const n = { ...p }; delete n[errKey]; return n; });
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -108,7 +108,7 @@ export default function StaffPage() {
<h1 className="text-xl font-bold">Sales Staff</h1> <h1 className="text-xl font-bold">Sales Staff</h1>
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link> <Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]"> Accounting</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
</div> </div>
{/* Success Toast */} {/* Success Toast */}
@@ -120,7 +120,7 @@ export default function StaffPage() {
)} )}
{showForm && ( {showForm && (
<form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} noValidate className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Salesperson</h3> <h3 className="text-sm font-semibold">New Salesperson</h3>
{errors._form && ( {errors._form && (
@@ -140,7 +140,7 @@ export default function StaffPage() {
type={f.key === 'email' ? 'email' : 'text'} type={f.key === 'email' ? 'email' : 'text'}
value={(form as any)[f.key]} value={(form as any)[f.key]}
onChange={e => handleChange(f.key, e.target.value)} onChange={e => handleChange(f.key, e.target.value)}
className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#ffb800] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`} className={`w-full px-2 py-1.5 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none focus:border-[#F0A623] ${errors[f.key] ? 'border-red-500/60' : 'border-[#3a322b]'}`}
/> />
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>} {errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
</div> </div>
@@ -149,34 +149,34 @@ export default function StaffPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs text-[#888]">Commission Tiers ({new Date().getFullYear()})</label> <label className="text-xs text-[#888]">Commission Tiers ({new Date().getFullYear()})</label>
<button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#ffb800] hover:underline">+ Add Tier</button> <button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#F0A623] hover:underline">+ Add Tier</button>
</div> </div>
{tiers.map((tier, i) => ( {tiers.map((tier, i) => (
<div key={i} className="flex gap-2 mb-2 items-start"> <div key={i} className="flex gap-2 mb-2 items-start">
<span className="text-xs text-[#555] w-12 pt-5">Tier {tier.tier_order}</span> <span className="text-xs text-[#555] w-12 pt-5">Tier {tier.tier_order}</span>
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Threshold ($, blank = no cap)</label> <label className="text-xs text-[#555] block mb-0.5">Threshold ($, blank = no cap)</label>
<input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="number" step="0.01" value={tier.threshold_cents} onChange={e => handleTierChange(i, 'threshold_cents', e.target.value)} placeholder="100000" className="w-full px-2 py-1 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<label className="text-xs text-[#555] block mb-0.5">Rate (%) <span className="text-red-400">*</span></label> <label className="text-xs text-[#555] block mb-0.5">Rate (%) <span className="text-red-400">*</span></label>
<input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#252525]'}`} /> <input type="number" step="0.1" value={tier.rate} onChange={e => handleTierChange(i, 'rate', e.target.value)} placeholder="25" className={`w-full px-2 py-1 bg-[#0a0a0a] border rounded text-xs text-white focus:outline-none ${errors[`tier_rate_${i}`] ? 'border-red-500/60' : 'border-[#3a322b]'}`} />
{errors[`tier_rate_${i}`] && <p className="mt-1 text-xs text-red-400">{errors[`tier_rate_${i}`]}</p>} {errors[`tier_rate_${i}`] && <p className="mt-1 text-xs text-red-400">{errors[`tier_rate_${i}`]}</p>}
</div> </div>
</div> </div>
))} ))}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
<button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={handleCancel} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Name','Email','Status','Commission YTD','Outstanding',''].map(h => ( {['Name','Email','Status','Commission YTD','Outstanding',''].map(h => (
<th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-4 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -188,16 +188,16 @@ export default function StaffPage() {
) : staff.length === 0 ? ( ) : staff.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr> <tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr>
) : staff.map((s: any) => ( ) : staff.map((s: any) => (
<tr key={s.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${s.status === 'terminated' ? 'opacity-50' : ''}`}> <tr key={s.id} className={`border-b border-[#231d18] hover:bg-[#231d18] ${s.status === 'terminated' ? 'opacity-50' : ''}`}>
<td className="px-4 py-2 text-white">{s.full_name}</td> <td className="px-4 py-2 text-white">{s.full_name}</td>
<td className="px-4 py-2 text-[#888] text-xs">{s.email}</td> <td className="px-4 py-2 text-[#888] text-xs">{s.email}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${s.status === 'active' ? 'bg-green-400/10 text-green-400' : s.status === 'terminated' ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>{s.status}</span>
</td> </td>
<td className="px-4 py-2 text-green-400 text-xs">{fmt(s.commission_ytd ?? 0)}</td> <td className="px-4 py-2 text-green-400 text-xs">{fmt(s.commission_ytd ?? 0)}</td>
<td className="px-4 py-2 text-yellow-400 text-xs">{fmt(s.commission_outstanding ?? 0)}</td> <td className="px-4 py-2 text-yellow-400 text-xs">{fmt(s.commission_outstanding ?? 0)}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link> <Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400', scheduled: 'bg-blue-400/10 text-blue-400',
paused: 'bg-gray-600/20 text-gray-400', paused: 'bg-gray-600/20 text-gray-400',
expired: 'bg-red-400/10 text-red-400', expired: 'bg-red-400/10 text-red-400',
cancelled: 'bg-[#1a1a1a] text-[#555]', cancelled: 'bg-[#231d18] text-[#555]',
}; };
export default function AdOpsIOsPage() { export default function AdOpsIOsPage() {
@@ -73,7 +73,7 @@ export default function AdOpsIOsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
const selectedProduct = products.find((p: any) => p.id === form.product_id); const selectedProduct = products.find((p: any) => p.id === form.product_id);
@@ -88,63 +88,63 @@ export default function AdOpsIOsPage() {
<h1 className="text-xl font-bold">Insertion Orders</h1> <h1 className="text-xl font-bold">Insertion Orders</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)} {['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)} {['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New IO Extension</h3> <h3 className="text-sm font-semibold">New IO Extension</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Existing IO ID *</label> <label className="text-xs text-[#888] block mb-1">Existing IO ID *</label>
<input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={form.existing_io_id} onChange={e => setForm(p => ({ ...p, existing_io_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Client</label> <label className="text-xs text-[#888] block mb-1">Client</label>
<select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.client_id} onChange={e => setForm(p => ({ ...p, client_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)} {clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Salesperson</label> <label className="text-xs text-[#888] block mb-1">Salesperson</label>
<select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.staff_id} onChange={e => setForm(p => ({ ...p, staff_id: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">None</option> <option value="">None</option>
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)} {staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Start Date *</label> <label className="text-xs text-[#888] block mb-1">Start Date *</label>
<input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" required value={form.start_date} onChange={e => setForm(p => ({ ...p, start_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">End Date</label> <label className="text-xs text-[#888] block mb-1">End Date</label>
<input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={form.end_date} onChange={e => setForm(p => ({ ...p, end_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Flight Status</label> <label className="text-xs text-[#888] block mb-1">Flight Status</label>
<select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={form.flight_status} onChange={e => setForm(p => ({ ...p, flight_status: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['scheduled','active','paused'].map(s => <option key={s} value={s}>{s}</option>)} {['scheduled','active','paused'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Banner Image URL</label> <label className="text-xs text-[#888] block mb-1">Banner Image URL</label>
<input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.banner_image_url} onChange={e => setForm(p => ({ ...p, banner_image_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Click-Through URL</label> <label className="text-xs text-[#888] block mb-1">Click-Through URL</label>
<input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.click_through_url} onChange={e => setForm(p => ({ ...p, click_through_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div className="flex items-center gap-3 pt-4"> <div className="flex items-center gap-3 pt-4">
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer"> <label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
@@ -159,47 +159,47 @@ export default function AdOpsIOsPage() {
{form.is_comp && ( {form.is_comp && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Comp Reason</label> <label className="text-xs text-[#888] block mb-1">Comp Reason</label>
<input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.comp_reason} onChange={e => setForm(p => ({ ...p, comp_reason: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
)} )}
{isEmailProduct && ( {isEmailProduct && (
<> <>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Email HTML URL</label> <label className="text-xs text-[#888] block mb-1">Email HTML URL</label>
<input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.email_html_url} onChange={e => setForm(p => ({ ...p, email_html_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label> <label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label>
<input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <input type="date" value={form.email_scheduled_date} onChange={e => setForm(p => ({ ...p, email_scheduled_date: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">List Segment</label> <label className="text-xs text-[#888] block mb-1">List Segment</label>
<input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.email_list_segment} onChange={e => setForm(p => ({ ...p, email_list_segment: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</> </>
)} )}
{isAdvertorial && ( {isAdvertorial && (
<div> <div>
<label className="text-xs text-[#888] block mb-1">Page URL (permanent)</label> <label className="text-xs text-[#888] block mb-1">Page URL (permanent)</label>
<input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.page_url} onChange={e => setForm(p => ({ ...p, page_url: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
)} )}
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <textarea value={form.notes} onChange={e => setForm(p => ({ ...p, notes: e.target.value }))} rows={2} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['IO ID','Client','Product','Start','End','Status','Comp','Paid','Actions'].map(h => ( {['IO ID','Client','Product','Start','End','Status','Comp','Paid','Actions'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -211,8 +211,8 @@ export default function AdOpsIOsPage() {
) : ios.length === 0 ? ( ) : ios.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No IO extensions found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No IO extensions found</td></tr>
) : ios.map((io: any) => ( ) : ios.map((io: any) => (
<tr key={io.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={io.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{io.existing_io_id}</td> <td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{io.existing_io_id}</td>
<td className="px-3 py-2 text-white text-xs">{io.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{io.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{io.start_date}</td> <td className="px-3 py-2 text-[#888] text-xs">{io.start_date}</td>
@@ -227,7 +227,7 @@ export default function AdOpsIOsPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${io.is_paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{io.is_paid ? 'Paid' : 'Unpaid'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${io.is_paid ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{io.is_paid ? 'Paid' : 'Unpaid'}</span>
</td> </td>
<td className="px-3 py-2 flex gap-2"> <td className="px-3 py-2 flex gap-2">
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link> <Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
{io.flight_status === 'expired' && ( {io.flight_status === 'expired' && (
<button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button> <button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button>
)} )}

View File

@@ -22,7 +22,7 @@ export default function AdOpsNotificationsPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -33,10 +33,10 @@ export default function AdOpsNotificationsPage() {
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Type','IO','Recipient','Via','Sent At','Status','Message'].map(h => ( {['Type','IO','Recipient','Via','Sent At','Status','Message'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -48,9 +48,9 @@ export default function AdOpsNotificationsPage() {
) : notifications.length === 0 ? ( ) : notifications.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr>
) : notifications.map((n: any) => ( ) : notifications.map((n: any) => (
<tr key={n.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={n.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td> <td className="px-3 py-2 text-[#F0A623] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{n.recipient_email ?? n.recipient_phone ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{n.recipient_email ?? n.recipient_phone ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_via}</td> <td className="px-3 py-2 text-[#555] text-xs">{n.sent_via}</td>
<td className="px-3 py-2 text-[#555] text-xs">{n.sent_at ? new Date(n.sent_at).toLocaleString() : '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{n.sent_at ? new Date(n.sent_at).toLocaleString() : '—'}</td>

View File

@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/20', scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/20',
paused: 'bg-gray-600/20 text-gray-400 border-gray-600/20', paused: 'bg-gray-600/20 text-gray-400 border-gray-600/20',
expired: 'bg-red-400/10 text-red-400 border-red-400/20', expired: 'bg-red-400/10 text-red-400 border-red-400/20',
cancelled: 'bg-[#1a1a1a] text-[#555] border-[#252525]', cancelled: 'bg-[#231d18] text-[#555] border-[#3a322b]',
}; };
export default function AdOpsDashboardPage() { export default function AdOpsDashboardPage() {
@@ -43,7 +43,7 @@ export default function AdOpsDashboardPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -64,7 +64,7 @@ export default function AdOpsDashboardPage() {
{ label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' }, { label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' },
{ label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' }, { label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' },
].map(c => ( ].map(c => (
<div key={c.key} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={c.key} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{stats?.[c.key] ?? 0}</p> <p className={`text-2xl font-bold ${c.color}`}>{stats?.[c.key] ?? 0}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</div> </div>
@@ -73,25 +73,25 @@ export default function AdOpsDashboardPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)} {['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)} {['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{/* Active Flights Table */} {/* Active Flights Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
<h2 className="text-sm font-semibold">Active Flights</h2> <h2 className="text-sm font-semibold">Active Flights</h2>
<Link href="/admin/adops/ios" className="text-xs text-[#ffb800] hover:underline">View All IOs </Link> <Link href="/admin/adops/ios" className="text-xs text-[#F0A623] hover:underline">View All IOs </Link>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Client','Product','Site','Start','End','Status','Salesperson','Paid',''].map(h => ( {['Client','Product','Site','Start','End','Status','Salesperson','Paid',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -103,7 +103,7 @@ export default function AdOpsDashboardPage() {
) : flights.length === 0 ? ( ) : flights.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr>
) : flights.map((f: any) => ( ) : flights.map((f: any) => (
<tr key={f.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={f.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{f.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{f.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{f.rmp_orders?.site ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{f.rmp_orders?.site ?? '—'}</td>
@@ -117,7 +117,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${f.is_paid ? 'bg-green-400/10 text-green-400' : f.is_comp ? 'bg-blue-400/10 text-blue-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{f.is_comp ? 'Comp' : f.is_paid ? 'Paid' : 'Unpaid'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${f.is_paid ? 'bg-green-400/10 text-green-400' : f.is_comp ? 'bg-blue-400/10 text-blue-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{f.is_comp ? 'Comp' : f.is_paid ? 'Paid' : 'Unpaid'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#ffb800] hover:underline">View</Link> <Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#F0A623] hover:underline">View</Link>
</td> </td>
</tr> </tr>
))} ))}
@@ -126,13 +126,13 @@ export default function AdOpsDashboardPage() {
</div> </div>
{/* Email Schedule */} {/* Email Schedule */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525]"> <div className="px-4 py-3 border-b border-[#3a322b]">
<h2 className="text-sm font-semibold">Email Distribution Schedule</h2> <h2 className="text-sm font-semibold">Email Distribution Schedule</h2>
</div> </div>
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Client','Type','Site','Scheduled Date','Status',''].map(h => ( {['Client','Type','Site','Scheduled Date','Status',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -142,7 +142,7 @@ export default function AdOpsDashboardPage() {
{emailSchedule.length === 0 ? ( {emailSchedule.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-6 text-center text-[#555] text-xs">No email blasts scheduled</td></tr> <tr><td colSpan={6} className="px-4 py-6 text-center text-[#555] text-xs">No email blasts scheduled</td></tr>
) : emailSchedule.map((e: any) => ( ) : emailSchedule.map((e: any) => (
<tr key={e.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={e.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{e.rmp_clients?.company_name ?? '—'}</td> <td className="px-3 py-2 text-white text-xs">{e.rmp_clients?.company_name ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{e.rmp_products?.product_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#888] text-xs">{e.rmp_products?.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{e.rmp_orders?.site ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{e.rmp_orders?.site ?? '—'}</td>
@@ -151,7 +151,7 @@ export default function AdOpsDashboardPage() {
<span className={`px-2 py-0.5 rounded-full text-xs ${e.email_sent_at ? 'bg-green-400/10 text-green-400' : 'bg-blue-400/10 text-blue-400'}`}>{e.email_sent_at ? 'Sent' : 'Pending'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${e.email_sent_at ? 'bg-green-400/10 text-green-400' : 'bg-blue-400/10 text-blue-400'}`}>{e.email_sent_at ? 'Sent' : 'Pending'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#ffb800] hover:underline">Preview</a>} {e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#F0A623] hover:underline">Preview</a>}
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -49,7 +49,7 @@ export default function AdOpsProductsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -60,62 +60,62 @@ export default function AdOpsProductsPage() {
<h1 className="text-xl font-bold">Product Catalog</h1> <h1 className="text-xl font-bold">Product Catalog</h1>
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link> <Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]"> Ad Operations</Link>
</div> </div>
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button> <button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showForm && ( {showForm && (
<form onSubmit={handleCreate} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleCreate} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">New Product</h3> <h3 className="text-sm font-semibold">New Product</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3"> <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site *</label> <label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={form.site} onChange={e => setForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Product Type *</label> <label className="text-xs text-[#888] block mb-1">Product Type *</label>
<select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={form.product_type} onChange={e => setForm(p => ({ ...p, product_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Name *</label> <label className="text-xs text-[#888] block mb-1">Name *</label>
<input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={form.name} onChange={e => setForm(p => ({ ...p, name: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Dimensions</label> <label className="text-xs text-[#888] block mb-1">Dimensions</label>
<input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.dimensions} onChange={e => setForm(p => ({ ...p, dimensions: e.target.value }))} placeholder="e.g. 300x250" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Duration (days)</label> <label className="text-xs text-[#888] block mb-1">Duration (days)</label>
<input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input type="number" value={form.duration_days} onChange={e => setForm(p => ({ ...p, duration_days: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div className="col-span-2 md:col-span-3"> <div className="col-span-2 md:col-span-3">
<label className="text-xs text-[#888] block mb-1">Description</label> <label className="text-xs text-[#888] block mb-1">Description</label>
<input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={form.description} onChange={e => setForm(p => ({ ...p, description: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
<button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Site','Type','Name','Dimensions','Duration','Active',''].map(h => ( {['Site','Type','Name','Dimensions','Duration','Active',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -127,17 +127,17 @@ export default function AdOpsProductsPage() {
) : products.length === 0 ? ( ) : products.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr>
) : products.map((p: any) => ( ) : products.map((p: any) => (
<tr key={p.id} className={`border-b border-[#1a1a1a] hover:bg-[#161616] ${!p.active ? 'opacity-50' : ''}`}> <tr key={p.id} className={`border-b border-[#231d18] hover:bg-[#231d18] ${!p.active ? 'opacity-50' : ''}`}>
<td className="px-3 py-2 text-[#888] text-xs capitalize">{p.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{p.site}</td>
<td className="px-3 py-2 text-[#555] text-xs">{p.product_type?.replace(/_/g, ' ')}</td> <td className="px-3 py-2 text-[#555] text-xs">{p.product_type?.replace(/_/g, ' ')}</td>
<td className="px-3 py-2 text-white text-xs">{p.name}</td> <td className="px-3 py-2 text-white text-xs">{p.name}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.dimensions ?? '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{p.dimensions ?? '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{p.duration_days ? `${p.duration_days}d` : '—'}</td> <td className="px-3 py-2 text-[#888] text-xs">{p.duration_days ? `${p.duration_days}d` : '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${p.active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{p.active ? 'Active' : 'Inactive'}</span>
</td> </td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#ffb800] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button> <button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#F0A623] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -64,7 +64,7 @@ interface MonthlyTrend {
// ─── Mock / derived data helpers ────────────────────────────────────────────── // ─── Mock / derived data helpers ──────────────────────────────────────────────
const CATEGORY_COLORS = [ const CATEGORY_COLORS = [
'#ffb800', '#f59e0b', '#10b981', '#8b5cf6', '#F0A623', '#f59e0b', '#10b981', '#8b5cf6',
'#ef4444', '#06b6d4', '#f97316', '#84cc16', '#ef4444', '#06b6d4', '#f97316', '#84cc16',
]; ];
@@ -78,9 +78,9 @@ interface StatCardProps {
icon: React.ReactNode; icon: React.ReactNode;
} }
function StatCard({ label, value, sub, accent = '#ffb800', icon }: StatCardProps) { function StatCard({ label, value, sub, accent = '#F0A623', icon }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex items-start gap-4">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }} style={{ background: `${accent}18` }}
@@ -112,7 +112,7 @@ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p> <p className="text-[#555] text-sm font-body">Loading analytics</p>
</div> </div>
</div> </div>
@@ -266,7 +266,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* ── Page Header ─────────────────────────────────────────────────── */} {/* ── Page Header ─────────────────────────────────────────────────── */}
<div className="bg-[#111] border-b border-[#252525]"> <div className="bg-[#111] border-b border-[#3a322b]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-5 flex items-center justify-between gap-4"> <div className="max-w-7xl mx-auto px-4 sm:px-6 py-5 flex items-center justify-between gap-4">
<div> <div>
<h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1> <h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1>
@@ -275,20 +275,20 @@ export default function AdminAnalyticsPage() {
</p> </p>
</div> </div>
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<Link href="/admin/articles" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/articles" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
Articles Articles
</Link> </Link>
<span className="text-[#333]">·</span> <span className="text-[#333]">·</span>
<Link href="/admin/users" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/users" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
Users Users
</Link> </Link>
<span className="text-[#333]">·</span> <span className="text-[#333]">·</span>
<Link href="/admin/import" className="text-[#555] hover:text-[#ffb800] text-xs font-body uppercase tracking-wider transition-colors"> <Link href="/admin/import" className="text-[#555] hover:text-[#F0A623] text-xs font-body uppercase tracking-wider transition-colors">
WP Import WP Import
</Link> </Link>
<button <button
onClick={fetchAnalytics} onClick={fetchAnalytics}
className="ml-2 px-3 py-1.5 bg-[#1a1a1a] border border-[#333] hover:border-[#ffb800] text-[#888] hover:text-[#ffb800] text-xs font-body rounded transition-colors" className="ml-2 px-3 py-1.5 bg-[#231d18] border border-[#333] hover:border-[#F0A623] text-[#888] hover:text-[#F0A623] text-xs font-body rounded transition-colors"
> >
Refresh Refresh
</button> </button>
@@ -312,7 +312,7 @@ export default function AdminAnalyticsPage() {
label="Total Articles" label="Total Articles"
value={metrics?.totalArticles ?? 0} value={metrics?.totalArticles ?? 0}
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>} icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>}
accent="#ffb800" accent="#F0A623"
/> />
<StatCard <StatCard
label="Published" label="Published"
@@ -361,7 +361,7 @@ export default function AdminAnalyticsPage() {
value={`${publishRate}%`} value={`${publishRate}%`}
sub="Published / total" sub="Published / total"
icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>} icon={<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>}
accent="#ffb800" accent="#F0A623"
/> />
</div> </div>
</section> </section>
@@ -369,7 +369,7 @@ export default function AdminAnalyticsPage() {
{/* ── Monthly Trend ────────────────────────────────────────────── */} {/* ── Monthly Trend ────────────────────────────────────────────── */}
<section> <section>
<SectionHeader title="Monthly Trends" subtitle="Published, imported, and native articles over the last 6 months" /> <SectionHeader title="Monthly Trends" subtitle="Published, imported, and native articles over the last 6 months" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{monthlyTrends.length > 0 ? ( {monthlyTrends.length > 0 ? (
<ResponsiveContainer width="100%" height={240}> <ResponsiveContainer width="100%" height={240}>
<LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}> <LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
@@ -395,7 +395,7 @@ export default function AdminAnalyticsPage() {
{/* Author Contributions */} {/* Author Contributions */}
<section> <section>
<SectionHeader title="Author Contributions" subtitle="Top contributors by article count" /> <SectionHeader title="Author Contributions" subtitle="Top contributors by article count" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{authorStats.length > 0 ? ( {authorStats.length > 0 ? (
<ResponsiveContainer width="100%" height={280}> <ResponsiveContainer width="100%" height={280}>
<BarChart <BarChart
@@ -429,7 +429,7 @@ export default function AdminAnalyticsPage() {
{/* Top Categories */} {/* Top Categories */}
<section> <section>
<SectionHeader title="Top-Performing Categories" subtitle="Article distribution by category" /> <SectionHeader title="Top-Performing Categories" subtitle="Article distribution by category" />
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
{categoryStats.length > 0 ? ( {categoryStats.length > 0 ? (
<> <>
<ResponsiveContainer width="100%" height={200}> <ResponsiveContainer width="100%" height={200}>
@@ -481,7 +481,7 @@ export default function AdminAnalyticsPage() {
<SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" /> <SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{importStats.map((stat) => ( {importStats.map((stat) => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div key={stat.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p> <p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p>
<span <span
@@ -526,7 +526,7 @@ export default function AdminAnalyticsPage() {
{/* ── Author Detail Table ──────────────────────────────────────── */} {/* ── Author Detail Table ──────────────────────────────────────── */}
<section> <section>
<SectionHeader title="Author Detail Breakdown" subtitle="Per-author article status distribution" /> <SectionHeader title="Author Detail Breakdown" subtitle="Per-author article status distribution" />
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm font-body"> <table className="w-full text-sm font-body">
<thead> <thead>
@@ -550,7 +550,7 @@ export default function AdminAnalyticsPage() {
return ( return (
<tr <tr
key={a.author_name} key={a.author_name}
className={`border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`} className={`border-b border-[#231d18] hover:bg-[#151515] transition-colors ${i % 2 === 0 ? '' : 'bg-[#1c1815]'}`}
> >
<td className="px-4 py-3 text-white font-medium">{a.author_name}</td> <td className="px-4 py-3 text-white font-medium">{a.author_name}</td>
<td className="px-4 py-3 text-right text-[#888]">{a.total}</td> <td className="px-4 py-3 text-right text-[#888]">{a.total}</td>

View File

@@ -33,7 +33,7 @@ const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10', published: 'text-green-400 bg-green-400/10',
draft: 'text-yellow-400 bg-yellow-400/10', draft: 'text-yellow-400 bg-yellow-400/10',
pending: 'text-orange-400 bg-orange-400/10', pending: 'text-orange-400 bg-orange-400/10',
archived: 'text-[#555] bg-[#1a1a1a]', archived: 'text-[#555] bg-[#231d18]',
}; };
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
@@ -384,7 +384,7 @@ export default function AdminArticlesPage() {
if (loading || (!user && !loading)) { if (loading || (!user && !loading)) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -406,10 +406,10 @@ export default function AdminArticlesPage() {
)} )}
{/* Page Header */} {/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]"> <div className="bg-[#1c1815] border-b border-[#3a322b]">
<div className="max-w-7xl mx-auto px-4 py-5"> <div className="max-w-7xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] transition-colors text-sm">Home</Link> <Link href="/home-page" className="text-[#555] hover:text-[#F0A623] transition-colors text-sm">Home</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<span className="text-[#888] text-sm">Admin</span> <span className="text-[#888] text-sm">Admin</span>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
@@ -423,13 +423,13 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setShowNewArticle(true)} onClick={() => setShowNewArticle(true)}
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#ffb800] hover:bg-[#d99700] rounded px-3 py-2 transition-colors"> className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#F0A623] hover:bg-[#BA7517] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
New Article New Article
</button> </button>
<Link <Link
href="/admin/import" href="/admin/import"
className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#ffb800] border border-[#252525] hover:border-[#ffb800] rounded px-3 py-2 transition-colors"> className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#F0A623] border border-[#3a322b] hover:border-[#F0A623] rounded px-3 py-2 transition-colors">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /></svg>
WP Import WP Import
</Link> </Link>
@@ -471,7 +471,7 @@ export default function AdminArticlesPage() {
{ label: 'Drafts', value: counts.draft, color: 'text-yellow-400' }, { label: 'Drafts', value: counts.draft, color: 'text-yellow-400' },
{ label: 'Archived', value: counts.archived, color: 'text-[#555]' }, { label: 'Archived', value: counts.archived, color: 'text-[#555]' },
].map((s) => ( ].map((s) => (
<div key={s.label} className="bg-[#111] border border-[#252525] rounded-lg p-4 text-center"> <div key={s.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 text-center">
<p className={`text-2xl font-bold ${s.color}`}>{s.value}</p> <p className={`text-2xl font-bold ${s.color}`}>{s.value}</p>
<p className="text-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p> <p className="text-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p>
</div> </div>
@@ -479,7 +479,7 @@ export default function AdminArticlesPage() {
</div> </div>
{/* Status Tabs */} {/* Status Tabs */}
<div className="flex items-center gap-1 border-b border-[#252525] overflow-x-auto"> <div className="flex items-center gap-1 border-b border-[#3a322b] overflow-x-auto">
{tabs.map((tab) => ( {tabs.map((tab) => (
<button <button
key={tab.key} key={tab.key}
@@ -492,7 +492,7 @@ export default function AdminArticlesPage() {
{tab.label} {tab.label}
{tab.key !== 'all' && ( {tab.key !== 'all' && (
<span className={`text-[10px] px-1.5 py-0.5 rounded-full ${ <span className={`text-[10px] px-1.5 py-0.5 rounded-full ${
activeTab === tab.key ? 'bg-current/10' : 'bg-[#1a1a1a]' activeTab === tab.key ? 'bg-current/10' : 'bg-[#231d18]'
}`}> }`}>
{counts[tab.key]} {counts[tab.key]}
</span> </span>
@@ -512,13 +512,13 @@ export default function AdminArticlesPage() {
placeholder="Search articles..." placeholder="Search articles..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
<select <select
value={sourceFilter} value={sourceFilter}
onChange={(e) => setSourceFilter(e.target.value as SourceFilter)} onChange={(e) => setSourceFilter(e.target.value as SourceFilter)}
className="bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"> className="bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="all">All Sources</option> <option value="all">All Sources</option>
<option value="imported">Imported (WP)</option> <option value="imported">Imported (WP)</option>
<option value="native">Native</option> <option value="native">Native</option>
@@ -527,13 +527,13 @@ export default function AdminArticlesPage() {
{/* Bulk Actions Bar */} {/* Bulk Actions Bar */}
{selectedIds.size > 0 && ( {selectedIds.size > 0 && (
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#4a3500] rounded-lg px-4 py-3"> <div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#412402] rounded-lg px-4 py-3">
<span className="text-[#ffb800] text-sm font-bold">{selectedIds.size} selected</span> <span className="text-[#F0A623] text-sm font-bold">{selectedIds.size} selected</span>
<div className="flex-1" /> <div className="flex-1" />
<select <select
value={bulkAction} value={bulkAction}
onChange={(e) => setBulkAction(e.target.value as BulkAction | '')} onChange={(e) => setBulkAction(e.target.value as BulkAction | '')}
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#ffb800]"> className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#F0A623]">
<option value="">Bulk Action</option> <option value="">Bulk Action</option>
<option value="publish">Publish</option> <option value="publish">Publish</option>
<option value="pending">Submit for Review</option> <option value="pending">Submit for Review</option>
@@ -544,7 +544,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleBulkAction} onClick={handleBulkAction}
disabled={!bulkAction || bulkProcessing} disabled={!bulkAction || bulkProcessing}
className="flex items-center gap-1.5 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors"> className="flex items-center gap-1.5 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-4 py-1.5 rounded transition-colors">
{bulkProcessing ? ( {bulkProcessing ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null} ) : null}
@@ -568,15 +568,15 @@ export default function AdminArticlesPage() {
<span className="text-orange-400 text-sm font-bold">Approval Queue</span> <span className="text-orange-400 text-sm font-bold">Approval Queue</span>
<span className="text-[#555] text-xs ml-1"> Review and approve or reject each submission</span> <span className="text-[#555] text-xs ml-1"> Review and approve or reject each submission</span>
</div> </div>
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{pendingArticles.map((article) => ( {pendingArticles.map((article) => (
<div key={article.id} className={`flex items-start gap-4 px-4 py-4 hover:bg-[#141414] transition-colors ${processingIds.has(article.id) ? 'opacity-50 pointer-events-none' : ''}`}> <div key={article.id} className={`flex items-start gap-4 px-4 py-4 hover:bg-[#141414] transition-colors ${processingIds.has(article.id) ? 'opacity-50 pointer-events-none' : ''}`}>
{article.featured_image ? ( {article.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#231d18]">
<AppImage src={article.featured_image} alt={article.featured_image_alt || article.title} width={64} height={48} className="w-full h-full object-cover" /> <AppImage src={article.featured_image} alt={article.featured_image_alt || article.title} width={64} height={48} className="w-full h-full object-cover" />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-16 h-12 rounded bg-[#231d18] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -587,9 +587,9 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-3 mt-1"> <div className="flex items-center gap-3 mt-1">
<span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span> <span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span>
{article.category && ( {article.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span> <span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
)} )}
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10'}`}> <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'}`}>
{article.source === 'imported' ? 'WP' : 'Native'} {article.source === 'imported' ? 'WP' : 'Native'}
</span> </span>
</div> </div>
@@ -606,13 +606,13 @@ export default function AdminArticlesPage() {
</button> </button>
<button <button
onClick={() => handleStatusChange(article, 'draft')} onClick={() => handleStatusChange(article, 'draft')}
className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#1a1a1a] px-3 py-1.5 rounded transition-colors"> className="flex items-center gap-1.5 text-xs font-bold text-[#666] border border-[#333] hover:border-[#555] hover:bg-[#231d18] px-3 py-1.5 rounded transition-colors">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg> <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
Reject Reject
</button> </button>
<button <button
onClick={() => openEdit(article)} onClick={() => openEdit(article)}
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]" className="p-1.5 text-[#555] hover:text-[#F0A623] transition-colors rounded hover:bg-[#231d18]"
title="Edit"> title="Edit">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
@@ -627,14 +627,14 @@ export default function AdminArticlesPage() {
)} )}
{/* Articles Table */} {/* Articles Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Table Header */} {/* Table Header */}
<div className="flex items-center gap-3 px-4 py-3 border-b border-[#252525] bg-[#0d0d0d]"> <div className="flex items-center gap-3 px-4 py-3 border-b border-[#3a322b] bg-[#1c1815]">
<input <input
type="checkbox" type="checkbox"
checked={articles.length > 0 && selectedIds.size === articles.length} checked={articles.length > 0 && selectedIds.size === articles.length}
onChange={toggleSelectAll} onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer" className="w-4 h-4 rounded border-[#333] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/> />
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold flex-1">Article</span> <span className="text-[10px] text-[#555] uppercase tracking-wider font-bold flex-1">Article</span>
<span className="text-[10px] text-[#555] uppercase tracking-wider font-bold w-24 hidden md:block">Category</span> <span className="text-[10px] text-[#555] uppercase tracking-wider font-bold w-24 hidden md:block">Category</span>
@@ -646,7 +646,7 @@ export default function AdminArticlesPage() {
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : articles.length === 0 ? ( ) : articles.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
@@ -656,7 +656,7 @@ export default function AdminArticlesPage() {
<p className="text-[#555] text-sm">No articles found</p> <p className="text-[#555] text-sm">No articles found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{articles.map((article) => ( {articles.map((article) => (
<div <div
key={article.id} key={article.id}
@@ -668,13 +668,13 @@ export default function AdminArticlesPage() {
type="checkbox" type="checkbox"
checked={selectedIds.has(article.id)} checked={selectedIds.has(article.id)}
onChange={() => toggleSelect(article.id)} onChange={() => toggleSelect(article.id)}
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer flex-shrink-0" className="w-4 h-4 rounded border-[#333] bg-[#231d18] accent-[#F0A623] cursor-pointer flex-shrink-0"
/> />
{/* Thumbnail + Title */} {/* Thumbnail + Title */}
<div className="flex items-center gap-3 flex-1 min-w-0"> <div className="flex items-center gap-3 flex-1 min-w-0">
{article.featured_image ? ( {article.featured_image ? (
<div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-12 h-9 rounded overflow-hidden bg-[#231d18]">
<AppImage <AppImage
src={article.featured_image} src={article.featured_image}
alt={article.featured_image_alt || article.title} alt={article.featured_image_alt || article.title}
@@ -684,7 +684,7 @@ export default function AdminArticlesPage() {
/> />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-12 h-9 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-12 h-9 rounded bg-[#231d18] flex items-center justify-center">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -699,7 +699,7 @@ export default function AdminArticlesPage() {
{/* Category */} {/* Category */}
<div className="w-24 hidden md:block"> <div className="w-24 hidden md:block">
{article.category ? ( {article.category ? (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{article.category} {article.category}
</span> </span>
) : ( ) : (
@@ -710,7 +710,7 @@ export default function AdminArticlesPage() {
{/* Source */} {/* Source */}
<div className="w-20 hidden lg:block"> <div className="w-20 hidden lg:block">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${ <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10' article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'
}`}> }`}>
{article.source === 'imported' ? 'WP' : 'Native'} {article.source === 'imported' ? 'WP' : 'Native'}
</span> </span>
@@ -730,7 +730,7 @@ export default function AdminArticlesPage() {
{/* Status */} {/* Status */}
<div className="w-28"> <div className="w-28">
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#1a1a1a]'}`}> <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${STATUS_COLORS[article.status] || 'text-[#555] bg-[#231d18]'}`}>
{article.status === 'pending' ? 'Pending' : article.status} {article.status === 'pending' ? 'Pending' : article.status}
</span> </span>
</div> </div>
@@ -741,7 +741,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => openEdit(article)} onClick={() => openEdit(article)}
title="Edit" title="Edit"
className="p-1.5 text-[#555] hover:text-[#ffb800] transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-[#F0A623] transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" /> <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" /> <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
@@ -753,7 +753,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'pending')} onClick={() => handleStatusChange(article, 'pending')}
title="Submit for Review" title="Submit for Review"
className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-orange-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" /> <circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
@@ -765,7 +765,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'published')} onClick={() => handleStatusChange(article, 'published')}
title="Approve & Publish" title="Approve & Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" /> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg> </svg>
@@ -777,7 +777,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'draft')} onClick={() => handleStatusChange(article, 'draft')}
title="Reject (back to draft)" title="Reject (back to draft)"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /> <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
</svg> </svg>
@@ -789,7 +789,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'published')} onClick={() => handleStatusChange(article, 'published')}
title="Publish" title="Publish"
className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-green-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" /> <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
</svg> </svg>
@@ -801,7 +801,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleStatusChange(article, 'archived')} onClick={() => handleStatusChange(article, 'archived')}
title="Archive" title="Archive"
className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-yellow-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" /> <polyline points="21 8 21 21 3 21 3 8" /><rect x="1" y="3" width="22" height="5" /><line x1="10" y1="12" x2="14" y2="12" />
</svg> </svg>
@@ -812,7 +812,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={() => handleDelete(article)} onClick={() => handleDelete(article)}
title="Delete" title="Delete"
className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#1a1a1a]"> className="p-1.5 text-[#555] hover:text-red-400 transition-colors rounded hover:bg-[#231d18]">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="3 6 5 6 21 6" /> <polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" /> <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
@@ -834,12 +834,12 @@ export default function AdminArticlesPage() {
{/* New Article Modal */} {/* New Article Modal */}
{showNewArticle && ( {showNewArticle && (
<div className="fixed inset-0 z-50 flex items-start justify-center p-4 bg-black/80 overflow-y-auto"> <div className="fixed inset-0 z-50 flex items-start justify-center p-4 bg-black/80 overflow-y-auto">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-4xl shadow-2xl my-8"> <div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-4xl shadow-2xl my-8">
{/* Modal Header */} {/* Modal Header */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] sticky top-0 bg-[#111] z-10 rounded-t-xl"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b] sticky top-0 bg-[#111] z-10 rounded-t-xl">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h2 className="text-base font-bold text-white font-heading">New Article</h2> <h2 className="text-base font-bold text-white font-heading">New Article</h2>
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#ffb800]/10 px-1.5 py-0.5 rounded">Native</span> <span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#F0A623]/10 px-1.5 py-0.5 rounded">Native</span>
</div> </div>
<button <button
onClick={() => setShowNewArticle(false)} onClick={() => setShowNewArticle(false)}
@@ -859,7 +859,7 @@ export default function AdminArticlesPage() {
value={newTitle} value={newTitle}
onChange={(e) => setNewTitle(e.target.value)} onChange={(e) => setNewTitle(e.target.value)}
placeholder="Enter article title..." placeholder="Enter article title..."
className="w-full bg-[#1a1a1a] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#231d18] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
@@ -871,7 +871,7 @@ export default function AdminArticlesPage() {
onChange={(e) => setNewExcerpt(e.target.value)} onChange={(e) => setNewExcerpt(e.target.value)}
placeholder="Brief summary shown in article listings..." placeholder="Brief summary shown in article listings..."
rows={2} rows={2}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#ffb800] placeholder-[#444] resize-none" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#F0A623] placeholder-[#444] resize-none"
/> />
</div> </div>
@@ -894,7 +894,7 @@ export default function AdminArticlesPage() {
value={newAuthor} value={newAuthor}
onChange={(e) => setNewAuthor(e.target.value)} onChange={(e) => setNewAuthor(e.target.value)}
placeholder="Author name" placeholder="Author name"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -904,7 +904,7 @@ export default function AdminArticlesPage() {
value={newCategory} value={newCategory}
onChange={(e) => setNewCategory(e.target.value)} onChange={(e) => setNewCategory(e.target.value)}
placeholder="e.g. Technology" placeholder="e.g. Technology"
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -912,7 +912,7 @@ export default function AdminArticlesPage() {
<select <select
value={newStatus} value={newStatus}
onChange={(e) => setNewStatus(e.target.value as 'draft' | 'pending' | 'published')} onChange={(e) => setNewStatus(e.target.value as 'draft' | 'pending' | 'published')}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="draft">Draft</option> <option value="draft">Draft</option>
<option value="pending">Submit for Review</option> <option value="pending">Submit for Review</option>
<option value="published">Publish Now</option> <option value="published">Publish Now</option>
@@ -929,7 +929,7 @@ export default function AdminArticlesPage() {
value={newFeaturedImage} value={newFeaturedImage}
onChange={(e) => setNewFeaturedImage(e.target.value)} onChange={(e) => setNewFeaturedImage(e.target.value)}
placeholder="https://..." placeholder="https://..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
<div> <div>
@@ -939,21 +939,21 @@ export default function AdminArticlesPage() {
value={newFeaturedImageAlt} value={newFeaturedImageAlt}
onChange={(e) => setNewFeaturedImageAlt(e.target.value)} onChange={(e) => setNewFeaturedImageAlt(e.target.value)}
placeholder="Describe the image..." placeholder="Describe the image..."
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
</div> </div>
{/* Featured image preview */} {/* Featured image preview */}
{newFeaturedImage && ( {newFeaturedImage && (
<div className="rounded-lg overflow-hidden border border-[#252525] max-h-48"> <div className="rounded-lg overflow-hidden border border-[#3a322b] max-h-48">
<img src={newFeaturedImage} alt={newFeaturedImageAlt || 'Featured image preview'} className="w-full h-48 object-cover" /> <img src={newFeaturedImage} alt={newFeaturedImageAlt || 'Featured image preview'} className="w-full h-48 object-cover" />
</div> </div>
)} )}
</div> </div>
{/* Modal Footer */} {/* Modal Footer */}
<div className="flex items-center justify-between px-6 py-4 border-t border-[#252525] bg-[#0d0d0d] rounded-b-xl"> <div className="flex items-center justify-between px-6 py-4 border-t border-[#3a322b] bg-[#1c1815] rounded-b-xl">
<p className="text-[11px] text-[#444]"> <p className="text-[11px] text-[#444]">
{newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'} {newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'}
</p> </p>
@@ -966,7 +966,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleNewArticleSave} onClick={handleNewArticleSave}
disabled={newSaving || !newTitle.trim()} disabled={newSaving || !newTitle.trim()}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{newSaving ? ( {newSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : ( ) : (
@@ -983,8 +983,8 @@ export default function AdminArticlesPage() {
{/* Edit Modal */} {/* Edit Modal */}
{editingArticle && ( {editingArticle && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/70">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl"> <div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg shadow-2xl">
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#3a322b]">
<h2 className="text-base font-bold text-white font-heading">Edit Article</h2> <h2 className="text-base font-bold text-white font-heading">Edit Article</h2>
<button <button
onClick={() => setEditingArticle(null)} onClick={() => setEditingArticle(null)}
@@ -1002,7 +1002,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editTitle} value={editTitle}
onChange={(e) => setEditTitle(e.target.value)} onChange={(e) => setEditTitle(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -1012,7 +1012,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editAuthor} value={editAuthor}
onChange={(e) => setEditAuthor(e.target.value)} onChange={(e) => setEditAuthor(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -1021,7 +1021,7 @@ export default function AdminArticlesPage() {
type="text" type="text"
value={editCategory} value={editCategory}
onChange={(e) => setEditCategory(e.target.value)} onChange={(e) => setEditCategory(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
</div> </div>
@@ -1030,7 +1030,7 @@ export default function AdminArticlesPage() {
<select <select
value={editStatus} value={editStatus}
onChange={(e) => setEditStatus(e.target.value)} onChange={(e) => setEditStatus(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623]">
<option value="draft">Draft</option> <option value="draft">Draft</option>
<option value="pending">Pending Review</option> <option value="pending">Pending Review</option>
<option value="published">Published</option> <option value="published">Published</option>
@@ -1040,19 +1040,19 @@ export default function AdminArticlesPage() {
{/* Scheduling — only shown for draft status */} {/* Scheduling — only shown for draft status */}
{editStatus === 'draft' && ( {editStatus === 'draft' && (
<div className="bg-[#0d1a2e] border border-[#4a3500] rounded-lg p-4"> <div className="bg-[#0d1a2e] border border-[#412402] rounded-lg p-4">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" /> <circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
</svg> </svg>
<label className="text-xs text-[#ffb800] font-bold uppercase tracking-wider">Schedule Publish</label> <label className="text-xs text-[#F0A623] font-bold uppercase tracking-wider">Schedule Publish</label>
</div> </div>
<input <input
type="datetime-local" type="datetime-local"
value={editScheduledAt} value={editScheduledAt}
onChange={(e) => setEditScheduledAt(e.target.value)} onChange={(e) => setEditScheduledAt(e.target.value)}
min={new Date().toISOString().slice(0, 16)} min={new Date().toISOString().slice(0, 16)}
className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] [color-scheme:dark]" className="w-full bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] [color-scheme:dark]"
/> />
{editScheduledAt && ( {editScheduledAt && (
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
@@ -1074,14 +1074,14 @@ export default function AdminArticlesPage() {
<div className="flex items-center gap-2 pt-1"> <div className="flex items-center gap-2 pt-1">
<span className="text-xs text-[#555]">Source:</span> <span className="text-xs text-[#555]">Source:</span>
<span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${ <span className={`text-[10px] font-bold uppercase tracking-wider px-1.5 py-0.5 rounded ${
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#ffb800] bg-[#ffb800]/10' editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#F0A623] bg-[#F0A623]/10'
}`}> }`}>
{editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'} {editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'}
</span> </span>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#252525]"> <div className="flex items-center justify-end gap-3 px-5 py-4 border-t border-[#3a322b]">
<button <button
onClick={() => setEditingArticle(null)} onClick={() => setEditingArticle(null)}
className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2"> className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2">
@@ -1090,7 +1090,7 @@ export default function AdminArticlesPage() {
<button <button
onClick={handleEditSave} onClick={handleEditSave}
disabled={editSaving || !editTitle.trim()} disabled={editSaving || !editTitle.trim()}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{editSaving ? ( {editSaving ? (
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : null} ) : null}

View File

@@ -50,13 +50,13 @@ const ACTION_COLORS: Record<string, string> = {
article_published: 'text-green-400 bg-green-400/10', article_published: 'text-green-400 bg-green-400/10',
article_unpublished: 'text-yellow-400 bg-yellow-400/10', article_unpublished: 'text-yellow-400 bg-yellow-400/10',
article_deleted: 'text-red-400 bg-red-400/10', article_deleted: 'text-red-400 bg-red-400/10',
article_archived: 'text-[#888] bg-[#1a1a1a]', article_archived: 'text-[#888] bg-[#231d18]',
article_status_changed: 'text-purple-400 bg-purple-400/10', article_status_changed: 'text-purple-400 bg-purple-400/10',
user_role_changed: 'text-orange-400 bg-orange-400/10', user_role_changed: 'text-orange-400 bg-orange-400/10',
user_activated: 'text-green-400 bg-green-400/10', user_activated: 'text-green-400 bg-green-400/10',
user_deactivated: 'text-red-400 bg-red-400/10', user_deactivated: 'text-red-400 bg-red-400/10',
user_invited: 'text-blue-400 bg-blue-400/10', user_invited: 'text-blue-400 bg-blue-400/10',
user_invitation_revoked: 'text-[#888] bg-[#1a1a1a]', user_invitation_revoked: 'text-[#888] bg-[#231d18]',
import_completed: 'text-teal-400 bg-teal-400/10', import_completed: 'text-teal-400 bg-teal-400/10',
import_failed: 'text-red-400 bg-red-400/10', import_failed: 'text-red-400 bg-red-400/10',
}; };
@@ -150,7 +150,7 @@ interface StatCardProps {
function StatCard({ label, value, accent, icon }: StatCardProps) { function StatCard({ label, value, accent, icon }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-center gap-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accent}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0 ${accent}`}>
{icon} {icon}
</div> </div>
@@ -170,16 +170,16 @@ interface DetailModalProps {
} }
function DetailModal({ log, onClose }: DetailModalProps) { function DetailModal({ log, onClose }: DetailModalProps) {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action; const actionLabel = ACTION_LABELS[log.action] || log.action;
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div className="absolute inset-0 bg-black/70" onClick={onClose} /> <div className="absolute inset-0 bg-black/70" onClick={onClose} />
<div className="relative bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg shadow-2xl"> <div className="relative bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg shadow-2xl">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-5 border-b border-[#252525]"> <div className="flex items-center justify-between p-5 border-b border-[#3a322b]">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}> <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}>
{ACTION_ICONS[log.action] || getDefaultIcon(log.action)} {ACTION_ICONS[log.action] || getDefaultIcon(log.action)}
@@ -247,7 +247,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
{log.metadata && Object.keys(log.metadata).length > 0 && ( {log.metadata && Object.keys(log.metadata).length > 0 && (
<div> <div>
<p className="text-[#555] text-xs font-body uppercase tracking-wider mb-1">Metadata</p> <p className="text-[#555] text-xs font-body uppercase tracking-wider mb-1">Metadata</p>
<div className="bg-[#0d0d0d] border border-[#1e1e1e] rounded-lg p-3"> <div className="bg-[#1c1815] border border-[#1e1e1e] rounded-lg p-3">
<pre className="text-[#aaa] text-xs font-mono whitespace-pre-wrap break-all"> <pre className="text-[#aaa] text-xs font-mono whitespace-pre-wrap break-all">
{JSON.stringify(log.metadata, null, 2)} {JSON.stringify(log.metadata, null, 2)}
</pre> </pre>
@@ -340,7 +340,7 @@ export default function AuditLogPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -368,7 +368,7 @@ export default function AuditLogPage() {
</div> </div>
<button <button
onClick={fetchLogs} onClick={fetchLogs}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body" className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-[#888] hover:text-white hover:border-[#333] transition-all text-sm font-body"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -382,7 +382,7 @@ export default function AuditLogPage() {
<StatCard <StatCard
label="Total Events" label="Total Events"
value={logs.length} value={logs.length}
accent="bg-[#ffb800]/10 text-[#ffb800]" accent="bg-[#F0A623]/10 text-[#F0A623]"
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
@@ -433,19 +433,19 @@ export default function AuditLogPage() {
placeholder="Search by user, item, or action…" placeholder="Search by user, item, or action…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#ffb800] font-body" className="w-full bg-[#111] border border-[#3a322b] rounded-lg pl-9 pr-4 py-2.5 text-sm text-white placeholder-[#444] focus:outline-none focus:border-[#F0A623] font-body"
/> />
</div> </div>
{/* Action Type Filter */} {/* Action Type Filter */}
<div className="flex items-center gap-1 bg-[#111] border border-[#252525] rounded-lg p-1"> <div className="flex items-center gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{(['all', 'article', 'user', 'import'] as ActionFilter[]).map((f) => ( {(['all', 'article', 'user', 'import'] as ActionFilter[]).map((f) => (
<button <button
key={f} key={f}
onClick={() => setActionFilter(f)} onClick={() => setActionFilter(f)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${ className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
actionFilter === f actionFilter === f
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-[#888]' : 'text-[#555] hover:text-[#888]'
}`} }`}
> >
@@ -466,9 +466,9 @@ export default function AuditLogPage() {
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-xl overflow-hidden">
{/* Table Header */} {/* Table Header */}
<div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#0d0d0d]"> <div className="hidden md:grid grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-4 px-5 py-3 border-b border-[#1e1e1e] bg-[#1c1815]">
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Action</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Action</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Performed By</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Performed By</p>
<p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Affected Item</p> <p className="text-[#444] text-xs font-body font-semibold uppercase tracking-wider">Affected Item</p>
@@ -480,7 +480,7 @@ export default function AuditLogPage() {
{loadingData && ( {loadingData && (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading audit logs</p> <p className="text-[#555] text-sm font-body">Loading audit logs</p>
</div> </div>
</div> </div>
@@ -489,14 +489,14 @@ export default function AuditLogPage() {
{/* Empty */} {/* Empty */}
{!loadingData && filteredLogs.length === 0 && ( {!loadingData && filteredLogs.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 gap-3"> <div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-[#1a1a1a] flex items-center justify-center"> <div className="w-12 h-12 rounded-full bg-[#231d18] flex items-center justify-center">
<svg className="w-6 h-6 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-6 h-6 text-[#333]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg> </svg>
</div> </div>
<p className="text-[#555] text-sm font-body">No audit log entries found</p> <p className="text-[#555] text-sm font-body">No audit log entries found</p>
{searchQuery && ( {searchQuery && (
<button onClick={() => setSearchQuery('')} className="text-[#ffb800] text-xs font-body hover:underline"> <button onClick={() => setSearchQuery('')} className="text-[#F0A623] text-xs font-body hover:underline">
Clear search Clear search
</button> </button>
)} )}
@@ -505,7 +505,7 @@ export default function AuditLogPage() {
{/* Rows */} {/* Rows */}
{!loadingData && filteredLogs.map((log, idx) => { {!loadingData && filteredLogs.map((log, idx) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action; const actionLabel = ACTION_LABELS[log.action] || log.action;
const icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action); const icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action);
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
@@ -513,8 +513,8 @@ export default function AuditLogPage() {
return ( return (
<div <div
key={log.id} key={log.id}
className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#0d0d0d] transition-colors cursor-pointer ${ className={`grid grid-cols-1 md:grid-cols-[2fr_1.5fr_1.5fr_1fr_auto] gap-3 md:gap-4 px-5 py-4 hover:bg-[#1c1815] transition-colors cursor-pointer ${
idx !== filteredLogs.length - 1 ? 'border-b border-[#1a1a1a]' : '' idx !== filteredLogs.length - 1 ? 'border-b border-[#231d18]' : ''
}`} }`}
onClick={() => setSelectedLog(log)} onClick={() => setSelectedLog(log)}
> >
@@ -530,7 +530,7 @@ export default function AuditLogPage() {
{/* Performed By */} {/* Performed By */}
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] border border-[#252525] flex items-center justify-center flex-shrink-0"> <div className="w-6 h-6 rounded-full bg-[#231d18] border border-[#3a322b] flex items-center justify-center flex-shrink-0">
<span className="text-[#888] text-xs font-body font-bold"> <span className="text-[#888] text-xs font-body font-bold">
{performerName.charAt(0).toUpperCase()} {performerName.charAt(0).toUpperCase()}
</span> </span>
@@ -569,7 +569,7 @@ export default function AuditLogPage() {
<div className="flex items-center"> <div className="flex items-center">
<button <button
onClick={(e) => { e.stopPropagation(); setSelectedLog(log); }} onClick={(e) => { e.stopPropagation(); setSelectedLog(log); }}
className="text-[#444] hover:text-[#ffb800] transition-colors p-1" className="text-[#444] hover:text-[#F0A623] transition-colors p-1"
aria-label="View details" aria-label="View details"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -593,7 +593,7 @@ export default function AuditLogPage() {
<button <button
onClick={() => setPage((p) => Math.max(0, p - 1))} onClick={() => setPage((p) => Math.max(0, p - 1))}
disabled={page === 0} disabled={page === 0}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all" className="px-3 py-1.5 bg-[#111] border border-[#3a322b] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
> >
Previous Previous
</button> </button>
@@ -601,7 +601,7 @@ export default function AuditLogPage() {
<button <button
onClick={() => setPage((p) => p + 1)} onClick={() => setPage((p) => p + 1)}
disabled={filteredLogs.length < PAGE_SIZE} disabled={filteredLogs.length < PAGE_SIZE}
className="px-3 py-1.5 bg-[#111] border border-[#252525] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all" className="px-3 py-1.5 bg-[#111] border border-[#3a322b] rounded-lg text-xs font-body text-[#888] hover:text-white hover:border-[#333] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
> >
Next Next
</button> </button>

View File

@@ -162,7 +162,7 @@ export default function BannedTermsPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -190,17 +190,17 @@ export default function BannedTermsPage() {
<p className="text-[#555] text-sm font-body mt-1">Manage restricted company names and keywords visible to administrators only</p> <p className="text-[#555] text-sm font-body mt-1">Manage restricted company names and keywords visible to administrators only</p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors"> <button onClick={exportCSV} className="flex items-center gap-2 px-3 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Export CSV Export CSV
</button> </button>
<button onClick={handleRetroactiveScan} disabled={scanning} <button onClick={handleRetroactiveScan} disabled={scanning}
className="flex items-center gap-2 px-3 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50"> className="flex items-center gap-2 px-3 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors disabled:opacity-50">
{scanning ? <div className="w-4 h-4 border-2 border-[#888]/30 border-t-[#888] rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} {scanning ? <div className="w-4 h-4 border-2 border-[#888]/30 border-t-[#888] rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
Scan Published Posts Scan Published Posts
</button> </button>
<button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }} <button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors"> className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Term Add Term
</button> </button>
@@ -209,7 +209,7 @@ export default function BannedTermsPage() {
{/* Retroactive scan results */} {/* Retroactive scan results */}
{scanResults !== null && ( {scanResults !== null && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-display font-bold text-white">Retroactive Scan Results {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found</h3> <h3 className="text-sm font-display font-bold text-white">Retroactive Scan Results {scanResults.length} match{scanResults.length !== 1 ? 'es' : ''} found</h3>
<button onClick={() => setScanResults(null)} className="text-[#555] hover:text-white text-xs font-body">Dismiss</button> <button onClick={() => setScanResults(null)} className="text-[#555] hover:text-white text-xs font-body">Dismiss</button>
@@ -234,18 +234,18 @@ export default function BannedTermsPage() {
{/* Add/Edit Form */} {/* Add/Edit Form */}
{showAddForm && ( {showAddForm && (
<div className="mb-6 bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">{editingId ? 'Edit Term' : 'Add Banned Term'}</h3> <h3 className="text-sm font-display font-bold text-white mb-4">{editingId ? 'Edit Term' : 'Add Banned Term'}</h3>
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Term / Company Name *</label> <label className="block text-xs font-body text-[#888] mb-1.5">Term / Company Name *</label>
<input type="text" value={form.term} onChange={e => setForm(f => ({ ...f, term: e.target.value }))} placeholder="e.g. Sony, Acme Corp" <input type="text" value={form.term} onChange={e => setForm(f => ({ ...f, term: e.target.value }))} placeholder="e.g. Sony, Acme Corp"
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" required /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" required />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Ban Level</label> <label className="block text-xs font-body text-[#888] mb-1.5">Ban Level</label>
<select value={form.ban_level} onChange={e => setForm(f => ({ ...f, ban_level: e.target.value as 'soft' | 'hard' }))} <select value={form.ban_level} onChange={e => setForm(f => ({ ...f, ban_level: e.target.value as 'soft' | 'hard' }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="soft">Soft Ban Flag for review (contributor doesn't know)</option> <option value="soft">Soft Ban Flag for review (contributor doesn't know)</option>
<option value="hard">Hard Ban — Block submission entirely</option> <option value="hard">Hard Ban — Block submission entirely</option>
</select> </select>
@@ -253,7 +253,7 @@ export default function BannedTermsPage() {
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label> <label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))} <select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="both">Both Sites</option> <option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
@@ -262,15 +262,15 @@ export default function BannedTermsPage() {
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="block text-xs font-body text-[#888] mb-1.5">Internal Notes (not shown to contributors)</label> <label className="block text-xs font-body text-[#888] mb-1.5">Internal Notes (not shown to contributors)</label>
<textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} rows={2} placeholder="Reason for ban, context..." <textarea value={form.notes} onChange={e => setForm(f => ({ ...f, notes: e.target.value }))} rows={2} placeholder="Reason for ban, context..."
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800] resize-none" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623] resize-none" />
</div> </div>
<div className="md:col-span-2 flex gap-3"> <div className="md:col-span-2 flex gap-3">
<button type="submit" disabled={saving} <button type="submit" disabled={saving}
className="px-5 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50"> className="px-5 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{saving ? 'Saving...' : editingId ? 'Update Term' : 'Add Term'} {saving ? 'Saving...' : editingId ? 'Update Term' : 'Add Term'}
</button> </button>
<button type="button" onClick={() => { setShowAddForm(false); setEditingId(null); setForm(EMPTY_FORM); }} <button type="button" onClick={() => { setShowAddForm(false); setEditingId(null); setForm(EMPTY_FORM); }}
className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors"> className="px-4 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">
Cancel Cancel
</button> </button>
</div> </div>
@@ -281,22 +281,22 @@ export default function BannedTermsPage() {
{/* Filters */} {/* Filters */}
<div className="mb-4 flex flex-wrap gap-3"> <div className="mb-4 flex flex-wrap gap-3">
<input type="text" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search terms..." <input type="text" value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search terms..."
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#ffb800] w-48" /> className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#F0A623] w-48" />
<select value={filterBanLevel} onChange={e => setFilterBanLevel(e.target.value)} <select value={filterBanLevel} onChange={e => setFilterBanLevel(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Ban Levels</option> <option value="">All Ban Levels</option>
<option value="soft">Soft Ban</option> <option value="soft">Soft Ban</option>
<option value="hard">Hard Ban</option> <option value="hard">Hard Ban</option>
</select> </select>
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)} <select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Sites</option> <option value="">All Sites</option>
<option value="avbeat">AV Beat</option> <option value="avbeat">AV Beat</option>
<option value="avbeat">AV Beat</option> <option value="avbeat">AV Beat</option>
<option value="both">Both</option> <option value="both">Both</option>
</select> </select>
<select value={filterActive} onChange={e => setFilterActive(e.target.value)} <select value={filterActive} onChange={e => setFilterActive(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="">All Status</option> <option value="">All Status</option>
<option value="true">Active</option> <option value="true">Active</option>
<option value="false">Inactive</option> <option value="false">Inactive</option>
@@ -304,8 +304,8 @@ export default function BannedTermsPage() {
</div> </div>
{/* Terms Table */} {/* Terms Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Banned Terms ({terms.length})</h2> <h2 className="text-sm font-display font-bold text-white">Banned Terms ({terms.length})</h2>
</div> </div>
{terms.length === 0 ? ( {terms.length === 0 ? (
@@ -316,15 +316,15 @@ export default function BannedTermsPage() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Term', 'Ban Level', 'Site Scope', 'Status', 'Date Added', 'Added By', 'Notes', 'Actions'].map(h => ( {['Term', 'Ban Level', 'Site Scope', 'Status', 'Date Added', 'Added By', 'Notes', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#231d18]">
{terms.map(term => ( {terms.map(term => (
<tr key={term.id} className={`hover:bg-[#0d0d0d] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}> <tr key={term.id} className={`hover:bg-[#1c1815] transition-colors ${!term.is_active ? 'opacity-50' : ''}`}>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="text-sm font-body font-semibold text-white">{term.term}</span> <span className="text-sm font-body font-semibold text-white">{term.term}</span>
</td> </td>
@@ -339,7 +339,7 @@ export default function BannedTermsPage() {
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span> <span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}> <span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>
{term.is_active ? 'Active' : 'Inactive'} {term.is_active ? 'Active' : 'Inactive'}
</span> </span>
</td> </td>
@@ -348,7 +348,7 @@ export default function BannedTermsPage() {
<td className="px-4 py-3 text-xs text-[#555] font-body max-w-xs truncate">{term.notes || ''}</td> <td className="px-4 py-3 text-xs text-[#555] font-body max-w-xs truncate">{term.notes || ''}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={() => handleEdit(term)} className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Edit</button> <button onClick={() => handleEdit(term)} className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Edit</button>
<button onClick={() => handleDeactivate(term.id, term.is_active)} disabled={actionLoading === term.id} <button onClick={() => handleDeactivate(term.id, term.is_active)} disabled={actionLoading === term.id}
className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50"> className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50">
{term.is_active ? 'Deactivate' : 'Reactivate'} {term.is_active ? 'Deactivate' : 'Reactivate'}
@@ -366,7 +366,7 @@ export default function BannedTermsPage() {
</div> </div>
{/* Legend */} {/* Legend */}
<div className="mt-6 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mt-6 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Ban Level Reference</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Ban Level Reference</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>

View File

@@ -54,7 +54,7 @@ function StatBlock({ label, b }: { label: string; b: StatBucket }) {
<span className="font-semibold">{fmtNum(b.clicks)}</span> <span className="font-semibold">{fmtNum(b.clicks)}</span>
<span className="text-[#6b7280]"> clk</span> <span className="text-[#6b7280]"> clk</span>
<span className="mx-1 text-[#374151]">·</span> <span className="mx-1 text-[#374151]">·</span>
<span className="text-[#ffd25a]">{pct(b.clicks, b.impressions)}</span> <span className="text-[#F0A623]">{pct(b.clicks, b.impressions)}</span>
</div> </div>
</div> </div>
); );
@@ -140,7 +140,7 @@ export default function BannerRows({
<input <input
value={current.name} value={current.name}
onChange={(e) => field(b.id, "name", e.target.value)} onChange={(e) => field(b.id, "name", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/> />
</label> </label>
@@ -150,7 +150,7 @@ export default function BannerRows({
value={current.click_url || ""} value={current.click_url || ""}
onChange={(e) => field(b.id, "click_url", e.target.value)} onChange={(e) => field(b.id, "click_url", e.target.value)}
placeholder="https://…" placeholder="https://…"
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/> />
</label> </label>
@@ -160,7 +160,7 @@ export default function BannerRows({
type="date" type="date"
value={fmtDate(current.start_date)} value={fmtDate(current.start_date)}
onChange={(e) => field(b.id, "start_date", e.target.value ? new Date(e.target.value).toISOString() : null)} onChange={(e) => field(b.id, "start_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/> />
</label> </label>
@@ -170,7 +170,7 @@ export default function BannerRows({
type="date" type="date"
value={fmtDate(current.end_date)} value={fmtDate(current.end_date)}
onChange={(e) => field(b.id, "end_date", e.target.value ? new Date(e.target.value).toISOString() : null)} onChange={(e) => field(b.id, "end_date", e.target.value ? new Date(e.target.value).toISOString() : null)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
/> />
</label> </label>
@@ -179,7 +179,7 @@ export default function BannerRows({
<select <select
value={current.status} value={current.status}
onChange={(e) => field(b.id, "status", e.target.value)} onChange={(e) => field(b.id, "status", e.target.value)}
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
> >
<option value="active">active</option> <option value="active">active</option>
<option value="scheduled">scheduled</option> <option value="scheduled">scheduled</option>
@@ -210,7 +210,7 @@ export default function BannerRows({
<button <button
onClick={() => save(b.id)} onClick={() => save(b.id)}
disabled={!dirty || pending} disabled={!dirty || pending}
className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#d99700] disabled:opacity-40 disabled:cursor-not-allowed" className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#BA7517] disabled:opacity-40 disabled:cursor-not-allowed"
> >
Save changes Save changes
</button> </button>
@@ -219,7 +219,7 @@ export default function BannerRows({
</div> </div>
<Link <Link
href={`/admin/banners/${b.id}/analytics`} href={`/admin/banners/${b.id}/analytics`}
className="text-sm text-[#ffd25a] hover:underline" className="text-sm text-[#F0A623] hover:underline"
> >
View detailed analytics View detailed analytics
</Link> </Link>

View File

@@ -68,7 +68,7 @@ export default function AnalyticsCharts({
</div> </div>
<section className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6"> <section className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<ChartCard title="Impressions per day" data={imp} stroke="#ffd25a" /> <ChartCard title="Impressions per day" data={imp} stroke="#F0A623" />
<ChartCard title="Clicks per day" data={clk} stroke="#34d399" /> <ChartCard title="Clicks per day" data={clk} stroke="#34d399" />
</section> </section>
@@ -121,7 +121,7 @@ function TopListCard({ title, rows }: { title: string; rows: TopRow[] }) {
{rows.map((r) => ( {rows.map((r) => (
<tr key={r.label} className="border-b border-[#1f2937] last:border-0"> <tr key={r.label} className="border-b border-[#1f2937] last:border-0">
<td className="py-2 pr-3 text-[#e5e7eb] truncate max-w-[400px]" title={r.label}>{r.label}</td> <td className="py-2 pr-3 text-[#e5e7eb] truncate max-w-[400px]" title={r.label}>{r.label}</td>
<td className="py-2 text-right text-[#ffd25a]">{r.count.toLocaleString()}</td> <td className="py-2 text-right text-[#F0A623]">{r.count.toLocaleString()}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -142,7 +142,7 @@ export default async function BannerAnalyticsPage({
return ( return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]"> <main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/banners" className="text-sm text-[#ffd25a] hover:underline"> <Link href="/admin/banners" className="text-sm text-[#F0A623] hover:underline">
Back to banners Back to banners
</Link> </Link>
@@ -159,7 +159,7 @@ export default async function BannerAnalyticsPage({
<p className="text-sm text-[#9ca3af]"> <p className="text-sm text-[#9ca3af]">
{b.size} · {b.status} ·{" "} {b.size} · {b.status} ·{" "}
{b.click_url && ( {b.click_url && (
<a className="text-[#ffd25a] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer"> <a className="text-[#F0A623] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
{b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""} {b.click_url.slice(0, 80)}{b.click_url.length > 80 ? "…" : ""}
</a> </a>
)} )}

View File

@@ -113,7 +113,7 @@ export default async function AdminBannersPage() {
after the next refresh. after the next refresh.
</p> </p>
</div> </div>
<Link href="/admin" className="text-sm text-[#ffd25a] hover:underline"> <Link href="/admin" className="text-sm text-[#F0A623] hover:underline">
Admin home Admin home
</Link> </Link>
</header> </header>

View File

@@ -171,7 +171,7 @@ export default function CompanyCoveragePage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading company coverage</p> <p className="text-[#555] text-sm font-body">Loading company coverage</p>
</div> </div>
</div> </div>
@@ -218,7 +218,7 @@ export default function CompanyCoveragePage() {
<button <button
onClick={handleScan} onClick={handleScan}
disabled={scanning} disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
> >
{scanning ? ( {scanning ? (
<> <>
@@ -255,14 +255,14 @@ export default function CompanyCoveragePage() {
<div className="lg:col-span-2"> <div className="lg:col-span-2">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-4 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-4 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{(['queued', 'published', 'dismissed'] as const).map((tab) => ( {(['queued', 'published', 'dismissed'] as const).map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${ className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
activeTab === tab activeTab === tab
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -272,7 +272,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* Queue List */} {/* Queue List */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{queue.length === 0 ? ( {queue.length === 0 ? (
<div className="px-5 py-16 text-center"> <div className="px-5 py-16 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -282,13 +282,13 @@ export default function CompanyCoveragePage() {
{activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`} {activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`}
</p> </p>
{activeTab === 'queued' && ( {activeTab === 'queued' && (
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#ffb800] hover:text-blue-300 font-body underline"> <button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#F0A623] hover:text-blue-300 font-body underline">
Run scan now Run scan now
</button> </button>
)} )}
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{queue.map((item) => ( {queue.map((item) => (
<div key={item.id} className="p-4"> <div key={item.id} className="p-4">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
@@ -332,7 +332,7 @@ export default function CompanyCoveragePage() {
</button> </button>
<button <button
onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)} onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)}
className="px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors" className="px-3 py-1.5 bg-[#231d18] hover:bg-[#222] text-[#888] text-xs font-body rounded-md transition-colors"
> >
{expandedItem === item.id ? 'Hide' : 'Preview'} {expandedItem === item.id ? 'Hide' : 'Preview'}
</button> </button>
@@ -349,7 +349,7 @@ export default function CompanyCoveragePage() {
{/* Expanded Draft Preview */} {/* Expanded Draft Preview */}
{expandedItem === item.id && ( {expandedItem === item.id && (
<div className="mt-4 bg-[#0d0d0d] border border-[#1a1a1a] rounded-lg p-4"> <div className="mt-4 bg-[#1c1815] border border-[#231d18] rounded-lg p-4">
<p className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">AI Draft Preview</p> <p className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">AI Draft Preview</p>
<h3 className="text-base font-display font-bold text-white mb-2">{item.ai_draft_title}</h3> <h3 className="text-base font-display font-bold text-white mb-2">{item.ai_draft_title}</h3>
<p className="text-sm text-[#888] font-body italic mb-3">{item.ai_draft_excerpt}</p> <p className="text-sm text-[#888] font-body italic mb-3">{item.ai_draft_excerpt}</p>
@@ -370,11 +370,11 @@ export default function CompanyCoveragePage() {
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Auto-Pilot Toggle */} {/* Auto-Pilot Toggle */}
<div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-5 transition-colors ${settings.autopilot_enabled ? 'border-green-500/40' : 'border-[#3a322b]'}`}>
<div className="flex items-start justify-between gap-3 mb-3"> <div className="flex items-start justify-between gap-3 mb-3">
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<svg className="w-4 h-4 text-[#ffb800]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#F0A623]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg> </svg>
<h3 className="text-sm font-display font-bold text-white">Auto-Pilot</h3> <h3 className="text-sm font-display font-bold text-white">Auto-Pilot</h3>
@@ -405,7 +405,7 @@ export default function CompanyCoveragePage() {
<p className="text-xs text-green-400 font-body font-semibold">Auto-Pilot is ON stories publish automatically</p> <p className="text-xs text-green-400 font-body font-semibold">Auto-Pilot is ON stories publish automatically</p>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 bg-[#1a1a1a] rounded-md px-3 py-2"> <div className="flex items-center gap-2 bg-[#231d18] rounded-md px-3 py-2">
<div className="w-2 h-2 rounded-full bg-[#444] flex-shrink-0" /> <div className="w-2 h-2 rounded-full bg-[#444] flex-shrink-0" />
<p className="text-xs text-[#666] font-body">Manual review required before publishing</p> <p className="text-xs text-[#666] font-body">Manual review required before publishing</p>
</div> </div>
@@ -413,7 +413,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* Reminder Settings */} {/* Reminder Settings */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
@@ -421,14 +421,14 @@ export default function CompanyCoveragePage() {
<h3 className="text-sm font-display font-bold text-white">Reminder Email</h3> <h3 className="text-sm font-display font-bold text-white">Reminder Email</h3>
</div> </div>
<p className="text-xs text-[#555] font-body mb-2">New story suggestions are emailed to:</p> <p className="text-xs text-[#555] font-body mb-2">New story suggestions are emailed to:</p>
<div className="bg-[#0d0d0d] border border-[#1a1a1a] rounded-md px-3 py-2"> <div className="bg-[#1c1815] border border-[#231d18] rounded-md px-3 py-2">
<p className="text-xs text-[#ffb800] font-body font-mono break-all">{settings.reminder_email}</p> <p className="text-xs text-[#F0A623] font-body font-mono break-all">{settings.reminder_email}</p>
</div> </div>
<p className="text-xs text-[#444] font-body mt-2">Reminders are sent when Auto-Pilot is off and new companies are detected.</p> <p className="text-xs text-[#444] font-body mt-2">Reminders are sent when Auto-Pilot is off and new companies are detected.</p>
</div> </div>
{/* Last Scan Info */} {/* Last Scan Info */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" /> <circle cx="12" cy="12" r="10" /><path strokeLinecap="round" d="M12 6v6l4 2" />
@@ -456,7 +456,7 @@ export default function CompanyCoveragePage() {
</div> </div>
{/* How It Works */} {/* How It Works */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">How It Works</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">How It Works</h3>
<ol className="space-y-2"> <ol className="space-y-2">
{[ {[
@@ -467,7 +467,7 @@ export default function CompanyCoveragePage() {
'Email reminder sent to coverage contact', 'Email reminder sent to coverage contact',
].map((step, i) => ( ].map((step, i) => (
<li key={i} className="flex items-start gap-2"> <li key={i} className="flex items-start gap-2">
<span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#1a1a1a] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5"> <span className="flex-shrink-0 w-4 h-4 rounded-full bg-[#231d18] text-[#555] text-xs font-body font-bold flex items-center justify-center mt-0.5">
{i + 1} {i + 1}
</span> </span>
<span className="text-xs text-[#666] font-body leading-relaxed">{step}</span> <span className="text-xs text-[#666] font-body leading-relaxed">{step}</span>

View File

@@ -245,7 +245,7 @@ export default function CompanyTrackerPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -279,7 +279,7 @@ export default function CompanyTrackerPage() {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Auto-Pilot toggle */} {/* Auto-Pilot toggle */}
<div className="flex items-center gap-3 bg-[#111] border border-[#252525] rounded-lg px-4 py-2.5"> <div className="flex items-center gap-3 bg-[#111] border border-[#3a322b] rounded-lg px-4 py-2.5">
<div> <div>
<div className="text-xs font-body font-bold text-white">Auto-Pilot</div> <div className="text-xs font-body font-bold text-white">Auto-Pilot</div>
<div className="text-xs text-[#555] font-body">{settings.autopilot_enabled ? 'Stories auto-publish' : 'Manual review'}</div> <div className="text-xs text-[#555] font-body">{settings.autopilot_enabled ? 'Stories auto-publish' : 'Manual review'}</div>
@@ -291,13 +291,13 @@ export default function CompanyTrackerPage() {
await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_settings', autopilot_enabled: newVal, reminder_email: settings.reminder_email }) }); await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'update_settings', autopilot_enabled: newVal, reminder_email: settings.reminder_email }) });
showToast(newVal ? '🚀 Auto-Pilot enabled' : '⏸ Auto-Pilot disabled'); showToast(newVal ? '🚀 Auto-Pilot enabled' : '⏸ Auto-Pilot disabled');
}} }}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`} className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}
> >
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<button onClick={handleScan} disabled={scanning} <button onClick={handleScan} disabled={scanning}
className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors"> className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 disabled:opacity-50 text-white text-sm font-body font-semibold rounded-lg transition-colors">
{scanning ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} {scanning ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>}
{scanning ? 'Scanning…' : 'Scan Articles Now'} {scanning ? 'Scanning…' : 'Scan Articles Now'}
</button> </button>
@@ -313,7 +313,7 @@ export default function CompanyTrackerPage() {
{ label: 'Companies Found', value: settings.last_scan_companies_found }, { label: 'Companies Found', value: settings.last_scan_companies_found },
{ label: 'Tracked Companies', value: companies.length || '—' }, { label: 'Tracked Companies', value: companies.length || '—' },
].map(stat => ( ].map(stat => (
<div key={stat.label} className="bg-[#111] border border-[#252525] rounded-lg px-4 py-3"> <div key={stat.label} className="bg-[#111] border border-[#3a322b] rounded-lg px-4 py-3">
<div className="text-lg font-display font-bold text-white">{stat.value}</div> <div className="text-lg font-display font-bold text-white">{stat.value}</div>
<div className="text-xs text-[#555] font-body">{stat.label}</div> <div className="text-xs text-[#555] font-body">{stat.label}</div>
</div> </div>
@@ -322,10 +322,10 @@ export default function CompanyTrackerPage() {
)} )}
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{tabs.map(tab => ( {tabs.map(tab => (
<button key={tab.id} onClick={() => setActiveTab(tab.id)} <button key={tab.id} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#1a1a1a] text-white' : 'text-[#555] hover:text-[#888]'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-medium transition-colors ${activeTab === tab.id ? 'bg-[#231d18] text-white' : 'text-[#555] hover:text-[#888]'}`}>
{tab.label} {tab.label}
</button> </button>
))} ))}
@@ -337,12 +337,12 @@ export default function CompanyTrackerPage() {
<div className="flex flex-wrap gap-3 mb-4"> <div className="flex flex-wrap gap-3 mb-4">
{(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(s => ( {(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(s => (
<button key={s} onClick={() => setQueueStatus(s)} <button key={s} onClick={() => setQueueStatus(s)}
className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#555] hover:text-white'}`}> className={`px-3 py-1.5 rounded-lg text-xs font-body font-medium transition-colors capitalize ${queueStatus === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#555] hover:text-white'}`}>
{s === 'generated' ? 'Pending Review' : s} {s === 'generated' ? 'Pending Review' : s}
</button> </button>
))} ))}
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)} <select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none"> className="bg-[#111] border border-[#3a322b] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
<option value="1">AV Beat</option> <option value="1">AV Beat</option>
<option value="2">AV Beat</option> <option value="2">AV Beat</option>
@@ -350,18 +350,18 @@ export default function CompanyTrackerPage() {
</div> </div>
{queue.length === 0 ? ( {queue.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-12 text-center"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-12 text-center">
<p className="text-[#555] text-sm font-body">No stories in this queue. Run a scan to generate new story suggestions.</p> <p className="text-[#555] text-sm font-body">No stories in this queue. Run a scan to generate new story suggestions.</p>
</div> </div>
) : ( ) : (
<div className="space-y-3"> <div className="space-y-3">
{queue.map(item => ( {queue.map(item => (
<div key={item.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={item.id} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 flex items-start gap-4"> <div className="px-5 py-4 flex items-start gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<SiteBadge siteId={item.site_id} /> <SiteBadge siteId={item.site_id} />
<span className="text-xs px-2 py-0.5 rounded bg-[#1a1a1a] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span> <span className="text-xs px-2 py-0.5 rounded bg-[#231d18] text-[#888] font-body capitalize">{item.story_type.replace(/_/g, ' ')}</span>
{item.quality_confidence && ( {item.quality_confidence && (
<span className={`text-xs px-2 py-0.5 rounded font-body ${item.quality_confidence >= 0.85 ? 'bg-green-500/10 text-green-400' : 'bg-yellow-500/10 text-yellow-400'}`}> <span className={`text-xs px-2 py-0.5 rounded font-body ${item.quality_confidence >= 0.85 ? 'bg-green-500/10 text-green-400' : 'bg-yellow-500/10 text-yellow-400'}`}>
{Math.round(item.quality_confidence * 100)}% confidence {Math.round(item.quality_confidence * 100)}% confidence
@@ -398,7 +398,7 @@ export default function CompanyTrackerPage() {
</div> </div>
</div> </div>
{expandedItem === item.id && item.generated_content && ( {expandedItem === item.id && item.generated_content && (
<div className="px-5 pb-5 border-t border-[#1a1a1a]"> <div className="px-5 pb-5 border-t border-[#231d18]">
<div className="mt-4 bg-[#0a0a0a] rounded-lg p-4 text-sm text-[#aaa] font-body leading-relaxed max-h-64 overflow-y-auto" <div className="mt-4 bg-[#0a0a0a] rounded-lg p-4 text-sm text-[#aaa] font-body leading-relaxed max-h-64 overflow-y-auto"
dangerouslySetInnerHTML={{ __html: item.generated_content }} /> dangerouslySetInnerHTML={{ __html: item.generated_content }} />
<div className="mt-3 flex items-center gap-3"> <div className="mt-3 flex items-center gap-3">
@@ -430,30 +430,30 @@ export default function CompanyTrackerPage() {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<p className="text-sm text-[#555] font-body">{companies.length} companies tracked</p> <p className="text-sm text-[#555] font-body">{companies.length} companies tracked</p>
<button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors"> <button onClick={() => setShowAddCompany(true)} className="flex items-center gap-2 px-4 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></svg>
Add Company Add Company
</button> </button>
</div> </div>
{showAddCompany && ( {showAddCompany && (
<div className="mb-4 bg-[#111] border border-[#252525] rounded-lg p-5"> <div className="mb-4 bg-[#111] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-sm font-display font-bold text-white mb-4">Add Company Manually</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Add Company Manually</h3>
<form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4"> <form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Company Name *</label> <label className="block text-xs font-body text-[#888] mb-1.5">Company Name *</label>
<input type="text" value={addCompanyForm.company_name} onChange={e => setAddCompanyForm(f => ({ ...f, company_name: e.target.value }))} required <input type="text" value={addCompanyForm.company_name} onChange={e => setAddCompanyForm(f => ({ ...f, company_name: e.target.value }))} required
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Website URL</label> <label className="block text-xs font-body text-[#888] mb-1.5">Website URL</label>
<input type="text" value={addCompanyForm.company_website} onChange={e => setAddCompanyForm(f => ({ ...f, company_website: e.target.value }))} placeholder="example.com" <input type="text" value={addCompanyForm.company_website} onChange={e => setAddCompanyForm(f => ({ ...f, company_website: e.target.value }))} placeholder="example.com"
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label> <label className="block text-xs font-body text-[#888] mb-1.5">Site Scope</label>
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))} <select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="both">Both Sites</option> <option value="both">Both Sites</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option> <option value="avbeat">AV Beat only</option>
@@ -462,51 +462,51 @@ export default function CompanyTrackerPage() {
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Crawl Priority</label> <label className="block text-xs font-body text-[#888] mb-1.5">Crawl Priority</label>
<select value={addCompanyForm.crawl_priority} onChange={e => setAddCompanyForm(f => ({ ...f, crawl_priority: e.target.value }))} <select value={addCompanyForm.crawl_priority} onChange={e => setAddCompanyForm(f => ({ ...f, crawl_priority: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]"> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]">
<option value="standard">Standard (weekly)</option> <option value="standard">Standard (weekly)</option>
<option value="high">High (every 3 days)</option> <option value="high">High (every 3 days)</option>
<option value="critical">Critical (daily)</option> <option value="critical">Critical (daily)</option>
</select> </select>
</div> </div>
<div className="md:col-span-2 flex gap-3"> <div className="md:col-span-2 flex gap-3">
<button type="submit" className="px-5 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button> <button type="submit" className="px-5 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
<button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#1a1a1a] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button> <button type="button" onClick={() => setShowAddCompany(false)} className="px-4 py-2 bg-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button>
</div> </div>
</form> </form>
</div> </div>
)} )}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Company', 'Website', 'Mentions', 'Sites', 'Auto-Stories', 'Priority', 'Last Crawl', 'Actions'].map(h => ( {['Company', 'Website', 'Mentions', 'Sites', 'Auto-Stories', 'Priority', 'Last Crawl', 'Actions'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#231d18]">
{companies.length === 0 ? ( {companies.length === 0 ? (
<tr><td colSpan={8} className="px-4 py-12 text-center text-[#555] text-sm font-body">No companies tracked yet. Run a scan to auto-detect companies from published articles.</td></tr> <tr><td colSpan={8} className="px-4 py-12 text-center text-[#555] text-sm font-body">No companies tracked yet. Run a scan to auto-detect companies from published articles.</td></tr>
) : companies.map(company => ( ) : companies.map(company => (
<tr key={company.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={company.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td> <td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#ffb800] hover:underline">{company.company_website}</a> : '—'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#F0A623] hover:underline">{company.company_website}</a> : '—'}</td>
<td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td> <td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td>
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td> <td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id} <button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}> className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${company.auto_story_enabled ? 'translate-x-4' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${company.auto_story_enabled ? 'translate-x-4' : 'translate-x-0'}`} />
</button> </button>
</td> </td>
<td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{company.crawl_priority}</span></td> <td className="px-4 py-3"><span className={`text-xs px-2 py-0.5 rounded font-body capitalize ${company.crawl_priority === 'critical' ? 'bg-red-500/10 text-red-400' : company.crawl_priority === 'high' ? 'bg-yellow-500/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{company.crawl_priority}</span></td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.last_crawled ? timeAgo(company.last_crawled) : 'Never'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{company.last_crawled ? timeAgo(company.last_crawled) : 'Never'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={async () => { await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'force_crawl', company_id: company.id }) }); showToast('Crawl scheduled'); }} <button onClick={async () => { await fetch('/api/admin/company-coverage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'force_crawl', company_id: company.id }) }); showToast('Crawl scheduled'); }}
className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Force Crawl</button> className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
<button onClick={() => handleDeleteCompany(company.id)} disabled={actionLoading === company.id} <button onClick={() => handleDeleteCompany(company.id)} disabled={actionLoading === company.id}
className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button> className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button>
</div> </div>
@@ -522,21 +522,21 @@ export default function CompanyTrackerPage() {
{/* ── Crawl Log Tab ── */} {/* ── Crawl Log Tab ── */}
{activeTab === 'crawl-log' && ( {activeTab === 'crawl-log' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Date', 'Company', 'Source URL', 'Items Found', 'Stories Queued', 'Status'].map(h => ( {['Date', 'Company', 'Source URL', 'Items Found', 'Stories Queued', 'Status'].map(h => (
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th> <th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#231d18]">
{crawlLogs.length === 0 ? ( {crawlLogs.length === 0 ? (
<tr><td colSpan={6} className="px-4 py-12 text-center text-[#555] text-sm font-body">No crawl history yet.</td></tr> <tr><td colSpan={6} className="px-4 py-12 text-center text-[#555] text-sm font-body">No crawl history yet.</td></tr>
) : crawlLogs.map(log => ( ) : crawlLogs.map(log => (
<tr key={log.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={log.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-4 py-3 text-xs text-[#555] font-body">{new Date(log.crawled_at).toLocaleString()}</td> <td className="px-4 py-3 text-xs text-[#555] font-body">{new Date(log.crawled_at).toLocaleString()}</td>
<td className="px-4 py-3 text-sm text-white font-body">{log.company_name}</td> <td className="px-4 py-3 text-sm text-white font-body">{log.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body truncate max-w-xs">{log.source_url || '—'}</td> <td className="px-4 py-3 text-xs text-[#555] font-body truncate max-w-xs">{log.source_url || '—'}</td>
@@ -558,7 +558,7 @@ export default function CompanyTrackerPage() {
{/* ── Settings Tab ── */} {/* ── Settings Tab ── */}
{activeTab === 'settings' && ( {activeTab === 'settings' && (
<div className="max-w-2xl space-y-6"> <div className="max-w-2xl space-y-6">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Auto-Pilot & Reminders</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Auto-Pilot & Reminders</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -567,19 +567,19 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">When enabled, high-confidence stories publish automatically without review</div> <div className="text-xs text-[#555] font-body mt-0.5">When enabled, high-confidence stories publish automatically without review</div>
</div> </div>
<button onClick={() => setSettings(s => ({ ...s, autopilot_enabled: !s.autopilot_enabled }))} <button onClick={() => setSettings(s => ({ ...s, autopilot_enabled: !s.autopilot_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${settings.autopilot_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Reminder Email</label> <label className="block text-xs font-body text-[#888] mb-1.5">Reminder Email</label>
<input type="email" value={settings.reminder_email} onChange={e => setSettings(s => ({ ...s, reminder_email: e.target.value }))} <input type="email" value={settings.reminder_email} onChange={e => setSettings(s => ({ ...s, reminder_email: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" /> className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<h3 className="text-sm font-display font-bold text-white mb-4">Story Generation Settings</h3> <h3 className="text-sm font-display font-bold text-white mb-4">Story Generation Settings</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -588,7 +588,7 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Master switch for all automated story generation</div> <div className="text-xs text-[#555] font-body mt-0.5">Master switch for all automated story generation</div>
</div> </div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_story_global_enabled: !s.auto_story_global_enabled }))} <button onClick={() => setTrackerSettings(s => ({ ...s, auto_story_global_enabled: !s.auto_story_global_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_story_global_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_story_global_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
@@ -598,25 +598,25 @@ export default function CompanyTrackerPage() {
<div className="text-xs text-[#555] font-body mt-0.5">Publish stories with confidence {Math.round(trackerSettings.auto_publish_confidence_threshold * 100)}% automatically</div> <div className="text-xs text-[#555] font-body mt-0.5">Publish stories with confidence {Math.round(trackerSettings.auto_publish_confidence_threshold * 100)}% automatically</div>
</div> </div>
<button onClick={() => setTrackerSettings(s => ({ ...s, auto_publish_enabled: !s.auto_publish_enabled }))} <button onClick={() => setTrackerSettings(s => ({ ...s, auto_publish_enabled: !s.auto_publish_enabled }))}
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#ffb800]' : 'bg-[#333]'}`}> className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#F0A623]' : 'bg-[#333]'}`}>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_publish_enabled ? 'translate-x-5' : 'translate-x-0'}`} /> <span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow transition-transform ${trackerSettings.auto_publish_enabled ? 'translate-x-5' : 'translate-x-0'}`} />
</button> </button>
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Mention Threshold (min mentions to qualify for auto-stories)</label> <label className="block text-xs font-body text-[#888] mb-1.5">Mention Threshold (min mentions to qualify for auto-stories)</label>
<input type="number" min={1} max={20} value={trackerSettings.mention_threshold} onChange={e => setTrackerSettings(s => ({ ...s, mention_threshold: parseInt(e.target.value) }))} <input type="number" min={1} max={20} value={trackerSettings.mention_threshold} onChange={e => setTrackerSettings(s => ({ ...s, mention_threshold: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" /> className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="block text-xs font-body text-[#888] mb-1.5">Max Stories Per Company Per Week</label> <label className="block text-xs font-body text-[#888] mb-1.5">Max Stories Per Company Per Week</label>
<input type="number" min={1} max={20} value={trackerSettings.max_stories_per_company_per_week} onChange={e => setTrackerSettings(s => ({ ...s, max_stories_per_company_per_week: parseInt(e.target.value) }))} <input type="number" min={1} max={20} value={trackerSettings.max_stories_per_company_per_week} onChange={e => setTrackerSettings(s => ({ ...s, max_stories_per_company_per_week: parseInt(e.target.value) }))}
className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#ffb800]" /> className="w-32 bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
</div> </div>
<button onClick={handleSaveSettings} disabled={savingSettings} <button onClick={handleSaveSettings} disabled={savingSettings}
className="px-6 py-2.5 bg-[#ffb800] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50"> className="px-6 py-2.5 bg-[#F0A623] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors disabled:opacity-50">
{savingSettings ? 'Saving...' : 'Save Settings'} {savingSettings ? 'Saving...' : 'Save Settings'}
</button> </button>
</div> </div>

View File

@@ -97,7 +97,7 @@ function StatCard({
good?: boolean; good?: boolean;
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-2"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-2">
<p className={`text-2xl font-display font-bold ${accent}`}>{value}</p> <p className={`text-2xl font-display font-bold ${accent}`}>{value}</p>
<p className="text-sm text-[#888] font-body">{label}</p> <p className="text-sm text-[#888] font-body">{label}</p>
{sub && <p className="text-xs text-[#555] font-body">{sub}</p>} {sub && <p className="text-xs text-[#555] font-body">{sub}</p>}
@@ -173,7 +173,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
}; };
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-4">
<h3 className="text-sm font-display font-bold text-white">Manual Controls</h3> <h3 className="text-sm font-display font-bold text-white">Manual Controls</h3>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div> <div>
@@ -181,7 +181,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
<select <select
value={site} value={site}
onChange={(e) => setSite(e.target.value as RMPSite)} onChange={(e) => setSite(e.target.value as RMPSite)}
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#F0A623]"
> >
{Object.entries(SITE_LABELS).map(([key, val]) => ( {Object.entries(SITE_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option> <option key={key} value={key}>{val.label}</option>
@@ -195,21 +195,21 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
value={topic} value={topic}
onChange={(e) => setTopic(e.target.value)} onChange={(e) => setTopic(e.target.value)}
placeholder="e.g. ATSC 3.0 deployment update" placeholder="e.g. ATSC 3.0 deployment update"
className="w-full bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-sm text-white font-body placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleGenerate} onClick={handleGenerate}
disabled={loading} disabled={loading}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors" className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
> >
{loading ? 'Generating…' : 'Generate Article'} {loading ? 'Generating…' : 'Generate Article'}
</button> </button>
<button <button
onClick={handleSEOBatch} onClick={handleSEOBatch}
disabled={loading} disabled={loading}
className="flex-1 bg-[#1a1a1a] hover:bg-[#252525] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors" className="flex-1 bg-[#231d18] hover:bg-[#3a322b] disabled:opacity-50 border border-[#333] text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
> >
{loading ? 'Running…' : 'Batch SEO Meta'} {loading ? 'Running…' : 'Batch SEO Meta'}
</button> </button>
@@ -279,7 +279,7 @@ export default function ContentStatusPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading content status</p> <p className="text-[#555] text-sm font-body">Loading content status</p>
</div> </div>
</div> </div>
@@ -296,7 +296,7 @@ export default function ContentStatusPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] px-6 py-4"> <div className="border-b border-[#231d18] bg-[#1c1815] px-6 py-4">
<div className="max-w-7xl mx-auto flex items-center justify-between"> <div className="max-w-7xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors text-sm font-body"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors text-sm font-body">
@@ -327,7 +327,7 @@ export default function ContentStatusPage() {
</button> </button>
<button <button
onClick={fetchAll} onClick={fetchAll}
className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#1a1a1a] transition-colors" className="text-xs font-body px-3 py-1.5 rounded-lg border border-[#333] text-white bg-[#111] hover:bg-[#231d18] transition-colors"
> >
Refresh Now Refresh Now
</button> </button>
@@ -395,7 +395,7 @@ export default function ContentStatusPage() {
{Object.entries(SITE_LABELS).map(([key, cfg]) => { {Object.entries(SITE_LABELS).map(([key, cfg]) => {
const count = status?.siteBreakdown?.[key] ?? 0; const count = status?.siteBreakdown?.[key] ?? 0;
return ( return (
<div key={key} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex flex-col gap-2"> <div key={key} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex flex-col gap-2">
<p className={`text-xs font-body font-semibold ${cfg.color}`}>{cfg.label}</p> <p className={`text-xs font-body font-semibold ${cfg.color}`}>{cfg.label}</p>
<p className="text-3xl font-display font-bold text-white">{count}</p> <p className="text-3xl font-display font-bold text-white">{count}</p>
<p className="text-xs text-[#555] font-body">Target: {cfg.target}</p> <p className="text-xs text-[#555] font-body">Target: {cfg.target}</p>
@@ -484,7 +484,7 @@ export default function ContentStatusPage() {
{/* API Reference */} {/* API Reference */}
<section> <section>
<h2 className="text-sm font-display font-bold text-[#888] uppercase tracking-wider mb-4">API Endpoints</h2> <h2 className="text-sm font-display font-bold text-[#888] uppercase tracking-wider mb-4">API Endpoints</h2>
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 grid grid-cols-1 sm:grid-cols-2 gap-3">
{[ {[
{ method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' }, { method: 'GET', path: '/api/content/audit', desc: 'Full content audit report' },
{ method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' }, { method: 'GET', path: '/api/content/pipeline-status', desc: 'Live pipeline + node status' },

View File

@@ -90,7 +90,7 @@ function StatCard({
label, label,
value, value,
sub, sub,
accent = '#ffb800', accent = '#F0A623',
icon, icon,
trend, trend,
}: { }: {
@@ -102,7 +102,7 @@ function StatCard({
trend?: { value: number; label: string }; trend?: { value: number; label: string };
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-xl p-5 flex flex-col gap-3"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-5 flex flex-col gap-3">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
@@ -134,7 +134,7 @@ function StatCard({
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1.5 font-semibold">{label}</p>} {label && <p className="text-[#888] mb-1.5 font-semibold">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -252,7 +252,7 @@ export default function EmailAnalyticsDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading analytics</p> <p className="text-[#555] text-sm font-body">Loading analytics</p>
</div> </div>
</div> </div>
@@ -262,7 +262,7 @@ export default function EmailAnalyticsDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10"> <div className="border-b border-[#231d18] bg-[#1c1815] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -270,7 +270,7 @@ export default function EmailAnalyticsDashboard() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg> </svg>
</Link> </Link>
<div className="w-px h-5 bg-[#252525]" /> <div className="w-px h-5 bg-[#3a322b]" />
<span className="text-white text-sm font-body font-semibold">Email Analytics</span> <span className="text-white text-sm font-body font-semibold">Email Analytics</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -280,17 +280,17 @@ export default function EmailAnalyticsDashboard() {
onClick={() => setTimeRange(r)} onClick={() => setTimeRange(r)}
className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${
timeRange === r timeRange === r
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]' : 'text-[#555] hover:text-white border border-[#3a322b] hover:border-[#444]'
}`} }`}
> >
{r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'} {r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'}
</button> </button>
))} ))}
<div className="w-px h-6 bg-[#252525] mx-1" /> <div className="w-px h-6 bg-[#3a322b] mx-1" />
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" />
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
label="Active Subscribers" label="Active Subscribers"
value={fmt(stats.activeSubscribers)} value={fmt(stats.activeSubscribers)}
sub={`${fmt(stats.unsubscribed)} unsubscribed`} sub={`${fmt(stats.unsubscribed)} unsubscribed`}
accent="#ffb800" accent="#F0A623"
trend={{ value: 12, label: 'vs last month' }} trend={{ value: 12, label: 'vs last month' }}
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -365,7 +365,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Send Volume Chart */} {/* Send Volume Chart */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Send Volume Over Time" title="Send Volume Over Time"
subtitle="Total emails sent, opens, and clicks per month" subtitle="Total emails sent, opens, and clicks per month"
@@ -378,7 +378,7 @@ export default function EmailAnalyticsDashboard() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => fmt(v)} /> <YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => fmt(v)} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888', paddingTop: 12 }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888', paddingTop: 12 }} />
<Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#ffb800" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="sent" name="Sent" fill="#3b82f620" stroke="#F0A623" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="opens" name="Opens" fill="#10b98120" stroke="#10b981" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="opens" name="Opens" fill="#10b98120" stroke="#10b981" strokeWidth={1} radius={[3, 3, 0, 0]} />
<Bar dataKey="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} /> <Bar dataKey="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} />
</ComposedChart> </ComposedChart>
@@ -387,7 +387,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Open Rate & CTR Trend */} {/* Open Rate & CTR Trend */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Open Rate & Click-Through Rate Trend" title="Open Rate & Click-Through Rate Trend"
subtitle="Monthly engagement rates as a percentage of recipients" subtitle="Monthly engagement rates as a percentage of recipients"
@@ -397,8 +397,8 @@ export default function EmailAnalyticsDashboard() {
<AreaChart data={filteredRates} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}> <AreaChart data={filteredRates} margin={{ top: 4, right: 8, left: -10, bottom: 0 }}>
<defs> <defs>
<linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#ffb800" stopOpacity={0.2} /> <stop offset="5%" stopColor="#F0A623" stopOpacity={0.2} />
<stop offset="95%" stopColor="#ffb800" stopOpacity={0} /> <stop offset="95%" stopColor="#F0A623" stopOpacity={0} />
</linearGradient> </linearGradient>
<linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#10b981" stopOpacity={0.2} /> <stop offset="5%" stopColor="#10b981" stopOpacity={0.2} />
@@ -418,10 +418,10 @@ export default function EmailAnalyticsDashboard() {
<Area <Area
type="monotone" type="monotone"
dataKey="Open Rate" dataKey="Open Rate"
stroke="#ffb800" stroke="#F0A623"
strokeWidth={2} strokeWidth={2}
fill="url(#openGrad)" fill="url(#openGrad)"
dot={{ r: 3, fill: '#ffb800' }} dot={{ r: 3, fill: '#F0A623' }}
activeDot={{ r: 5 }} activeDot={{ r: 5 }}
/> />
<Area <Area
@@ -441,7 +441,7 @@ export default function EmailAnalyticsDashboard() {
{/* Subscriber Growth */} {/* Subscriber Growth */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Growth Over Time */} {/* Growth Over Time */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Subscriber Growth" title="Subscriber Growth"
subtitle="Total subscribers and monthly net change" subtitle="Total subscribers and monthly net change"
@@ -463,14 +463,14 @@ export default function EmailAnalyticsDashboard() {
fill="#8b5cf618" fill="#8b5cf618"
dot={false} dot={false}
/> />
<Bar dataKey="net" name="Net New" fill="#ffb800" radius={[3, 3, 0, 0]} /> <Bar dataKey="net" name="Net New" fill="#F0A623" radius={[3, 3, 0, 0]} />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>
</div> </div>
{/* New vs Unsubscribed */} {/* New vs Unsubscribed */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader <SectionHeader
title="New Subscribers vs Unsubscribes" title="New Subscribers vs Unsubscribes"
subtitle="Monthly acquisition vs churn" subtitle="Monthly acquisition vs churn"
@@ -492,7 +492,7 @@ export default function EmailAnalyticsDashboard() {
</div> </div>
{/* Benchmark Comparison */} {/* Benchmark Comparison */}
<div className="bg-[#111] border border-[#252525] rounded-xl p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-6">
<SectionHeader <SectionHeader
title="Performance vs Industry Benchmarks" title="Performance vs Industry Benchmarks"
subtitle="How your metrics compare to email marketing industry averages" subtitle="How your metrics compare to email marketing industry averages"
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
label: 'Open Rate', label: 'Open Rate',
yours: stats.avgOpenRate, yours: stats.avgOpenRate,
benchmark: 21.5, benchmark: 21.5,
color: '#ffb800', color: '#F0A623',
unit: '%', unit: '%',
}, },
{ {
@@ -584,7 +584,7 @@ export default function EmailAnalyticsDashboard() {
<div className="flex flex-wrap gap-3 pb-4"> <div className="flex flex-wrap gap-3 pb-4">
<Link <Link
href="/admin/newsletter" href="/admin/newsletter"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />
@@ -593,7 +593,7 @@ export default function EmailAnalyticsDashboard() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
@@ -602,7 +602,7 @@ export default function EmailAnalyticsDashboard() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#252525] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors" className="flex items-center gap-2 px-4 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-sm font-body text-[#888] hover:text-white hover:border-[#444] transition-colors"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />

View File

@@ -75,7 +75,7 @@ function DomainsContent() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -86,41 +86,41 @@ function DomainsContent() {
<h1 className="text-xl font-bold">Sending Domains</h1> <h1 className="text-xl font-bold">Sending Domains</h1>
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link> <Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div> </div>
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button> <button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
{showAddForm && ( {showAddForm && (
<form onSubmit={handleAddDomain} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleAddDomain} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Rotation Domain</h3> <h3 className="text-sm font-semibold">Add Rotation Domain</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Site *</label> <label className="text-xs text-[#888] block mb-1">Site *</label>
<select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select required value={addForm.site} onChange={e => setAddForm(p => ({ ...p, site: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="">Select</option> <option value="">Select</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Domain *</label> <label className="text-xs text-[#888] block mb-1">Domain *</label>
<input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={addForm.domain} onChange={e => setAddForm(p => ({ ...p, domain: e.target.value }))} placeholder="mail.example.com" className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={addForm.domain_type} onChange={e => setAddForm(p => ({ ...p, domain_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)} {['subdomain','sister_domain','primary'].map(t => <option key={t} value={t}>{t}</option>)}
</select> </select>
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
@@ -131,10 +131,10 @@ function DomainsContent() {
<div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3"> <div className="bg-[#111] border border-yellow-400/30 rounded-lg p-5 w-80 space-y-3">
<h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3> <h3 className="text-sm font-semibold text-yellow-400"> Override DNS Verification</h3>
<p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p> <p className="text-xs text-[#888]">This allows sending before DNS is verified. Use with caution.</p>
<textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none" /> <textarea value={bypassReason} onChange={e => setBypassReason(e.target.value)} placeholder="Reason for bypass…" rows={3} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none" />
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button> <button onClick={() => setBypass(showBypass)} disabled={!bypassReason} className="px-3 py-1.5 bg-yellow-600 rounded text-xs font-medium hover:bg-yellow-500 disabled:opacity-50">Set Bypass</button>
<button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button onClick={() => setShowBypass(null)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</div> </div>
</div> </div>
@@ -142,12 +142,12 @@ function DomainsContent() {
<div className="space-y-2"> <div className="space-y-2">
{loadingData ? ( {loadingData ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-8 text-center text-[#555] text-xs">Loading</div>
) : domains.length === 0 ? ( ) : domains.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-8 text-center text-[#555] text-xs">No domains found</div>
) : domains.map((d: any) => ( ) : domains.map((d: any) => (
<div key={d.id} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={d.id} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#161616]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}> <div className="px-4 py-3 flex items-center gap-3 cursor-pointer hover:bg-[#231d18]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-white">{d.domain}</span> <span className="text-sm font-medium text-white">{d.domain}</span>
@@ -156,7 +156,7 @@ function DomainsContent() {
<span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span> <span className={`px-1.5 py-0.5 rounded text-xs ${d.dns_fully_verified ? 'bg-green-400/10 text-green-400' : 'bg-yellow-400/10 text-yellow-400'}`}>{d.dns_fully_verified ? 'DNS OK' : 'DNS Pending'}</span>
{d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>} {d.blacklisted && <span className="px-1.5 py-0.5 bg-red-400/10 text-red-400 rounded text-xs">Blacklisted</span>}
{d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>} {d.bypass_verification && <span className="px-1.5 py-0.5 bg-yellow-400/10 text-yellow-400 rounded text-xs">Bypass Active</span>}
<span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span> <span className={`px-1.5 py-0.5 rounded text-xs ${d.is_active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{d.is_active ? 'Active' : 'Inactive'}</span>
</div> </div>
<div className="flex gap-3 mt-1 text-xs text-[#555]"> <div className="flex gap-3 mt-1 text-xs text-[#555]">
<span>Health: {d.health_score}/100</span> <span>Health: {d.health_score}/100</span>
@@ -167,7 +167,7 @@ function DomainsContent() {
</div> </div>
{expanded === d.id && ( {expanded === d.id && (
<div className="border-t border-[#1a1a1a] px-4 py-3 space-y-3"> <div className="border-t border-[#231d18] px-4 py-3 space-y-3">
{/* DNS checks */} {/* DNS checks */}
<div className="grid grid-cols-2 gap-x-6"> <div className="grid grid-cols-2 gap-x-6">
<DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} /> <DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} />
@@ -178,7 +178,7 @@ function DomainsContent() {
</div> </div>
{/* DNS helper panel */} {/* DNS helper panel */}
<div className="bg-[#0a0a0a] border border-[#1a1a1a] rounded p-3 space-y-2"> <div className="bg-[#0a0a0a] border border-[#231d18] rounded p-3 space-y-2">
<p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p> <p className="text-xs font-semibold text-[#888]">DNS Record Helper (GoDaddy)</p>
<div className="space-y-1 text-xs text-[#555] font-mono"> <div className="space-y-1 text-xs text-[#555] font-mono">
<p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p> <p>SPF: <span className="text-[#888]">v=spf1 include:sendgrid.net ~all</span></p>
@@ -197,10 +197,10 @@ function DomainsContent() {
{/* Actions */} {/* Actions */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] disabled:opacity-50"> <button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] disabled:opacity-50">
{verifying === d.id ? 'Verifying…' : 'Run Full Verification'} {verifying === d.id ? 'Verifying…' : 'Run Full Verification'}
</button> </button>
<button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white disabled:opacity-30"> <button onClick={() => toggleActive(d.id, d.is_active)} disabled={!d.dns_fully_verified && !d.bypass_verification && !d.is_active} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white disabled:opacity-30">
{d.is_active ? 'Deactivate' : 'Activate'} {d.is_active ? 'Deactivate' : 'Activate'}
</button> </button>
<button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50"> <button onClick={() => setShowBypass(d.id)} className="px-3 py-1.5 bg-yellow-900/30 border border-yellow-400/20 rounded text-xs text-yellow-400 hover:bg-yellow-900/50">
@@ -219,7 +219,7 @@ function DomainsContent() {
export default function EmailDomainsPage() { export default function EmailDomainsPage() {
return ( return (
<Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>}> <Suspense fallback={<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>}>
<DomainsContent /> <DomainsContent />
</Suspense> </Suspense>
); );

View File

@@ -8,7 +8,7 @@ const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) { function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
return ( return (
<span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#1a1a1a] text-[#555]'}`}> <span className={`inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded ${valid === true ? 'bg-green-400/10 text-green-400' : valid === false ? 'bg-red-400/10 text-red-400' : 'bg-[#231d18] text-[#555]'}`}>
{valid === true ? '✓' : valid === false ? '✗' : '?'} {label} {valid === true ? '✓' : valid === false ? '✗' : '?'} {label}
</span> </span>
); );
@@ -42,7 +42,7 @@ export default function EmailDashboard() {
domains: domains.filter((d: any) => d.site === site), domains: domains.filter((d: any) => d.site === site),
})); }));
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -61,7 +61,7 @@ export default function EmailDashboard() {
{ label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' }, { label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' },
{ label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' }, { label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' },
].map(c => ( ].map(c => (
<div key={c.label} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={c.label} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<p className={`text-2xl font-bold ${c.color}`}>{c.value}</p> <p className={`text-2xl font-bold ${c.color}`}>{c.value}</p>
<p className="text-xs text-[#888] mt-1">{c.label}</p> <p className="text-xs text-[#888] mt-1">{c.label}</p>
</div> </div>
@@ -74,10 +74,10 @@ export default function EmailDashboard() {
const primary = siteDomains.find((d: any) => d.is_primary); const primary = siteDomains.find((d: any) => d.is_primary);
const suppCount = stats?.suppressions_by_site?.[site] ?? 0; const suppCount = stats?.suppressions_by_site?.[site] ?? 0;
return ( return (
<div key={site} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <div key={site} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-sm font-semibold capitalize">{site}</h3> <h3 className="text-sm font-semibold capitalize">{site}</h3>
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#ffb800] hover:underline">Manage </Link> <Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#F0A623] hover:underline">Manage </Link>
</div> </div>
<div className="text-xs text-[#888]"> <div className="text-xs text-[#888]">
<span className="text-white">{primary?.domain ?? 'No primary'}</span> <span className="text-white">{primary?.domain ?? 'No primary'}</span>
@@ -113,7 +113,7 @@ export default function EmailDashboard() {
{ label: 'Suppressions', href: '/admin/email/suppressions' }, { label: 'Suppressions', href: '/admin/email/suppressions' },
{ label: 'Send Log', href: '/admin/email/send-log' }, { label: 'Send Log', href: '/admin/email/send-log' },
].map(l => ( ].map(l => (
<Link key={l.href} href={l.href} className="bg-[#111] border border-[#252525] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#ffb800] transition-colors"> <Link key={l.href} href={l.href} className="bg-[#111] border border-[#3a322b] rounded-lg p-3 text-center text-sm text-[#888] hover:text-white hover:border-[#F0A623] transition-colors">
{l.label} {l.label}
</Link> </Link>
))} ))}

View File

@@ -29,7 +29,7 @@ export default function SendLogPage() {
useEffect(() => { if (user) load(); }, [user, load]); useEffect(() => { if (user) load(); }, [user, load]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -41,20 +41,20 @@ export default function SendLogPage() {
</div> </div>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option> <option value="">All Sites</option>
{SITES.map(s => <option key={s} value={s}>{s}</option>)} {SITES.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option> <option value="">All Statuses</option>
{['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)} {['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Site','Recipient','Subject','Type','Domain','Status','Sent At','Opens','Clicks'].map(h => ( {['Site','Recipient','Subject','Type','Domain','Status','Sent At','Opens','Clicks'].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium whitespace-nowrap">{h}</th>
))} ))}
@@ -66,14 +66,14 @@ export default function SendLogPage() {
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No send log entries</td></tr> <tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No send log entries</td></tr>
) : logs.map((l: any) => ( ) : logs.map((l: any) => (
<tr key={l.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={l.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-[#888] text-xs capitalize">{l.site}</td> <td className="px-3 py-2 text-[#888] text-xs capitalize">{l.site}</td>
<td className="px-3 py-2 text-white text-xs">{l.recipient_email}</td> <td className="px-3 py-2 text-white text-xs">{l.recipient_email}</td>
<td className="px-3 py-2 text-[#ccc] text-xs max-w-xs truncate">{l.subject ?? '—'}</td> <td className="px-3 py-2 text-[#ccc] text-xs max-w-xs truncate">{l.subject ?? '—'}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.send_type}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.send_type}</td>
<td className="px-3 py-2 text-[#555] text-xs">{l.rmp_email_sending_domains?.domain ?? '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.rmp_email_sending_domains?.domain ?? '—'}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{l.status}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${l.status === 'sent' ? 'bg-green-400/10 text-green-400' : l.status === 'failed' ? 'bg-red-400/10 text-red-400' : l.status === 'suppressed' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{l.status}</span>
</td> </td>
<td className="px-3 py-2 text-[#555] text-xs">{l.sent_at ? new Date(l.sent_at).toLocaleString() : '—'}</td> <td className="px-3 py-2 text-[#555] text-xs">{l.sent_at ? new Date(l.sent_at).toLocaleString() : '—'}</td>
<td className="px-3 py-2 text-[#888] text-xs">{l.opens}</td> <td className="px-3 py-2 text-[#888] text-xs">{l.opens}</td>

View File

@@ -72,7 +72,7 @@ export default function SuppressionsPage() {
load(); load();
} }
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -84,29 +84,29 @@ export default function SuppressionsPage() {
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link> <Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]"> Email</Link>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button> <button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
</div> </div>
</div> </div>
{/* Site selector */} {/* Site selector */}
<div className="flex gap-1 flex-wrap"> <div className="flex gap-1 flex-wrap">
{SITES.map(s => ( {SITES.map(s => (
<button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#ffb800] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{s}</button> <button key={s} onClick={() => setSite(s)} className={`px-3 py-1 rounded text-xs capitalize ${site === s ? 'bg-[#F0A623] text-white' : 'bg-[#111] border border-[#3a322b] text-[#888]'}`}>{s}</button>
))} ))}
</div> </div>
{/* Stats */} {/* Stats */}
{stats && ( {stats && (
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-red-400">{stats.total ?? 0}</p> <p className="text-xl font-bold text-red-400">{stats.total ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Total Suppressed</p> <p className="text-xs text-[#888] mt-0.5">Total Suppressed</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-yellow-400">{stats.added_this_month ?? 0}</p> <p className="text-xl font-bold text-yellow-400">{stats.added_this_month ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Added This Month</p> <p className="text-xs text-[#888] mt-0.5">Added This Month</p>
</div> </div>
<div className="bg-[#111] border border-[#252525] rounded-lg p-3"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-3">
<p className="text-xl font-bold text-orange-400">{stats.hostile_replies ?? 0}</p> <p className="text-xl font-bold text-orange-400">{stats.hostile_replies ?? 0}</p>
<p className="text-xs text-[#888] mt-0.5">Hostile Replies</p> <p className="text-xs text-[#888] mt-0.5">Hostile Replies</p>
</div> </div>
@@ -115,44 +115,44 @@ export default function SuppressionsPage() {
{/* Filters */} {/* Filters */}
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
<select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none"> <select value={filterType} onChange={e => setFilterType(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Types</option> <option value="">All Types</option>
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#ffb800]" /> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search email…" className="px-2 py-1 bg-[#111] border border-[#3a322b] rounded text-xs text-white placeholder-[#555] focus:outline-none focus:border-[#F0A623]" />
</div> </div>
{showAddForm && ( {showAddForm && (
<form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <form onSubmit={handleAddSuppression} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<h3 className="text-sm font-semibold">Add Manual Suppression</h3> <h3 className="text-sm font-semibold">Add Manual Suppression</h3>
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Email *</label> <label className="text-xs text-[#888] block mb-1">Email *</label>
<input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required type="email" value={addForm.email} onChange={e => setAddForm(p => ({ ...p, email: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={addForm.suppression_type} onChange={e => setAddForm(p => ({ ...p, suppression_type: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)} {SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
</select> </select>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<label className="text-xs text-[#888] block mb-1">Notes</label> <label className="text-xs text-[#888] block mb-1">Notes</label>
<input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input value={addForm.notes} onChange={e => setAddForm(p => ({ ...p, notes: e.target.value }))} className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
<button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888] hover:text-white">Cancel</button> <button type="button" onClick={() => setShowAddForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
</div> </div>
</form> </form>
)} )}
{/* Suppressions table */} {/* Suppressions table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
{['Email','Type','Source','Reply Text','Domain','Date',''].map(h => ( {['Email','Type','Source','Reply Text','Domain','Date',''].map(h => (
<th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th> <th key={h} className="px-3 py-2 text-left text-xs text-[#555] font-medium">{h}</th>
))} ))}
@@ -164,10 +164,10 @@ export default function SuppressionsPage() {
) : suppressions.length === 0 ? ( ) : suppressions.length === 0 ? (
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr> <tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr>
) : suppressions.map((s: any) => ( ) : suppressions.map((s: any) => (
<tr key={s.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={s.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-3 py-2 text-white text-xs">{s.email}</td> <td className="px-3 py-2 text-white text-xs">{s.email}</td>
<td className="px-3 py-2"> <td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${s.suppression_type === 'hostile_reply' ? 'bg-red-400/10 text-red-400' : s.suppression_type === 'unsubscribe' ? 'bg-yellow-400/10 text-yellow-400' : 'bg-[#231d18] text-[#555]'}`}>{s.suppression_type?.replace(/_/g, ' ')}</span>
</td> </td>
<td className="px-3 py-2 text-[#555] text-xs">{s.source}</td> <td className="px-3 py-2 text-[#555] text-xs">{s.source}</td>
<td className="px-3 py-2 text-[#555] text-xs max-w-xs truncate">{s.reply_text ?? '—'}</td> <td className="px-3 py-2 text-[#555] text-xs max-w-xs truncate">{s.reply_text ?? '—'}</td>
@@ -183,40 +183,40 @@ export default function SuppressionsPage() {
</div> </div>
{/* Keyword Management */} {/* Keyword Management */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
<h2 className="text-sm font-semibold">Suppression Keywords</h2> <h2 className="text-sm font-semibold">Suppression Keywords</h2>
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#ffb800] hover:underline">+ Add Keyword</button> <button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#F0A623] hover:underline">+ Add Keyword</button>
</div> </div>
{showKeywordForm && ( {showKeywordForm && (
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#1a1a1a] flex gap-2 items-end"> <form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#231d18] flex gap-2 items-end">
<div> <div>
<label className="text-xs text-[#888] block mb-1">Keyword *</label> <label className="text-xs text-[#888] block mb-1">Keyword *</label>
<input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#ffb800]" /> <input required value={keywordForm.keyword} onChange={e => setKeywordForm(p => ({ ...p, keyword: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none focus:border-[#F0A623]" />
</div> </div>
<div> <div>
<label className="text-xs text-[#888] block mb-1">Type</label> <label className="text-xs text-[#888] block mb-1">Type</label>
<select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none"> <select value={keywordForm.keyword_type} onChange={e => setKeywordForm(p => ({ ...p, keyword_type: e.target.value }))} className="px-2 py-1.5 bg-[#0a0a0a] border border-[#3a322b] rounded text-xs text-white focus:outline-none">
<option value="unsubscribe">unsubscribe</option> <option value="unsubscribe">unsubscribe</option>
<option value="hostile">hostile</option> <option value="hostile">hostile</option>
</select> </select>
</div> </div>
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#ffb800] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button> <button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
<button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888]">Cancel</button> <button type="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#231d18] border border-[#3a322b] rounded text-xs text-[#888]">Cancel</button>
</form> </form>
)} )}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead><tr className="border-b border-[#1a1a1a]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead> <thead><tr className="border-b border-[#231d18]">{['Keyword','Type','Active',''].map(h => <th key={h} className="px-4 py-2 text-left text-xs text-[#555]">{h}</th>)}</tr></thead>
<tbody> <tbody>
{keywords.map((k: any) => ( {keywords.map((k: any) => (
<tr key={k.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]"> <tr key={k.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
<td className="px-4 py-2 text-white text-xs">{k.keyword}</td> <td className="px-4 py-2 text-white text-xs">{k.keyword}</td>
<td className="px-4 py-2 text-[#888] text-xs">{k.keyword_type}</td> <td className="px-4 py-2 text-[#888] text-xs">{k.keyword_type}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span> <span className={`px-2 py-0.5 rounded-full text-xs ${k.active ? 'bg-green-400/10 text-green-400' : 'bg-[#231d18] text-[#555]'}`}>{k.active ? 'Active' : 'Inactive'}</span>
</td> </td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#ffb800] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button> <button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#F0A623] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
</td> </td>
</tr> </tr>
))} ))}

View File

@@ -91,7 +91,7 @@ interface StatCardProps {
function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) { function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}>
<span className={accentColor}>{icon}</span> <span className={accentColor}>{icon}</span>
@@ -116,7 +116,7 @@ function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: Stat
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -167,7 +167,7 @@ const EMPTY_STATS: ForumStats = {
}, },
}; };
const CATEGORY_COLORS = ['#ffb800', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16']; const CATEGORY_COLORS = ['#F0A623', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
export default function ForumDashboardPage() { export default function ForumDashboardPage() {
const { user, loading } = useAuth(); const { user, loading } = useAuth();
@@ -204,7 +204,7 @@ export default function ForumDashboardPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading forum dashboard</p> <p className="text-[#555] text-sm font-body">Loading forum dashboard</p>
</div> </div>
</div> </div>
@@ -236,7 +236,7 @@ export default function ForumDashboardPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={fetchStats} onClick={fetchStats}
className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#252525] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body" className="flex items-center gap-2 px-3 py-2 bg-[#111] border border-[#3a322b] rounded-lg text-xs text-[#888] hover:text-white hover:border-[#333] transition-all font-body"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
@@ -245,7 +245,7 @@ export default function ForumDashboardPage() {
</button> </button>
<Link <Link
href="/admin/forum-moderation" href="/admin/forum-moderation"
className="flex items-center gap-2 px-3 py-2 bg-[#ffb800] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold" className="flex items-center gap-2 px-3 py-2 bg-[#F0A623] rounded-lg text-xs text-white hover:bg-blue-500 transition-colors font-body font-semibold"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
@@ -386,7 +386,7 @@ export default function ForumDashboardPage() {
{/* Thread Growth Chart */} {/* Thread Growth Chart */}
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader <SectionHeader
title="Thread Growth" title="Thread Growth"
subtitle="Weekly new threads and replies over the past 8 weeks" subtitle="Weekly new threads and replies over the past 8 weeks"
@@ -398,7 +398,7 @@ export default function ForumDashboardPage() {
) : ( ) : (
<ResponsiveContainer width="100%" height={240}> <ResponsiveContainer width="100%" height={240}>
<LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}> <LineChart data={weeklyGrowth} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" /> <CartesianGrid strokeDasharray="3 3" stroke="#231d18" />
<XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} /> <XAxis dataKey="week" tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} /> <YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
iconType="circle" iconType="circle"
iconSize={8} iconSize={8}
/> />
<Line type="monotone" dataKey="threads" stroke="#ffb800" strokeWidth={2} dot={{ fill: '#ffb800', r: 3 }} name="New Threads" /> <Line type="monotone" dataKey="threads" stroke="#F0A623" strokeWidth={2} dot={{ fill: '#F0A623', r: 3 }} name="New Threads" />
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" /> <Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -419,33 +419,33 @@ export default function ForumDashboardPage() {
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Top Categories */} {/* Top Categories */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-display font-bold text-white">Top Categories</h2> <h2 className="text-sm font-display font-bold text-white">Top Categories</h2>
<p className="text-xs text-[#555] font-body mt-0.5">By thread count</p> <p className="text-xs text-[#555] font-body mt-0.5">By thread count</p>
</div> </div>
<Link href="/forum" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View forum </Link> <Link href="/forum" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">View forum </Link>
</div> </div>
{topCategories.length === 0 ? ( {topCategories.length === 0 ? (
<div className="px-5 py-10 text-center"> <div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No categories found</p> <p className="text-[#555] text-sm font-body">No categories found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{topCategories.map((cat, idx) => { {topCategories.map((cat, idx) => {
const maxThreads = topCategories[0]?.thread_count ?? 1; const maxThreads = topCategories[0]?.thread_count ?? 1;
const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0; const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0;
const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length]; const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
return ( return (
<div key={cat.id} className="px-5 py-3 hover:bg-[#0d0d0d] transition-colors"> <div key={cat.id} className="px-5 py-3 hover:bg-[#1c1815] transition-colors">
<div className="flex items-center gap-3 mb-1.5"> <div className="flex items-center gap-3 mb-1.5">
<span className="text-base leading-none">{cat.icon}</span> <span className="text-base leading-none">{cat.icon}</span>
<span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span> <span className="text-sm text-white font-body flex-1 truncate">{cat.name}</span>
<span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span> <span className="text-xs text-[#888] font-body font-semibold">{cat.thread_count} threads</span>
<span className="text-xs text-[#555] font-body">{cat.post_count} posts</span> <span className="text-xs text-[#555] font-body">{cat.post_count} posts</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden ml-7"> <div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden ml-7">
<div <div
className="h-full rounded-full transition-all duration-500" className="h-full rounded-full transition-all duration-500"
style={{ width: `${pct}%`, backgroundColor: color }} style={{ width: `${pct}%`, backgroundColor: color }}
@@ -459,8 +459,8 @@ export default function ForumDashboardPage() {
</div> </div>
{/* Active Users */} {/* Active Users */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]"> <div className="px-5 py-4 border-b border-[#231d18]">
<h2 className="text-sm font-display font-bold text-white">Active Users</h2> <h2 className="text-sm font-display font-bold text-white">Active Users</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p> <p className="text-xs text-[#555] font-body mt-0.5">Most active in the last 30 days</p>
</div> </div>
@@ -469,13 +469,13 @@ export default function ForumDashboardPage() {
<p className="text-[#555] text-sm font-body">No active users found</p> <p className="text-[#555] text-sm font-body">No active users found</p>
</div> </div>
) : ( ) : (
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{activeUsers.map((u, idx) => ( {activeUsers.map((u, idx) => (
<div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#0d0d0d] transition-colors"> <div key={u.author_id} className="px-5 py-3 flex items-center gap-3 hover:bg-[#1c1815] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]"> <div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]">
{idx + 1} {idx + 1}
</div> </div>
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#ffb800] to-[#8b5cf6] flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#F0A623] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
<span className="text-xs font-bold text-white font-body"> <span className="text-xs font-bold text-white font-body">
{u.author_name.charAt(0).toUpperCase()} {u.author_name.charAt(0).toUpperCase()}
</span> </span>
@@ -498,14 +498,14 @@ export default function ForumDashboardPage() {
{/* Category Bar Chart */} {/* Category Bar Chart */}
{topCategories.length > 0 && ( {topCategories.length > 0 && (
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader <SectionHeader
title="Category Activity" title="Category Activity"
subtitle="Thread and post distribution across categories" subtitle="Thread and post distribution across categories"
/> />
<ResponsiveContainer width="100%" height={220}> <ResponsiveContainer width="100%" height={220}>
<BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}> <BarChart data={topCategories} margin={{ top: 5, right: 10, left: -20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1a1a1a" /> <CartesianGrid strokeDasharray="3 3" stroke="#231d18" />
<XAxis <XAxis
dataKey="name" dataKey="name"
tick={{ fill: '#555', fontSize: 10 }} tick={{ fill: '#555', fontSize: 10 }}
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
iconType="circle" iconType="circle"
iconSize={8} iconSize={8}
/> />
<Bar dataKey="thread_count" fill="#ffb800" radius={[3, 3, 0, 0]} name="Threads" /> <Bar dataKey="thread_count" fill="#F0A623" radius={[3, 3, 0, 0]} name="Threads" />
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" /> <Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -533,22 +533,22 @@ export default function ForumDashboardPage() {
{/* Moderation Stats */} {/* Moderation Stats */}
<section className="mb-8"> <section className="mb-8">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2> <h2 className="text-sm font-display font-bold text-white">Moderation Stats</h2>
<p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p> <p className="text-xs text-[#555] font-body mt-0.5">Current moderation state across all forum content</p>
</div> </div>
<Link href="/admin/forum-moderation" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">Open moderation </Link> <Link href="/admin/forum-moderation" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">Open moderation </Link>
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#1a1a1a]"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 divide-x divide-y sm:divide-y-0 divide-[#231d18]">
{[ {[
{ label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: 'Flagged Threads', value: moderation.flaggedThreads, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' }, { label: 'Flagged Replies', value: moderation.flaggedReplies, color: 'text-red-400', bg: 'bg-red-500/10' },
{ label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' }, { label: 'Locked Threads', value: moderation.lockedThreads, color: 'text-orange-400', bg: 'bg-orange-500/10' },
{ label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' }, { label: 'Pinned Threads', value: moderation.pinnedThreads, color: 'text-yellow-400', bg: 'bg-yellow-500/10' },
{ label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' }, { label: 'Featured Threads', value: moderation.featuredThreads, color: 'text-blue-400', bg: 'bg-blue-500/10' },
{ label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#1a1a1a]' }, { label: 'Hidden Replies', value: moderation.hiddenReplies, color: 'text-[#888]', bg: 'bg-[#231d18]' },
].map(({ label, value, color, bg }) => ( ].map(({ label, value, color, bg }) => (
<div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center"> <div key={label} className="px-4 py-5 flex flex-col items-center gap-1 text-center">
<p className={`text-2xl font-bold font-display ${color}`}>{value}</p> <p className={`text-2xl font-bold font-display ${color}`}>{value}</p>

View File

@@ -62,7 +62,7 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
return ( return (
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4"> <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 px-4">
<div className="bg-[#1a1a1a] border border-[#333] rounded-xl p-6 w-full max-w-md"> <div className="bg-[#231d18] border border-[#333] rounded-xl p-6 w-full max-w-md">
<h3 className="text-white font-heading font-bold text-lg mb-2">Flag Content</h3> <h3 className="text-white font-heading font-bold text-lg mb-2">Flag Content</h3>
<p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p> <p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p>
<textarea <textarea
@@ -70,10 +70,10 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
onChange={e => setReason(e.target.value)} onChange={e => setReason(e.target.value)}
rows={3} rows={3}
placeholder="e.g. Spam, harassment, off-topic..." placeholder="e.g. Spam, harassment, off-topic..."
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4" className="w-full bg-[#1c1815] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ef4444] resize-none mb-4"
/> />
<div className="flex gap-3 justify-end"> <div className="flex gap-3 justify-end">
<button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#252525] hover:bg-[#333] rounded-lg transition-colors"> <button onClick={onCancel} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white bg-[#3a322b] hover:bg-[#333] rounded-lg transition-colors">
Cancel Cancel
</button> </button>
<button onClick={() => onConfirm(reason)} className="px-4 py-2 text-sm font-body font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"> <button onClick={() => onConfirm(reason)} className="px-4 py-2 text-sm font-body font-semibold text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors">
@@ -119,14 +119,14 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onCancel={() => setShowFlagModal(false)} onCancel={() => setShowFlagModal(false)}
/> />
)} )}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : thread.is_flagged ? 'border-red-500/40' : 'border-[#3a322b]'}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Title + badges */} {/* Title + badges */}
<div className="flex flex-wrap items-center gap-2 mb-1"> <div className="flex flex-wrap items-center gap-2 mb-1">
<Link <Link
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="text-white font-body font-semibold text-sm hover:text-[#ffb800] transition-colors truncate max-w-xs" className="text-white font-body font-semibold text-sm hover:text-[#F0A623] transition-colors truncate max-w-xs"
target="_blank" target="_blank"
> >
{thread.title} {thread.title}
@@ -159,7 +159,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_pinned ? 'unpin' : 'pin')} onClick={() => onAction(thread.id, thread.is_pinned ? 'unpin' : 'pin')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_pinned thread.is_pinned
? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-amber-400' ? 'bg-amber-400/20 text-amber-400 hover:bg-amber-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-amber-400'
}`} }`}
> >
{thread.is_pinned ? 'Unpin' : 'Pin'} {thread.is_pinned ? 'Unpin' : 'Pin'}
@@ -169,7 +169,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_featured ? 'unfeature' : 'feature')} onClick={() => onAction(thread.id, thread.is_featured ? 'unfeature' : 'feature')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_featured thread.is_featured
? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-yellow-400' ? 'bg-yellow-400/20 text-yellow-400 hover:bg-yellow-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-yellow-400'
}`} }`}
> >
{thread.is_featured ? 'Unfeature' : 'Feature'} {thread.is_featured ? 'Unfeature' : 'Feature'}
@@ -179,7 +179,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
onClick={() => onAction(thread.id, thread.is_locked ? 'unlock' : 'lock')} onClick={() => onAction(thread.id, thread.is_locked ? 'unlock' : 'lock')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
thread.is_locked thread.is_locked
? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-orange-400' ? 'bg-orange-400/20 text-orange-400 hover:bg-orange-400/30' :'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-orange-400'
}`} }`}
> >
{thread.is_locked ? 'Unlock' : 'Lock'} {thread.is_locked ? 'Unlock' : 'Lock'}
@@ -199,7 +199,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
<button <button
disabled={isLoading} disabled={isLoading}
onClick={() => setShowFlagModal(true)} onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50" className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-red-400 transition-colors disabled:opacity-50"
> >
Flag Flag
</button> </button>
@@ -235,7 +235,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onCancel={() => setShowFlagModal(false)} onCancel={() => setShowFlagModal(false)}
/> />
)} )}
<div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#252525]'}`}> <div className={`bg-[#111] border rounded-lg p-4 transition-colors ${isAiFlagged ? 'border-purple-500/50' : reply.is_flagged ? 'border-red-500/40' : reply.is_hidden ? 'border-[#333] opacity-60' : 'border-[#3a322b]'}`}>
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
{/* Thread link + badges */} {/* Thread link + badges */}
@@ -243,13 +243,13 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
{reply.forum_threads && ( {reply.forum_threads && (
<Link <Link
href={`/forum/thread/${reply.forum_threads.id}`} href={`/forum/thread/${reply.forum_threads.id}`}
className="text-[#ffb800] font-body text-xs hover:underline truncate max-w-xs" className="text-[#F0A623] font-body text-xs hover:underline truncate max-w-xs"
target="_blank" target="_blank"
> >
{reply.forum_threads.title} {reply.forum_threads.title}
</Link> </Link>
)} )}
{reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#252525]" />} {reply.is_hidden && <StatusBadge label="👁 Hidden" color="text-[#888] bg-[#3a322b]" />}
{isAiFlagged && <StatusBadge label="🤖 AI Flagged" color="text-purple-400 bg-purple-400/10" />} {isAiFlagged && <StatusBadge label="🤖 AI Flagged" color="text-purple-400 bg-purple-400/10" />}
{reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />} {reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />}
</div> </div>
@@ -273,8 +273,8 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
onClick={() => onAction(reply.id, reply.is_hidden ? 'unhide' : 'hide')} onClick={() => onAction(reply.id, reply.is_hidden ? 'unhide' : 'hide')}
className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${ className={`px-2.5 py-1 text-xs font-body font-semibold rounded-md transition-colors disabled:opacity-50 ${
reply.is_hidden reply.is_hidden
? 'bg-[#252525] text-[#aaa] hover:bg-[#333]' ? 'bg-[#3a322b] text-[#aaa] hover:bg-[#333]'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white' : 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white'
}`} }`}
> >
{reply.is_hidden ? 'Unhide' : 'Hide'} {reply.is_hidden ? 'Unhide' : 'Hide'}
@@ -294,7 +294,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
<button <button
disabled={isLoading} disabled={isLoading}
onClick={() => setShowFlagModal(true)} onClick={() => setShowFlagModal(true)}
className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-red-400 transition-colors disabled:opacity-50" className="px-2.5 py-1 text-xs font-body font-semibold rounded-md bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-red-400 transition-colors disabled:opacity-50"
> >
Flag Flag
</button> </button>
@@ -398,7 +398,7 @@ export default function ForumModerationPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -435,10 +435,10 @@ export default function ForumModerationPage() {
)} )}
{/* Page Header */} {/* Page Header */}
<div className="bg-[#1a1208] border-b border-[#2a3a50]"> <div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2"> <nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link> <Link href="/admin" className="hover:text-[#F0A623] transition-colors">Admin</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">Forum Moderation</span> <span className="text-[#aab4c4]">Forum Moderation</span>
</nav> </nav>
@@ -453,7 +453,7 @@ export default function ForumModerationPage() {
<span className="text-xs text-purple-400 font-body bg-purple-400/10 border border-purple-400/20 px-3 py-1.5 rounded-lg"> <span className="text-xs text-purple-400 font-body bg-purple-400/10 border border-purple-400/20 px-3 py-1.5 rounded-lg">
🤖 Claude AI Auto-Moderation Active 🤖 Claude AI Auto-Moderation Active
</span> </span>
<span className="text-xs text-[#555] font-body bg-[#1a1a1a] border border-[#252525] px-3 py-1.5 rounded-lg"> <span className="text-xs text-[#555] font-body bg-[#231d18] border border-[#3a322b] px-3 py-1.5 rounded-lg">
🛡 Moderator View 🛡 Moderator View
</span> </span>
</div> </div>
@@ -463,14 +463,14 @@ export default function ForumModerationPage() {
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit mb-6"> <div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit mb-6">
{(['threads', 'replies'] as Tab[]).map(tab => ( {(['threads', 'replies'] as Tab[]).map(tab => (
<button <button
key={tab} key={tab}
onClick={() => { setActiveTab(tab); setPage(1); }} onClick={() => { setActiveTab(tab); setPage(1); }}
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${ className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
activeTab === tab activeTab === tab
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -488,8 +488,8 @@ export default function ForumModerationPage() {
onClick={() => { setThreadFilter(f.value); setPage(1); }} onClick={() => { setThreadFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
threadFilter === f.value threadFilter === f.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]' : 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
}`} }`}
> >
{f.label} {f.label}
@@ -501,8 +501,8 @@ export default function ForumModerationPage() {
onClick={() => { setReplyFilter(f.value); setPage(1); }} onClick={() => { setReplyFilter(f.value); setPage(1); }}
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
replyFilter === f.value replyFilter === f.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]' : 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
}`} }`}
> >
{f.label} {f.label}
@@ -515,7 +515,7 @@ export default function ForumModerationPage() {
{/* Content */} {/* Content */}
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-7 h-7 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-7 h-7 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : activeTab === 'threads' ? ( ) : activeTab === 'threads' ? (
<> <>
@@ -559,7 +559,7 @@ export default function ForumModerationPage() {
<button <button
disabled={page === 1} disabled={page === 1}
onClick={() => setPage(p => p - 1)} onClick={() => setPage(p => p - 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors" className="px-4 py-2 text-sm font-body text-[#888] bg-[#231d18] border border-[#3a322b] rounded-lg hover:bg-[#3a322b] disabled:opacity-40 transition-colors"
> >
Prev Prev
</button> </button>
@@ -567,7 +567,7 @@ export default function ForumModerationPage() {
<button <button
disabled={page === totalPages} disabled={page === totalPages}
onClick={() => setPage(p => p + 1)} onClick={() => setPage(p => p + 1)}
className="px-4 py-2 text-sm font-body text-[#888] bg-[#1a1a1a] border border-[#252525] rounded-lg hover:bg-[#252525] disabled:opacity-40 transition-colors" className="px-4 py-2 text-sm font-body text-[#888] bg-[#231d18] border border-[#3a322b] rounded-lg hover:bg-[#3a322b] disabled:opacity-40 transition-colors"
> >
Next Next
</button> </button>

View File

@@ -136,13 +136,13 @@ export default function AdminForumSeedPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="bg-[#1a1208] border-b border-[#2a3a50]"> <div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-6"> <div className="max-w-container mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2"> <nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#ffb800] transition-colors">Admin</Link> <Link href="/admin" className="hover:text-[#F0A623] transition-colors">Admin</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<Link href="/forum" className="hover:text-[#ffb800] transition-colors">Forum</Link> <Link href="/forum" className="hover:text-[#F0A623] transition-colors">Forum</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">Seed Tool</span> <span className="text-[#aab4c4]">Seed Tool</span>
</nav> </nav>
@@ -157,17 +157,17 @@ export default function AdminForumSeedPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Editor */} {/* Left: Editor */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="flex border-b border-[#252525]"> <div className="flex border-b border-[#3a322b]">
<button <button
onClick={() => setActiveTab('import')} onClick={() => setActiveTab('import')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#ffb800]' : 'text-[#666] hover:text-[#aaa]'}`} className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#F0A623]' : 'text-[#666] hover:text-[#aaa]'}`}
> >
Import Editor Import Editor
</button> </button>
<button <button
onClick={() => setActiveTab('json')} onClick={() => setActiveTab('json')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#ffb800]' : 'text-[#666] hover:text-[#aaa]'}`} className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#F0A623]' : 'text-[#666] hover:text-[#aaa]'}`}
> >
JSON Schema JSON Schema
</button> </button>
@@ -182,20 +182,20 @@ export default function AdminForumSeedPage() {
value={seedData} value={seedData}
onChange={e => setSeedData(e.target.value)} onChange={e => setSeedData(e.target.value)}
rows={24} rows={24}
className="w-full bg-[#0d0d0d] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#1c1815] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#F0A623] resize-none"
spellCheck={false} spellCheck={false}
/> />
<div className="flex gap-3 mt-3"> <div className="flex gap-3 mt-3">
<button <button
onClick={handleImport} onClick={handleImport}
disabled={importing} disabled={importing}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
{importing ? 'Importing...' : 'Import Threads'} {importing ? 'Importing...' : 'Import Threads'}
</button> </button>
<button <button
onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))} onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))}
className="bg-[#252525] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors" className="bg-[#3a322b] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors"
> >
Reset to Templates Reset to Templates
</button> </button>
@@ -206,7 +206,7 @@ export default function AdminForumSeedPage() {
{activeTab === 'json' && ( {activeTab === 'json' && (
<div className="p-4"> <div className="p-4">
<p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p> <p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p>
<pre className="bg-[#0d0d0d] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto"> <pre className="bg-[#1c1815] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto">
{`[ {`[
{ {
"category_slug": "live-production", "category_slug": "live-production",
@@ -226,7 +226,7 @@ export default function AdminForumSeedPage() {
<h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4> <h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => ( {['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => (
<code key={slug} className="bg-[#0d0d0d] border border-[#333] text-[#ffb800] font-mono text-xs px-2 py-1 rounded">{slug}</code> <code key={slug} className="bg-[#1c1815] border border-[#333] text-[#F0A623] font-mono text-xs px-2 py-1 rounded">{slug}</code>
))} ))}
</div> </div>
</div> </div>
@@ -239,7 +239,7 @@ export default function AdminForumSeedPage() {
<div className="space-y-4"> <div className="space-y-4">
{/* Import Results */} {/* Import Results */}
{results.length > 0 && ( {results.length > 0 && (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Import Results</h3> <h3 className="text-white font-heading font-semibold mb-3">Import Results</h3>
<div className="flex gap-4 mb-3"> <div className="flex gap-4 mb-3">
<div className="text-center"> <div className="text-center">
@@ -265,20 +265,20 @@ export default function AdminForumSeedPage() {
)} )}
{/* Quick Links */} {/* Quick Links */}
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-4"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3> <h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3>
<div className="space-y-2"> <div className="space-y-2">
<Link href="/forum" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm"> <Link href="/forum" className="flex items-center gap-2 text-[#F0A623] hover:underline font-body text-sm">
<span></span> View Forum <span></span> View Forum
</Link> </Link>
<Link href="/admin" className="flex items-center gap-2 text-[#ffb800] hover:underline font-body text-sm"> <Link href="/admin" className="flex items-center gap-2 text-[#F0A623] hover:underline font-body text-sm">
<span></span> Admin Dashboard <span></span> Admin Dashboard
</Link> </Link>
</div> </div>
</div> </div>
{/* Tips */} {/* Tips */}
<div className="bg-[#1a1208] border border-[#2a3a50] rounded-lg p-4"> <div className="bg-[#231d18] border border-[#2a3a50] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3> <h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3>
<ul className="text-[#aab4c4] font-body text-xs space-y-1.5"> <ul className="text-[#aab4c4] font-body text-xs space-y-1.5">
<li> Threads are imported as real posts they appear immediately in the forum</li> <li> Threads are imported as real posts they appear immediately in the forum</li>

View File

@@ -134,7 +134,7 @@ export default function WPImportPage() {
if (loading || (!user && !loading)) { if (loading || (!user && !loading)) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -142,10 +142,10 @@ export default function WPImportPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]"> <div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]">
{/* Page Header */} {/* Page Header */}
<div className="bg-[#111111] border-b border-[#252525]"> <div className="bg-[#1c1815] border-b border-[#3a322b]">
<div className="max-w-6xl mx-auto px-4 py-5"> <div className="max-w-6xl mx-auto px-4 py-5">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
<Link href="/home-page" className="text-[#555] hover:text-[#ffb800] transition-colors text-sm"> <Link href="/home-page" className="text-[#555] hover:text-[#F0A623] transition-colors text-sm">
Home Home
</Link> </Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
@@ -158,14 +158,14 @@ export default function WPImportPage() {
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1> <h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
<p className="text-[#666] text-sm mt-1"> <p className="text-[#666] text-sm mt-1">
Import posts from{' '} Import posts from{' '}
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#ffb800] hover:underline"> <a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#F0A623] hover:underline">
avbeat.com avbeat.com
</a>{' '} </a>{' '}
via WordPress REST API via WordPress REST API
</p> </p>
</div> </div>
<div className="flex items-center gap-2 text-xs text-[#555] bg-[#1a1a1a] border border-[#252525] rounded px-3 py-2"> <div className="flex items-center gap-2 text-xs text-[#555] bg-[#231d18] border border-[#3a322b] rounded px-3 py-2">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" /> <circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
</svg> </svg>
{posts.length} posts imported {posts.length} posts imported
@@ -176,9 +176,9 @@ export default function WPImportPage() {
<div className="max-w-6xl mx-auto px-4 py-6 space-y-6"> <div className="max-w-6xl mx-auto px-4 py-6 space-y-6">
{/* Import Controls */} {/* Import Controls */}
<div className="bg-[#111111] border border-[#252525] rounded-lg p-5"> <div className="bg-[#1c1815] border border-[#3a322b] rounded-lg p-5">
<h2 className="text-base font-bold text-white font-heading mb-4 flex items-center gap-2"> <h2 className="text-base font-bold text-white font-heading mb-4 flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#F0A623]">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" /> <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><polyline points="7 10 12 15 17 10" /><line x1="12" y1="15" x2="12" y2="3" />
</svg> </svg>
Run Import Run Import
@@ -191,7 +191,7 @@ export default function WPImportPage() {
value={maxPages} value={maxPages}
onChange={(e) => setMaxPages(Number(e.target.value))} onChange={(e) => setMaxPages(Number(e.target.value))}
disabled={isImporting} disabled={isImporting}
className="bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] disabled:opacity-50"> className="bg-[#231d18] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] disabled:opacity-50">
<option value={1}>1 page (~20 posts)</option> <option value={1}>1 page (~20 posts)</option>
<option value={3}>3 pages (~60 posts)</option> <option value={3}>3 pages (~60 posts)</option>
<option value={5}>5 pages (~100 posts)</option> <option value={5}>5 pages (~100 posts)</option>
@@ -204,7 +204,7 @@ export default function WPImportPage() {
<button <button
onClick={handleImport} onClick={handleImport}
disabled={isImporting} disabled={isImporting}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] disabled:bg-[#4a3500] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors"> className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] disabled:bg-[#412402] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
{isImporting ? ( {isImporting ? (
<> <>
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
@@ -223,16 +223,16 @@ export default function WPImportPage() {
{/* Import Progress / Result */} {/* Import Progress / Result */}
{isImporting && ( {isImporting && (
<div className="mt-4 bg-[#0d1a2e] border border-[#4a3500] rounded p-4"> <div className="mt-4 bg-[#0d1a2e] border border-[#412402] rounded p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-5 h-5 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin flex-shrink-0" /> <div className="w-5 h-5 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div> <div>
<p className="text-[#ffb800] text-sm font-bold">Import in progress</p> <p className="text-[#F0A623] text-sm font-bold">Import in progress</p>
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p> <p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
</div> </div>
</div> </div>
<div className="mt-3 h-1.5 bg-[#1a1208] rounded-full overflow-hidden"> <div className="mt-3 h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div className="h-full bg-[#ffb800] rounded-full animate-pulse w-2/3" /> <div className="h-full bg-[#F0A623] rounded-full animate-pulse w-2/3" />
</div> </div>
</div> </div>
)} )}
@@ -261,7 +261,7 @@ export default function WPImportPage() {
{[ {[
{ label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' }, { label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' },
{ label: 'Imported', value: importResult.totalImported, color: 'text-green-400' }, { label: 'Imported', value: importResult.totalImported, color: 'text-green-400' },
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#ffb800]' }, { label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#F0A623]' },
{ label: 'Errors', value: importResult.totalErrors, color: importResult.totalErrors ? 'text-red-400' : 'text-[#555]' }, { label: 'Errors', value: importResult.totalErrors, color: importResult.totalErrors ? 'text-red-400' : 'text-[#555]' },
].map((stat) => ( ].map((stat) => (
<div key={stat.label} className="bg-[#111] rounded p-2.5 text-center"> <div key={stat.label} className="bg-[#111] rounded p-2.5 text-center">
@@ -277,14 +277,14 @@ export default function WPImportPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-0 border-b border-[#252525]"> <div className="flex gap-0 border-b border-[#3a322b]">
{(['posts', 'logs'] as const).map((tab) => ( {(['posts', 'logs'] as const).map((tab) => (
<button <button
key={tab} key={tab}
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${ className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
activeTab === tab activeTab === tab
? 'border-[#ffb800] text-[#ffb800]' ? 'border-[#F0A623] text-[#F0A623]'
: 'border-transparent text-[#666] hover:text-[#999]' : 'border-transparent text-[#666] hover:text-[#999]'
}`}> }`}>
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`} {tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
@@ -306,17 +306,17 @@ export default function WPImportPage() {
placeholder="Search posts..." placeholder="Search posts..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="w-full bg-[#111] border border-[#3a322b] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
/> />
</div> </div>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredPosts.length === 0 ? ( ) : filteredPosts.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg"> <div className="text-center py-16 bg-[#111] border border-[#3a322b] rounded-lg">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333] mx-auto mb-3"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333] mx-auto mb-3">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" /> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
</svg> </svg>
@@ -327,10 +327,10 @@ export default function WPImportPage() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{filteredPosts.map((post) => ( {filteredPosts.map((post) => (
<div key={post.id} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors"> <div key={post.id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex items-start gap-4 hover:border-[#333] transition-colors">
{/* Thumbnail */} {/* Thumbnail */}
{post.featured_image ? ( {post.featured_image ? (
<div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#1a1a1a]"> <div className="flex-shrink-0 w-16 h-12 rounded overflow-hidden bg-[#231d18]">
<AppImage <AppImage
src={post.featured_image} src={post.featured_image}
alt={post.featured_image_alt || post.title} alt={post.featured_image_alt || post.title}
@@ -340,7 +340,7 @@ export default function WPImportPage() {
/> />
</div> </div>
) : ( ) : (
<div className="flex-shrink-0 w-16 h-12 rounded bg-[#1a1a1a] flex items-center justify-center"> <div className="flex-shrink-0 w-16 h-12 rounded bg-[#231d18] flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-[#333]">
<rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" /> <rect x="3" y="3" width="18" height="18" rx="2" /><circle cx="8.5" cy="8.5" r="1.5" /><polyline points="21 15 16 10 5 21" />
</svg> </svg>
@@ -354,7 +354,7 @@ export default function WPImportPage() {
<h3 className="text-sm font-bold text-[#cccccc] leading-snug line-clamp-1">{post.title}</h3> <h3 className="text-sm font-bold text-[#cccccc] leading-snug line-clamp-1">{post.title}</h3>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1"> <div className="flex flex-wrap items-center gap-x-3 gap-y-1 mt-1">
{post.category && ( {post.category && (
<span className="text-[10px] font-bold uppercase tracking-wider text-[#ffb800] bg-[#0d1a2e] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-wider text-[#F0A623] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
{post.category} {post.category}
</span> </span>
)} )}
@@ -368,7 +368,7 @@ export default function WPImportPage() {
href={`https://www.avbeat.com/${post.wp_slug}`} href={`https://www.avbeat.com/${post.wp_slug}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#555] hover:text-[#ffb800] transition-colors p-1" className="text-[#555] hover:text-[#F0A623] transition-colors p-1"
title="View on avbeat.com"> title="View on avbeat.com">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" /> <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
@@ -402,16 +402,16 @@ export default function WPImportPage() {
<div> <div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg"> <div className="text-center py-16 bg-[#111] border border-[#3a322b] rounded-lg">
<p className="text-[#555] text-sm">No import history yet.</p> <p className="text-[#555] text-sm">No import history yet.</p>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{logs.map((log) => ( {logs.map((log) => (
<div key={log.id} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={log.id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{log.status === 'completed' ? ( {log.status === 'completed' ? (
@@ -439,7 +439,7 @@ export default function WPImportPage() {
{ label: 'Skipped', value: log.total_skipped }, { label: 'Skipped', value: log.total_skipped },
{ label: 'Errors', value: log.total_errors }, { label: 'Errors', value: log.total_errors },
].map((stat) => ( ].map((stat) => (
<div key={stat.label} className="bg-[#0d0d0d] rounded p-2 text-center"> <div key={stat.label} className="bg-[#1c1815] rounded p-2 text-center">
<p className="text-base font-bold text-[#cccccc]">{stat.value}</p> <p className="text-base font-bold text-[#cccccc]">{stat.value}</p>
<p className="text-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p> <p className="text-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p>
</div> </div>

View File

@@ -67,11 +67,11 @@ function fmtDate(dateStr: string): string {
// ─── Sub-components ─────────────────────────────────────────────────────────── // ─── Sub-components ───────────────────────────────────────────────────────────
function StatCard({ label, value, sub, accent = '#ffb800', icon }: { function StatCard({ label, value, sub, accent = '#F0A623', icon }: {
label: string; value: string; sub: string; accent?: string; icon: React.ReactNode; label: string; value: string; sub: string; accent?: string; icon: React.ReactNode;
}) { }) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-5 flex items-start gap-4">
<div <div
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0" className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
style={{ background: `${accent}18` }} style={{ background: `${accent}18` }}
@@ -90,7 +90,7 @@ function StatCard({ label, value, sub, accent = '#ffb800', icon }: {
function ChartTooltip({ active, payload, label }: any) { function ChartTooltip({ active, payload, label }: any) {
if (!active || !payload?.length) return null; if (!active || !payload?.length) return null;
return ( return (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl"> <div className="bg-[#231d18] border border-[#333] rounded-lg px-3 py-2 text-xs font-body shadow-xl">
{label && <p className="text-[#888] mb-1">{label}</p>} {label && <p className="text-[#888] mb-1">{label}</p>}
{payload.map((entry: any, i: number) => ( {payload.map((entry: any, i: number) => (
<p key={i} style={{ color: entry.color }} className="leading-5"> <p key={i} style={{ color: entry.color }} className="leading-5">
@@ -243,7 +243,7 @@ export default function NewsletterAnalyticsPage() {
if (loading || loadingData) { if (loading || loadingData) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -258,7 +258,7 @@ export default function NewsletterAnalyticsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-white"> <div className="min-h-screen bg-[#0a0a0a] text-white">
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0d0d0d] sticky top-0 z-10"> <div className="border-b border-[#231d18] bg-[#1c1815] sticky top-0 z-10">
<div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-white transition-colors"> <Link href="/admin" className="text-[#555] hover:text-white transition-colors">
@@ -266,7 +266,7 @@ export default function NewsletterAnalyticsPage() {
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" /> <path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
</svg> </svg>
</Link> </Link>
<div className="w-px h-5 bg-[#252525]" /> <div className="w-px h-5 bg-[#3a322b]" />
<Link href="/admin/newsletter" className="text-[#555] hover:text-white text-sm font-body transition-colors"> <Link href="/admin/newsletter" className="text-[#555] hover:text-white text-sm font-body transition-colors">
Newsletter Newsletter
</Link> </Link>
@@ -278,14 +278,14 @@ export default function NewsletterAnalyticsPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
Templates Templates
</Link> </Link>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right"> <div className="text-right">
<p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p> <p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p> <p className="text-[#555] text-xs font-body">active subscribers</p>
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
label="Avg Open Rate" label="Avg Open Rate"
value={pct(avgOpenRate)} value={pct(avgOpenRate)}
sub={`across ${digestData.length} digests`} sub={`across ${digestData.length} digests`}
accent="#ffb800" accent="#F0A623"
icon={ icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
@@ -352,7 +352,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Open & Click Rate Trend */} {/* Open & Click Rate Trend */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader title="Open & Click Rate Trend" subtitle="Rate per digest over time" /> <SectionHeader title="Open & Click Rate Trend" subtitle="Rate per digest over time" />
<div className="h-64"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -362,7 +362,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} /> <YAxis tick={{ fill: '#555', fontSize: 10 }} tickLine={false} axisLine={false} tickFormatter={(v) => `${v}%`} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Line type="monotone" dataKey="Open Rate" stroke="#ffb800" strokeWidth={2} dot={{ r: 3, fill: '#ffb800' }} activeDot={{ r: 5 }} /> <Line type="monotone" dataKey="Open Rate" stroke="#F0A623" strokeWidth={2} dot={{ r: 3, fill: '#F0A623' }} activeDot={{ r: 5 }} />
<Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} /> <Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -370,7 +370,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Subscriber Growth Chart */} {/* Subscriber Growth Chart */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-6"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-6">
<SectionHeader title="Subscriber Growth" subtitle="Monthly net new subscribers vs unsubscribes" /> <SectionHeader title="Subscriber Growth" subtitle="Monthly net new subscribers vs unsubscribes" />
<div className="h-64"> <div className="h-64">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
@@ -380,7 +380,7 @@ export default function NewsletterAnalyticsPage() {
<YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} /> <YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} />
<Tooltip content={<ChartTooltip />} /> <Tooltip content={<ChartTooltip />} />
<Legend wrapperStyle={{ fontSize: 11, color: '#888' }} /> <Legend wrapperStyle={{ fontSize: 11, color: '#888' }} />
<Bar dataKey="subscribers" name="Total Subscribers" fill="#ffb800" radius={[3, 3, 0, 0]} /> <Bar dataKey="subscribers" name="Total Subscribers" fill="#F0A623" radius={[3, 3, 0, 0]} />
<Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} /> <Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} />
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
@@ -388,7 +388,7 @@ export default function NewsletterAnalyticsPage() {
</div> </div>
{/* Per-Digest Performance Table */} {/* Per-Digest Performance Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-6 py-5 border-b border-[#1e1e1e]"> <div className="px-6 py-5 border-b border-[#1e1e1e]">
<SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" /> <SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" />
</div> </div>
@@ -421,7 +421,7 @@ export default function NewsletterAnalyticsPage() {
{sortedDigests.map((digest, i) => ( {sortedDigests.map((digest, i) => (
<tr <tr
key={digest.id} key={digest.id}
className={`border-b border-[#1a1a1a] hover:bg-[#161616] transition-colors ${i % 2 === 0 ? '' : 'bg-[#0d0d0d]'}`} className={`border-b border-[#231d18] hover:bg-[#231d18] transition-colors ${i % 2 === 0 ? '' : 'bg-[#1c1815]'}`}
> >
<td className="px-4 py-3 text-white max-w-xs"> <td className="px-4 py-3 text-white max-w-xs">
<p className="truncate font-medium" title={digest.subject}>{digest.subject}</p> <p className="truncate font-medium" title={digest.subject}>{digest.subject}</p>

View File

@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
article_blocks: payload.article_blocks, article_blocks: payload.article_blocks,
layout: 'featured', layout: 'featured',
style: 'dark', style: 'dark',
accent_color: '#ffb800', accent_color: '#F0A623',
is_preset: false, is_preset: false,
}), }),
}); });
@@ -296,7 +296,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Top Bar ── */} {/* ── Top Bar ── */}
<div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#1a1a1a]"> <div className="sticky top-0 z-40 bg-[#0a0a0a]/95 backdrop-blur border-b border-[#231d18]">
<div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4"> <div className="max-w-screen-xl mx-auto px-4 h-14 flex items-center justify-between gap-4">
<div className="flex items-center gap-3 min-w-0"> <div className="flex items-center gap-3 min-w-0">
<Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0"> <Link href="/admin/newsletter" className="text-[#555] hover:text-white transition-colors flex-shrink-0">
@@ -313,7 +313,7 @@ export default function CampaignEditorPage() {
className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48" className="bg-transparent text-sm font-medium text-white placeholder-[#444] outline-none min-w-0 w-48"
/> />
<span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${ <span className={`text-xs px-2 py-0.5 rounded-full font-medium flex-shrink-0 ${
campaign.status === 'draft' ? 'bg-[#1a1a1a] text-[#666]' : campaign.status === 'draft' ? 'bg-[#231d18] text-[#666]' :
campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400' campaign.status === 'scheduled'? 'bg-yellow-400/10 text-yellow-400' : 'bg-green-400/10 text-green-400'
}`}> }`}>
{campaign.status} {campaign.status}
@@ -339,7 +339,7 @@ export default function CampaignEditorPage() {
<button <button
onClick={() => handleSave('draft')} onClick={() => handleSave('draft')}
disabled={saving} disabled={saving}
className="px-3 py-1.5 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50" className="px-3 py-1.5 text-xs font-medium bg-[#231d18] hover:bg-[#222] border border-[#3a322b] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
> >
{saving ? 'Saving…' : 'Save Draft'} {saving ? 'Saving…' : 'Save Draft'}
</button> </button>
@@ -393,9 +393,9 @@ export default function CampaignEditorPage() {
<div className="space-y-2"> <div className="space-y-2">
{loadingArticles ? ( {loadingArticles ? (
Array.from({ length: 5 }).map((_, i) => ( Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4 animate-pulse"> <div key={i} className="bg-[#111] border border-[#231d18] rounded-xl p-4 animate-pulse">
<div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" /> <div className="h-3 bg-[#1e1e1e] rounded w-3/4 mb-2" />
<div className="h-2.5 bg-[#1a1a1a] rounded w-1/2" /> <div className="h-2.5 bg-[#231d18] rounded w-1/2" />
</div> </div>
)) ))
) : articles.length === 0 ? ( ) : articles.length === 0 ? (
@@ -409,7 +409,7 @@ export default function CampaignEditorPage() {
onClick={() => toggleArticle(article)} onClick={() => toggleArticle(article)}
className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${ className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${
selected selected
? 'border-white/20 bg-white/5' :'border-[#1a1a1a] hover:border-[#2a2a2a]' ? 'border-white/20 bg-white/5' :'border-[#231d18] hover:border-[#3a322b]'
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
@@ -427,7 +427,7 @@ export default function CampaignEditorPage() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
{article.category && ( {article.category && (
<span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-semibold uppercase tracking-wider text-[#555] bg-[#231d18] px-1.5 py-0.5 rounded">
{article.category} {article.category}
</span> </span>
)} )}
@@ -454,7 +454,7 @@ export default function CampaignEditorPage() {
{(activePanel === 'settings' || activePanel === 'articles') && ( {(activePanel === 'settings' || activePanel === 'articles') && (
<> <>
{/* Subject Line */} {/* Subject Line */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Subject Line</h3>
<span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}> <span className={`text-[10px] font-mono ${subjectCharCount > 60 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -477,7 +477,7 @@ export default function CampaignEditorPage() {
</div> </div>
{/* Preview Text */} {/* Preview Text */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Preview Text</h3>
<span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}> <span className={`text-[10px] font-mono ${previewCharCount > 90 ? 'text-yellow-400' : 'text-[#444]'}`}>
@@ -497,7 +497,7 @@ export default function CampaignEditorPage() {
</div> </div>
{/* Recipient Segment */} {/* Recipient Segment */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Recipient Segment</h3>
<div className="space-y-2"> <div className="space-y-2">
{SEGMENTS.map((seg) => { {SEGMENTS.map((seg) => {
@@ -509,7 +509,7 @@ export default function CampaignEditorPage() {
onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))} onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))}
className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${ className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${
active active
? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#1a1a1a] text-[#888] hover:border-[#2a2a2a] hover:text-white' ? 'bg-white/5 border-white/20 text-white' :'bg-[#0a0a0a] border-[#231d18] text-[#888] hover:border-[#3a322b] hover:text-white'
}`} }`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@@ -529,7 +529,7 @@ export default function CampaignEditorPage() {
})} })}
</div> </div>
{recipientCount > 0 && ( {recipientCount > 0 && (
<div className="mt-3 pt-3 border-t border-[#1a1a1a] flex items-center gap-2"> <div className="mt-3 pt-3 border-t border-[#231d18] flex items-center gap-2">
<svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
@@ -543,7 +543,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Selected Articles ── */} {/* ── Selected Articles ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Selected Articles</h3>
<span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span> <span className="text-[10px] text-[#555]">{campaign.articles.length} article{campaign.articles.length !== 1 ? 's' : ''}</span>
@@ -559,7 +559,7 @@ export default function CampaignEditorPage() {
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
{campaign.articles.map((article, idx) => ( {campaign.articles.map((article, idx) => (
<div key={article.id} className="bg-[#0a0a0a] border border-[#1a1a1a] rounded-lg p-3"> <div key={article.id} className="bg-[#0a0a0a] border border-[#231d18] rounded-lg p-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
{/* Order controls */} {/* Order controls */}
<div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5"> <div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5">
@@ -618,16 +618,16 @@ export default function CampaignEditorPage() {
{/* ── Preview Panel ── */} {/* ── Preview Panel ── */}
{activePanel === 'preview' && ( {activePanel === 'preview' && (
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl overflow-hidden"> <div className="bg-[#111] border border-[#231d18] rounded-xl overflow-hidden">
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-4 py-3 border-b border-[#231d18] flex items-center justify-between">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider">Email Preview</h3>
<span className="text-[10px] text-[#555]">Inbox view</span> <span className="text-[10px] text-[#555]">Inbox view</span>
</div> </div>
{/* Inbox row preview */} {/* Inbox row preview */}
<div className="px-4 py-3 border-b border-[#1a1a1a] bg-[#0d0d0d]"> <div className="px-4 py-3 border-b border-[#231d18] bg-[#1c1815]">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0"> <div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0">
<span className="text-[8px] font-bold text-[#666]">BB</span> <span className="text-[8px] font-bold text-[#666]">BB</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -644,7 +644,7 @@ export default function CampaignEditorPage() {
{/* Email body preview */} {/* Email body preview */}
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto"> <div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
{/* Header */} {/* Header */}
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]"> <div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#231d18]">
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p> <p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
{campaign.previewText && ( {campaign.previewText && (
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p> <p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
@@ -657,7 +657,7 @@ export default function CampaignEditorPage() {
) : ( ) : (
campaign.articles.map((article, idx) => ( campaign.articles.map((article, idx) => (
<div key={article.id} className={`rounded-lg border p-3 ${ <div key={article.id} className={`rounded-lg border p-3 ${
article.featured ? 'border-white/10 bg-white/3' : 'border-[#1a1a1a] bg-[#0a0a0a]' article.featured ? 'border-white/10 bg-white/3' : 'border-[#231d18] bg-[#0a0a0a]'
}`}> }`}>
{article.featured && ( {article.featured && (
<span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span> <span className="text-[9px] font-semibold uppercase tracking-wider text-yellow-400 mb-1 block">Featured Story</span>
@@ -679,7 +679,7 @@ export default function CampaignEditorPage() {
)} )}
{/* Footer */} {/* Footer */}
<div className="text-center pt-2 border-t border-[#1a1a1a]"> <div className="text-center pt-2 border-t border-[#231d18]">
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p> <p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
</div> </div>
</div> </div>
@@ -687,7 +687,7 @@ export default function CampaignEditorPage() {
)} )}
{/* ── Publish Summary ── */} {/* ── Publish Summary ── */}
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl p-4"> <div className="bg-[#111] border border-[#231d18] rounded-xl p-4">
<h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3> <h3 className="text-xs font-semibold text-white uppercase tracking-wider mb-3">Campaign Summary</h3>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
@@ -710,7 +710,7 @@ export default function CampaignEditorPage() {
<button <button
onClick={() => handleSave('draft')} onClick={() => handleSave('draft')}
disabled={saving} disabled={saving}
className="flex-1 py-2 text-xs font-medium bg-[#1a1a1a] hover:bg-[#222] border border-[#2a2a2a] text-[#aaa] rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 text-xs font-medium bg-[#231d18] hover:bg-[#222] border border-[#3a322b] text-[#aaa] rounded-lg transition-colors disabled:opacity-50"
> >
{saving ? 'Saving…' : 'Save Draft'} {saving ? 'Saving…' : 'Save Draft'}
</button> </button>

View File

@@ -356,7 +356,7 @@ export default function AdminNewsletterPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -373,7 +373,7 @@ export default function AdminNewsletterPage() {
)} )}
{/* Header */} {/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4"> <div className="bg-[#111] border-b border-[#3a322b] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between"> <div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors"> <Link href="/admin" className="text-[#555] hover:text-[#888] transition-colors">
@@ -389,7 +389,7 @@ export default function AdminNewsletterPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/newsletter/templates" href="/admin/newsletter/templates"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
@@ -397,7 +397,7 @@ export default function AdminNewsletterPage() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/campaign-editor" href="/admin/newsletter/campaign-editor"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#ffb800] hover:bg-[#d99700] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-white bg-[#F0A623] hover:bg-[#BA7517] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
</svg> </svg>
@@ -405,18 +405,18 @@ export default function AdminNewsletterPage() {
</Link> </Link>
<Link <Link
href="/admin/newsletter/analytics" href="/admin/newsletter/analytics"
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
</svg> </svg>
Analytics Analytics
</Link> </Link>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right"> <div className="text-right">
<p className="text-white font-display font-bold text-xl">{activeCount}</p> <p className="text-white font-display font-bold text-xl">{activeCount}</p>
<p className="text-[#555] text-xs font-body">active subscribers</p> <p className="text-[#555] text-xs font-body">active subscribers</p>
</div> </div>
<div className="w-px h-8 bg-[#252525]" /> <div className="w-px h-8 bg-[#3a322b]" />
<div className="text-right"> <div className="text-right">
<p className="text-white font-display font-bold text-xl">{totalCount}</p> <p className="text-white font-display font-bold text-xl">{totalCount}</p>
<p className="text-[#555] text-xs font-body">total</p> <p className="text-[#555] text-xs font-body">total</p>
@@ -427,15 +427,15 @@ export default function AdminNewsletterPage() {
<div className="max-w-6xl mx-auto px-6 py-6"> <div className="max-w-6xl mx-auto px-6 py-6">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
<button <button
onClick={() => setActiveTab('subscribers')} onClick={() => setActiveTab('subscribers')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'subscribers' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}>
Subscribers Subscribers
</button> </button>
<button <button
onClick={() => setActiveTab('send-digest')} onClick={() => setActiveTab('send-digest')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`}> className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors ${activeTab === 'send-digest' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}>
Send Digest Send Digest
</button> </button>
</div> </div>
@@ -452,12 +452,12 @@ export default function AdminNewsletterPage() {
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="search-input flex-1 py-2 px-3 text-sm" className="search-input flex-1 py-2 px-3 text-sm"
/> />
<div className="flex gap-1 bg-[#111] border border-[#252525] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{(['all', 'active', 'unsubscribed'] as const).map((s) => ( {(['all', 'active', 'unsubscribed'] as const).map((s) => (
<button <button
key={s} key={s}
onClick={() => setStatusFilter(s)} onClick={() => setStatusFilter(s)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white'}`} className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-colors ${statusFilter === s ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'}`}
> >
{s} {s}
</button> </button>
@@ -467,7 +467,7 @@ export default function AdminNewsletterPage() {
<button <button
onClick={handleExportCSV} onClick={handleExportCSV}
disabled={subscribers.length === 0} disabled={subscribers.length === 0}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap" className="flex items-center gap-1.5 px-3 py-2 text-xs font-body font-semibold text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors disabled:opacity-40 disabled:cursor-not-allowed whitespace-nowrap"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 015.25 21h13.5A2.25 2.25 0 0121 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /> <path strokeLinecap="round" strokeLinejoin="round" d="M3 16.5v2.25A2.25 2.25 0 015.25 21h13.5A2.25 2.25 0 0121 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
@@ -478,9 +478,9 @@ export default function AdminNewsletterPage() {
{/* Bulk Actions Bar */} {/* Bulk Actions Bar */}
{someSelected && ( {someSelected && (
<div className="flex items-center justify-between bg-[#1a1208] border border-[#4a3500] rounded-lg px-4 py-3 mb-4"> <div className="flex items-center justify-between bg-[#231d18] border border-[#412402] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="text-sm font-body font-semibold text-[#ffb800]"> <span className="text-sm font-body font-semibold text-[#F0A623]">
{selectedIds.size} selected {selectedIds.size} selected
</span> </span>
<button <button
@@ -512,17 +512,17 @@ export default function AdminNewsletterPage() {
Delete Delete
</button> </button>
{bulkLoading && ( {bulkLoading && (
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
)} )}
</div> </div>
</div> </div>
)} )}
{/* Table */} {/* Table */}
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : subscribers.length === 0 ? ( ) : subscribers.length === 0 ? (
<div className="text-center py-16"> <div className="text-center py-16">
@@ -534,13 +534,13 @@ export default function AdminNewsletterPage() {
) : ( ) : (
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#252525]"> <tr className="border-b border-[#3a322b]">
<th className="px-4 py-3 w-10"> <th className="px-4 py-3 w-10">
<input <input
type="checkbox" type="checkbox"
checked={allSelected} checked={allSelected}
onChange={toggleSelectAll} onChange={toggleSelectAll}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer" className="w-4 h-4 rounded border-[#444] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/> />
</th> </th>
<th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th> <th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th>
@@ -554,21 +554,21 @@ export default function AdminNewsletterPage() {
{subscribers.map((sub) => ( {subscribers.map((sub) => (
<tr <tr
key={sub.id} key={sub.id}
className={`border-b border-[#1a1a1a] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#0d0d0d]'}`} className={`border-b border-[#231d18] transition-colors ${selectedIds.has(sub.id) ? 'bg-[#0d1a2d]' : 'hover:bg-[#1c1815]'}`}
> >
<td className="px-4 py-3 w-10"> <td className="px-4 py-3 w-10">
<input <input
type="checkbox" type="checkbox"
checked={selectedIds.has(sub.id)} checked={selectedIds.has(sub.id)}
onChange={() => toggleSelect(sub.id)} onChange={() => toggleSelect(sub.id)}
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#ffb800] cursor-pointer" className="w-4 h-4 rounded border-[#444] bg-[#231d18] accent-[#F0A623] cursor-pointer"
/> />
</td> </td>
<td className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td> <td className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td>
<td className="px-4 py-3 hidden md:table-cell"> <td className="px-4 py-3 hidden md:table-cell">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{sub.topics?.slice(0, 3).map((t) => ( {sub.topics?.slice(0, 3).map((t) => (
<span key={t} className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{t}</span> <span key={t} className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{t}</span>
))} ))}
{sub.topics?.length > 3 && ( {sub.topics?.length > 3 && (
<span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span> <span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span>
@@ -576,7 +576,7 @@ export default function AdminNewsletterPage() {
</div> </div>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]'}`}> <span className={`text-xs font-body font-bold px-2 py-1 rounded-full ${sub.status === 'active' ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#231d18]'}`}>
{sub.status} {sub.status}
</span> </span>
</td> </td>
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
{activeTab === 'send-digest' && ( {activeTab === 'send-digest' && (
<div className="max-w-2xl"> <div className="max-w-2xl">
{/* SMTP warning if not configured */} {/* SMTP warning if not configured */}
<div className="bg-[#1a1208] border border-[#4a3500] rounded-lg p-4 mb-6 flex gap-3"> <div className="bg-[#231d18] border border-[#412402] rounded-lg p-4 mb-6 flex gap-3">
<svg className="w-5 h-5 text-[#ffb800] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#F0A623] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg> </svg>
<div> <div>
<p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p> <p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p>
<p className="text-[#777] text-xs font-body mt-0.5"> <p className="text-[#777] text-xs font-body mt-0.5">
Set <code className="text-[#ffb800]">SMTP_HOST</code>, <code className="text-[#ffb800]">SMTP_USER</code>, <code className="text-[#ffb800]">SMTP_PASS</code>, and <code className="text-[#ffb800]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending. Set <code className="text-[#F0A623]">SMTP_HOST</code>, <code className="text-[#F0A623]">SMTP_USER</code>, <code className="text-[#F0A623]">SMTP_PASS</code>, and <code className="text-[#F0A623]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
</p> </p>
</div> </div>
</div> </div>
{/* Applied template badge */} {/* Applied template badge */}
{appliedTemplate && ( {appliedTemplate && (
<div className="flex items-center justify-between bg-[#1a1208] border border-[#4a3500] rounded-lg px-4 py-3 mb-4"> <div className="flex items-center justify-between bg-[#231d18] border border-[#412402] rounded-lg px-4 py-3 mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: appliedTemplate.accent_color + '20' }}> <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: appliedTemplate.accent_color + '20' }}>
<span style={{ color: appliedTemplate.accent_color }}> <span style={{ color: appliedTemplate.accent_color }}>
@@ -662,7 +662,7 @@ export default function AdminNewsletterPage() {
<p className="text-xs font-body text-[#555]">Fill in the fields below or start from a template</p> <p className="text-xs font-body text-[#555]">Fill in the fields below or start from a template</p>
<button <button
onClick={() => { fetchTemplates(); setShowTemplateDrawer(true); }} onClick={() => { fetchTemplates(); setShowTemplateDrawer(true); }}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#ffb800] border border-[#4a3500] rounded-lg hover:bg-[#1a1208] transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#F0A623] border border-[#412402] rounded-lg hover:bg-[#231d18] transition-colors"
> >
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
@@ -725,14 +725,14 @@ export default function AdminNewsletterPage() {
<button <button
type="button" type="button"
onClick={addArticle} onClick={addArticle}
className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors" className="text-xs text-[#F0A623] hover:text-blue-400 font-body font-semibold transition-colors"
> >
+ Add Article + Add Article
</button> </button>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
{digestArticles.map((article, index) => ( {digestArticles.map((article, index) => (
<div key={index} className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div key={index} className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs font-body text-[#555]">Article {index + 1}</span> <span className="text-xs font-body text-[#555]">Article {index + 1}</span>
{digestArticles.length > 1 && ( {digestArticles.length > 1 && (
@@ -803,11 +803,11 @@ export default function AdminNewsletterPage() {
{/* Template Picker Drawer */} {/* Template Picker Drawer */}
{showTemplateDrawer && ( {showTemplateDrawer && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center p-0 sm:p-4" onClick={() => setShowTemplateDrawer(false)}> <div className="fixed inset-0 z-50 bg-black/70 flex items-end sm:items-center justify-center p-0 sm:p-4" onClick={() => setShowTemplateDrawer(false)}>
<div className="bg-[#111] border border-[#252525] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}> <div className="bg-[#111] border border-[#3a322b] rounded-t-2xl sm:rounded-xl w-full sm:max-w-2xl max-h-[80vh] overflow-hidden flex flex-col" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] flex-shrink-0"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b] flex-shrink-0">
<h2 className="font-display font-bold text-white text-base">Choose a Template</h2> <h2 className="font-display font-bold text-white text-base">Choose a Template</h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin/newsletter/templates" className="text-xs text-[#ffb800] font-body hover:text-blue-400 transition-colors"> <Link href="/admin/newsletter/templates" className="text-xs text-[#F0A623] font-body hover:text-blue-400 transition-colors">
Manage templates Manage templates
</Link> </Link>
<button onClick={() => setShowTemplateDrawer(false)} className="text-[#555] hover:text-white transition-colors"> <button onClick={() => setShowTemplateDrawer(false)} className="text-[#555] hover:text-white transition-colors">
@@ -820,12 +820,12 @@ export default function AdminNewsletterPage() {
<div className="overflow-y-auto flex-1 p-4"> <div className="overflow-y-auto flex-1 p-4">
{loadingTemplates ? ( {loadingTemplates ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : templates.length === 0 ? ( ) : templates.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<p className="text-[#555] font-body text-sm">No templates found.</p> <p className="text-[#555] font-body text-sm">No templates found.</p>
<Link href="/admin/newsletter/templates" className="text-[#ffb800] text-sm font-body hover:text-blue-400 mt-2 inline-block"> <Link href="/admin/newsletter/templates" className="text-[#F0A623] text-sm font-body hover:text-blue-400 mt-2 inline-block">
Create your first template Create your first template
</Link> </Link>
</div> </div>
@@ -835,7 +835,7 @@ export default function AdminNewsletterPage() {
<button <button
key={tpl.id} key={tpl.id}
onClick={() => applyTemplate(tpl)} onClick={() => applyTemplate(tpl)}
className="flex items-start gap-3 p-4 bg-[#0d0d0d] border border-[#252525] rounded-xl text-left hover:border-[#ffb800] hover:bg-[#1a1208] transition-colors group" className="flex items-start gap-3 p-4 bg-[#1c1815] border border-[#3a322b] rounded-xl text-left hover:border-[#F0A623] hover:bg-[#231d18] transition-colors group"
> >
<div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: tpl.accent_color + '20' }}> <div className="w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 mt-0.5" style={{ backgroundColor: tpl.accent_color + '20' }}>
<span style={{ color: tpl.accent_color }}> <span style={{ color: tpl.accent_color }}>
@@ -851,8 +851,8 @@ export default function AdminNewsletterPage() {
</div> </div>
{tpl.description && <p className="text-xs text-[#777] font-body line-clamp-1">{tpl.description}</p>} {tpl.description && <p className="text-xs text-[#777] font-body line-clamp-1">{tpl.description}</p>}
<div className="flex gap-1.5 mt-1.5"> <div className="flex gap-1.5 mt-1.5">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span> <span className="text-xs bg-[#231d18] text-[#888] px-1.5 py-0.5 rounded font-body capitalize">{tpl.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span> <span className="text-xs bg-[#231d18] text-[#888] px-1.5 py-0.5 rounded font-body">{tpl.article_blocks?.length || 0} slots</span>
</div> </div>
</div> </div>
</button> </button>

View File

@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
]; ];
const ACCENT_COLORS = [ const ACCENT_COLORS = [
'#ffb800', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316', '#F0A623', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
]; ];
const LAYOUT_ICONS: Record<string, React.ReactNode> = { const LAYOUT_ICONS: Record<string, React.ReactNode> = {
@@ -83,7 +83,7 @@ const emptyForm = (): Omit<NewsletterTemplate, 'id' | 'is_preset' | 'created_at'
subject_prefix: '', subject_prefix: '',
header_text: '', header_text: '',
footer_text: '', footer_text: '',
accent_color: '#ffb800', accent_color: '#F0A623',
article_blocks: [{ title: '', excerpt: '', url: '', category: '' }], article_blocks: [{ title: '', excerpt: '', url: '', category: '' }],
}); });
@@ -226,7 +226,7 @@ export default function NewsletterTemplatesPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -242,7 +242,7 @@ export default function NewsletterTemplatesPage() {
)} )}
{/* Header */} {/* Header */}
<div className="bg-[#111] border-b border-[#252525] px-6 py-4"> <div className="bg-[#111] border-b border-[#3a322b] px-6 py-4">
<div className="max-w-6xl mx-auto flex items-center justify-between"> <div className="max-w-6xl mx-auto flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors"> <Link href="/admin/newsletter" className="text-[#555] hover:text-[#888] transition-colors">
@@ -272,11 +272,11 @@ export default function NewsletterTemplatesPage() {
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Preset Templates</h2> <h2 className="font-display font-bold text-white text-base">Preset Templates</h2>
<span className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{presets.length}</span> <span className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
</div> </div>
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -298,15 +298,15 @@ export default function NewsletterTemplatesPage() {
<div> <div>
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 mb-4">
<h2 className="font-display font-bold text-white text-base">Custom Templates</h2> <h2 className="font-display font-bold text-white text-base">Custom Templates</h2>
<span className="text-xs bg-[#1a1208] text-[#ffb800] px-2 py-0.5 rounded-full font-body">{custom.length}</span> <span className="text-xs bg-[#231d18] text-[#F0A623] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
</div> </div>
{!loadingData && custom.length === 0 ? ( {!loadingData && custom.length === 0 ? (
<div className="bg-[#111] border border-[#252525] rounded-lg p-10 text-center"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-10 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
</svg> </svg>
<p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p> <p className="text-[#555] font-body text-sm mb-3">No custom templates yet</p>
<button onClick={openNew} className="text-[#ffb800] text-sm font-body font-semibold hover:text-blue-400 transition-colors"> <button onClick={openNew} className="text-[#F0A623] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
Create your first template Create your first template
</button> </button>
</div> </div>
@@ -330,8 +330,8 @@ export default function NewsletterTemplatesPage() {
{/* Template Form Modal */} {/* Template Form Modal */}
{showForm && ( {showForm && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4"> <div className="fixed inset-0 z-50 bg-black/70 flex items-start justify-center overflow-y-auto py-8 px-4">
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-2xl"> <div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-2xl">
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b]">
<h2 className="font-display font-bold text-white text-base"> <h2 className="font-display font-bold text-white text-base">
{editingId ? 'Edit Template' : 'New Template'} {editingId ? 'Edit Template' : 'New Template'}
</h2> </h2>
@@ -376,8 +376,8 @@ export default function NewsletterTemplatesPage() {
key={opt.value} key={opt.value}
type="button" type="button"
onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))} onClick={() => setForm((f) => ({ ...f, layout: opt.value as NewsletterTemplate['layout'] }))}
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#ffb800] bg-[#1a1208]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}> className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#F0A623] bg-[#231d18]' : 'border-[#3a322b] bg-[#1c1815] hover:border-[#333]'}`}>
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#ffb800]' : 'text-[#555]'}`}> <span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#F0A623]' : 'text-[#555]'}`}>
{LAYOUT_ICONS[opt.value]} {LAYOUT_ICONS[opt.value]}
</span> </span>
<div> <div>
@@ -398,7 +398,7 @@ export default function NewsletterTemplatesPage() {
key={opt.value} key={opt.value}
type="button" type="button"
onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))} onClick={() => setForm((f) => ({ ...f, style: opt.value as NewsletterTemplate['style'] }))}
className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#ffb800] bg-[#1a1208] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}> className={`flex-1 py-2.5 px-3 rounded-lg border text-sm font-body font-semibold transition-colors ${form.style === opt.value ? 'border-[#F0A623] bg-[#231d18] text-white' : 'border-[#3a322b] bg-[#1c1815] text-[#888] hover:border-[#333]'}`}>
{opt.label} {opt.label}
</button> </button>
))} ))}
@@ -469,14 +469,14 @@ export default function NewsletterTemplatesPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label> <label className="text-xs font-body font-bold text-[#888] uppercase tracking-wider">Article Block Slots</label>
<button type="button" onClick={addBlock} className="text-xs text-[#ffb800] hover:text-blue-400 font-body font-semibold transition-colors"> <button type="button" onClick={addBlock} className="text-xs text-[#F0A623] hover:text-blue-400 font-body font-semibold transition-colors">
+ Add Slot + Add Slot
</button> </button>
</div> </div>
<p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p> <p className="text-xs text-[#555] font-body mb-3">Define placeholder slots for articles. These will be pre-filled when using the template in Send Digest.</p>
<div className="space-y-2"> <div className="space-y-2">
{form.article_blocks.map((block, i) => ( {form.article_blocks.map((block, i) => (
<div key={i} className="bg-[#0d0d0d] border border-[#252525] rounded-lg p-3"> <div key={i} className="bg-[#1c1815] border border-[#3a322b] rounded-lg p-3">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-body text-[#555]">Slot {i + 1}</span> <span className="text-xs font-body text-[#555]">Slot {i + 1}</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -486,7 +486,7 @@ export default function NewsletterTemplatesPage() {
type="checkbox" type="checkbox"
checked={!!block.featured} checked={!!block.featured}
onChange={(e) => updateBlock(i, 'featured', e.target.checked)} onChange={(e) => updateBlock(i, 'featured', e.target.checked)}
className="w-3 h-3 accent-[#ffb800]" className="w-3 h-3 accent-[#F0A623]"
/> />
<span className="text-xs font-body text-[#888]">Featured</span> <span className="text-xs font-body text-[#888]">Featured</span>
</label> </label>
@@ -527,7 +527,7 @@ export default function NewsletterTemplatesPage() {
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#252525]"> <div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-[#3a322b]">
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors"> <button onClick={() => setShowForm(false)} className="px-4 py-2 text-sm font-body text-[#888] hover:text-white transition-colors">
Cancel Cancel
</button> </button>
@@ -545,8 +545,8 @@ export default function NewsletterTemplatesPage() {
{/* Preview Modal */} {/* Preview Modal */}
{previewTemplate && ( {previewTemplate && (
<div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}> <div className="fixed inset-0 z-50 bg-black/70 flex items-center justify-center p-4" onClick={() => setPreviewTemplate(null)}>
<div className="bg-[#111] border border-[#252525] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}> <div className="bg-[#111] border border-[#3a322b] rounded-xl w-full max-w-lg" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-6 py-4 border-b border-[#3a322b]">
<h2 className="font-display font-bold text-white text-base">Template Preview</h2> <h2 className="font-display font-bold text-white text-base">Template Preview</h2>
<button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors"> <button onClick={() => setPreviewTemplate(null)} className="text-[#555] hover:text-white transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -565,27 +565,27 @@ export default function NewsletterTemplatesPage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-3 text-center"> <div className="grid grid-cols-3 gap-3 text-center">
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Layout</p> <p className="text-xs text-[#555] font-body mb-1">Layout</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p> <p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p>
</div> </div>
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Style</p> <p className="text-xs text-[#555] font-body mb-1">Style</p>
<p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p> <p className="text-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p>
</div> </div>
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Slots</p> <p className="text-xs text-[#555] font-body mb-1">Slots</p>
<p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p> <p className="text-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p>
</div> </div>
</div> </div>
{previewTemplate.subject_prefix && ( {previewTemplate.subject_prefix && (
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p> <p className="text-xs text-[#555] font-body mb-1">Subject Prefix</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p> <p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p>
</div> </div>
)} )}
{previewTemplate.header_text && ( {previewTemplate.header_text && (
<div className="bg-[#0d0d0d] rounded-lg p-3"> <div className="bg-[#1c1815] rounded-lg p-3">
<p className="text-xs text-[#555] font-body mb-1">Header Text</p> <p className="text-xs text-[#555] font-body mb-1">Header Text</p>
<p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p> <p className="text-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p>
</div> </div>
@@ -595,9 +595,9 @@ export default function NewsletterTemplatesPage() {
<p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p> <p className="text-xs text-[#555] font-body mb-2">Article Slots ({previewTemplate.article_blocks.length})</p>
<div className="space-y-1.5"> <div className="space-y-1.5">
{previewTemplate.article_blocks.map((b, i) => ( {previewTemplate.article_blocks.map((b, i) => (
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] rounded px-3 py-2"> <div key={i} className="flex items-center gap-2 bg-[#1c1815] rounded px-3 py-2">
<span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span> <span className="text-xs text-[#555] font-body w-12 flex-shrink-0">Slot {i + 1}</span>
{b.featured && <span className="text-xs text-[#ffb800] font-body bg-[#1a1208] px-1.5 py-0.5 rounded">Featured</span>} {b.featured && <span className="text-xs text-[#F0A623] font-body bg-[#231d18] px-1.5 py-0.5 rounded">Featured</span>}
{b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>} {b.category && <span className="text-xs text-[#888] font-body">{b.category}</span>}
{b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>} {b.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>}
</div> </div>
@@ -614,7 +614,7 @@ export default function NewsletterTemplatesPage() {
{!previewTemplate.is_preset && ( {!previewTemplate.is_preset && (
<button <button
onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }} onClick={() => { setPreviewTemplate(null); openEdit(previewTemplate); }}
className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="px-4 py-2 text-sm font-body text-[#888] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
Edit Edit
</button> </button>
)} )}
@@ -637,7 +637,7 @@ interface TemplateCardProps {
function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) { function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) {
return ( return (
<div className="bg-[#111] border border-[#252525] rounded-xl overflow-hidden hover:border-[#333] transition-colors group"> <div className="bg-[#111] border border-[#3a322b] rounded-xl overflow-hidden hover:border-[#333] transition-colors group">
{/* Color bar */} {/* Color bar */}
<div className="h-1" style={{ backgroundColor: template.accent_color }} /> <div className="h-1" style={{ backgroundColor: template.accent_color }} />
<div className="p-4"> <div className="p-4">
@@ -660,27 +660,27 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
)} )}
<div className="flex flex-wrap gap-1.5 mb-4"> <div className="flex flex-wrap gap-1.5 mb-4">
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span> <span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.layout}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span> <span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body capitalize">{template.style}</span>
<span className="text-xs bg-[#1a1a1a] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span> <span className="text-xs bg-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => onPreview(template)} onClick={() => onPreview(template)}
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#ffb800] border border-[#4a3500] rounded-lg hover:bg-[#1a1208] transition-colors"> className="flex-1 py-1.5 text-xs font-body font-semibold text-[#F0A623] border border-[#412402] rounded-lg hover:bg-[#231d18] transition-colors">
Preview Preview
</button> </button>
<Link <Link
href={`/admin/newsletter?template=${template.id}`} href={`/admin/newsletter?template=${template.id}`}
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#1a1208] border border-[#4a3500] rounded-lg hover:bg-[#4a3500] transition-colors"> className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#231d18] border border-[#412402] rounded-lg hover:bg-[#412402] transition-colors">
Use Use
</Link> </Link>
{!template.is_preset && ( {!template.is_preset && (
<> <>
<button <button
onClick={() => onEdit(template)} onClick={() => onEdit(template)}
className="p-1.5 text-[#555] hover:text-white border border-[#252525] rounded-lg transition-colors"> className="p-1.5 text-[#555] hover:text-white border border-[#3a322b] rounded-lg transition-colors">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" /> <path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125" />
</svg> </svg>
@@ -688,7 +688,7 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
<button <button
onClick={() => onDelete(template.id)} onClick={() => onDelete(template.id)}
disabled={deletingId === template.id} disabled={deletingId === template.id}
className="p-1.5 text-[#555] hover:text-red-400 border border-[#252525] rounded-lg transition-colors disabled:opacity-50"> className="p-1.5 text-[#555] hover:text-red-400 border border-[#3a322b] rounded-lg transition-colors disabled:opacity-50">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> <path strokeLinecap="round" strokeLinejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
</svg> </svg>

View File

@@ -166,7 +166,7 @@ function StatCard({ label, value, icon, color, active, onClick }: StatCardProps)
className={`w-full text-left p-4 rounded-lg border transition-all ${ className={`w-full text-left p-4 rounded-lg border transition-all ${
active active
? `${color} border-current/30` ? `${color} border-current/30`
: 'bg-[#111] border-[#252525] hover:border-[#333]' : 'bg-[#111] border-[#3a322b] hover:border-[#333]'
}`} }`}
> >
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
@@ -191,7 +191,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
return ( return (
<div <div
className={`relative p-4 rounded-lg border transition-all ${ className={`relative p-4 rounded-lg border transition-all ${
n.read ? 'bg-[#0d0d0d] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}` n.read ? 'bg-[#1c1815] border-[#1e1e1e]' : `${cfg.bg} ${cfg.border}`
}`} }`}
> >
{/* Unread dot */} {/* Unread dot */}
@@ -202,7 +202,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{/* Icon */} {/* Icon */}
<div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${ <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center ${
n.read ? 'bg-[#1a1a1a]' : cfg.badge n.read ? 'bg-[#231d18]' : cfg.badge
}`}> }`}>
<TypeIcon type={n.type} /> <TypeIcon type={n.type} />
</div> </div>
@@ -244,7 +244,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
{n.actionUrl && ( {n.actionUrl && (
<Link <Link
href={n.actionUrl} href={n.actionUrl}
className="text-xs font-body font-bold text-[#ffb800] hover:text-blue-300 transition-colors" className="text-xs font-body font-bold text-[#F0A623] hover:text-blue-300 transition-colors"
> >
{n.actionLabel || 'View →'} {n.actionLabel || 'View →'}
</Link> </Link>
@@ -474,7 +474,7 @@ export default function NotificationsPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin mx-auto mb-3" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-[#555] text-sm font-body">Loading notifications</p> <p className="text-[#555] text-sm font-body">Loading notifications</p>
</div> </div>
</div> </div>
@@ -512,7 +512,7 @@ export default function NotificationsPage() {
{stats.unread > 0 && ( {stats.unread > 0 && (
<button <button
onClick={handleMarkAllRead} onClick={handleMarkAllRead}
className="px-3 py-1.5 text-xs font-body font-bold text-[#ffb800] border border-[#ffb800]/30 rounded hover:bg-[#ffb800]/10 transition-colors" className="px-3 py-1.5 text-xs font-body font-bold text-[#F0A623] border border-[#F0A623]/30 rounded hover:bg-[#F0A623]/10 transition-colors"
> >
Mark all read Mark all read
</button> </button>
@@ -520,7 +520,7 @@ export default function NotificationsPage() {
<button <button
onClick={handleRefresh} onClick={handleRefresh}
disabled={refreshing} disabled={refreshing}
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#252525] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50" className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body text-[#888] border border-[#3a322b] rounded hover:border-[#333] hover:text-white transition-colors disabled:opacity-50"
> >
<RefreshIcon className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} /> <RefreshIcon className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} />
Refresh Refresh
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
label="Unread" label="Unread"
value={stats.unread} value={stats.unread}
icon={<BellIcon className="w-4 h-4" />} icon={<BellIcon className="w-4 h-4" />}
color="text-[#ffb800] bg-[#ffb800]/5 border-[#ffb800]/30" color="text-[#F0A623] bg-[#F0A623]/5 border-[#F0A623]/30"
active={filter === 'unread'} active={filter === 'unread'}
onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')} onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')}
/> />
@@ -580,7 +580,7 @@ export default function NotificationsPage() {
onClick={() => setFilter(f)} onClick={() => setFilter(f)}
className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${ className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${
filter === f filter === f
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#555] hover:text-[#888] hover:bg-[#111]' : 'text-[#555] hover:text-[#888] hover:bg-[#111]'
}`} }`}
> >

View File

@@ -44,8 +44,8 @@ function timeAgo(dateStr: string): string {
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
published: 'text-green-400 bg-green-400/10', published: 'text-green-400 bg-green-400/10',
pending: 'text-yellow-400 bg-yellow-400/10', pending: 'text-yellow-400 bg-yellow-400/10',
draft: 'text-[#888] bg-[#1a1a1a]', draft: 'text-[#888] bg-[#231d18]',
archived: 'text-[#555] bg-[#1a1a1a]', archived: 'text-[#555] bg-[#231d18]',
imported: 'text-blue-400 bg-blue-400/10', imported: 'text-blue-400 bg-blue-400/10',
}; };
@@ -91,7 +91,7 @@ interface MetricCardProps {
function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) { function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) {
const content = ( const content = (
<div className={`bg-[#111] border border-[#252525] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}> <div className={`bg-[#111] border border-[#3a322b] rounded-lg p-5 flex flex-col gap-3 hover:border-[#333] transition-colors ${href ? 'cursor-pointer' : ''}`}>
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}> <div className={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}>
{icon} {icon}
@@ -125,15 +125,15 @@ interface QuickActionProps {
function QuickAction({ label, description, href, icon, accent }: QuickActionProps) { function QuickAction({ label, description, href, icon, accent }: QuickActionProps) {
return ( return (
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#252525] rounded-lg hover:border-[#ffb800] hover:bg-[#0d1a2d] transition-all group"> <Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#3a322b] rounded-lg hover:border-[#F0A623] hover:bg-[#0d1a2d] transition-all group">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}> <div className={`w-9 h-9 rounded-lg flex items-center justify-center flex-shrink-0 ${accent} group-hover:scale-105 transition-transform`}>
{icon} {icon}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-body font-semibold text-white group-hover:text-[#ffb800] transition-colors">{label}</p> <p className="text-sm font-body font-semibold text-white group-hover:text-[#F0A623] transition-colors">{label}</p>
<p className="text-xs text-[#555] font-body truncate">{description}</p> <p className="text-xs text-[#555] font-body truncate">{description}</p>
</div> </div>
<svg className="w-4 h-4 text-[#444] group-hover:text-[#ffb800] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#444] group-hover:text-[#F0A623] ml-auto flex-shrink-0 transition-colors" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</svg> </svg>
</Link> </Link>
@@ -239,7 +239,7 @@ export default function AdminDashboardPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading dashboard</p> <p className="text-[#555] text-sm font-body">Loading dashboard</p>
</div> </div>
</div> </div>
@@ -345,7 +345,7 @@ export default function AdminDashboardPage() {
<MetricCard <MetricCard
label="Drafts" label="Drafts"
value={metrics.draftArticles} value={metrics.draftArticles}
accent="bg-[#1a1a1a]" accent="bg-[#231d18]"
href="/admin/articles" href="/admin/articles"
icon={ icon={
<svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#888]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -356,7 +356,7 @@ export default function AdminDashboardPage() {
<MetricCard <MetricCard
label="Archived" label="Archived"
value={metrics.archivedArticles} value={metrics.archivedArticles}
accent="bg-[#1a1a1a]" accent="bg-[#231d18]"
href="/admin/articles" href="/admin/articles"
icon={ icon={
<svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="w-5 h-5 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
@@ -372,20 +372,20 @@ export default function AdminDashboardPage() {
{/* Recent Activity Feed */} {/* Recent Activity Feed */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2> <h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
<Link href="/admin/articles" className="text-xs text-[#ffb800] hover:text-blue-300 font-body transition-colors">View all </Link> <Link href="/admin/articles" className="text-xs text-[#F0A623] hover:text-blue-300 font-body transition-colors">View all </Link>
</div> </div>
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{recentActivity.length === 0 ? ( {recentActivity.length === 0 ? (
<div className="px-5 py-10 text-center"> <div className="px-5 py-10 text-center">
<p className="text-[#555] text-sm font-body">No recent activity found</p> <p className="text-[#555] text-sm font-body">No recent activity found</p>
</div> </div>
) : ( ) : (
recentActivity.map((item) => ( recentActivity.map((item) => (
<div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#0d0d0d] transition-colors"> <div key={`${item.source}-${item.id}`} className="px-5 py-3 flex items-start gap-3 hover:bg-[#1c1815] transition-colors">
<div className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-6 h-6 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 mt-0.5">
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']} {ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -412,8 +412,8 @@ export default function AdminDashboardPage() {
{/* Quick Actions */} {/* Quick Actions */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
<div className="px-5 py-4 border-b border-[#1a1a1a]"> <div className="px-5 py-4 border-b border-[#231d18]">
<h2 className="text-sm font-display font-bold text-white">Quick Actions</h2> <h2 className="text-sm font-display font-bold text-white">Quick Actions</h2>
</div> </div>
<div className="p-4 flex flex-col gap-2"> <div className="p-4 flex flex-col gap-2">
@@ -587,21 +587,21 @@ export default function AdminDashboardPage() {
</div> </div>
{/* Status Summary Mini-Card */} {/* Status Summary Mini-Card */}
<div className="bg-[#111] border border-[#252525] rounded-lg p-4"> <div className="bg-[#111] border border-[#3a322b] rounded-lg p-4">
<h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3> <h3 className="text-xs font-body font-bold text-[#555] uppercase tracking-widest mb-3">Content Status</h3>
<div className="space-y-2"> <div className="space-y-2">
{[ {[
{ label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles }, { label: 'Published', value: metrics.publishedArticles, color: 'bg-green-400', total: metrics.totalArticles },
{ label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles }, { label: 'Pending', value: metrics.pendingApprovals, color: 'bg-yellow-400', total: metrics.totalArticles },
{ label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles }, { label: 'Draft', value: metrics.draftArticles, color: 'bg-[#444]', total: metrics.totalArticles },
{ label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#2a2a2a]', total: metrics.totalArticles }, { label: 'Archived', value: metrics.archivedArticles, color: 'bg-[#3a322b]', total: metrics.totalArticles },
].map(({ label, value, color, total }) => ( ].map(({ label, value, color, total }) => (
<div key={label}> <div key={label}>
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-xs text-[#888] font-body">{label}</span> <span className="text-xs text-[#888] font-body">{label}</span>
<span className="text-xs text-white font-body font-semibold">{value}</span> <span className="text-xs text-white font-body font-semibold">{value}</span>
</div> </div>
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden"> <div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden">
<div <div
className={`h-full rounded-full ${color} transition-all duration-500`} className={`h-full rounded-full ${color} transition-all duration-500`}
style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }} style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }}

View File

@@ -41,7 +41,7 @@ export default function ReviewActions({ id, currentStatus }: Props) {
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
placeholder="Reviewer note (optional)" placeholder="Reviewer note (optional)"
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#ffb800] focus:outline-none" className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#F0A623] focus:outline-none"
rows={2} rows={2}
/> />
<div className="mt-3 flex flex-wrap gap-3 text-sm"> <div className="mt-3 flex flex-wrap gap-3 text-sm">

View File

@@ -41,12 +41,12 @@ export default async function ReviewDetailPage({ params }: { params: Promise<{ i
return ( return (
<main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]"> <main className="mx-auto max-w-6xl px-6 py-10 text-[#e5e7eb]">
<Link href="/admin/review-queue" className="text-sm text-[#ffd25a] hover:underline"> <Link href="/admin/review-queue" className="text-sm text-[#F0A623] hover:underline">
Back to queue Back to queue
</Link> </Link>
<header className="mt-4 mb-6 border-b border-[#222] pb-4"> <header className="mt-4 mb-6 border-b border-[#222] pb-4">
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#ffd25a]"> <div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#F0A623]">
<span>{persona?.name || "—"}</span> <span>{persona?.name || "—"}</span>
<span className="text-[#4b5563]">·</span> <span className="text-[#4b5563]">·</span>
<span>{art.category}</span> <span>{art.category}</span>

View File

@@ -70,7 +70,7 @@ export default async function ReviewQueuePage() {
return ( return (
<li <li
key={r.id} key={r.id}
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#ffb800] transition-colors" className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#F0A623] transition-colors"
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
{persona?.avatar_url ? ( {persona?.avatar_url ? (
@@ -85,7 +85,7 @@ export default async function ReviewQueuePage() {
<div className="h-14 w-14 rounded-full bg-[#1f2937]" /> <div className="h-14 w-14 rounded-full bg-[#1f2937]" />
)} )}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="text-xs uppercase tracking-wider text-[#ffd25a]"> <div className="text-xs uppercase tracking-wider text-[#F0A623]">
{persona?.name || "—"} · {r.category} {persona?.name || "—"} · {r.category}
</div> </div>
<h2 className="text-xl font-semibold mt-1">{r.title}</h2> <h2 className="text-xl font-semibold mt-1">{r.title}</h2>
@@ -100,7 +100,7 @@ export default async function ReviewQueuePage() {
<div className="mt-4 flex flex-wrap items-center gap-3 text-xs"> <div className="mt-4 flex flex-wrap items-center gap-3 text-xs">
<Link <Link
href={`/admin/review-queue/${r.id}`} href={`/admin/review-queue/${r.id}`}
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#d99700]" className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#BA7517]"
> >
Review Review
</Link> </Link>

View File

@@ -12,7 +12,7 @@ export default function RmpHubPage() {
useEffect(() => { if (!loading && !user) router?.push('/login'); }, [user, loading, router]); useEffect(() => { if (!loading && !user) router?.push('/login'); }, [user, loading, router]);
if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /></div>; if (loading) return <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"><div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" /></div>;
if (!user) return null; if (!user) return null;
return ( return (
@@ -68,7 +68,7 @@ export default function RmpHubPage() {
], ],
}, },
]?.map(section => ( ]?.map(section => (
<div key={section?.title} className="bg-[#111] border border-[#252525] rounded-lg p-4 space-y-3"> <div key={section?.title} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 space-y-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-2xl">{section?.icon}</span> <span className="text-2xl">{section?.icon}</span>
<div> <div>
@@ -78,19 +78,19 @@ export default function RmpHubPage() {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
{section?.links?.map(l => ( {section?.links?.map(l => (
<Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors"> <Link key={l?.href} href={l?.href} className="block px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#231d18] transition-colors">
{l?.label} {l?.label}
</Link> </Link>
))} ))}
</div> </div>
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a1208] border border-[#2a3a50] rounded text-xs text-[#ffb800] hover:bg-[#0d1a2d] transition-colors"> <Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">
Open {section?.title} Open {section?.title}
</Link> </Link>
</div> </div>
))} ))}
</div> </div>
<div className="bg-[#0d1117] border border-[#1a1208] rounded-lg p-4"> <div className="bg-[#0d1117] border border-[#231d18] rounded-lg p-4">
<p className="text-xs text-[#555] mb-3">Quick Navigation</p> <p className="text-xs text-[#555] mb-3">Quick Navigation</p>
<RmpAdminNav /> <RmpAdminNav />
</div> </div>

View File

@@ -61,7 +61,7 @@ const ROLE_COLORS: Record<string, string> = {
editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20', editor: 'text-purple-400 bg-purple-400/10 border-purple-400/20',
moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20', moderator: 'text-orange-400 bg-orange-400/10 border-orange-400/20',
contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20', contributor: 'text-blue-400 bg-blue-400/10 border-blue-400/20',
reader: 'text-[#888] bg-[#1a1a1a] border-[#333]', reader: 'text-[#888] bg-[#231d18] border-[#333]',
}; };
const ROLE_ICONS: Record<string, React.ReactNode> = { const ROLE_ICONS: Record<string, React.ReactNode> = {
@@ -324,7 +324,7 @@ export default function AdminRolesPage() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
<p className="text-[#555] text-sm font-body">Loading roles panel</p> <p className="text-[#555] text-sm font-body">Loading roles panel</p>
</div> </div>
</div> </div>
@@ -376,7 +376,7 @@ export default function AdminRolesPage() {
key={role} key={role}
onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }} onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }}
className={`bg-[#111] border rounded-lg p-4 text-left hover:border-[#333] transition-all ${ className={`bg-[#111] border rounded-lg p-4 text-left hover:border-[#333] transition-all ${
selectedRole === role ? 'border-[#ffb800] bg-[#0d1a2d]' : 'border-[#252525]' selectedRole === role ? 'border-[#F0A623] bg-[#0d1a2d]' : 'border-[#3a322b]'
}`} }`}
> >
<div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body font-semibold border mb-2 ${color}`}> <div className={`inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-body font-semibold border mb-2 ${color}`}>
@@ -391,7 +391,7 @@ export default function AdminRolesPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] rounded-lg p-1 w-fit"> <div className="flex gap-1 mb-6 bg-[#111] border border-[#3a322b] rounded-lg p-1 w-fit">
{([ {([
{ id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' }, { id: 'assign', label: 'Assign Roles', icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z' },
{ id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' }, { id: 'permissions', label: 'Permissions', icon: 'M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z' },
@@ -402,7 +402,7 @@ export default function AdminRolesPage() {
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${ className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${
activeTab === tab.id activeTab === tab.id
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -416,9 +416,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Assign Roles ── */} {/* ── Tab: Assign Roles ── */}
{activeTab === 'assign' && ( {activeTab === 'assign' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Toolbar */} {/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3"> <div className="px-5 py-4 border-b border-[#231d18] flex flex-col sm:flex-row gap-3">
<div className="relative flex-1"> <div className="relative flex-1">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24"> <svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[#555]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
@@ -428,13 +428,13 @@ export default function AdminRolesPage() {
placeholder="Search users…" placeholder="Search users…"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#252525] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg pl-9 pr-4 py-2 text-sm text-white placeholder-[#555] font-body focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<select <select
value={selectedRole} value={selectedRole}
onChange={(e) => setSelectedRole(e.target.value)} onChange={(e) => setSelectedRole(e.target.value)}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#ffb800]" className="bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#F0A623]"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
{ROLES.map((r) => ( {ROLES.map((r) => (
@@ -447,7 +447,7 @@ export default function AdminRolesPage() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-[#1a1a1a]"> <tr className="border-b border-[#231d18]">
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">User</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">User</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Current Role</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Current Role</th>
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider hidden sm:table-cell">Status</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider hidden sm:table-cell">Status</th>
@@ -455,7 +455,7 @@ export default function AdminRolesPage() {
<th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Assign Role</th> <th className="px-5 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">Assign Role</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-[#1a1a1a]"> <tbody className="divide-y divide-[#231d18]">
{filteredUsers.length === 0 ? ( {filteredUsers.length === 0 ? (
<tr> <tr>
<td colSpan={5} className="px-5 py-10 text-center text-[#555] text-sm font-body"> <td colSpan={5} className="px-5 py-10 text-center text-[#555] text-sm font-body">
@@ -464,10 +464,10 @@ export default function AdminRolesPage() {
</tr> </tr>
) : ( ) : (
filteredUsers.map((u) => ( filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-[#0d0d0d] transition-colors"> <tr key={u.id} className="hover:bg-[#1c1815] transition-colors">
<td className="px-5 py-3"> <td className="px-5 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]"> <div className="w-8 h-8 rounded-full bg-[#231d18] flex items-center justify-center flex-shrink-0 text-xs font-body font-bold text-[#888]">
{u.avatar_url {u.avatar_url
? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" /> ? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" />
: getInitials(u.full_name || u.email) : getInitials(u.full_name || u.email)
@@ -484,7 +484,7 @@ export default function AdminRolesPage() {
</td> </td>
<td className="px-5 py-3 hidden sm:table-cell"> <td className="px-5 py-3 hidden sm:table-cell">
<span className={`text-xs font-body font-semibold px-2 py-0.5 rounded-full ${ <span className={`text-xs font-body font-semibold px-2 py-0.5 rounded-full ${
u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#1a1a1a]' u.is_active ? 'text-green-400 bg-green-400/10' : 'text-[#555] bg-[#231d18]'
}`}> }`}>
{u.is_active ? 'Active' : 'Inactive'} {u.is_active ? 'Active' : 'Inactive'}
</span> </span>
@@ -500,14 +500,14 @@ export default function AdminRolesPage() {
defaultValue={u.role} defaultValue={u.role}
onChange={(e) => handleAssignRole(u.id, e.target.value)} onChange={(e) => handleAssignRole(u.id, e.target.value)}
disabled={processingUserId === u.id} disabled={processingUserId === u.id}
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#ffb800] disabled:opacity-50" className="bg-[#1c1815] border border-[#3a322b] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#F0A623] disabled:opacity-50"
> >
{ROLES.map((r) => ( {ROLES.map((r) => (
<option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option> <option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option>
))} ))}
</select> </select>
{processingUserId === u.id && ( {processingUserId === u.id && (
<div className="w-4 h-4 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-4 h-4 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
)} )}
</div> </div>
</td> </td>
@@ -519,7 +519,7 @@ export default function AdminRolesPage() {
</div> </div>
{filteredUsers.length > 0 && ( {filteredUsers.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]"> <div className="px-5 py-3 border-t border-[#231d18]">
<p className="text-xs text-[#555] font-body">{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown</p> <p className="text-xs text-[#555] font-body">{filteredUsers.length} user{filteredUsers.length !== 1 ? 's' : ''} shown</p>
</div> </div>
)} )}
@@ -535,9 +535,9 @@ export default function AdminRolesPage() {
const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader']; const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader'];
return ( return (
<div key={perm.role} className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div key={perm.role} className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Role Header */} {/* Role Header */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-body font-semibold border ${color}`}> <div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-body font-semibold border ${color}`}>
{ROLE_ICONS[perm.role]} {ROLE_ICONS[perm.role]}
@@ -557,7 +557,7 @@ export default function AdminRolesPage() {
<button <button
onClick={handleSavePermissions} onClick={handleSavePermissions}
disabled={savingPermissions} disabled={savingPermissions}
className="px-3 py-1.5 text-xs font-body font-semibold bg-[#ffb800] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5" className="px-3 py-1.5 text-xs font-body font-semibold bg-[#F0A623] hover:bg-blue-500 text-white rounded-lg transition-colors disabled:opacity-50 flex items-center gap-1.5"
> >
{savingPermissions && <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />} {savingPermissions && <div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin" />}
Save Changes Save Changes
@@ -593,12 +593,12 @@ export default function AdminRolesPage() {
{isEditing ? ( {isEditing ? (
<button <button
onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))} onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))}
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#ffb800]' : 'bg-[#333]'}`} className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#F0A623]' : 'bg-[#333]'}`}
> >
<span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${val ? 'translate-x-4' : 'translate-x-0.5'}`} /> <span className={`absolute top-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform ${val ? 'translate-x-4' : 'translate-x-0.5'}`} />
</button> </button>
) : ( ) : (
<span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#1a1a1a]'}`}> <span className={`w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0 ${val ? 'bg-green-400/10' : 'bg-[#231d18]'}`}>
{val ? ( {val ? (
<svg className="w-3 h-3 text-green-400" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24"> <svg className="w-3 h-3 text-green-400" fill="none" stroke="currentColor" strokeWidth="3" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
@@ -631,9 +631,9 @@ export default function AdminRolesPage() {
{/* ── Tab: Activity Logs ── */} {/* ── Tab: Activity Logs ── */}
{activeTab === 'activity' && ( {activeTab === 'activity' && (
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden"> <div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
{/* Toolbar */} {/* Toolbar */}
<div className="px-5 py-4 border-b border-[#1a1a1a] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between"> <div className="px-5 py-4 border-b border-[#231d18] flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
<h2 className="text-sm font-display font-bold text-white">Role-Based Activity</h2> <h2 className="text-sm font-display font-bold text-white">Role-Based Activity</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-[#555] font-body">Filter by role:</span> <span className="text-xs text-[#555] font-body">Filter by role:</span>
@@ -644,8 +644,8 @@ export default function AdminRolesPage() {
onClick={() => setActivityRoleFilter(r)} onClick={() => setActivityRoleFilter(r)}
className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${ className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${
activityRoleFilter === r activityRoleFilter === r
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white' : 'bg-[#231d18] text-[#888] hover:text-white'
}`} }`}
> >
{r} {r}
@@ -656,7 +656,7 @@ export default function AdminRolesPage() {
</div> </div>
{/* Log List */} {/* Log List */}
<div className="divide-y divide-[#1a1a1a]"> <div className="divide-y divide-[#231d18]">
{activityLogs.length === 0 ? ( {activityLogs.length === 0 ? (
<div className="px-5 py-12 text-center"> <div className="px-5 py-12 text-center">
<svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24"> <svg className="w-10 h-10 text-[#333] mx-auto mb-3" fill="none" stroke="currentColor" strokeWidth="1.5" viewBox="0 0 24 24">
@@ -666,13 +666,13 @@ export default function AdminRolesPage() {
</div> </div>
) : ( ) : (
activityLogs.map((log) => { activityLogs.map((log) => {
const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#1a1a1a]'; const actionColor = ACTION_COLORS[log.action] || 'text-[#888] bg-[#231d18]';
const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' '); const actionLabel = ACTION_LABELS[log.action] || log.action.replace(/_/g, ' ');
const performer = log.user_profiles?.full_name || log.performed_by_email || 'System'; const performer = log.user_profiles?.full_name || log.performed_by_email || 'System';
const performerRole = log.user_profiles?.role; const performerRole = log.user_profiles?.role;
return ( return (
<div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#0d0d0d] transition-colors"> <div key={log.id} className="px-5 py-3.5 flex items-start gap-4 hover:bg-[#1c1815] transition-colors">
{/* Action badge */} {/* Action badge */}
<div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}> <div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}>
{actionLabel} {actionLabel}
@@ -720,7 +720,7 @@ export default function AdminRolesPage() {
</div> </div>
{activityLogs.length > 0 && ( {activityLogs.length > 0 && (
<div className="px-5 py-3 border-t border-[#1a1a1a]"> <div className="px-5 py-3 border-t border-[#231d18]">
<p className="text-xs text-[#555] font-body">{activityLogs.length} log entries shown</p> <p className="text-xs text-[#555] font-body">{activityLogs.length} log entries shown</p>
</div> </div>
)} )}

View File

@@ -223,7 +223,7 @@ export default function AdminSuspensionsPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -288,7 +288,7 @@ export default function AdminSuspensionsPage() {
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${ className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab activeTab === tab
? 'text-[#ffb800] border-[#ffb800]' ? 'text-[#F0A623] border-[#F0A623]'
: 'text-[#555] border-transparent hover:text-[#888]' : 'text-[#555] border-transparent hover:text-[#888]'
}`} }`}
> >
@@ -304,7 +304,7 @@ export default function AdminSuspensionsPage() {
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<> <>
@@ -336,7 +336,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{s.user?.avatar_url ? ( {s.user?.avatar_url ? (
<img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" /> <img src={s.user.avatar_url} alt={s.user.full_name || s.user.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -415,7 +415,7 @@ export default function AdminSuspensionsPage() {
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] text-xs font-bold flex-shrink-0"> <div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] text-xs font-bold flex-shrink-0">
{(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()} {(s.user?.full_name?.[0] || s.user?.email?.[0] || '?').toUpperCase()}
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
@@ -465,7 +465,7 @@ export default function AdminSuspensionsPage() {
placeholder="Search users..." placeholder="Search users..."
value={userSearch} value={userSearch}
onChange={(e) => setUserSearch(e.target.value)} onChange={(e) => setUserSearch(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
</div> </div>
@@ -488,7 +488,7 @@ export default function AdminSuspensionsPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? ( {u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" /> <img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -572,7 +572,7 @@ export default function AdminSuspensionsPage() {
<form onSubmit={handleSuspend} className="px-6 py-5 space-y-4"> <form onSubmit={handleSuspend} className="px-6 py-5 space-y-4">
{/* User info */} {/* User info */}
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3"> <div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{selectedUser.avatar_url ? ( {selectedUser.avatar_url ? (
<img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" /> <img src={selectedUser.avatar_url} alt={selectedUser.full_name || selectedUser.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -637,7 +637,7 @@ export default function AdminSuspensionsPage() {
<select <select
value={suspendDuration ?? ''} value={suspendDuration ?? ''}
onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)} onChange={(e) => setSuspendDuration(e.target.value ? parseInt(e.target.value) : null)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
{DURATION_OPTIONS.filter(d => d.value !== null).map((d) => ( {DURATION_OPTIONS.filter(d => d.value !== null).map((d) => (
<option key={d.value} value={d.value!}>{d.label}</option> <option key={d.value} value={d.value!}>{d.label}</option>
@@ -657,7 +657,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setSuspendReason(e.target.value)} onChange={(e) => setSuspendReason(e.target.value)}
placeholder="Explain the reason for this action..." placeholder="Explain the reason for this action..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/> />
</div> </div>
@@ -729,7 +729,7 @@ export default function AdminSuspensionsPage() {
onChange={(e) => setLiftReason(e.target.value)} onChange={(e) => setLiftReason(e.target.value)}
placeholder="Reason for lifting this restriction..." placeholder="Reason for lifting this restriction..."
rows={2} rows={2}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/> />
</div> </div>
<div className="flex items-center gap-3 pt-1"> <div className="flex items-center gap-3 pt-1">

View File

@@ -31,14 +31,14 @@ interface Invitation {
type Tab = 'users' | 'invitations'; type Tab = 'users' | 'invitations';
const ROLE_OPTIONS = [ const ROLE_OPTIONS = [
{ value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#1a1a1a]' }, { value: 'reader', label: 'Reader', color: 'text-[#888] bg-[#231d18]' },
{ value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' }, { value: 'contributor', label: 'Contributor', color: 'text-blue-400 bg-blue-400/10' },
{ value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' }, { value: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' },
{ value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' }, { value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' },
]; ];
const ROLE_COLORS: Record<string, string> = { const ROLE_COLORS: Record<string, string> = {
reader: 'text-[#888] bg-[#1a1a1a]', reader: 'text-[#888] bg-[#231d18]',
contributor: 'text-blue-400 bg-blue-400/10', contributor: 'text-blue-400 bg-blue-400/10',
editor: 'text-purple-400 bg-purple-400/10', editor: 'text-purple-400 bg-purple-400/10',
admin: 'text-red-400 bg-red-400/10', admin: 'text-red-400 bg-red-400/10',
@@ -47,7 +47,7 @@ const ROLE_COLORS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
pending: 'text-yellow-400 bg-yellow-400/10', pending: 'text-yellow-400 bg-yellow-400/10',
accepted: 'text-green-400 bg-green-400/10', accepted: 'text-green-400 bg-green-400/10',
revoked: 'text-[#555] bg-[#1a1a1a]', revoked: 'text-[#555] bg-[#231d18]',
expired: 'text-orange-400 bg-orange-400/10', expired: 'text-orange-400 bg-orange-400/10',
email_failed: 'text-red-400 bg-red-400/10', email_failed: 'text-red-400 bg-red-400/10',
}; };
@@ -246,7 +246,7 @@ export default function AdminUsersPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -276,7 +276,7 @@ export default function AdminUsersPage() {
</div> </div>
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
className="flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors" className="flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" /> <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.41 2 2 0 0 1 3.6 1.22h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.96a16 16 0 0 0 6 6l.96-.96a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 21.5 16.5z" />
@@ -306,7 +306,7 @@ export default function AdminUsersPage() {
onClick={() => setActiveTab(tab)} onClick={() => setActiveTab(tab)}
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${ className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
activeTab === tab activeTab === tab
? 'text-[#ffb800] border-[#ffb800]' ? 'text-[#F0A623] border-[#F0A623]'
: 'text-[#555] border-transparent hover:text-[#888]' : 'text-[#555] border-transparent hover:text-[#888]'
}`} }`}
> >
@@ -331,14 +331,14 @@ export default function AdminUsersPage() {
placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'} placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'}
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#111827] border border-[#1f2937] text-white text-sm rounded-lg pl-9 pr-3 py-2 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
{activeTab === 'users' && ( {activeTab === 'users' && (
<select <select
value={roleFilter} value={roleFilter}
onChange={(e) => setRoleFilter(e.target.value)} onChange={(e) => setRoleFilter(e.target.value)}
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623]"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
{ROLE_OPTIONS.map((r) => ( {ROLE_OPTIONS.map((r) => (
@@ -353,7 +353,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden"> <div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredUsers.length === 0 ? ( ) : filteredUsers.length === 0 ? (
<div className="text-center py-16 text-[#555]"> <div className="text-center py-16 text-[#555]">
@@ -379,7 +379,7 @@ export default function AdminUsersPage() {
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{u.avatar_url ? ( {u.avatar_url ? (
<img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" /> <img src={u.avatar_url} alt={u.full_name || u.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -393,7 +393,7 @@ export default function AdminUsersPage() {
</div> </div>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[u.role] || 'text-[#888] bg-[#231d18]'}`}>
{u.role} {u.role}
</span> </span>
</td> </td>
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
<button <button
onClick={() => openEditRole(u)} onClick={() => openEditRole(u)}
disabled={processingIds.has(u.id)} disabled={processingIds.has(u.id)}
className="text-[#555] hover:text-[#ffb800] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50" className="text-[#555] hover:text-[#F0A623] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
> >
Edit Role Edit Role
</button> </button>
@@ -455,7 +455,7 @@ export default function AdminUsersPage() {
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden"> <div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-16"> <div className="flex items-center justify-center py-16">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filteredInvitations.length === 0 ? ( ) : filteredInvitations.length === 0 ? (
<div className="text-center py-16 text-[#555]"> <div className="text-center py-16 text-[#555]">
@@ -465,7 +465,7 @@ export default function AdminUsersPage() {
<p className="text-sm">No invitations sent yet</p> <p className="text-sm">No invitations sent yet</p>
<button <button
onClick={() => setShowInviteModal(true)} onClick={() => setShowInviteModal(true)}
className="mt-3 text-[#ffb800] hover:text-[#ffd25a] text-sm transition-colors" className="mt-3 text-[#F0A623] hover:text-[#F0A623] text-sm transition-colors"
> >
Send your first invitation Send your first invitation
</button> </button>
@@ -491,19 +491,19 @@ export default function AdminUsersPage() {
<tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors"> <tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
<td className="px-5 py-3.5"> <td className="px-5 py-3.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] text-xs font-bold flex-shrink-0"> <div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] text-xs font-bold flex-shrink-0">
{inv.email[0].toUpperCase()} {inv.email[0].toUpperCase()}
</div> </div>
<span className="text-white text-sm">{inv.email}</span> <span className="text-white text-sm">{inv.email}</span>
</div> </div>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold uppercase tracking-wide ${ROLE_COLORS[inv.role] || 'text-[#888] bg-[#231d18]'}`}>
{inv.role} {inv.role}
</span> </span>
</td> </td>
<td className="px-4 py-3.5"> <td className="px-4 py-3.5">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#1a1a1a]'}`}> <span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold capitalize ${STATUS_COLORS[displayStatus] || 'text-[#888] bg-[#231d18]'}`}>
{displayStatus.replace('_', ' ')} {displayStatus.replace('_', ' ')}
</span> </span>
</td> </td>
@@ -564,7 +564,7 @@ export default function AdminUsersPage() {
value={inviteEmail} value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)} onChange={(e) => setInviteEmail(e.target.value)}
placeholder="contributor@example.com" placeholder="contributor@example.com"
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -574,7 +574,7 @@ export default function AdminUsersPage() {
<select <select
value={inviteRole} value={inviteRole}
onChange={(e) => setInviteRole(e.target.value)} onChange={(e) => setInviteRole(e.target.value)}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value="contributor">Contributor can submit articles for review</option> <option value="contributor">Contributor can submit articles for review</option>
<option value="editor">Editor can publish and manage articles</option> <option value="editor">Editor can publish and manage articles</option>
@@ -590,7 +590,7 @@ export default function AdminUsersPage() {
onChange={(e) => setInviteMessage(e.target.value)} onChange={(e) => setInviteMessage(e.target.value)}
placeholder="Add a personal note to the invitation email..." placeholder="Add a personal note to the invitation email..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 placeholder-[#444] focus:outline-none focus:border-[#F0A623] resize-none"
/> />
</div> </div>
<div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3"> <div className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
@@ -609,7 +609,7 @@ export default function AdminUsersPage() {
<button <button
type="submit" type="submit"
disabled={inviteSending || !inviteEmail.trim()} disabled={inviteSending || !inviteEmail.trim()}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2" className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
> >
{inviteSending ? ( {inviteSending ? (
<> <>
@@ -644,7 +644,7 @@ export default function AdminUsersPage() {
</div> </div>
<div className="px-6 py-5 space-y-4"> <div className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3"> <div className="flex items-center gap-3 bg-[#0d1117] rounded-lg p-3">
<div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#ffb800] font-bold text-sm flex-shrink-0 overflow-hidden"> <div className="w-9 h-9 rounded-full bg-[#1f2937] flex items-center justify-center text-[#F0A623] font-bold text-sm flex-shrink-0 overflow-hidden">
{editingUser.avatar_url ? ( {editingUser.avatar_url ? (
<img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" /> <img src={editingUser.avatar_url} alt={editingUser.full_name || editingUser.email} className="w-full h-full object-cover" />
) : ( ) : (
@@ -666,7 +666,7 @@ export default function AdminUsersPage() {
key={r.value} key={r.value}
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${ className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
editRole === r.value editRole === r.value
? 'border-[#ffb800] bg-[#ffb800]/10' ? 'border-[#F0A623] bg-[#F0A623]/10'
: 'border-[#1f2937] hover:border-[#2a3a50]' : 'border-[#1f2937] hover:border-[#2a3a50]'
}`} }`}
> >
@@ -679,9 +679,9 @@ export default function AdminUsersPage() {
className="sr-only" className="sr-only"
/> />
<span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${ <span className={`w-3.5 h-3.5 rounded-full border-2 flex items-center justify-center flex-shrink-0 ${
editRole === r.value ? 'border-[#ffb800]' : 'border-[#444]' editRole === r.value ? 'border-[#F0A623]' : 'border-[#444]'
}`}> }`}>
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#ffb800]" />} {editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623]" />}
</span> </span>
<span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}> <span className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}>
{r.label} {r.label}
@@ -700,7 +700,7 @@ export default function AdminUsersPage() {
<button <button
onClick={handleEditSave} onClick={handleEditSave}
disabled={editSaving || editRole === editingUser.role} disabled={editSaving || editRole === editingUser.role}
className="flex-1 bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2" className="flex-1 bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-semibold py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2"
> >
{editSaving ? ( {editSaving ? (
<> <>

View File

@@ -223,7 +223,7 @@ export default function WPUsersAdminPage() {
</thead> </thead>
<tbody> <tbody>
{filteredUsers.map(u => ( {filteredUsers.map(u => (
<tr key={u.id} className="border-b border-[#1a1a1a] hover:bg-[#151515] transition-colors"> <tr key={u.id} className="border-b border-[#231d18] hover:bg-[#151515] transition-colors">
<td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td> <td className="px-4 py-3 text-[#555] font-mono text-xs">{u.wp_id}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className="text-white font-medium">{u.wp_username}</span> <span className="text-white font-medium">{u.wp_username}</span>

View File

@@ -44,7 +44,7 @@ export default function AdvertisePage() {
{/* Hero */} {/* Hero */}
{/* DO NOT OVERRIDE — Advertise page hero section */} {/* DO NOT OVERRIDE — Advertise page hero section */}
<div className="bg-[#0d1520] border-b border-[#4a3500] py-12"> <div className="bg-[#0d1520] border-b border-[#412402] py-12">
<div className="max-w-container mx-auto px-4 text-center"> <div className="max-w-container mx-auto px-4 text-center">
<span className="section-label mb-3 inline-block">Advertise</span> <span className="section-label mb-3 inline-block">Advertise</span>
<h1 className="font-heading text-white text-4xl font-bold mb-4">Reach Pro AV &amp; Live Production Professionals</h1> <h1 className="font-heading text-white text-4xl font-bold mb-4">Reach Pro AV &amp; Live Production Professionals</h1>
@@ -64,12 +64,12 @@ export default function AdvertisePage() {
<section className="mb-12"> <section className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Our Audience</span> <span className="section-label">Our Audience</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat) => ( {stats.map((stat) => (
<div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center"> <div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center">
<div className="font-heading text-[#ffb800] text-2xl font-bold mb-1">{stat.value}</div> <div className="font-heading text-[#F0A623] text-2xl font-bold mb-1">{stat.value}</div>
<div className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div> <div className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div>
</div> </div>
))} ))}
@@ -80,25 +80,25 @@ export default function AdvertisePage() {
<section className="mb-12"> <section className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Ad Placements</span> <span className="section-label">Ad Placements</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm font-body"> <table className="w-full text-sm font-body">
<thead> <thead>
<tr className="border-b border-[#222]"> <tr className="border-b border-[#222]">
<th className="text-left py-3 px-4 text-[#ffb800] font-bold text-xs uppercase tracking-wider">Placement</th> <th className="text-left py-3 px-4 text-[#F0A623] font-bold text-xs uppercase tracking-wider">Placement</th>
<th className="text-left py-3 px-4 text-[#ffb800] font-bold text-xs uppercase tracking-wider">Dimensions</th> <th className="text-left py-3 px-4 text-[#F0A623] font-bold text-xs uppercase tracking-wider">Dimensions</th>
<th className="text-left py-3 px-4 text-[#ffb800] font-bold text-xs uppercase tracking-wider">Location</th> <th className="text-left py-3 px-4 text-[#F0A623] font-bold text-xs uppercase tracking-wider">Location</th>
<th className="text-left py-3 px-4 text-[#ffb800] font-bold text-xs uppercase tracking-wider">Rate</th> <th className="text-left py-3 px-4 text-[#F0A623] font-bold text-xs uppercase tracking-wider">Rate</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{adZones.map((zone, i) => ( {adZones.map((zone, i) => (
<tr key={zone.name} className={`border-b border-[#1a1a1a] ${i % 2 === 0 ? "bg-[#0d0d0d]" : "bg-[#111]"}`}> <tr key={zone.name} className={`border-b border-[#231d18] ${i % 2 === 0 ? "bg-[#1c1815]" : "bg-[#111]"}`}>
<td className="py-3 px-4 text-[#e0e0e0] font-medium">{zone.name}</td> <td className="py-3 px-4 text-[#e0e0e0] font-medium">{zone.name}</td>
<td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td> <td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td>
<td className="py-3 px-4 text-[#777]">{zone.placement}</td> <td className="py-3 px-4 text-[#777]">{zone.placement}</td>
<td className="py-3 px-4 text-[#ffb800] font-medium">{zone.cpm}</td> <td className="py-3 px-4 text-[#F0A623] font-medium">{zone.cpm}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -114,7 +114,7 @@ export default function AdvertisePage() {
<section id="contact" className="mb-12"> <section id="contact" className="mb-12">
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Contact Sales</span> <span className="section-label">Contact Sales</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-8">
{/* Contact info */} {/* Contact info */}
@@ -123,23 +123,23 @@ export default function AdvertisePage() {
<h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3> <h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<p className="font-body text-[#ffb800] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p> <p className="font-body text-[#F0A623] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p>
<p className="font-body text-[#e0e0e0] font-medium">Jeff Victor</p> <p className="font-body text-[#e0e0e0] font-medium">Jeff Victor</p>
<a href="mailto:jeff@avbeat.com" className="font-body text-[#ffb800] text-sm hover:underline"> <a href="mailto:jeff@avbeat.com" className="font-body text-[#F0A623] text-sm hover:underline">
jeff@avbeat.com jeff@avbeat.com
</a> </a>
</div> </div>
<div className="border-t border-[#222] pt-4"> <div className="border-t border-[#222] pt-4">
<p className="font-body text-[#ffb800] text-xs font-bold uppercase tracking-wider mb-1">Publication</p> <p className="font-body text-[#F0A623] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
<p className="font-body text-[#777] text-sm">AV Beat</p> <p className="font-body text-[#777] text-sm">AV Beat</p>
<p className="font-body text-[#777] text-sm">Relevant Media Properties</p> <p className="font-body text-[#777] text-sm">Relevant Media Properties</p>
</div> </div>
<div className="border-t border-[#222] pt-4"> <div className="border-t border-[#222] pt-4">
<p className="font-body text-[#ffb800] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p> <p className="font-body text-[#F0A623] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p>
<ul className="space-y-1.5"> <ul className="space-y-1.5">
{["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => ( {["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => (
<li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body"> <li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body">
<span className="text-[#ffb800] mt-0.5"></span> <span className="text-[#F0A623] mt-0.5"></span>
{item} {item}
</li> </li>
))} ))}
@@ -152,16 +152,16 @@ export default function AdvertisePage() {
{/* Form */} {/* Form */}
<div className="lg:col-span-8"> <div className="lg:col-span-8">
{submitted ? ( {submitted ? (
<div className="bg-[#111] border border-[#ffb800] p-8 text-center"> <div className="bg-[#111] border border-[#F0A623] p-8 text-center">
<div className="w-12 h-12 rounded-full bg-[#ffb800]/20 flex items-center justify-center mx-auto mb-4"> <div className="w-12 h-12 rounded-full bg-[#F0A623]/20 flex items-center justify-center mx-auto mb-4">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path d="M5 12l4 4 10-10" stroke="#ffb800" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M5 12l4 4 10-10" stroke="#F0A623" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3> <h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
<p className="font-body text-[#777] text-sm"> <p className="font-body text-[#777] text-sm">
Thanks for reaching out. Jeff will be in touch within one business day at{" "} Thanks for reaching out. Jeff will be in touch within one business day at{" "}
<a href="mailto:jeff@avbeat.com" className="text-[#ffb800] hover:underline">jeff@avbeat.com</a>. <a href="mailto:jeff@avbeat.com" className="text-[#F0A623] hover:underline">jeff@avbeat.com</a>.
</p> </p>
</div> </div>
) : ( ) : (
@@ -219,7 +219,7 @@ export default function AdvertisePage() {
<select <select
value={formData.budget} value={formData.budget}
onChange={(e) => setFormData({ ...formData, budget: e.target.value })} onChange={(e) => setFormData({ ...formData, budget: e.target.value })}
className="search-input w-full py-2.5 px-3 text-sm bg-[#1a1a1a]"> className="search-input w-full py-2.5 px-3 text-sm bg-[#231d18]">
<option value="">Select a range...</option> <option value="">Select a range...</option>
<option value="under-5k">Under $5,000</option> <option value="under-5k">Under $5,000</option>
<option value="5k-15k">$5,000 $15,000</option> <option value="5k-15k">$5,000 $15,000</option>
@@ -245,7 +245,7 @@ export default function AdvertisePage() {
{loading ? "Sending..." : "Send Inquiry"} {loading ? "Sending..." : "Send Inquiry"}
</button> </button>
<p className="text-[#555] text-xs font-body"> <p className="text-[#555] text-xs font-body">
Or email directly: <a href="mailto:jeff@avbeat.com" className="text-[#ffb800] hover:underline">jeff@avbeat.com</a> Or email directly: <a href="mailto:jeff@avbeat.com" className="text-[#F0A623] hover:underline">jeff@avbeat.com</a>
</p> </p>
</form> </form>
)} )}

View File

@@ -38,7 +38,7 @@ export async function POST(req: NextRequest) {
subject_prefix: subject_prefix || '', subject_prefix: subject_prefix || '',
header_text: header_text || '', header_text: header_text || '',
footer_text: footer_text || '', footer_text: footer_text || '',
accent_color: accent_color || '#ffb800', accent_color: accent_color || '#F0A623',
article_blocks: article_blocks || [], article_blocks: article_blocks || [],
is_preset: false, is_preset: false,
}) })

View File

@@ -227,11 +227,11 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Breadcrumb navigation */} {/* Breadcrumb navigation */}
{/* DO NOT OVERRIDE — article breadcrumb and back navigation */} {/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]"> <div className="bg-[#1c1815] border-b border-[#231d18]">
<div className="max-w-container mx-auto px-4 py-2.5 flex items-center gap-2 text-xs font-body text-[#555]"> <div className="max-w-container mx-auto px-4 py-2.5 flex items-center gap-2 text-xs font-body text-[#555]">
<Link href="/home-page" className="hover:text-[#ffb800] transition-colors">Home</Link> <Link href="/home-page" className="hover:text-[#F0A623] transition-colors">Home</Link>
<span>/</span> <span>/</span>
<Link href={sectionHref} className="hover:text-[#ffb800] transition-colors">{sectionLabel}</Link> <Link href={sectionHref} className="hover:text-[#F0A623] transition-colors">{sectionLabel}</Link>
<span>/</span> <span>/</span>
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span> <span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
</div> </div>
@@ -245,7 +245,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Article Header */} {/* Article Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#ffb800] hover:underline"> <Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#F0A623] hover:underline">
{article.category} {article.category}
</Link> </Link>
<span className="text-xs text-[#777]">{article.date}</span> <span className="text-xs text-[#777]">{article.date}</span>
@@ -271,7 +271,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div> <div>
<Link <Link
href={`/authors/${article.authorSlug}`} href={`/authors/${article.authorSlug}`}
className="font-heading font-bold text-sm hover:text-[#ffb800] transition-colors text-[#e0e0e0]"> className="font-heading font-bold text-sm hover:text-[#F0A623] transition-colors text-[#e0e0e0]">
{article.author} {article.author}
</Link> </Link>
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p> <p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
@@ -286,7 +286,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<> <>
<a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`} <a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`}
aria-label="Share via email" title="Email" aria-label="Share via email" title="Email"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ffb800] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ffb800]"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#F0A623] hover:bg-[#231d18] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#F0A623]">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" /> <rect x="2" y="4" width="20" height="16" rx="2" />
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /> <path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
@@ -294,39 +294,39 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
</a> </a>
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`} <a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn" target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
</a> </a>
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`} <a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`}
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)" target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#231d18] transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a> </a>
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`} <a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1877f2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#1877f2] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a> </a>
<a href={`https://www.reddit.com/submit?url=${encodeURIComponent(articleUrl)}&title=${encodeURIComponent(article.title)}`} <a href={`https://www.reddit.com/submit?url=${encodeURIComponent(articleUrl)}&title=${encodeURIComponent(article.title)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit" target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ff4500] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ff4500] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>
</a> </a>
<a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + " " + articleUrl)}`} <a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + " " + articleUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp" target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#25d366] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#25d366] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.711.306 1.265.489 1.697.626.713.226 1.362.194 1.875.118.572-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.711.306 1.265.489 1.697.626.713.226 1.362.194 1.875.118.572-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413z"/></svg>
</a> </a>
<button type="button" <button type="button"
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }} onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
aria-label="Copy article link" title="Copy link" aria-label="Copy article link" title="Copy link"
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#ffb800] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#F0A623] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/> <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/> <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg> </svg>
</button> </button>
<span className="w-px h-5 bg-[#252525] mx-1" aria-hidden="true" /> <span className="w-px h-5 bg-[#3a322b] mx-1" aria-hidden="true" />
</> </>
); );
})()} })()}
@@ -334,7 +334,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
onClick={handleSaveToReadingList} onClick={handleSaveToReadingList}
disabled={savingState !== "idle"} disabled={savingState !== "idle"}
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"} aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
className="text-xs font-bold uppercase tracking-widest text-[#ffb800] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ffb800]"> className="text-xs font-bold uppercase tracking-widest text-[#F0A623] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#F0A623]">
{savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"} {savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"}
</button> </button>
</div> </div>
@@ -368,7 +368,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{article.tags.map((tag) => ( {article.tags.map((tag) => (
<span <span
key={tag} key={tag}
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#ffb800] hover:text-[#ffb800] transition-colors rounded-sm cursor-default"> className="px-3 py-1 text-xs font-body bg-[#231d18] border border-[#3a322b] text-[#888] hover:border-[#F0A623] hover:text-[#F0A623] transition-colors rounded-sm cursor-default">
{tag} {tag}
</span> </span>
))} ))}
@@ -380,7 +380,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div className="pt-6 border-t border-[#222]"> <div className="pt-6 border-t border-[#222]">
<Link <Link
href={sectionHref} href={sectionHref}
className="inline-flex items-center gap-2 text-[#ffb800] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ffb800]"> className="inline-flex items-center gap-2 text-[#F0A623] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#F0A623]">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<polyline points="15 18 9 12 15 6" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
@@ -398,7 +398,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
{/* Related Articles */} {/* Related Articles */}
{/* DO NOT OVERRIDE — related articles sidebar */} {/* DO NOT OVERRIDE — related articles sidebar */}
<div className="bg-[#111] border border-[#222] p-5"> <div className="bg-[#111] border border-[#222] p-5">
<h3 className="font-body font-bold text-xs text-[#ffb800] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]"> <h3 className="font-body font-bold text-xs text-[#F0A623] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
Related Articles Related Articles
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
@@ -417,10 +417,10 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
/> />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-[#ffb800] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5"> <p className="text-[#F0A623] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
{related.category} {related.category}
</p> </p>
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#ffb800] transition-colors line-clamp-2 leading-snug"> <p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug">
{related.title} {related.title}
</p> </p>
</div> </div>
@@ -435,19 +435,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
recently-active threads so the box is never empty. */} recently-active threads so the box is never empty. */}
{relatedForumThreads.length > 0 && ( {relatedForumThreads.length > 0 && (
<div className="bg-[#111] border border-[#222] p-5"> <div className="bg-[#111] border border-[#222] p-5">
<h3 className="font-body font-bold text-xs text-[#ffb800] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]"> <h3 className="font-body font-bold text-xs text-[#F0A623] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
Related Forum Posts Related Forum Posts
</h3> </h3>
<ul className="space-y-3.5"> <ul className="space-y-3.5">
{relatedForumThreads.map((t) => ( {relatedForumThreads.map((t) => (
<li key={t.id}> <li key={t.id}>
<Link href={`/forum/thread/${t.id}`} className="group block"> <Link href={`/forum/thread/${t.id}`} className="group block">
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#ffb800] transition-colors line-clamp-2 leading-snug"> <p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#F0A623] transition-colors line-clamp-2 leading-snug">
{t.title} {t.title}
</p> </p>
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]"> <p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
{t.category_name && ( {t.category_name && (
<span className="text-[#ffb800] font-body font-bold uppercase tracking-wider"> <span className="text-[#F0A623] font-body font-bold uppercase tracking-wider">
{t.category_name} {t.category_name}
</span> </span>
)} )}
@@ -461,7 +461,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
</ul> </ul>
<Link <Link
href="/forum" href="/forum"
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#ffb800] hover:underline font-body font-semibold uppercase tracking-wider" className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#F0A623] hover:underline font-body font-semibold uppercase tracking-wider"
> >
Browse all forum threads Browse all forum threads
</Link> </Link>
@@ -479,7 +479,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
<div <div
role="status" role="status"
aria-live="polite" aria-live="polite"
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#ffb800] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50"> className="fixed bottom-4 right-4 bg-[#231d18] border border-[#F0A623] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
{saveToast} {saveToast}
</div> </div>
)} )}

View File

@@ -118,7 +118,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
}; };
return ( return (
<section className="mt-10 pt-8 border-t border-[#2a2a2a]"> <section className="mt-10 pt-8 border-t border-[#3a322b]">
{/* Section Header */} {/* Section Header */}
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
<span className="section-label">Comments</span> <span className="section-label">Comments</span>
@@ -127,15 +127,15 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
{comments.length} {comments.length === 1 ? "comment" : "comments"} {comments.length} {comments.length === 1 ? "comment" : "comments"}
</span> </span>
)} )}
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
{/* Comment Form */} {/* Comment Form */}
{user ? ( {user ? (
<form onSubmit={handleSubmit} className="mb-8"> <form onSubmit={handleSubmit} className="mb-8">
<div className="flex gap-3"> <div className="flex gap-3">
<div className="w-8 h-8 rounded-full bg-[#4a3500] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5"> <div className="w-8 h-8 rounded-full bg-[#412402] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5">
<span className="font-body text-[10px] font-bold text-[#ffb800]"> <span className="font-body text-[10px] font-bold text-[#F0A623]">
{getInitials(user.user_metadata?.full_name || user.email || "U")} {getInitials(user.user_metadata?.full_name || user.email || "U")}
</span> </span>
</div> </div>
@@ -146,10 +146,10 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
placeholder="Share your thoughts on this article..." placeholder="Share your thoughts on this article..."
rows={3} rows={3}
maxLength={1000} maxLength={1000}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800] transition-colors placeholder-[#444] resize-none" className="w-full bg-[#231d18] border border-[#3a322b] text-[#e0e0e0] font-body text-sm px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623] transition-colors placeholder-[#444] resize-none"
/> />
{error && ( {error && (
<p className="font-body text-xs text-[#d60701] mt-1.5">{error}</p> <p className="font-body text-xs text-[#D85A30] mt-1.5">{error}</p>
)} )}
<div className="flex items-center justify-between mt-2"> <div className="flex items-center justify-between mt-2">
<span className="font-body text-[11px] text-[#555]"> <span className="font-body text-[11px] text-[#555]">
@@ -158,7 +158,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
<button <button
type="submit" type="submit"
disabled={submitting || !newComment.trim()} disabled={submitting || !newComment.trim()}
className="bg-[#ffb800] hover:bg-[#d99700] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]"> className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
{submitting ? "Posting..." : "Post Comment"} {submitting ? "Posting..." : "Post Comment"}
</button> </button>
</div> </div>
@@ -166,14 +166,14 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
</div> </div>
</form> </form>
) : ( ) : (
<div className="mb-8 bg-[#0d1520] border border-[#4a3500] rounded-sm p-5 flex items-center justify-between gap-4"> <div className="mb-8 bg-[#0d1520] border border-[#412402] rounded-sm p-5 flex items-center justify-between gap-4">
<div> <div>
<p className="font-body text-sm font-bold text-[#e0e0e0] mb-0.5">Join the conversation</p> <p className="font-body text-sm font-bold text-[#e0e0e0] mb-0.5">Join the conversation</p>
<p className="font-body text-xs text-[#666]">Sign in to post a comment on this article.</p> <p className="font-body text-xs text-[#666]">Sign in to post a comment on this article.</p>
</div> </div>
<Link <Link
href="/login" href="/login"
className="flex-shrink-0 bg-[#ffb800] hover:bg-[#d99700] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors"> className="flex-shrink-0 bg-[#F0A623] hover:bg-[#BA7517] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors">
Sign In Sign In
</Link> </Link>
</div> </div>
@@ -208,7 +208,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
const isOwn = user?.id === comment.user_id; const isOwn = user?.id === comment.user_id;
return ( return (
<div key={comment.id} className="flex gap-3 group"> <div key={comment.id} className="flex gap-3 group">
<div className="w-8 h-8 rounded-full bg-[#1a1208] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5 overflow-hidden"> <div className="w-8 h-8 rounded-full bg-[#231d18] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5 overflow-hidden">
{comment.user_profiles?.avatar_url ? ( {comment.user_profiles?.avatar_url ? (
<img <img
src={comment.user_profiles.avatar_url} src={comment.user_profiles.avatar_url}
@@ -216,7 +216,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
className="w-full h-full object-cover" className="w-full h-full object-cover"
/> />
) : ( ) : (
<span className="font-body text-[10px] font-bold text-[#ffb800]"> <span className="font-body text-[10px] font-bold text-[#F0A623]">
{getInitials(name)} {getInitials(name)}
</span> </span>
)} )}
@@ -230,7 +230,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
onClick={() => handleDelete(comment.id)} onClick={() => handleDelete(comment.id)}
disabled={deletingId === comment.id} disabled={deletingId === comment.id}
aria-label="Delete comment" aria-label="Delete comment"
className="ml-auto opacity-0 group-hover:opacity-100 font-body text-[11px] text-[#555] hover:text-[#d60701] transition-all disabled:opacity-30"> className="ml-auto opacity-0 group-hover:opacity-100 font-body text-[11px] text-[#555] hover:text-[#D85A30] transition-all disabled:opacity-30">
{deletingId === comment.id ? "Deleting..." : "Delete"} {deletingId === comment.id ? "Deleting..." : "Delete"}
</button> </button>
)} )}

View File

@@ -55,7 +55,7 @@ export default function ClientReadout() {
}, []); }, []);
return ( return (
<pre style={{ padding: 20, fontFamily: "monospace", whiteSpace: "pre-wrap", background: "#1a1a1a", color: "#bbb", borderTop: "1px solid #333" }}> <pre style={{ padding: 20, fontFamily: "monospace", whiteSpace: "pre-wrap", background: "#231d18", color: "#bbb", borderTop: "1px solid #333" }}>
{JSON.stringify(out, null, 2)} {JSON.stringify(out, null, 2)}
</pre> </pre>
); );

View File

@@ -113,7 +113,7 @@ export default function AuthorProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="bg-[#111111] min-h-screen"> <main className="bg-[#1c1815] min-h-screen">
<aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside> <aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside>
{/* Cover Image */} {/* Cover Image */}
<div className="relative h-40 md:h-52 overflow-hidden"> <div className="relative h-40 md:h-52 overflow-hidden">
@@ -126,14 +126,14 @@ export default function AuthorProfilePage() {
className="w-full h-full object-cover opacity-40" className="w-full h-full object-cover opacity-40"
/> />
)} )}
<div className="absolute inset-0 bg-gradient-to-t from-[#111111] via-[#111111]/60 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-t from-[#1c1815] via-[#1c1815]/60 to-transparent" />
</div> </div>
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
{/* Author Header */} {/* Author Header */}
<div className="relative -mt-16 md:-mt-20 mb-6"> <div className="relative -mt-16 md:-mt-20 mb-6">
<div className="flex flex-col sm:flex-row sm:items-end gap-4"> <div className="flex flex-col sm:flex-row sm:items-end gap-4">
<div className="w-24 h-24 md:w-28 md:h-28 rounded-full overflow-hidden border-4 border-[#111111] flex-shrink-0 bg-[#1a1a1a]"> <div className="w-24 h-24 md:w-28 md:h-28 rounded-full overflow-hidden border-4 border-[#1c1815] flex-shrink-0 bg-[#231d18]">
{profile && ( {profile && (
<AppImage <AppImage
src={profile.avatar_url} src={profile.avatar_url}
@@ -145,10 +145,10 @@ export default function AuthorProfilePage() {
)} )}
</div> </div>
<div className="pb-1 flex-1"> <div className="pb-1 flex-1">
<h1 className="font-heading text-2xl md:text-3xl font-bold text-[#e8e8e8] leading-tight"> <h1 className="font-heading text-2xl md:text-3xl font-bold text-[#FBEFE0] leading-tight">
{loading ? "Loading…" : profile?.display_name || "Author"} {loading ? "Loading…" : profile?.display_name || "Author"}
</h1> </h1>
<p className="font-body text-sm text-[#ffb800] mt-0.5"> <p className="font-body text-sm text-[#F0A623] mt-0.5">
{profile?.role || "Contributor"} {profile?.role || "Contributor"}
</p> </p>
<div className="flex flex-wrap items-center gap-3 mt-2"> <div className="flex flex-wrap items-center gap-3 mt-2">
@@ -180,7 +180,7 @@ export default function AuthorProfilePage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label={`${profile.display_name} on Twitter`} aria-label={`${profile.display_name} on Twitter`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#ffb800] hover:text-[#ffb800] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#231d18] border border-[#3a322b] hover:border-[#F0A623] hover:text-[#F0A623] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" /> <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
@@ -193,7 +193,7 @@ export default function AuthorProfilePage() {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
aria-label={`${profile.display_name} on LinkedIn`} aria-label={`${profile.display_name} on LinkedIn`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#ffb800] hover:text-[#ffb800] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#231d18] border border-[#3a322b] hover:border-[#F0A623] hover:text-[#F0A623] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" /> <path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
@@ -204,7 +204,7 @@ export default function AuthorProfilePage() {
<a <a
href={`mailto:${profile.email}`} href={`mailto:${profile.email}`}
aria-label={`Email ${profile.display_name}`} aria-label={`Email ${profile.display_name}`}
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#ffb800] hover:text-[#ffb800] text-[#888] transition-colors rounded-sm" className="w-8 h-8 flex items-center justify-center bg-[#231d18] border border-[#3a322b] hover:border-[#F0A623] hover:text-[#F0A623] text-[#888] transition-colors rounded-sm"
> >
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" /> <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
@@ -223,7 +223,7 @@ export default function AuthorProfilePage() {
{profile.specialties.map((spec) => ( {profile.specialties.map((spec) => (
<span <span
key={spec} key={spec}
className="font-body text-xs text-[#ffb800] bg-[#4a3500]/30 border border-[#4a3500] px-2.5 py-1 rounded-sm" className="font-body text-xs text-[#F0A623] bg-[#412402]/30 border border-[#412402] px-2.5 py-1 rounded-sm"
> >
{spec} {spec}
</span> </span>
@@ -232,12 +232,12 @@ export default function AuthorProfilePage() {
)} )}
{/* Tabs */} {/* Tabs */}
<div className="flex gap-0 border-b border-[#2a2a2a] mb-6"> <div className="flex gap-0 border-b border-[#3a322b] mb-6">
<button <button
onClick={() => setActiveTab("articles")} onClick={() => setActiveTab("articles")}
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${ className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
activeTab === "articles" activeTab === "articles"
? "border-[#ffb800] text-[#ffb800]" ? "border-[#F0A623] text-[#F0A623]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`} }`}
> >
@@ -247,7 +247,7 @@ export default function AuthorProfilePage() {
onClick={() => setActiveTab("about")} onClick={() => setActiveTab("about")}
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${ className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
activeTab === "about" activeTab === "about"
? "border-[#ffb800] text-[#ffb800]" ? "border-[#F0A623] text-[#F0A623]"
: "border-transparent text-[#666] hover:text-[#aaa]" : "border-transparent text-[#666] hover:text-[#aaa]"
}`} }`}
> >
@@ -271,7 +271,7 @@ export default function AuthorProfilePage() {
<Link <Link
key={article.slug} key={article.slug}
href={`/articles/${article.slug}`} href={`/articles/${article.slug}`}
className="group bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#ffb800] transition-colors rounded-sm overflow-hidden article-card" className="group bg-[#231d18] border border-[#3a322b] hover:border-[#F0A623] transition-colors rounded-sm overflow-hidden article-card"
> >
<div className="img-zoom h-44 overflow-hidden"> <div className="img-zoom h-44 overflow-hidden">
<AppImage <AppImage
@@ -284,13 +284,13 @@ export default function AuthorProfilePage() {
</div> </div>
<div className="p-4"> <div className="p-4">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#ffb800]"> <span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#F0A623]">
{article.category} {article.category}
</span> </span>
<span className="font-body text-[10px] text-[#555]">·</span> <span className="font-body text-[10px] text-[#555]">·</span>
<span className="font-body text-[10px] text-[#555]">{article.readTime}</span> <span className="font-body text-[10px] text-[#555]">{article.readTime}</span>
</div> </div>
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#ffb800] transition-colors leading-snug mb-2 line-clamp-2"> <h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#F0A623] transition-colors leading-snug mb-2 line-clamp-2">
{article.title} {article.title}
</h3> </h3>
<p className="font-body text-xs text-[#777] leading-relaxed line-clamp-2 mb-3"> <p className="font-body text-xs text-[#777] leading-relaxed line-clamp-2 mb-3">
@@ -308,7 +308,7 @@ export default function AuthorProfilePage() {
{/* About tab */} {/* About tab */}
{activeTab === "about" && profile && ( {activeTab === "about" && profile && (
<div className="max-w-3xl pb-10 space-y-5"> <div className="max-w-3xl pb-10 space-y-5">
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<p className="section-label mb-4">Biography</p> <p className="section-label mb-4">Biography</p>
{profile.bio ? ( {profile.bio ? (
<p className="font-body text-sm text-[#aaaaaa] leading-relaxed whitespace-pre-line"> <p className="font-body text-sm text-[#aaaaaa] leading-relaxed whitespace-pre-line">
@@ -328,7 +328,7 @@ export default function AuthorProfilePage() {
profile.address_line1 || profile.address_line1 ||
profile.city || profile.city ||
profile.country) && ( profile.country) && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<p className="section-label mb-4">Contact</p> <p className="section-label mb-4">Contact</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{addressLines.length > 0 && ( {addressLines.length > 0 && (
@@ -353,7 +353,7 @@ export default function AuthorProfilePage() {
</p> </p>
<a <a
href={`tel:${profile.phone.replace(/\s+/g, "")}`} href={`tel:${profile.phone.replace(/\s+/g, "")}`}
className="font-body text-sm text-[#ffb800] hover:underline" className="font-body text-sm text-[#F0A623] hover:underline"
> >
{profile.phone} {profile.phone}
</a> </a>
@@ -366,7 +366,7 @@ export default function AuthorProfilePage() {
</p> </p>
<a <a
href={`mailto:${profile.email}`} href={`mailto:${profile.email}`}
className="font-body text-sm text-[#ffb800] hover:underline break-all" className="font-body text-sm text-[#F0A623] hover:underline break-all"
> >
{profile.email} {profile.email}
</a> </a>
@@ -381,7 +381,7 @@ export default function AuthorProfilePage() {
href={profile.website} href={profile.website}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-body text-sm text-[#ffb800] hover:underline break-all" className="font-body text-sm text-[#F0A623] hover:underline break-all"
> >
{profile.website.replace(/^https?:\/\//, "").replace(/\/$/, "")} {profile.website.replace(/^https?:\/\//, "").replace(/\/$/, "")}
</a> </a>
@@ -392,7 +392,7 @@ export default function AuthorProfilePage() {
)} )}
{/* Profile facts */} {/* Profile facts */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6">
<p className="section-label mb-4">Profile</p> <p className="section-label mb-4">Profile</p>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{locationLine && ( {locationLine && (

View File

@@ -283,7 +283,7 @@ export default function ContributorDashboard() {
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${ className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm transition-colors text-left ${
view === item.id view === item.id
? 'bg-[#c9a84c]/10 text-[#c9a84c] border border-[#c9a84c]/20' ? 'bg-[#c9a84c]/10 text-[#c9a84c] border border-[#c9a84c]/20'
: 'text-[#888] hover:text-white hover:bg-[#1a1a1a]' : 'text-[#888] hover:text-white hover:bg-[#231d18]'
}`} }`}
> >
<span className="text-base w-5 text-center">{item.icon}</span> <span className="text-base w-5 text-center">{item.icon}</span>
@@ -339,7 +339,7 @@ export default function ContributorDashboard() {
<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"> <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 + New Post
</button> </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"> <button onClick={() => setView('my-posts')} className="px-5 py-2.5 bg-[#231d18] border border-[#333] text-white rounded-lg text-sm hover:bg-[#222] transition-colors">
View My Posts View My Posts
</button> </button>
</div> </div>
@@ -453,7 +453,7 @@ export default function ContributorDashboard() {
{submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'} {submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'}
</button> </button>
<button type="button" onClick={() => setView('overview')} <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"> className="px-5 py-2.5 bg-[#231d18] border border-[#333] text-[#888] rounded-lg text-sm hover:text-white hover:bg-[#222] transition-colors">
Cancel Cancel
</button> </button>
</div> </div>

View File

@@ -214,7 +214,7 @@ function renderMarkdown(text: string): string {
.replace(/^# (.+)$/gm, '<h1 class="text-xl font-bold text-white mt-5 mb-2">$1</h1>') .replace(/^# (.+)$/gm, '<h1 class="text-xl font-bold text-white mt-5 mb-2">$1</h1>')
.replace(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>') .replace(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>') .replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`([^`]+)`/g, '<code class="bg-[#1a1208] text-[#ffb800] px-1 py-0.5 rounded text-sm font-mono">$1</code>') .replace(/`([^`]+)`/g, '<code class="bg-[#231d18] text-[#F0A623] px-1 py-0.5 rounded text-sm font-mono">$1</code>')
.replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-[#aaa]">$1</li>') .replace(/^- (.+)$/gm, '<li class="ml-4 list-disc text-[#aaa]">$1</li>')
.replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal text-[#aaa]">$1</li>') .replace(/^\d+\. (.+)$/gm, '<li class="ml-4 list-decimal text-[#aaa]">$1</li>')
.replace(/\n\n/g, '</p><p class="mb-3 text-[#ccc] leading-relaxed">') .replace(/\n\n/g, '</p><p class="mb-3 text-[#ccc] leading-relaxed">')
@@ -294,7 +294,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
</button> </button>
<button <button
onClick={() => onSave(true)} onClick={() => onSave(true)}
className="bg-[#ffb800] hover:bg-[#d99700] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#F0A623] hover:bg-[#BA7517] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
Open in Editor Open in Editor
</button> </button>
@@ -353,7 +353,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
onClick={() => { setSelectedVariant(v.prompt); setCustomPrompt(''); }} onClick={() => { setSelectedVariant(v.prompt); setCustomPrompt(''); }}
className={`text-left text-xs px-3 py-2 rounded border transition-colors ${ className={`text-left text-xs px-3 py-2 rounded border transition-colors ${
selectedVariant === v.prompt selectedVariant === v.prompt
? 'border-[#ffb800] bg-[#4a3500] text-white' ? 'border-[#F0A623] bg-[#412402] text-white'
: 'border-[#1e2a3a] text-[#aaa] hover:border-[#2a3a50] hover:text-white hover:bg-[#161f2e]' : 'border-[#1e2a3a] text-[#aaa] hover:border-[#2a3a50] hover:text-white hover:bg-[#161f2e]'
}`} }`}
> >
@@ -370,7 +370,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
onChange={e => { setCustomPrompt(e.target.value); setSelectedVariant(null); }} onChange={e => { setCustomPrompt(e.target.value); setSelectedVariant(null); }}
placeholder="e.g. Rewrite this focusing on the European market impact..." placeholder="e.g. Rewrite this focusing on the European market impact..."
rows={3} rows={3}
className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#333] resize-none" className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#333] resize-none"
/> />
</div> </div>
@@ -386,7 +386,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
<button onClick={onClose} className="text-[#555] hover:text-[#aaa] text-sm transition-colors">Cancel</button> <button onClick={onClose} className="text-[#555] hover:text-[#aaa] text-sm transition-colors">Cancel</button>
<button <button
onClick={handleSubmit} onClick={handleSubmit}
className="bg-[#ffb800] hover:bg-[#d99700] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#F0A623] hover:bg-[#BA7517] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
Regenerate Regenerate
</button> </button>
@@ -418,7 +418,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
const found = PEN_NAME_PROFILES.find(p => p.name === e.target.value); const found = PEN_NAME_PROFILES.find(p => p.name === e.target.value);
if (found) side === 'left' ? setLeftPen(found) : setRightPen(found); if (found) side === 'left' ? setLeftPen(found) : setRightPen(found);
}} }}
className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded px-2 py-1.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded px-2 py-1.5 focus:outline-none focus:border-[#F0A623]"
> >
{PEN_NAME_PROFILES.map(p => ( {PEN_NAME_PROFILES.map(p => (
<option key={p.name} value={p.name}>{p.name} ({p.site})</option> <option key={p.name} value={p.name}>{p.name} ({p.site})</option>
@@ -434,7 +434,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
</span> </span>
<span className="text-white font-semibold text-sm">{profile.name}</span> <span className="text-white font-semibold text-sm">{profile.name}</span>
</div> </div>
<p className="text-[#ffb800] text-xs">{profile.title}</p> <p className="text-[#F0A623] text-xs">{profile.title}</p>
</div> </div>
<div> <div>
@@ -458,7 +458,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
<div> <div>
<p className="text-[#555] text-[10px] uppercase tracking-wider font-bold mb-1">Example Lead</p> <p className="text-[#555] text-[10px] uppercase tracking-wider font-bold mb-1">Example Lead</p>
<p className="text-[#aaa] text-xs italic leading-relaxed border-l-2 border-[#ffb800]/40 pl-3">"{profile.exampleLead}"</p> <p className="text-[#aaa] text-xs italic leading-relaxed border-l-2 border-[#F0A623]/40 pl-3">"{profile.exampleLead}"</p>
</div> </div>
<button <button
@@ -485,7 +485,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
<button <button
key={f} key={f}
onClick={() => setFilterSite(f)} onClick={() => setFilterSite(f)}
className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#ffb800] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`} className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#F0A623] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`}
> >
{f === 'ALL' ? 'All Writers' : f === 'BB' ? 'AV Beat' : 'AV Beat'} {f === 'ALL' ? 'All Writers' : f === 'BB' ? 'AV Beat' : 'AV Beat'}
</button> </button>
@@ -514,7 +514,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
> >
<div className="flex items-center gap-2 mb-0.5"> <div className="flex items-center gap-2 mb-0.5">
<span className={`text-[9px] font-bold px-1 rounded ${p.site === 'AV' ? 'bg-orange-900/40 text-orange-400' : 'bg-blue-900/40 text-blue-400'}`}>{p.site}</span> <span className={`text-[9px] font-bold px-1 rounded ${p.site === 'AV' ? 'bg-orange-900/40 text-orange-400' : 'bg-blue-900/40 text-blue-400'}`}>{p.site}</span>
<span className="text-white text-xs font-medium group-hover:text-[#ffb800] transition-colors">{p.name}</span> <span className="text-white text-xs font-medium group-hover:text-[#F0A623] transition-colors">{p.name}</span>
</div> </div>
<p className="text-[#555] text-[10px]">{p.title}</p> <p className="text-[#555] text-[10px]">{p.title}</p>
</button> </button>
@@ -560,14 +560,14 @@ function ResponseHistorySidebar({ messages, onJumpTo, onClose }: ResponseHistory
className="w-full text-left p-3 rounded border border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e] transition-colors group" className="w-full text-left p-3 rounded border border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e] transition-colors group"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-[10px] text-[#ffb800] font-bold">Response #{idx + 1}</span> <span className="text-[10px] text-[#F0A623] font-bold">Response #{idx + 1}</span>
<span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${msg.provider === 'ollama' ? 'bg-green-500' : 'bg-yellow-500'}`} /> <span className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${msg.provider === 'ollama' ? 'bg-green-500' : 'bg-yellow-500'}`} />
</div> </div>
<p className="text-[#aaa] text-xs line-clamp-3 leading-relaxed">{preview}</p> <p className="text-[#aaa] text-xs line-clamp-3 leading-relaxed">{preview}</p>
<div className="flex items-center gap-3 mt-2"> <div className="flex items-center gap-3 mt-2">
<span className="text-[10px] text-[#444]">{msg.timestamp.toLocaleTimeString()}</span> <span className="text-[10px] text-[#444]">{msg.timestamp.toLocaleTimeString()}</span>
<span className="text-[10px] text-[#444]">{wordCount} words</span> <span className="text-[10px] text-[#444]">{wordCount} words</span>
<span className="text-[10px] text-[#ffb800] opacity-0 group-hover:opacity-100 transition-opacity">Jump </span> <span className="text-[10px] text-[#F0A623] opacity-0 group-hover:opacity-100 transition-opacity">Jump </span>
</div> </div>
</button> </button>
); );
@@ -927,7 +927,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
{/* Header */} {/* Header */}
<div className="bg-[#111827] border-b border-[#1e2a3a] px-4 py-3 flex items-center justify-between"> <div className="bg-[#111827] border-b border-[#1e2a3a] px-4 py-3 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/admin" className="text-[#555] hover:text-[#ffb800] text-sm transition-colors"> Dashboard</Link> <Link href="/admin" className="text-[#555] hover:text-[#F0A623] text-sm transition-colors"> Dashboard</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<h1 className="text-white font-bold text-sm tracking-wide">AI Console</h1> <h1 className="text-white font-bold text-sm tracking-wide">AI Console</h1>
</div> </div>
@@ -943,18 +943,18 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onClick={() => setShowHistorySidebar(v => !v)} onClick={() => setShowHistorySidebar(v => !v)}
className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${ className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${
showHistorySidebar showHistorySidebar
? 'bg-[#4a3500] border-[#ffb800]/40 text-[#ffb800]' ? 'bg-[#412402] border-[#F0A623]/40 text-[#F0A623]'
: 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]' : 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]'
}`} }`}
title="Toggle response history sidebar" title="Toggle response history sidebar"
> >
History History
{assistantMessageCount > 0 && ( {assistantMessageCount > 0 && (
<span className="ml-1.5 bg-[#ffb800] text-white text-[9px] font-bold px-1 py-0.5 rounded-full">{assistantMessageCount}</span> <span className="ml-1.5 bg-[#F0A623] text-white text-[9px] font-bold px-1 py-0.5 rounded-full">{assistantMessageCount}</span>
)} )}
</button> </button>
<Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#ffb800] text-xs transition-colors">Health</Link> <Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#F0A623] text-xs transition-colors">Health</Link>
<Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#ffb800] text-xs transition-colors">Settings</Link> <Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#F0A623] text-xs transition-colors">Settings</Link>
<button onClick={handleNewSession} className="text-xs bg-[#1e2a3a] hover:bg-[#253347] text-[#aaa] px-3 py-1.5 rounded transition-colors"> <button onClick={handleNewSession} className="text-xs bg-[#1e2a3a] hover:bg-[#253347] text-[#aaa] px-3 py-1.5 rounded transition-colors">
+ New Session + New Session
</button> </button>
@@ -1002,7 +1002,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
key={session.id} key={session.id}
className={`group p-2.5 rounded border cursor-pointer transition-colors ${ className={`group p-2.5 rounded border cursor-pointer transition-colors ${
currentSessionId === session.id currentSessionId === session.id
? 'border-[#ffb800]/40 bg-[#1e2a3a]' ? 'border-[#F0A623]/40 bg-[#1e2a3a]'
: 'border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e]' : 'border-[#1e2a3a] hover:border-[#2a3a50] hover:bg-[#161f2e]'
}`} }`}
> >
@@ -1046,11 +1046,11 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onClick={() => { onClick={() => {
fetch('/api/ai/console?action=ping').then(r => r.json()).then(setAiStatus).catch(() => {}); fetch('/api/ai/console?action=ping').then(r => r.json()).then(setAiStatus).catch(() => {});
}} }}
className="text-[10px] text-[#555] hover:text-[#ffb800] transition-colors" className="text-[10px] text-[#555] hover:text-[#F0A623] transition-colors"
> >
Refresh Refresh
</button> </button>
<Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#ffb800] transition-colors"> <Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#F0A623] transition-colors">
Health Dashboard Health Dashboard
</Link> </Link>
</div> </div>
@@ -1066,7 +1066,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<select <select
value={selectedSite} value={selectedSite}
onChange={e => setSelectedSite(Number(e.target.value) as 1 | 2)} onChange={e => setSelectedSite(Number(e.target.value) as 1 | 2)}
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#ffb800]" className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#F0A623]"
> >
<option value={1}>AV Beat</option> <option value={1}>AV Beat</option>
<option value={2}>AV Beat</option> <option value={2}>AV Beat</option>
@@ -1077,7 +1077,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<select <select
value={selectedPenName} value={selectedPenName}
onChange={e => setSelectedPenName(e.target.value)} onChange={e => setSelectedPenName(e.target.value)}
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#ffb800]" className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#F0A623]"
> >
{penNames.map(name => ( {penNames.map(name => (
<option key={name} value={name}>{name}</option> <option key={name} value={name}>{name}</option>
@@ -1086,7 +1086,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</div> </div>
<button <button
onClick={() => setShowStyleGuides(true)} onClick={() => setShowStyleGuides(true)}
className="text-[10px] text-[#ffb800] hover:text-[#ffd25a] transition-colors ml-auto" className="text-[10px] text-[#F0A623] hover:text-[#F0A623] transition-colors ml-auto"
> >
View style guide View style guide
</button> </button>
@@ -1123,7 +1123,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</div> </div>
)} )}
<div className={`rounded-lg px-4 py-3 ${ <div className={`rounded-lg px-4 py-3 ${
msg.role === 'user' ?'bg-[#4a3500] text-white text-sm' :'bg-[#111827] border border-[#1e2a3a] text-[#ccc] text-sm' msg.role === 'user' ?'bg-[#412402] text-white text-sm' :'bg-[#111827] border border-[#1e2a3a] text-[#ccc] text-sm'
}`}> }`}>
{msg.role === 'user' ? ( {msg.role === 'user' ? (
<p className="whitespace-pre-wrap">{msg.content}</p> <p className="whitespace-pre-wrap">{msg.content}</p>
@@ -1151,7 +1151,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
</button> </button>
<button <button
onClick={() => setShowDraftPreview(true)} onClick={() => setShowDraftPreview(true)}
className="text-[10px] text-[#ffb800] hover:text-[#ffd25a] bg-[#111827] border border-[#ffb800]/30 px-2 py-1 rounded transition-colors font-medium" className="text-[10px] text-[#F0A623] hover:text-[#F0A623] bg-[#111827] border border-[#F0A623]/30 px-2 py-1 rounded transition-colors font-medium"
> >
Preview Draft Preview Draft
</button> </button>
@@ -1195,9 +1195,9 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-[#111827] border border-[#1e2a3a] rounded-lg px-4 py-3"> <div className="bg-[#111827] border border-[#1e2a3a] rounded-lg px-4 py-3">
<div className="flex gap-1"> <div className="flex gap-1">
<div className="w-1.5 h-1.5 bg-[#ffb800] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} /> <div className="w-1.5 h-1.5 bg-[#F0A623] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-1.5 h-1.5 bg-[#ffb800] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} /> <div className="w-1.5 h-1.5 bg-[#F0A623] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-1.5 h-1.5 bg-[#ffb800] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} /> <div className="w-1.5 h-1.5 bg-[#F0A623] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div> </div>
</div> </div>
</div> </div>
@@ -1225,13 +1225,13 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
onChange={e => setUrlInput(e.target.value)} onChange={e => setUrlInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleFetchUrl()} onKeyDown={e => e.key === 'Enter' && handleFetchUrl()}
placeholder="https://example.com/press-release" placeholder="https://example.com/press-release"
className="flex-1 bg-[#1e2a3a] border border-[#2a3a50] text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-[#ffb800] placeholder-[#444]" className="flex-1 bg-[#1e2a3a] border border-[#2a3a50] text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-[#F0A623] placeholder-[#444]"
autoFocus autoFocus
/> />
<button <button
onClick={handleFetchUrl} onClick={handleFetchUrl}
disabled={fetchingUrl} disabled={fetchingUrl}
className="bg-[#ffb800] hover:bg-[#d99700] text-white text-xs px-3 py-2 rounded transition-colors disabled:opacity-50" className="bg-[#F0A623] hover:bg-[#BA7517] text-white text-xs px-3 py-2 rounded transition-colors disabled:opacity-50"
> >
{fetchingUrl ? 'Fetching...' : 'Add'} {fetchingUrl ? 'Fetching...' : 'Add'}
</button> </button>
@@ -1244,19 +1244,19 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
<div className="flex gap-2 mb-2"> <div className="flex gap-2 mb-2">
<button <button
onClick={() => setShowUrlInput(!showUrlInput)} onClick={() => setShowUrlInput(!showUrlInput)}
className="text-xs text-[#555] hover:text-[#ffb800] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#F0A623] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add URL + Add URL
</button> </button>
<button <button
onClick={() => setInput(prev => prev + '\n\n[Paste story content here for reference]')} onClick={() => setInput(prev => prev + '\n\n[Paste story content here for reference]')}
className="text-xs text-[#555] hover:text-[#ffb800] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#F0A623] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add Story + Add Story
</button> </button>
<button <button
onClick={() => setInput(prev => prev + `\n\nWrite in the style of ${selectedPenName}.`)} onClick={() => setInput(prev => prev + `\n\nWrite in the style of ${selectedPenName}.`)}
className="text-xs text-[#555] hover:text-[#ffb800] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors" className="text-xs text-[#555] hover:text-[#F0A623] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
> >
+ Add Style + Add Style
</button> </button>
@@ -1274,13 +1274,13 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
}} }}
placeholder="Ask the AI to write a story, generate headlines, translate content..." placeholder="Ask the AI to write a story, generate headlines, translate content..."
rows={3} rows={3}
className="flex-1 bg-[#111827] border border-[#1e2a3a] text-white text-sm rounded-lg px-4 py-3 focus:outline-none focus:border-[#ffb800] placeholder-[#333] resize-none" className="flex-1 bg-[#111827] border border-[#1e2a3a] text-white text-sm rounded-lg px-4 py-3 focus:outline-none focus:border-[#F0A623] placeholder-[#333] resize-none"
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<button <button
onClick={() => handleSend()} onClick={() => handleSend()}
disabled={isStreaming || !input.trim()} disabled={isStreaming || !input.trim()}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium flex-1" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-40 disabled:cursor-not-allowed text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium flex-1"
> >
{isStreaming ? ( {isStreaming ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">

View File

@@ -117,7 +117,7 @@ export default function AIConsoleSettingsPage() {
if (loading || loadingSettings) { if (loading || loadingSettings) {
return ( return (
<div className="min-h-screen bg-[#0d1117] flex items-center justify-center"> <div className="min-h-screen bg-[#0d1117] flex items-center justify-center">
<div className="w-6 h-6 border-2 border-[#ffb800]/30 border-t-[#ffb800] rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623]/30 border-t-[#F0A623] rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -127,14 +127,14 @@ export default function AIConsoleSettingsPage() {
{/* Header */} {/* Header */}
<div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between"> <div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#ffb800] text-sm transition-colors"> AI Console</Link> <Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#F0A623] text-sm transition-colors"> AI Console</Link>
<span className="text-[#333]">/</span> <span className="text-[#333]">/</span>
<h1 className="text-white font-bold text-sm">AI Console Settings</h1> <h1 className="text-white font-bold text-sm">AI Console Settings</h1>
</div> </div>
<button <button
onClick={handleSave} onClick={handleSave}
disabled={saving} disabled={saving}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
> >
{saving ? 'Saving...' : 'Save Settings'} {saving ? 'Saving...' : 'Save Settings'}
</button> </button>
@@ -177,7 +177,7 @@ export default function AIConsoleSettingsPage() {
value={settings.ollama_endpoint} value={settings.ollama_endpoint}
onChange={e => update('ollama_endpoint', e.target.value)} onChange={e => update('ollama_endpoint', e.target.value)}
placeholder="http://192.168.1.x:11434 or https://your-tunnel.ngrok.io" placeholder="http://192.168.1.x:11434 or https://your-tunnel.ngrok.io"
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800] placeholder-[#333]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623] placeholder-[#333]"
/> />
<p className="text-[#444] text-[10px] mt-1"> <p className="text-[#444] text-[10px] mt-1">
Use direct IP if on the same network, or a tunnel URL (ngrok, Cloudflare Tunnel, Tailscale) for remote access. Use direct IP if on the same network, or a tunnel URL (ngrok, Cloudflare Tunnel, Tailscale) for remote access.
@@ -212,7 +212,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_default} value={settings.ollama_model_default}
onChange={e => update('ollama_model_default', e.target.value)} onChange={e => update('ollama_model_default', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value="">Auto (largest available)</option> <option value="">Auto (largest available)</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -223,7 +223,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_translation} value={settings.ollama_model_translation}
onChange={e => update('ollama_model_translation', e.target.value)} onChange={e => update('ollama_model_translation', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value="">Same as default</option> <option value="">Same as default</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -234,7 +234,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.ollama_model_background} value={settings.ollama_model_background}
onChange={e => update('ollama_model_background', e.target.value)} onChange={e => update('ollama_model_background', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value="">Same as default</option> <option value="">Same as default</option>
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)} {connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
@@ -250,7 +250,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('auto_failover', !settings.auto_failover)} onClick={() => update('auto_failover', !settings.auto_failover)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#ffb800]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#F0A623]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.auto_failover ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.auto_failover ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -263,7 +263,7 @@ export default function AIConsoleSettingsPage() {
value={settings.failover_timeout_seconds} value={settings.failover_timeout_seconds}
onChange={e => update('failover_timeout_seconds', Number(e.target.value))} onChange={e => update('failover_timeout_seconds', Number(e.target.value))}
min={3} max={30} min={3} max={30}
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
</div> </div>
@@ -301,7 +301,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_site_id} value={settings.default_site_id}
onChange={e => update('default_site_id', Number(e.target.value))} onChange={e => update('default_site_id', Number(e.target.value))}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value={1}>AV Beat</option> <option value={1}>AV Beat</option>
<option value={2}>AV Beat</option> <option value={2}>AV Beat</option>
@@ -312,7 +312,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_pen_name_bb} value={settings.default_pen_name_bb}
onChange={e => update('default_pen_name_bb', e.target.value)} onChange={e => update('default_pen_name_bb', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
{BB_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)} {BB_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
@@ -322,7 +322,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.default_pen_name_av} value={settings.default_pen_name_av}
onChange={e => update('default_pen_name_av', e.target.value)} onChange={e => update('default_pen_name_av', e.target.value)}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
{AV_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)} {AV_PEN_NAMES.map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
@@ -335,7 +335,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('save_session_history', !settings.save_session_history)} onClick={() => update('save_session_history', !settings.save_session_history)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#ffb800]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#F0A623]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.save_session_history ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.save_session_history ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -346,7 +346,7 @@ export default function AIConsoleSettingsPage() {
<select <select
value={settings.history_retention_days} value={settings.history_retention_days}
onChange={e => update('history_retention_days', Number(e.target.value))} onChange={e => update('history_retention_days', Number(e.target.value))}
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#F0A623]"
> >
<option value={30}>30 days</option> <option value={30}>30 days</option>
<option value={60}>60 days</option> <option value={60}>60 days</option>
@@ -361,7 +361,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('streaming_enabled', !settings.streaming_enabled)} onClick={() => update('streaming_enabled', !settings.streaming_enabled)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#ffb800]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#F0A623]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.streaming_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.streaming_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -380,7 +380,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('background_use_local', !settings.background_use_local)} onClick={() => update('background_use_local', !settings.background_use_local)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#ffb800]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#F0A623]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.background_use_local ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.background_use_local ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -393,7 +393,7 @@ export default function AIConsoleSettingsPage() {
value={settings.background_timeout_seconds} value={settings.background_timeout_seconds}
onChange={e => update('background_timeout_seconds', Number(e.target.value))} onChange={e => update('background_timeout_seconds', Number(e.target.value))}
min={30} max={300} min={30} max={300}
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#ffb800]" className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
@@ -404,7 +404,7 @@ export default function AIConsoleSettingsPage() {
</div> </div>
<button <button
onClick={() => update('priority_queue_enabled', !settings.priority_queue_enabled)} onClick={() => update('priority_queue_enabled', !settings.priority_queue_enabled)}
className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#ffb800]' : 'bg-[#2a3a50]'}`} className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#F0A623]' : 'bg-[#2a3a50]'}`}
> >
<div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.priority_queue_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} /> <div className={`w-4 h-4 bg-white rounded-full absolute top-0.5 transition-transform ${settings.priority_queue_enabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</button> </button>
@@ -422,7 +422,7 @@ export default function AIConsoleSettingsPage() {
'Classification and extraction tasks', 'Classification and extraction tasks',
].map((item, i) => ( ].map((item, i) => (
<li key={i} className="flex items-center gap-2 text-xs text-[#555]"> <li key={i} className="flex items-center gap-2 text-xs text-[#555]">
<span className="w-4 h-4 rounded-full bg-[#1e2a3a] text-[#ffb800] text-[10px] flex items-center justify-center font-bold flex-shrink-0">{i + 1}</span> <span className="w-4 h-4 rounded-full bg-[#1e2a3a] text-[#F0A623] text-[10px] flex items-center justify-center font-bold flex-shrink-0">{i + 1}</span>
{item} {item}
</li> </li>
))} ))}

View File

@@ -130,9 +130,9 @@ export default function OverviewTab({ companyId, selectedCompany }: Props) {
<Line <Line
type="monotone" type="monotone"
dataKey="revenue" dataKey="revenue"
stroke={selectedCompany === 'rmp' ? '#ffb800' : '#a855f7'} stroke={selectedCompany === 'rmp' ? '#F0A623' : '#a855f7'}
strokeWidth={2} strokeWidth={2}
dot={{ fill: selectedCompany === 'rmp' ? '#ffb800' : '#a855f7', r: 3 }} dot={{ fill: selectedCompany === 'rmp' ? '#F0A623' : '#a855f7', r: 3 }}
/> />
</LineChart> </LineChart>
</ResponsiveContainer> </ResponsiveContainer>

View File

@@ -269,13 +269,13 @@ export default function EditorialDashboard() {
function SortIcon({ col }: { col: typeof sortBy }) { function SortIcon({ col }: { col: typeof sortBy }) {
if (sortBy !== col) return <span className="text-[#444] ml-1"></span>; if (sortBy !== col) return <span className="text-[#444] ml-1"></span>;
return <span className="text-[#ffb800] ml-1">{sortDir === 'desc' ? '↓' : '↑'}</span>; return <span className="text-[#F0A623] ml-1">{sortDir === 'desc' ? '↓' : '↑'}</span>;
} }
if (loading || !authChecked) { if (loading || !authChecked) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -285,7 +285,7 @@ export default function EditorialDashboard() {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0]"> <div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0]">
{/* Header */} {/* Header */}
<div className="border-b border-[#1e1e1e] bg-[#0d0d0d]"> <div className="border-b border-[#1e1e1e] bg-[#1c1815]">
<div className="max-w-[1400px] mx-auto px-6 py-5 flex items-center justify-between gap-4"> <div className="max-w-[1400px] mx-auto px-6 py-5 flex items-center justify-between gap-4">
<div> <div>
<h1 className="text-xl font-bold text-white tracking-tight">Editorial Dashboard</h1> <h1 className="text-xl font-bold text-white tracking-tight">Editorial Dashboard</h1>
@@ -294,13 +294,13 @@ export default function EditorialDashboard() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/admin/articles" href="/admin/articles"
className="px-3 py-1.5 text-xs font-semibold bg-[#ffb800] hover:bg-blue-500 text-white rounded transition-colors" className="px-3 py-1.5 text-xs font-semibold bg-[#F0A623] hover:bg-blue-500 text-white rounded transition-colors"
> >
+ New Article + New Article
</Link> </Link>
<Link <Link
href="/dashboard/adops" href="/dashboard/adops"
className="px-3 py-1.5 text-xs font-semibold bg-[#1a1a1a] hover:bg-[#252525] text-[#aaa] border border-[#2a2a2a] rounded transition-colors" className="px-3 py-1.5 text-xs font-semibold bg-[#231d18] hover:bg-[#3a322b] text-[#aaa] border border-[#3a322b] rounded transition-colors"
> >
Ad Ops Ad Ops
</Link> </Link>
@@ -325,7 +325,7 @@ export default function EditorialDashboard() {
<button <button
key={card.label} key={card.label}
onClick={card.onClick} onClick={card.onClick}
className="bg-[#111] border border-[#1e1e1e] rounded-lg p-4 text-left hover:border-[#2a2a2a] transition-colors cursor-pointer" className="bg-[#111] border border-[#1e1e1e] rounded-lg p-4 text-left hover:border-[#3a322b] transition-colors cursor-pointer"
> >
<div className={`text-xl font-bold ${card.color}`}>{card.value}</div> <div className={`text-xl font-bold ${card.color}`}>{card.value}</div>
<div className="text-[11px] text-[#666] mt-1 uppercase tracking-wider">{card.label}</div> <div className="text-[11px] text-[#666] mt-1 uppercase tracking-wider">{card.label}</div>
@@ -344,7 +344,7 @@ export default function EditorialDashboard() {
onClick={() => setStatusFilter(s)} onClick={() => setStatusFilter(s)}
className={`px-3 py-1.5 text-xs font-semibold capitalize transition-colors ${ className={`px-3 py-1.5 text-xs font-semibold capitalize transition-colors ${
statusFilter === s statusFilter === s
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -360,7 +360,7 @@ export default function EditorialDashboard() {
placeholder="Search articles, clients, campaigns..." placeholder="Search articles, clients, campaigns..."
value={searchQuery} value={searchQuery}
onChange={e => setSearchQuery(e.target.value)} onChange={e => setSearchQuery(e.target.value)}
className="w-full bg-[#111] border border-[#1e1e1e] rounded-lg px-3 py-1.5 text-xs text-[#ccc] placeholder-[#444] focus:outline-none focus:border-[#ffb800] transition-colors" className="w-full bg-[#111] border border-[#1e1e1e] rounded-lg px-3 py-1.5 text-xs text-[#ccc] placeholder-[#444] focus:outline-none focus:border-[#F0A623] transition-colors"
/> />
{searchQuery && ( {searchQuery && (
<button <button
@@ -376,10 +376,10 @@ export default function EditorialDashboard() {
</div> </div>
{/* Pipeline Table */} {/* Pipeline Table */}
<div className="bg-[#0d0d0d] border border-[#1e1e1e] rounded-xl overflow-hidden"> <div className="bg-[#1c1815] border border-[#1e1e1e] rounded-xl overflow-hidden">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-6 h-6 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-6 h-6 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<div className="text-center py-20 text-[#444] text-sm">No articles found</div> <div className="text-center py-20 text-[#444] text-sm">No articles found</div>
@@ -387,7 +387,7 @@ export default function EditorialDashboard() {
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-xs"> <table className="w-full text-xs">
<thead> <thead>
<tr className="border-b border-[#1a1a1a] bg-[#0a0a0a]"> <tr className="border-b border-[#231d18] bg-[#0a0a0a]">
<th className="text-left px-4 py-3 text-[#555] font-semibold uppercase tracking-wider w-[30%]">Article</th> <th className="text-left px-4 py-3 text-[#555] font-semibold uppercase tracking-wider w-[30%]">Article</th>
<th className="text-left px-3 py-3 text-[#555] font-semibold uppercase tracking-wider">Status</th> <th className="text-left px-3 py-3 text-[#555] font-semibold uppercase tracking-wider">Status</th>
<th <th
@@ -437,7 +437,7 @@ export default function EditorialDashboard() {
<span className="text-[#555]">{article.author_name}</span> <span className="text-[#555]">{article.author_name}</span>
)} )}
{article.category && ( {article.category && (
<span className="text-[#ffb800]/70 bg-[#ffb800]/10 px-1.5 py-0.5 rounded text-[10px]"> <span className="text-[#F0A623]/70 bg-[#F0A623]/10 px-1.5 py-0.5 rounded text-[10px]">
{article.category} {article.category}
</span> </span>
)} )}

View File

@@ -256,7 +256,7 @@ export default function ShowCalendarPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center"> <div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
@@ -281,7 +281,7 @@ export default function ShowCalendarPage() {
)} )}
{/* Header */} {/* Header */}
<div className="border-b border-[#1a1a1a] bg-[#0f0f0f]"> <div className="border-b border-[#231d18] bg-[#0f0f0f]">
<div className="max-w-7xl mx-auto px-4 py-4"> <div className="max-w-7xl mx-auto px-4 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -294,13 +294,13 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button <button
onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }} onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }}
className="px-3 py-1.5 bg-[#ffb800] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors" className="px-3 py-1.5 bg-[#F0A623] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors"
> >
+ Add Event + Add Event
</button> </button>
<button <button
onClick={() => setShowGenerateModal(true)} onClick={() => setShowGenerateModal(true)}
className="px-3 py-1.5 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-3 py-1.5 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Generate Story Generate Story
</button> </button>
@@ -316,8 +316,8 @@ export default function ShowCalendarPage() {
onClick={() => setSiteFilter(s)} onClick={() => setSiteFilter(s)}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
siteFilter === s siteFilter === s
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]' : 'bg-[#231d18] text-[#888] hover:text-white border border-[#333]'
}`} }`}
> >
{s === '' ? 'All Sites' : s === '1' ? '🔵 AV Beat' : '🟠 AV Beat'} {s === '' ? 'All Sites' : s === '1' ? '🔵 AV Beat' : '🟠 AV Beat'}
@@ -326,14 +326,14 @@ export default function ShowCalendarPage() {
</div> </div>
{/* Tabs */} {/* Tabs */}
<div className="flex gap-1 mt-4 border-b border-[#1a1a1a]"> <div className="flex gap-1 mt-4 border-b border-[#231d18]">
{tabs.map(tab => ( {tabs.map(tab => (
<button <button
key={tab.id} key={tab.id}
onClick={() => setActiveTab(tab.id)} onClick={() => setActiveTab(tab.id)}
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${ className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
activeTab === tab.id activeTab === tab.id
? 'border-[#ffb800] text-white' ? 'border-[#F0A623] text-white'
: 'border-transparent text-[#666] hover:text-white' : 'border-transparent text-[#666] hover:text-white'
}`} }`}
> >
@@ -347,7 +347,7 @@ export default function ShowCalendarPage() {
<div className="max-w-7xl mx-auto px-4 py-6"> <div className="max-w-7xl mx-auto px-4 py-6">
{loadingData ? ( {loadingData ? (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<div className="w-8 h-8 border-2 border-[#ffb800] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
</div> </div>
) : ( ) : (
<> <>
@@ -383,7 +383,7 @@ export default function ShowCalendarPage() {
<span>Engine: {formatDate(event.story_engine_start_date)} {formatDate(event.story_engine_end_date)}</span> <span>Engine: {formatDate(event.story_engine_start_date)} {formatDate(event.story_engine_end_date)}</span>
{event.official_site && ( {event.official_site && (
<a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer" <a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer"
className="text-[#ffb800] hover:underline">{event.official_site}</a> className="text-[#F0A623] hover:underline">{event.official_site}</a>
)} )}
</div> </div>
<div className="flex items-center gap-3 mt-2 text-xs text-[#555]"> <div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
@@ -400,13 +400,13 @@ export default function ShowCalendarPage() {
<button <button
onClick={() => handleExtractStyle(event.event_name)} onClick={() => handleExtractStyle(event.event_name)}
disabled={actionLoading === 'style-' + event.event_name} disabled={actionLoading === 'style-' + event.event_name}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors disabled:opacity-50" className="px-2 py-1 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors disabled:opacity-50"
> >
{actionLoading === 'style-' + event.event_name ? 'Extracting...' : 'Extract Style'} {actionLoading === 'style-' + event.event_name ? 'Extracting...' : 'Extract Style'}
</button> </button>
<button <button
onClick={() => { setEditingEvent(event); setShowEventModal(true); }} onClick={() => { setEditingEvent(event); setShowEventModal(true); }}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors" className="px-2 py-1 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors"
> >
Edit Dates Edit Dates
</button> </button>
@@ -440,8 +440,8 @@ export default function ShowCalendarPage() {
onClick={() => setQueueStatus(s)} onClick={() => setQueueStatus(s)}
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${ className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
queueStatus === s queueStatus === s
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]' : 'bg-[#231d18] text-[#888] hover:text-white border border-[#333]'
}`} }`}
> >
{s.replace('_', ' ')} {s.replace('_', ' ')}
@@ -460,7 +460,7 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-2 mb-1 flex-wrap"> <div className="flex items-center gap-2 mb-1 flex-wrap">
<SiteBadge siteId={item.site_id} /> <SiteBadge siteId={item.site_id} />
{item.source_show && ( {item.source_show && (
<span className="text-xs text-[#555] bg-[#1a1a1a] px-1.5 py-0.5 rounded">{item.source_show}</span> <span className="text-xs text-[#555] bg-[#231d18] px-1.5 py-0.5 rounded">{item.source_show}</span>
)} )}
{item.show_phase && ( {item.show_phase && (
<span className={`text-xs ${PHASE_LABELS[item.show_phase]?.color || 'text-gray-500'}`}> <span className={`text-xs ${PHASE_LABELS[item.show_phase]?.color || 'text-gray-500'}`}>
@@ -470,7 +470,7 @@ export default function ShowCalendarPage() {
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span className={`text-xs px-1.5 py-0.5 rounded ${
item.status === 'published' ? 'bg-green-900/20 text-green-400' : item.status === 'published' ? 'bg-green-900/20 text-green-400' :
item.status === 'rejected' ? 'bg-red-900/20 text-red-400' : item.status === 'rejected' ? 'bg-red-900/20 text-red-400' :
item.status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#1a1a1a] text-[#888]' item.status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#231d18] text-[#888]'
}`}>{item.status}</span> }`}>{item.status}</span>
{item.quality_confidence !== null && ( {item.quality_confidence !== null && (
<span className={`text-xs ${item.quality_confidence >= 0.85 ? 'text-green-400' : item.quality_confidence >= 0.7 ? 'text-yellow-400' : 'text-red-400'}`}> <span className={`text-xs ${item.quality_confidence >= 0.85 ? 'text-green-400' : item.quality_confidence >= 0.7 ? 'text-yellow-400' : 'text-red-400'}`}>
@@ -490,7 +490,7 @@ export default function ShowCalendarPage() {
{/* Expanded content */} {/* Expanded content */}
{expandedItem === item.id && item.generated_content && ( {expandedItem === item.id && item.generated_content && (
<div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<div <div
className="text-[#aaa] text-xs prose prose-invert max-w-none" className="text-[#aaa] text-xs prose prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: item.generated_content.slice(0, 1500) + (item.generated_content.length > 1500 ? '...' : '') }} dangerouslySetInnerHTML={{ __html: item.generated_content.slice(0, 1500) + (item.generated_content.length > 1500 ? '...' : '') }}
@@ -501,7 +501,7 @@ export default function ShowCalendarPage() {
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<button <button
onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)} onClick={() => setExpandedItem(expandedItem === item.id ? null : item.id)}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors" className="px-2 py-1 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors"
> >
{expandedItem === item.id ? 'Hide' : 'Preview'} {expandedItem === item.id ? 'Hide' : 'Preview'}
</button> </button>
@@ -541,7 +541,7 @@ export default function ShowCalendarPage() {
<select <select
value={showFilter} value={showFilter}
onChange={e => setShowFilter(e.target.value)} onChange={e => setShowFilter(e.target.value)}
className="bg-[#1a1a1a] border border-[#333] text-white text-xs rounded px-2 py-1" className="bg-[#231d18] border border-[#333] text-white text-xs rounded px-2 py-1"
> >
<option value="">All Shows</option> <option value="">All Shows</option>
{events.map(e => <option key={e.id} value={e.id}>{e.event_name}</option>)} {events.map(e => <option key={e.id} value={e.id}>{e.event_name}</option>)}
@@ -564,13 +564,13 @@ export default function ShowCalendarPage() {
<tr><td colSpan={5} className="text-center py-8 text-[#555]">No exhibitors found.</td></tr> <tr><td colSpan={5} className="text-center py-8 text-[#555]">No exhibitors found.</td></tr>
)} )}
{exhibitors.map(ex => ( {exhibitors.map(ex => (
<tr key={ex.id} className="border-b border-[#1a1a1a] hover:bg-[#0f0f0f]"> <tr key={ex.id} className="border-b border-[#231d18] hover:bg-[#0f0f0f]">
<td className="px-4 py-3 text-white font-medium">{ex.company_name}</td> <td className="px-4 py-3 text-white font-medium">{ex.company_name}</td>
<td className="px-4 py-3 text-[#888]">{ex.booth_number || '—'}</td> <td className="px-4 py-3 text-[#888]">{ex.booth_number || '—'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`text-xs px-1.5 py-0.5 rounded ${ <span className={`text-xs px-1.5 py-0.5 rounded ${
ex.preview_story_status === 'published' ? 'bg-green-900/20 text-green-400' : ex.preview_story_status === 'published' ? 'bg-green-900/20 text-green-400' :
ex.preview_story_status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#1a1a1a] text-[#888]' ex.preview_story_status === 'generated'? 'bg-blue-900/20 text-blue-400' : 'bg-[#231d18] text-[#888]'
}`}>{ex.preview_story_status}</span> }`}>{ex.preview_story_status}</span>
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-3">
@@ -613,14 +613,14 @@ export default function ShowCalendarPage() {
Updated: {formatDate(profile.last_updated)} Updated: {formatDate(profile.last_updated)}
</p> </p>
{expandedItem === profile.id && profile.style_guide_text && ( {expandedItem === profile.id && profile.style_guide_text && (
<div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="mt-3 p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-[#aaa] text-xs whitespace-pre-wrap">{profile.style_guide_text}</p> <p className="text-[#aaa] text-xs whitespace-pre-wrap">{profile.style_guide_text}</p>
</div> </div>
)} )}
</div> </div>
<button <button
onClick={() => setExpandedItem(expandedItem === profile.id ? null : profile.id)} onClick={() => setExpandedItem(expandedItem === profile.id ? null : profile.id)}
className="px-2 py-1 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors flex-shrink-0" className="px-2 py-1 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-[#888] hover:text-white text-xs rounded transition-colors flex-shrink-0"
> >
{expandedItem === profile.id ? 'Hide' : 'View Guide'} {expandedItem === profile.id ? 'Hide' : 'View Guide'}
</button> </button>
@@ -637,24 +637,24 @@ export default function ShowCalendarPage() {
<div className="bg-[#111] border border-[#1e1e1e] rounded-xl p-6"> <div className="bg-[#111] border border-[#1e1e1e] rounded-xl p-6">
<h2 className="text-white font-semibold mb-4">Show Engine Settings</h2> <h2 className="text-white font-semibold mb-4">Show Engine Settings</h2>
<div className="space-y-4 text-sm text-[#888]"> <div className="space-y-4 text-sm text-[#888]">
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-white font-medium mb-1">Annual Date Discovery</p> <p className="text-white font-medium mb-1">Annual Date Discovery</p>
<p>Runs automatically on January 1 each year. Searches official show websites and Google for confirmed dates. Sends admin notification when dates are set or when manual review is needed.</p> <p>Runs automatically on January 1 each year. Searches official show websites and Google for confirmed dates. Sends admin notification when dates are set or when manual review is needed.</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-white font-medium mb-1">Pen Name Rosters</p> <p className="text-white font-medium mb-1">Pen Name Rosters</p>
<p className="mb-2"><strong className="text-[#aaa]">AV Beat (12 writers):</strong> Michael Strand, David Harlow, Karen Fielding, James Mercer (primary NAB), Peter Calloway, Sandra Voss, Brian Kowalski, Laura Pennington, Thomas Reeves, Christine Vale, Marcus Webb, Ellen Forsythe</p> <p className="mb-2"><strong className="text-[#aaa]">AV Beat (12 writers):</strong> Michael Strand, David Harlow, Karen Fielding, James Mercer (primary NAB), Peter Calloway, Sandra Voss, Brian Kowalski, Laura Pennington, Thomas Reeves, Christine Vale, Marcus Webb, Ellen Forsythe</p>
<p><strong className="text-[#aaa]">AV Beat (7 writers):</strong> Rex Chandler, Dana Flux, Derek Wainwright, Sloane Rigging, Chip Crosspoint, Blair Presenter, Jordan Lumen</p> <p><strong className="text-[#aaa]">AV Beat (7 writers):</strong> Rex Chandler, Dana Flux, Derek Wainwright, Sloane Rigging, Chip Crosspoint, Blair Presenter, Jordan Lumen</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-white font-medium mb-1">SEO Tier Priority</p> <p className="text-white font-medium mb-1">SEO Tier Priority</p>
<p>Tier 1 (Max): NAB Las Vegas, IBC, InfoComm, ISE · Tier 2 (High): NAB NY, BroadcastAsia, CABSAT, AES, InfoComm EDGE/China/LatAm · Tier 3 (Standard): MPTS, SET Expo, NATPE, InfoComm India, FORTÉ LIVE · Tier 4 (Light): NewsTECHForum, TAB, SVVS, Hamburg Open, AVIXA Regional</p> <p>Tier 1 (Max): NAB Las Vegas, IBC, InfoComm, ISE · Tier 2 (High): NAB NY, BroadcastAsia, CABSAT, AES, InfoComm EDGE/China/LatAm · Tier 3 (Standard): MPTS, SET Expo, NATPE, InfoComm India, FORTÉ LIVE · Tier 4 (Light): NewsTECHForum, TAB, SVVS, Hamburg Open, AVIXA Regional</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-white font-medium mb-1">NAB Cross-Post to AV Beat</p> <p className="text-white font-medium mb-1">NAB Cross-Post to AV Beat</p>
<p>NAB exhibitors with AV-eligible products (projectors, displays, conferencing, control systems, digital signage, installed audio, AV-over-IP) automatically generate stories on AV Beat using AV Beat pen names.</p> <p>NAB exhibitors with AV-eligible products (projectors, displays, conferencing, control systems, digital signage, installed audio, AV-over-IP) automatically generate stories on AV Beat using AV Beat pen names.</p>
</div> </div>
<div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#1a1a1a]"> <div className="p-3 bg-[#0a0a0a] rounded-lg border border-[#231d18]">
<p className="text-white font-medium mb-1">Blocked Competitor Sources</p> <p className="text-white font-medium mb-1">Blocked Competitor Sources</p>
<p>BB: tvtechnology.com, broadcastingcable.com, sportsvideo.org, digitaltveurope.com, nexttv.com · AV: ravepubs.com, avnetwork.com, commercialintegrator.com, systemscontractor.com, avinteractive.com · Note: infocommshow.org, iseurope.org, nabshow.com, avixa.org are PRIMARY sources not blocked.</p> <p>BB: tvtechnology.com, broadcastingcable.com, sportsvideo.org, digitaltveurope.com, nexttv.com · AV: ravepubs.com, avnetwork.com, commercialintegrator.com, systemscontractor.com, avinteractive.com · Note: infocommshow.org, iseurope.org, nabshow.com, avixa.org are PRIMARY sources not blocked.</p>
</div> </div>
@@ -779,14 +779,14 @@ export default function ShowCalendarPage() {
<button <button
type="submit" type="submit"
disabled={actionLoading === 'save-event'} disabled={actionLoading === 'save-event'}
className="flex-1 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
> >
{actionLoading === 'save-event' ? 'Saving...' : 'Save Event'} {actionLoading === 'save-event' ? 'Saving...' : 'Save Event'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => { setShowEventModal(false); setEditingEvent(null); }} onClick={() => { setShowEventModal(false); setEditingEvent(null); }}
className="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-4 py-2 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -857,14 +857,14 @@ export default function ShowCalendarPage() {
<button <button
type="submit" type="submit"
disabled={actionLoading === 'generate'} disabled={actionLoading === 'generate'}
className="flex-1 py-2 bg-[#ffb800] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50" className="flex-1 py-2 bg-[#F0A623] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
> >
{actionLoading === 'generate' ? 'Generating...' : 'Generate Story'} {actionLoading === 'generate' ? 'Generating...' : 'Generate Story'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowGenerateModal(false)} onClick={() => setShowGenerateModal(false)}
className="px-4 py-2 bg-[#1a1a1a] hover:bg-[#252525] border border-[#333] text-white text-sm rounded-lg transition-colors" className="px-4 py-2 bg-[#231d18] hover:bg-[#3a322b] border border-[#333] text-white text-sm rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>

View File

@@ -13,7 +13,7 @@ export default function Error({
console.error("[route error]", error); console.error("[route error]", error);
}, [error]); }, [error]);
return ( return (
<div className="min-h-screen flex items-center justify-center bg-[#0d0d0d] text-[#e0e0e0] p-6"> <div className="min-h-screen flex items-center justify-center bg-[#1c1815] text-[#e0e0e0] p-6">
<div className="max-w-md text-center"> <div className="max-w-md text-center">
<h2 className="font-heading text-2xl mb-3">Something went wrong</h2> <h2 className="font-heading text-2xl mb-3">Something went wrong</h2>
<p className="text-[#888] text-sm mb-4 font-body"> <p className="text-[#888] text-sm mb-4 font-body">
@@ -25,13 +25,13 @@ export default function Error({
<div className="flex gap-3 justify-center"> <div className="flex gap-3 justify-center">
<button <button
onClick={() => reset()} onClick={() => reset()}
className="px-4 py-2 bg-[#ffb800] text-white text-sm rounded-sm hover:bg-[#d99700] transition-colors" className="px-4 py-2 bg-[#F0A623] text-white text-sm rounded-sm hover:bg-[#BA7517] transition-colors"
> >
Try again Try again
</button> </button>
<Link <Link
href="/home-page" href="/home-page"
className="px-4 py-2 border border-[#333] text-sm rounded-sm hover:border-[#ffb800] transition-colors" className="px-4 py-2 border border-[#333] text-sm rounded-sm hover:border-[#F0A623] transition-colors"
> >
Home Home
</Link> </Link>

View File

@@ -74,27 +74,27 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
const getMetricValue = (thread: ForumThread) => { const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' }; if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#ffb800]' }; if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#F0A623]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' }; return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
}; };
return ( return (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-xl overflow-hidden mb-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-xl overflow-hidden mb-6">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#3a322b]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg">🏆</span> <span className="text-lg">🏆</span>
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2> <h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2>
</div> </div>
{/* Metric Tabs */} {/* Metric Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{metrics.map(m => ( {metrics.map(m => (
<button <button
key={m.value} key={m.value}
onClick={() => setActiveMetric(m.value)} onClick={() => setActiveMetric(m.value)}
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
activeMetric === m.value activeMetric === m.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -111,12 +111,12 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
<> <>
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-5 py-3"> <div key={i} className="flex items-center gap-3 px-5 py-3">
<div className="w-6 h-6 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-6 h-6 bg-[#3a322b] rounded animate-pulse flex-shrink-0" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<div className="h-3.5 bg-[#252525] rounded animate-pulse w-3/4" /> <div className="h-3.5 bg-[#3a322b] rounded animate-pulse w-3/4" />
<div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" /> <div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" />
</div> </div>
<div className="w-10 h-8 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-10 h-8 bg-[#3a322b] rounded animate-pulse flex-shrink-0" />
</div> </div>
))} ))}
</> </>
@@ -142,11 +142,11 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
</span> </span>
{/* Thread info */} {/* Thread info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-body text-sm font-semibold group-hover:text-[#ffb800] transition-colors truncate leading-snug"> <p className="text-white font-body text-sm font-semibold group-hover:text-[#F0A623] transition-colors truncate leading-snug">
{thread.title} {thread.title}
</p> </p>
<p className="text-[#555] font-body text-xs mt-0.5"> <p className="text-[#555] font-body text-xs mt-0.5">
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#ffb800] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#F0A623] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
</div> </div>
@@ -277,12 +277,12 @@ export default function ForumCategoryPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
{/* Breadcrumb + Header */} {/* Breadcrumb + Header */}
<div className="bg-[#1a1208] border-b border-[#2a3a50]"> <div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-8"> <div className="max-w-container mx-auto px-4 py-8">
<nav className="text-sm font-body text-[#666] mb-3"> <nav className="text-sm font-body text-[#666] mb-3">
<Link href="/forum" className="hover:text-[#ffb800] transition-colors">Forum</Link> <Link href="/forum" className="hover:text-[#F0A623] transition-colors">Forum</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
<span className="text-[#aab4c4]">{category?.name || slug}</span> <span className="text-[#aab4c4]">{category?.name || slug}</span>
</nav> </nav>
@@ -299,14 +299,14 @@ export default function ForumCategoryPage() {
{isAuthenticated ? ( {isAuthenticated ? (
<button <button
onClick={() => setShowNewThread(!showNewThread)} onClick={() => setShowNewThread(!showNewThread)}
className="flex-shrink-0 bg-[#ffb800] hover:bg-[#d99700] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors" className="flex-shrink-0 bg-[#F0A623] hover:bg-[#BA7517] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
> >
+ New Thread + New Thread
</button> </button>
) : ( ) : (
<Link <Link
href={`/forum/login?next=${typeof window === 'undefined' ? '/forum' : encodeURIComponent(window.location.pathname)}`} href={`/forum/login?next=${typeof window === 'undefined' ? '/forum' : encodeURIComponent(window.location.pathname)}`}
className="flex-shrink-0 bg-[#ffb800] hover:bg-[#d99700] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors" className="flex-shrink-0 bg-[#F0A623] hover:bg-[#BA7517] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
> >
Sign in to post Sign in to post
</Link> </Link>
@@ -318,7 +318,7 @@ export default function ForumCategoryPage() {
<div className="max-w-container mx-auto px-4 py-6"> <div className="max-w-container mx-auto px-4 py-6">
{/* New Thread Form — auth-gated */} {/* New Thread Form — auth-gated */}
{showNewThread && isAuthenticated && ( {showNewThread && isAuthenticated && (
<form onSubmit={handleNewThread} className="bg-[#1a1208] border border-[#2a3a50] rounded-lg p-5 mb-6"> <form onSubmit={handleNewThread} className="bg-[#231d18] border border-[#2a3a50] rounded-lg p-5 mb-6">
<h3 className="text-white font-heading font-semibold mb-4">Start a New Thread</h3> <h3 className="text-white font-heading font-semibold mb-4">Start a New Thread</h3>
<div className="space-y-3"> <div className="space-y-3">
<input <input
@@ -327,7 +327,7 @@ export default function ForumCategoryPage() {
value={newTitle} value={newTitle}
onChange={e => setNewTitle(e.target.value)} onChange={e => setNewTitle(e.target.value)}
required required
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#F0A623]"
/> />
<textarea <textarea
placeholder="What would you like to discuss? *" placeholder="What would you like to discuss? *"
@@ -335,14 +335,14 @@ export default function ForumCategoryPage() {
onChange={e => setNewBody(e.target.value)} onChange={e => setNewBody(e.target.value)}
required required
rows={5} rows={5}
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#F0A623] resize-none"
/> />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} /> <ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} />
<span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span> <span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span>
</div> </div>
{newError && ( {newError && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{newError} {newError}
</div> </div>
)} )}
@@ -350,14 +350,14 @@ export default function ForumCategoryPage() {
<button <button
type="submit" type="submit"
disabled={submitting} disabled={submitting}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
{submitting ? 'Posting...' : 'Post Thread'} {submitting ? 'Posting...' : 'Post Thread'}
</button> </button>
<button <button
type="button" type="button"
onClick={() => setShowNewThread(false)} onClick={() => setShowNewThread(false)}
className="bg-[#252525] hover:bg-[#333] text-[#aaa] font-body text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#3a322b] hover:bg-[#333] text-[#aaa] font-body text-sm px-5 py-2 rounded-lg transition-colors"
> >
Cancel Cancel
</button> </button>
@@ -378,7 +378,7 @@ export default function ForumCategoryPage() {
placeholder="Search threads in this category..." placeholder="Search threads in this category..."
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#ffb800] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors" className="w-full bg-[#231d18] border border-[#3a322b] focus:border-[#F0A623] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors"
/> />
{search && ( {search && (
<button <button
@@ -394,14 +394,14 @@ export default function ForumCategoryPage() {
</div> </div>
{/* Sort Options */} {/* Sort Options */}
<div className="flex gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1 flex-shrink-0"> <div className="flex gap-1 bg-[#231d18] border border-[#3a322b] rounded-lg p-1 flex-shrink-0">
{sortOptions.map(opt => ( {sortOptions.map(opt => (
<button <button
key={opt.value} key={opt.value}
onClick={() => setSort(opt.value)} onClick={() => setSort(opt.value)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
sort === opt.value sort === opt.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -414,7 +414,7 @@ export default function ForumCategoryPage() {
{(loading || threadsLoading) && ( {(loading || threadsLoading) && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(10)].map((_, i) => ( {[...Array(10)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-16 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#231d18] rounded-lg h-16 animate-pulse border border-[#3a322b]" />
))} ))}
</div> </div>
)} )}
@@ -445,12 +445,12 @@ export default function ForumCategoryPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#ffb800] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#231d18] hover:bg-[#1e2a3a] border border-[#3a322b] hover:border-[#F0A623] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
{thread.status === 'pinned' && ( {thread.status === 'pinned' && (
<span className="text-xs bg-[#ffb800]/20 text-[#ffb800] px-2 py-0.5 rounded font-body font-semibold">PINNED</span> <span className="text-xs bg-[#F0A623]/20 text-[#F0A623] px-2 py-0.5 rounded font-body font-semibold">PINNED</span>
)} )}
{thread.status === 'closed' && ( {thread.status === 'closed' && (
<span className="text-xs bg-[#555]/20 text-[#888] px-2 py-0.5 rounded font-body">CLOSED</span> <span className="text-xs bg-[#555]/20 text-[#888] px-2 py-0.5 rounded font-body">CLOSED</span>
@@ -458,12 +458,12 @@ export default function ForumCategoryPage() {
{thread.reply_count === 0 && ( {thread.reply_count === 0 && (
<span className="text-xs bg-[#f59e0b]/10 text-[#f59e0b] px-2 py-0.5 rounded font-body">UNANSWERED</span> <span className="text-xs bg-[#f59e0b]/10 text-[#f59e0b] px-2 py-0.5 rounded font-body">UNANSWERED</span>
)} )}
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#ffb800] transition-colors truncate"> <h3 className="text-white font-body font-semibold text-sm group-hover:text-[#F0A623] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
</div> </div>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#ffb800] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#F0A623] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
</div> </div>

View File

@@ -30,10 +30,10 @@ function ForgotInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#1c1815] via-[#1c1815] to-[#1c1815]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#ffb800] font-mono mb-2">Account recovery</div> <div className="text-[10px] uppercase tracking-widest text-[#F0A623] font-mono mb-2">Account recovery</div>
<h1 className="text-3xl font-heading font-bold text-white">Reset your password</h1> <h1 className="text-3xl font-heading font-bold text-white">Reset your password</h1>
<p className="text-[#888] font-body text-sm mt-2"> <p className="text-[#888] font-body text-sm mt-2">
Enter your email we&rsquo;ll send a link to set a new password. Enter your email we&rsquo;ll send a link to set a new password.
@@ -41,10 +41,10 @@ function ForgotInner() {
</div> </div>
<form <form
onSubmit={submit} onSubmit={submit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{err && ( {err && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{err} {err}
</div> </div>
)} )}
@@ -63,19 +63,19 @@ function ForgotInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={busy || !email.trim()} disabled={busy || !email.trim()}
className="w-full bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors" className="w-full bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
> >
{busy ? "Sending…" : "Send reset link"} {busy ? "Sending…" : "Send reset link"}
</button> </button>
<div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#252525]"> <div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#3a322b]">
Remembered it?{" "} Remembered it?{" "}
<Link href="/forum/login" className="text-[#ffb800] hover:underline font-semibold"> <Link href="/forum/login" className="text-[#F0A623] hover:underline font-semibold">
Back to sign in Back to sign in
</Link> </Link>
</div> </div>

View File

@@ -51,10 +51,10 @@ function ForumLoginInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#1c1815] via-[#1c1815] to-[#1c1815]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#ffb800] font-mono mb-2">Welcome back</div> <div className="text-[10px] uppercase tracking-widest text-[#F0A623] font-mono mb-2">Welcome back</div>
<h1 className="text-3xl font-heading font-bold text-white">Sign in to AV Beat</h1> <h1 className="text-3xl font-heading font-bold text-white">Sign in to AV Beat</h1>
<p className="text-[#888] font-body text-sm mt-2"> <p className="text-[#888] font-body text-sm mt-2">
Your account covers the forum, newsletter preferences, and saved articles. Your account covers the forum, newsletter preferences, and saved articles.
@@ -62,10 +62,10 @@ function ForumLoginInner() {
</div> </div>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -79,7 +79,7 @@ function ForumLoginInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -92,20 +92,20 @@ function ForumLoginInner() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password" autoComplete="current-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors" className="w-full bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
> >
{loading ? 'Signing in…' : 'Sign In'} {loading ? 'Signing in…' : 'Sign In'}
</button> </button>
<div className="text-right text-xs font-body"> <div className="text-right text-xs font-body">
<Link <Link
href={`/forum/forgot-password?email=${encodeURIComponent(email)}`} href={`/forum/forgot-password?email=${encodeURIComponent(email)}`}
className="text-[#888] hover:text-[#ffb800] hover:underline" className="text-[#888] hover:text-[#F0A623] hover:underline"
> >
Forgot password? Forgot password?
</Link> </Link>
@@ -115,7 +115,7 @@ function ForumLoginInner() {
No account yet?{' '} No account yet?{' '}
<Link <Link
href={`/forum/register?next=${encodeURIComponent(next)}`} href={`/forum/register?next=${encodeURIComponent(next)}`}
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
Create one Create one
</Link> </Link>

View File

@@ -103,27 +103,27 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
const getMetricValue = (thread: ForumThread) => { const getMetricValue = (thread: ForumThread) => {
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' }; if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#ffb800]' }; if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#F0A623]' };
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' }; return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
}; };
return ( return (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-xl overflow-hidden mb-8"> <div className="bg-[#231d18] border border-[#3a322b] rounded-xl overflow-hidden mb-8">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-[#252525]"> <div className="flex items-center justify-between px-5 py-4 border-b border-[#3a322b]">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-lg">🏆</span> <span className="text-lg">🏆</span>
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2> <h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2>
</div> </div>
{/* Metric Tabs */} {/* Metric Tabs */}
<div className="flex gap-1 bg-[#111] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#111] border border-[#3a322b] rounded-lg p-1">
{metrics.map(m => ( {metrics.map(m => (
<button <button
key={m.value} key={m.value}
onClick={() => setActiveMetric(m.value)} onClick={() => setActiveMetric(m.value)}
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
activeMetric === m.value activeMetric === m.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#666] hover:text-[#aaa]' : 'text-[#666] hover:text-[#aaa]'
}`} }`}
> >
@@ -140,12 +140,12 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
<> <>
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
<div key={i} className="flex items-center gap-3 px-5 py-3"> <div key={i} className="flex items-center gap-3 px-5 py-3">
<div className="w-6 h-6 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-6 h-6 bg-[#3a322b] rounded animate-pulse flex-shrink-0" />
<div className="flex-1 space-y-1.5"> <div className="flex-1 space-y-1.5">
<div className="h-3.5 bg-[#252525] rounded animate-pulse w-3/4" /> <div className="h-3.5 bg-[#3a322b] rounded animate-pulse w-3/4" />
<div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" /> <div className="h-2.5 bg-[#1e1e1e] rounded animate-pulse w-1/3" />
</div> </div>
<div className="w-10 h-8 bg-[#252525] rounded animate-pulse flex-shrink-0" /> <div className="w-10 h-8 bg-[#3a322b] rounded animate-pulse flex-shrink-0" />
</div> </div>
))} ))}
</> </>
@@ -171,13 +171,13 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
</span> </span>
{/* Thread info */} {/* Thread info */}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-body text-sm font-semibold group-hover:text-[#ffb800] transition-colors truncate leading-snug"> <p className="text-white font-body text-sm font-semibold group-hover:text-[#F0A623] transition-colors truncate leading-snug">
{thread.title} {thread.title}
</p> </p>
<p className="text-[#555] font-body text-xs mt-0.5"> <p className="text-[#555] font-body text-xs mt-0.5">
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#ffb800] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#F0A623] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{thread.forum_categories && ( {thread.forum_categories && (
<> · <span className="text-[#ffb800]">{thread.forum_categories.name}</span></> <> · <span className="text-[#F0A623]">{thread.forum_categories.name}</span></>
)} )}
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
@@ -283,10 +283,10 @@ export default function ForumIndexPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
{/* Hero — width-aligned with content below + featured-box pattern */} {/* Hero — width-aligned with content below + featured-box pattern */}
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
<div className="bg-[#1a1208] border-b border-[#2a3a50] py-5 px-4"> <div className="bg-[#231d18] border-b border-[#2a3a50] py-5 px-4">
<div className="flex items-start justify-between gap-4 flex-wrap"> <div className="flex items-start justify-between gap-4 flex-wrap">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-3 mb-1"> <div className="flex items-center gap-3 mb-1">
@@ -301,7 +301,7 @@ export default function ForumIndexPage() {
{currentUserId ? ( {currentUserId ? (
<Link <Link
href="/forum/new" href="/forum/new"
className="inline-flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors" className="inline-flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 5v14M5 12h14" /> <path d="M12 5v14M5 12h14" />
@@ -311,7 +311,7 @@ export default function ForumIndexPage() {
) : ( ) : (
<Link <Link
href="/forum/login?next=/forum" href="/forum/login?next=/forum"
className="inline-flex items-center gap-2 bg-[#ffb800] hover:bg-[#d99700] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors" className="inline-flex items-center gap-2 bg-[#F0A623] hover:bg-[#BA7517] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" /> <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
@@ -329,11 +329,11 @@ export default function ForumIndexPage() {
<div> <div>
{/* Feed Mode Tabs */} {/* Feed Mode Tabs */}
<div className="flex items-center gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1 mb-6 w-fit"> <div className="flex items-center gap-1 bg-[#231d18] border border-[#3a322b] rounded-lg p-1 mb-6 w-fit">
<button <button
onClick={() => setFeedMode('discover')} onClick={() => setFeedMode('discover')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${ className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
feedMode === 'discover' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white' feedMode === 'discover' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
@@ -344,7 +344,7 @@ export default function ForumIndexPage() {
<button <button
onClick={() => setFeedMode('following')} onClick={() => setFeedMode('following')}
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${ className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
feedMode === 'following' ? 'bg-[#ffb800] text-white' : 'text-[#888] hover:text-white' feedMode === 'following' ? 'bg-[#F0A623] text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
@@ -359,11 +359,11 @@ export default function ForumIndexPage() {
{feedMode === 'following' && ( {feedMode === 'following' && (
<div className="mb-8"> <div className="mb-8">
{!currentUserId && ( {!currentUserId && (
<div className="bg-[#1a1208] border border-[#2a3a50] rounded-lg p-8 text-center"> <div className="bg-[#231d18] border border-[#2a3a50] rounded-lg p-8 text-center">
<p className="text-[#aab4c4] font-body text-sm mb-3">Sign in to see threads from people you follow.</p> <p className="text-[#aab4c4] font-body text-sm mb-3">Sign in to see threads from people you follow.</p>
<Link <Link
href="/login" href="/login"
className="inline-block bg-[#ffb800] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#d99700] transition-colors" className="inline-block bg-[#F0A623] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#BA7517] transition-colors"
> >
Sign In Sign In
</Link> </Link>
@@ -372,19 +372,19 @@ export default function ForumIndexPage() {
{currentUserId && followingLoading && ( {currentUserId && followingLoading && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-16 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#231d18] rounded-lg h-16 animate-pulse border border-[#3a322b]" />
))} ))}
</div> </div>
)} )}
{currentUserId && !followingLoading && followingThreads.length === 0 && ( {currentUserId && !followingLoading && followingThreads.length === 0 && (
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-10 text-center"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-10 text-center">
<div className="text-3xl mb-3">👥</div> <div className="text-3xl mb-3">👥</div>
<p className="text-[#aaa] font-body text-sm font-semibold mb-1">Your following feed is empty</p> <p className="text-[#aaa] font-body text-sm font-semibold mb-1">Your following feed is empty</p>
<p className="text-[#555] font-body text-sm mb-4">Follow other members to see their threads here.</p> <p className="text-[#555] font-body text-sm mb-4">Follow other members to see their threads here.</p>
<Link <Link
href="/forum" href="/forum"
onClick={() => setFeedMode('discover')} onClick={() => setFeedMode('discover')}
className="inline-block bg-[#ffb800] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#d99700] transition-colors" className="inline-block bg-[#F0A623] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#BA7517] transition-colors"
> >
Discover Members Discover Members
</Link> </Link>
@@ -398,10 +398,10 @@ export default function ForumIndexPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#ffb800] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#231d18] hover:bg-[#1e2a3a] border border-[#3a322b] hover:border-[#F0A623] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#ffb800] transition-colors truncate"> <h3 className="text-white font-body font-semibold text-sm group-hover:text-[#F0A623] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
@@ -409,7 +409,7 @@ export default function ForumIndexPage() {
<Link <Link
href={`/profile/${thread.author_id}`} href={`/profile/${thread.author_id}`}
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
{thread.author_name} {thread.author_name}
</Link> </Link>
@@ -450,7 +450,7 @@ export default function ForumIndexPage() {
placeholder="Search threads..." placeholder="Search threads..."
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#ffb800] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors" className="w-full bg-[#231d18] border border-[#3a322b] focus:border-[#F0A623] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors"
/> />
{search && ( {search && (
<button <button
@@ -470,7 +470,7 @@ export default function ForumIndexPage() {
<select <select
value={selectedCategory} value={selectedCategory}
onChange={e => setSelectedCategory(e.target.value)} onChange={e => setSelectedCategory(e.target.value)}
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#ffb800] rounded-lg pl-3 pr-8 py-2.5 text-sm font-body text-white focus:outline-none transition-colors cursor-pointer min-w-[160px]" className="appearance-none bg-[#231d18] border border-[#3a322b] focus:border-[#F0A623] rounded-lg pl-3 pr-8 py-2.5 text-sm font-body text-white focus:outline-none transition-colors cursor-pointer min-w-[160px]"
> >
<option value="">All Categories</option> <option value="">All Categories</option>
{categories.map(cat => ( {categories.map(cat => (
@@ -483,14 +483,14 @@ export default function ForumIndexPage() {
</div> </div>
{/* Sort Options */} {/* Sort Options */}
<div className="flex gap-1 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg p-1"> <div className="flex gap-1 bg-[#231d18] border border-[#3a322b] rounded-lg p-1">
{sortOptions.map(opt => ( {sortOptions.map(opt => (
<button <button
key={opt.value} key={opt.value}
onClick={() => setSort(opt.value)} onClick={() => setSort(opt.value)}
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${ className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
sort === opt.value sort === opt.value
? 'bg-[#ffb800] text-white' ? 'bg-[#F0A623] text-white'
: 'text-[#888] hover:text-white' : 'text-[#888] hover:text-white'
}`} }`}
> >
@@ -506,7 +506,7 @@ export default function ForumIndexPage() {
{threadsLoading && ( {threadsLoading && (
<div className="space-y-2"> <div className="space-y-2">
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-14 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#231d18] rounded-lg h-14 animate-pulse border border-[#3a322b]" />
))} ))}
</div> </div>
)} )}
@@ -523,16 +523,16 @@ export default function ForumIndexPage() {
<Link <Link
key={thread.id} key={thread.id}
href={`/forum/thread/${thread.id}`} href={`/forum/thread/${thread.id}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#ffb800] rounded-lg px-4 py-3 transition-all group" className="flex items-center gap-4 bg-[#231d18] hover:bg-[#1e2a3a] border border-[#3a322b] hover:border-[#F0A623] rounded-lg px-4 py-3 transition-all group"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#ffb800] transition-colors truncate"> <h3 className="text-white font-body font-semibold text-sm group-hover:text-[#F0A623] transition-colors truncate">
{thread.title} {thread.title}
</h3> </h3>
<p className="text-[#666] font-body text-xs mt-0.5"> <p className="text-[#666] font-body text-xs mt-0.5">
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#ffb800] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link> by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#F0A623] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
{thread.forum_categories && ( {thread.forum_categories && (
<> · <span className="text-[#ffb800]">{thread.forum_categories.name}</span></> <> · <span className="text-[#F0A623]">{thread.forum_categories.name}</span></>
)} )}
{' · '}{timeAgo(thread.created_at)} {' · '}{timeAgo(thread.created_at)}
</p> </p>
@@ -560,7 +560,7 @@ export default function ForumIndexPage() {
{loading && ( {loading && (
<div className="space-y-3 mb-8"> <div className="space-y-3 mb-8">
{[...Array(8)].map((_, i) => ( {[...Array(8)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-lg h-20 animate-pulse border border-[#252525]" /> <div key={i} className="bg-[#231d18] rounded-lg h-20 animate-pulse border border-[#3a322b]" />
))} ))}
</div> </div>
)} )}
@@ -584,11 +584,11 @@ export default function ForumIndexPage() {
<Link <Link
key={cat.id} key={cat.id}
href={`/forum/${cat.slug}`} href={`/forum/${cat.slug}`}
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#ffb800] rounded-lg p-4 transition-all group" className="flex items-center gap-4 bg-[#231d18] hover:bg-[#1e2a3a] border border-[#3a322b] hover:border-[#F0A623] rounded-lg p-4 transition-all group"
> >
<span className="text-3xl flex-shrink-0 w-10 text-center">{cat.icon}</span> <span className="text-3xl flex-shrink-0 w-10 text-center">{cat.icon}</span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#ffb800] transition-colors"> <h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#F0A623] transition-colors">
{cat.name} {cat.name}
</h2> </h2>
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p> <p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
@@ -610,7 +610,7 @@ export default function ForumIndexPage() {
<div className="text-white font-body font-semibold text-sm">{cat.post_count}</div> <div className="text-white font-body font-semibold text-sm">{cat.post_count}</div>
<div className="text-[#666] font-body text-xs">posts</div> <div className="text-[#666] font-body text-xs">posts</div>
</div> </div>
<svg className="w-4 h-4 text-[#555] group-hover:text-[#ffb800] flex-shrink-0 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-4 h-4 text-[#555] group-hover:text-[#F0A623] flex-shrink-0 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg> </svg>
</Link> </Link>
@@ -624,12 +624,12 @@ export default function ForumIndexPage() {
)} )}
{/* Forum guidelines */} {/* Forum guidelines */}
<div className="mt-8 bg-[#1a1208] border border-[#2a3a50] rounded-lg p-5"> <div className="mt-8 bg-[#231d18] border border-[#2a3a50] rounded-lg p-5">
<h3 className="text-white font-heading font-semibold mb-2">Community Guidelines</h3> <h3 className="text-white font-heading font-semibold mb-2">Community Guidelines</h3>
<ul className="text-[#aab4c4] font-body text-sm space-y-1"> <ul className="text-[#aab4c4] font-body text-sm space-y-1">
<li> Be respectful and professional this is a community of peers</li> <li> Be respectful and professional this is a community of peers</li>
<li> Share real-world experience and cite sources when possible</li> <li> Share real-world experience and cite sources when possible</li>
<li> The <span className="text-[#ffb800]">AV Beat AI</span> assistant is available in threads to help answer questions it is clearly labeled</li> <li> The <span className="text-[#F0A623]">AV Beat AI</span> assistant is available in threads to help answer questions it is clearly labeled</li>
<li> No spam, self-promotion, or vendor pitches without disclosure</li> <li> No spam, self-promotion, or vendor pitches without disclosure</li>
</ul> </ul>
</div> </div>

View File

@@ -73,7 +73,7 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="max-w-2xl mx-auto px-4 py-12 text-[#666] font-body text-sm">Loading</div> <div className="max-w-2xl mx-auto px-4 py-12 text-[#666] font-body text-sm">Loading</div>
</main> </main>
<Footer /> <Footer />
@@ -85,7 +85,7 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="max-w-2xl mx-auto px-4 py-12 text-[#888] font-body text-sm"> <div className="max-w-2xl mx-auto px-4 py-12 text-[#888] font-body text-sm">
Profile unavailable. Profile unavailable.
</div> </div>
@@ -98,13 +98,13 @@ export default function EditForumProfilePage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="max-w-2xl mx-auto px-4 py-8"> <div className="max-w-2xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h1 className="text-2xl font-heading font-bold text-white">Edit Profile</h1> <h1 className="text-2xl font-heading font-bold text-white">Edit Profile</h1>
<Link <Link
href={`/forum/user/${profile.username}`} href={`/forum/user/${profile.username}`}
className="text-xs font-body text-[#ffb800] hover:underline" className="text-xs font-body text-[#F0A623] hover:underline"
> >
View public profile View public profile
</Link> </Link>
@@ -112,10 +112,10 @@ export default function EditForumProfilePage() {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4"
> >
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -132,7 +132,7 @@ export default function EditForumProfilePage() {
onChange={(e) => update('username', e.target.value)} onChange={(e) => update('username', e.target.value)}
pattern="[A-Za-z0-9_]{3,24}" pattern="[A-Za-z0-9_]{3,24}"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
<p className="text-[11px] font-body text-[#666] mt-1"> <p className="text-[11px] font-body text-[#666] mt-1">
3-24 characters. Letters, numbers, underscore. 3-24 characters. Letters, numbers, underscore.
@@ -145,7 +145,7 @@ export default function EditForumProfilePage() {
value={profile.display_name} value={profile.display_name}
onChange={(e) => update('display_name', e.target.value)} onChange={(e) => update('display_name', e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
@@ -154,7 +154,7 @@ export default function EditForumProfilePage() {
value={profile.bio || ''} value={profile.bio || ''}
onChange={(e) => update('bio', e.target.value)} onChange={(e) => update('bio', e.target.value)}
rows={4} rows={4}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623] resize-none"
/> />
</Field> </Field>
@@ -164,7 +164,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.role_title || ''} value={profile.role_title || ''}
onChange={(e) => update('role_title', e.target.value)} onChange={(e) => update('role_title', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
<Field label="Company"> <Field label="Company">
@@ -172,7 +172,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.company || ''} value={profile.company || ''}
onChange={(e) => update('company', e.target.value)} onChange={(e) => update('company', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
</div> </div>
@@ -183,7 +183,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.location_city || ''} value={profile.location_city || ''}
onChange={(e) => update('location_city', e.target.value)} onChange={(e) => update('location_city', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
<Field label="Country"> <Field label="Country">
@@ -191,7 +191,7 @@ export default function EditForumProfilePage() {
type="text" type="text"
value={profile.location_country || ''} value={profile.location_country || ''}
onChange={(e) => update('location_country', e.target.value)} onChange={(e) => update('location_country', e.target.value)}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
</div> </div>
@@ -202,14 +202,14 @@ export default function EditForumProfilePage() {
value={profile.avatar_url || ''} value={profile.avatar_url || ''}
onChange={(e) => update('avatar_url', e.target.value)} onChange={(e) => update('avatar_url', e.target.value)}
placeholder="https://…" placeholder="https://…"
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</Field> </Field>
<button <button
type="submit" type="submit"
disabled={saving} disabled={saving}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
> >
{saving ? 'Saving…' : 'Save Profile'} {saving ? 'Saving…' : 'Save Profile'}
</button> </button>

View File

@@ -101,11 +101,11 @@ function ForumRegisterInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#1c1815] via-[#1c1815] to-[#1c1815]">
<div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start"> <div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start">
{/* Left rail — what membership gets you */} {/* Left rail — what membership gets you */}
<aside className="hidden lg:block"> <aside className="hidden lg:block">
<div className="text-[10px] uppercase tracking-widest text-[#ffb800] font-mono mb-3">Membership</div> <div className="text-[10px] uppercase tracking-widest text-[#F0A623] font-mono mb-3">Membership</div>
<h1 className="font-heading text-white text-4xl xl:text-5xl font-bold leading-tight mb-4"> <h1 className="font-heading text-white text-4xl xl:text-5xl font-bold leading-tight mb-4">
Join AV Beat Join AV Beat
</h1> </h1>
@@ -128,10 +128,10 @@ function ForumRegisterInner() {
</li> </li>
))} ))}
</ul> </ul>
<div className="mt-6 text-[11px] text-[#666] leading-relaxed max-w-md border-t border-[#252525] pt-4"> <div className="mt-6 text-[11px] text-[#666] leading-relaxed max-w-md border-t border-[#3a322b] pt-4">
<strong className="text-[#888]">PR firms &amp; contributors:</strong> a separate contributor application is required to submit press releases. <strong className="text-[#888]">PR firms &amp; contributors:</strong> a separate contributor application is required to submit press releases.
{" "} {" "}
<Link href="/about/advertise" className="text-[#ffb800] hover:underline">Contact us</Link> if you represent a manufacturer or agency. <Link href="/about/advertise" className="text-[#F0A623] hover:underline">Contact us</Link> if you represent a manufacturer or agency.
</div> </div>
</aside> </aside>
@@ -145,14 +145,14 @@ function ForumRegisterInner() {
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4 shadow-2xl"
> >
<div className="hidden lg:block mb-2"> <div className="hidden lg:block mb-2">
<div className="text-[10px] uppercase tracking-widest text-[#ffb800] font-mono mb-1">Create your account</div> <div className="text-[10px] uppercase tracking-widest text-[#F0A623] font-mono mb-1">Create your account</div>
<div className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div> <div className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div>
</div> </div>
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -171,7 +171,7 @@ function ForumRegisterInner() {
onChange={(e) => setFullName(e.target.value)} onChange={(e) => setFullName(e.target.value)}
autoComplete="name" autoComplete="name"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -184,7 +184,7 @@ function ForumRegisterInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -198,7 +198,7 @@ function ForumRegisterInner() {
autoComplete="new-password" autoComplete="new-password"
required required
minLength={6} minLength={6}
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -211,29 +211,29 @@ function ForumRegisterInner() {
onChange={(e) => setConfirm(e.target.value)} onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors" className="w-full bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
> >
{loading ? 'Creating account…' : 'Create Free Account'} {loading ? 'Creating account…' : 'Create Free Account'}
</button> </button>
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2"> <p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
By creating an account you agree to our{' '} By creating an account you agree to our{' '}
<Link href="/terms" className="text-[#888] hover:text-[#ffb800] underline">terms</Link> <Link href="/terms" className="text-[#888] hover:text-[#F0A623] underline">terms</Link>
{' '}and{' '} {' '}and{' '}
<Link href="/privacy" className="text-[#888] hover:text-[#ffb800] underline">privacy policy</Link>. <Link href="/privacy" className="text-[#888] hover:text-[#F0A623] underline">privacy policy</Link>.
We&rsquo;ll never sell your email. We&rsquo;ll never sell your email.
</p> </p>
<SocialAuthButtons next={next} /> <SocialAuthButtons next={next} />
<div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#252525]"> <div className="text-center text-xs font-body text-[#888] pt-2 border-t border-[#3a322b]">
Already have an account?{' '} Already have an account?{' '}
<Link <Link
href={`/forum/login?next=${encodeURIComponent(next)}`} href={`/forum/login?next=${encodeURIComponent(next)}`}
className="text-[#ffb800] hover:underline font-semibold" className="text-[#F0A623] hover:underline font-semibold"
> >
Sign in Sign in
</Link> </Link>

View File

@@ -40,20 +40,20 @@ function ResetInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]"> <main className="min-h-screen bg-gradient-to-b from-[#1c1815] via-[#1c1815] to-[#1c1815]">
<div className="max-w-md mx-auto px-4 py-16"> <div className="max-w-md mx-auto px-4 py-16">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="text-[10px] uppercase tracking-widest text-[#ffb800] font-mono mb-2">Reset password</div> <div className="text-[10px] uppercase tracking-widest text-[#F0A623] font-mono mb-2">Reset password</div>
<h1 className="text-3xl font-heading font-bold text-white">Set a new password</h1> <h1 className="text-3xl font-heading font-bold text-white">Set a new password</h1>
</div> </div>
{hasSession === false ? ( {hasSession === false ? (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 text-center"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 text-center">
<p className="text-[#cbd5e1] mb-4 text-sm"> <p className="text-[#cbd5e1] mb-4 text-sm">
This reset link is expired or invalid. This reset link is expired or invalid.
</p> </p>
<Link <Link
href="/forum/forgot-password" href="/forum/forgot-password"
className="inline-block bg-[#ffb800] hover:bg-[#d99700] text-white font-semibold text-sm px-5 py-2 rounded" className="inline-block bg-[#F0A623] hover:bg-[#BA7517] text-white font-semibold text-sm px-5 py-2 rounded"
> >
Request a new link Request a new link
</Link> </Link>
@@ -61,10 +61,10 @@ function ResetInner() {
) : ( ) : (
<form <form
onSubmit={submit} onSubmit={submit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4 shadow-2xl"
> >
{err && ( {err && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{err} {err}
</div> </div>
)} )}
@@ -84,7 +84,7 @@ function ResetInner() {
autoComplete="new-password" autoComplete="new-password"
minLength={8} minLength={8}
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -97,13 +97,13 @@ function ResetInner() {
onChange={(e) => setConfirm(e.target.value)} onChange={(e) => setConfirm(e.target.value)}
autoComplete="new-password" autoComplete="new-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={busy} disabled={busy}
className="w-full bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-semibold text-sm px-5 py-3 rounded-lg" className="w-full bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-semibold text-sm px-5 py-3 rounded-lg"
> >
{busy ? "Saving…" : "Update password"} {busy ? "Saving…" : "Update password"}
</button> </button>

View File

@@ -67,13 +67,13 @@ function timeAgo(dateStr: string): string {
function Avatar({ name, isAI }: { name: string; isAI: boolean }) { function Avatar({ name, isAI }: { name: string; isAI: boolean }) {
if (isAI) { if (isAI) {
return ( return (
<div className="w-9 h-9 rounded-full bg-[#4a3500] border border-[#ffb800] flex items-center justify-center flex-shrink-0"> <div className="w-9 h-9 rounded-full bg-[#412402] border border-[#F0A623] flex items-center justify-center flex-shrink-0">
<span className="text-[#ffb800] text-xs font-bold">AI</span> <span className="text-[#F0A623] text-xs font-bold">AI</span>
</div> </div>
); );
} }
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
const colors = ['#4a3500', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a']; const colors = ['#412402', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
const colorIdx = name.charCodeAt(0) % colors.length; const colorIdx = name.charCodeAt(0) % colors.length;
return ( return (
<div <div
@@ -133,7 +133,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
} }
}; };
const scoreColor = counts.vote_score > 0 ? 'text-[#ffb800]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]'; const scoreColor = counts.vote_score > 0 ? 'text-[#F0A623]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]';
return ( return (
<div className="flex items-center gap-1 mt-2"> <div className="flex items-center gap-1 mt-2">
@@ -144,8 +144,8 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Upvote'} title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Upvote'}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${ className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
currentVote === 1 currentVote === 1
? 'bg-[#ffb800]/20 border-[#ffb800] text-[#ffb800]' ? 'bg-[#F0A623]/20 border-[#F0A623] text-[#F0A623]'
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#ffb800] hover:text-[#ffb800]' : 'bg-transparent border-[#3a322b] text-[#666] hover:border-[#F0A623] hover:text-[#F0A623]'
} disabled:opacity-40 disabled:cursor-not-allowed`} } disabled:opacity-40 disabled:cursor-not-allowed`}
aria-label="Upvote" aria-label="Upvote"
> >
@@ -167,7 +167,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Downvote'} title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Downvote'}
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${ className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
currentVote === -1 currentVote === -1
? 'bg-red-900/30 border-red-700 text-red-400' :'bg-transparent border-[#2a2a2a] text-[#666] hover:border-red-700 hover:text-red-400' ? 'bg-red-900/30 border-red-700 text-red-400' :'bg-transparent border-[#3a322b] text-[#666] hover:border-red-700 hover:text-red-400'
} disabled:opacity-40 disabled:cursor-not-allowed`} } disabled:opacity-40 disabled:cursor-not-allowed`}
aria-label="Downvote" aria-label="Downvote"
> >
@@ -178,7 +178,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
</button> </button>
{!isAuthenticated && ( {!isAuthenticated && (
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#ffb800] ml-1 transition-colors"> <Link href="/login" className="text-xs font-body text-[#555] hover:text-[#F0A623] ml-1 transition-colors">
Sign in to vote Sign in to vote
</Link> </Link>
)} )}
@@ -300,12 +300,12 @@ export default function ForumThreadPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside> <aside className="float-right ml-6 mb-6 hidden lg:block max-w-[300px]"><SidebarAdStack /></aside>
<div className="max-w-container mx-auto px-4 py-10 space-y-4"> <div className="max-w-container mx-auto px-4 py-10 space-y-4">
<div className="h-8 bg-[#1a1a1a] rounded animate-pulse w-2/3" /> <div className="h-8 bg-[#231d18] rounded animate-pulse w-2/3" />
<div className="h-32 bg-[#1a1a1a] rounded animate-pulse" /> <div className="h-32 bg-[#231d18] rounded animate-pulse" />
<div className="h-20 bg-[#1a1a1a] rounded animate-pulse" /> <div className="h-20 bg-[#231d18] rounded animate-pulse" />
</div> </div>
</main> </main>
<Footer /> <Footer />
@@ -317,12 +317,12 @@ export default function ForumThreadPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="max-w-container mx-auto px-4 py-10"> <div className="max-w-container mx-auto px-4 py-10">
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4 text-red-400 font-body"> <div className="bg-red-900/20 border border-red-800 rounded-lg p-4 text-red-400 font-body">
{error || 'Thread not found.'} {error || 'Thread not found.'}
</div> </div>
<Link href="/forum" className="mt-4 inline-block text-[#ffb800] hover:underline font-body text-sm"> <Link href="/forum" className="mt-4 inline-block text-[#F0A623] hover:underline font-body text-sm">
Back to Forum Back to Forum
</Link> </Link>
</div> </div>
@@ -337,16 +337,16 @@ export default function ForumThreadPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
{/* Breadcrumb */} {/* Breadcrumb */}
<div className="bg-[#1a1208] border-b border-[#2a3a50]"> <div className="bg-[#231d18] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-4"> <div className="max-w-container mx-auto px-4 py-4">
<nav className="text-sm font-body text-[#666]"> <nav className="text-sm font-body text-[#666]">
<Link href="/forum" className="hover:text-[#ffb800] transition-colors">Forum</Link> <Link href="/forum" className="hover:text-[#F0A623] transition-colors">Forum</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
{category && ( {category && (
<> <>
<Link href={`/forum/${category.slug}`} className="hover:text-[#ffb800] transition-colors">{category.name}</Link> <Link href={`/forum/${category.slug}`} className="hover:text-[#F0A623] transition-colors">{category.name}</Link>
<span className="mx-2"></span> <span className="mx-2"></span>
</> </>
)} )}
@@ -361,12 +361,12 @@ export default function ForumThreadPage() {
<h1 className="text-2xl font-heading font-bold text-white mb-6 leading-snug">{thread.title}</h1> <h1 className="text-2xl font-heading font-bold text-white mb-6 leading-snug">{thread.title}</h1>
{/* Original Post */} {/* Original Post */}
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-5 mb-4"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-5 mb-4">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Avatar name={thread.author_name} isAI={false} /> <Avatar name={thread.author_name} isAI={false} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#ffb800] hover:underline">{thread.author_name}</Link> <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#F0A623] hover:underline">{thread.author_name}</Link>
<span className="text-[#555] font-body text-xs">·</span> <span className="text-[#555] font-body text-xs">·</span>
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span> <span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
<span className="text-[#555] font-body text-xs">·</span> <span className="text-[#555] font-body text-xs">·</span>
@@ -395,17 +395,17 @@ export default function ForumThreadPage() {
<div <div
className={`border rounded-lg p-5 ${ className={`border rounded-lg p-5 ${
reply.is_ai_response reply.is_ai_response
? 'bg-[#0d1f35] border-[#4a3500]' ? 'bg-[#0d1f35] border-[#412402]'
: 'bg-[#1a1a1a] border-[#252525]' : 'bg-[#231d18] border-[#3a322b]'
}`} }`}
> >
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Avatar name={reply.author_name} isAI={reply.is_ai_response} /> <Avatar name={reply.author_name} isAI={reply.is_ai_response} />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-2 flex-wrap"> <div className="flex items-center gap-2 mb-2 flex-wrap">
<Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#ffb800] hover:underline">{reply.author_name}</Link> <Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#F0A623] hover:underline">{reply.author_name}</Link>
{reply.is_ai_response && ( {reply.is_ai_response && (
<span className="text-xs bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/30 px-2 py-0.5 rounded font-body font-semibold"> <span className="text-xs bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/30 px-2 py-0.5 rounded font-body font-semibold">
AI Assistant AI Assistant
</span> </span>
)} )}
@@ -441,14 +441,14 @@ export default function ForumThreadPage() {
)} )}
{/* AI Assistant Panel */} {/* AI Assistant Panel */}
<div className="bg-[#0d1f35] border border-[#4a3500] rounded-lg p-5 mb-6"> <div className="bg-[#0d1f35] border border-[#412402] rounded-lg p-5 mb-6">
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<div className="w-7 h-7 rounded-full bg-[#4a3500] border border-[#ffb800] flex items-center justify-center"> <div className="w-7 h-7 rounded-full bg-[#412402] border border-[#F0A623] flex items-center justify-center">
<span className="text-[#ffb800] text-xs font-bold">AI</span> <span className="text-[#F0A623] text-xs font-bold">AI</span>
</div> </div>
<div> <div>
<span className="text-white font-body font-semibold text-sm">AV Beat AI Assistant</span> <span className="text-white font-body font-semibold text-sm">AV Beat AI Assistant</span>
<span className="ml-2 text-xs bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/30 px-2 py-0.5 rounded font-body">AI Not a human</span> <span className="ml-2 text-xs bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/30 px-2 py-0.5 rounded font-body">AI Not a human</span>
</div> </div>
</div> </div>
<p className="text-[#7a9ab8] font-body text-xs mb-3"> <p className="text-[#7a9ab8] font-body text-xs mb-3">
@@ -461,12 +461,12 @@ export default function ForumThreadPage() {
value={aiQuestion} value={aiQuestion}
onChange={e => setAiQuestion(e.target.value)} onChange={e => setAiQuestion(e.target.value)}
disabled={aiLoading} disabled={aiLoading}
className="flex-1 bg-[#111] border border-[#2a3a50] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#3a5a7a] focus:outline-none focus:border-[#ffb800] disabled:opacity-50" className="flex-1 bg-[#111] border border-[#2a3a50] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#3a5a7a] focus:outline-none focus:border-[#F0A623] disabled:opacity-50"
/> />
<button <button
type="submit" type="submit"
disabled={aiLoading || !aiQuestion.trim()} disabled={aiLoading || !aiQuestion.trim()}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors flex-shrink-0" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors flex-shrink-0"
> >
{aiLoading ? ( {aiLoading ? (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
@@ -486,7 +486,7 @@ export default function ForumThreadPage() {
{/* Reply Form — auth-gated */} {/* Reply Form — auth-gated */}
{thread.status !== 'closed' && isAuthenticated && ( {thread.status !== 'closed' && isAuthenticated && (
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-5"> <div className="bg-[#231d18] border border-[#3a322b] rounded-lg p-5">
<h3 className="text-white font-heading font-semibold mb-4">Post a Reply</h3> <h3 className="text-white font-heading font-semibold mb-4">Post a Reply</h3>
<form onSubmit={handleReply} className="space-y-3"> <form onSubmit={handleReply} className="space-y-3">
<textarea <textarea
@@ -495,21 +495,21 @@ export default function ForumThreadPage() {
onChange={e => setReplyBody(e.target.value)} onChange={e => setReplyBody(e.target.value)}
required required
rows={5} rows={5}
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#ffb800] resize-none" className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#F0A623] resize-none"
/> />
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} /> <ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
<span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span> <span className="text-[10px] text-[#666]">Auto-optimized, never &gt; 20 MB</span>
</div> </div>
{replyError && ( {replyError && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{replyError} {replyError}
</div> </div>
)} )}
<button <button
type="submit" type="submit"
disabled={submitting || !replyBody.trim()} disabled={submitting || !replyBody.trim()}
className="bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
{submitting ? 'Posting...' : 'Post Reply'} {submitting ? 'Posting...' : 'Post Reply'}
</button> </button>
@@ -518,7 +518,7 @@ export default function ForumThreadPage() {
)} )}
{thread.status !== 'closed' && !isAuthenticated && ( {thread.status !== 'closed' && !isAuthenticated && (
<div className="bg-[#0d1f35] border border-[#4a3500] rounded-lg p-5 text-center"> <div className="bg-[#0d1f35] border border-[#412402] rounded-lg p-5 text-center">
<h3 className="text-white font-heading font-semibold mb-1">Join the discussion</h3> <h3 className="text-white font-heading font-semibold mb-1">Join the discussion</h3>
<p className="text-[#7a9ab8] font-body text-sm mb-4"> <p className="text-[#7a9ab8] font-body text-sm mb-4">
Sign in or create a free account to post replies. Sign in or create a free account to post replies.
@@ -526,13 +526,13 @@ export default function ForumThreadPage() {
<div className="flex items-center justify-center gap-3"> <div className="flex items-center justify-center gap-3">
<Link <Link
href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`} href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
className="bg-[#ffb800] hover:bg-[#d99700] text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="bg-[#F0A623] hover:bg-[#BA7517] text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
Sign In Sign In
</Link> </Link>
<Link <Link
href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`} href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
className="border border-[#ffb800] text-[#ffb800] hover:bg-[#ffb800]/10 font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors" className="border border-[#F0A623] text-[#F0A623] hover:bg-[#F0A623]/10 font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
> >
Register Register
</Link> </Link>
@@ -541,7 +541,7 @@ export default function ForumThreadPage() {
)} )}
{thread.status === 'closed' && ( {thread.status === 'closed' && (
<div className="bg-[#1a1a1a] border border-[#333] rounded-lg p-4 text-center text-[#666] font-body text-sm"> <div className="bg-[#231d18] border border-[#333] rounded-lg p-4 text-center text-[#666] font-body text-sm">
This thread is closed to new replies. This thread is closed to new replies.
</div> </div>
)} )}

View File

@@ -107,7 +107,7 @@ export default function ForumUserPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="bg-[#111111] min-h-screen"> <main className="bg-[#1c1815] min-h-screen">
<div className="max-w-container mx-auto px-4 py-12"> <div className="max-w-container mx-auto px-4 py-12">
<p className="font-body text-sm text-[#666]">Loading profile</p> <p className="font-body text-sm text-[#666]">Loading profile</p>
</div> </div>
@@ -120,11 +120,11 @@ export default function ForumUserPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="bg-[#111111] min-h-screen"> <main className="bg-[#1c1815] min-h-screen">
<div className="max-w-container mx-auto px-4 py-12"> <div className="max-w-container mx-auto px-4 py-12">
<h1 className="font-heading text-2xl font-bold text-[#e8e8e8] mb-2">User not found</h1> <h1 className="font-heading text-2xl font-bold text-[#FBEFE0] mb-2">User not found</h1>
<p className="font-body text-sm text-[#888]">No forum member with username "{username}".</p> <p className="font-body text-sm text-[#888]">No forum member with username "{username}".</p>
<Link href="/forum" className="font-body text-sm text-[#ffb800] hover:underline mt-4 inline-block"> Back to The Crew Lounge</Link> <Link href="/forum" className="font-body text-sm text-[#F0A623] hover:underline mt-4 inline-block"> Back to The Crew Lounge</Link>
</div> </div>
</main> </main>
<Footer /> <Footer />
@@ -139,16 +139,16 @@ export default function ForumUserPage() {
return ( return (
<> <>
<Header /> <Header />
<main className="bg-[#111111] min-h-screen"> <main className="bg-[#1c1815] min-h-screen">
<div className="max-w-container mx-auto px-4 py-8"> <div className="max-w-container mx-auto px-4 py-8">
{/* Header card */} {/* Header card */}
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6 mb-6"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-6 mb-6">
<div className="flex flex-col sm:flex-row sm:items-start gap-5"> <div className="flex flex-col sm:flex-row sm:items-start gap-5">
<div className="w-20 h-20 sm:w-24 sm:h-24 rounded-full overflow-hidden border-2 border-[#2a2a2a] flex-shrink-0 bg-[#111111] flex items-center justify-center"> <div className="w-20 h-20 sm:w-24 sm:h-24 rounded-full overflow-hidden border-2 border-[#3a322b] flex-shrink-0 bg-[#1c1815] flex items-center justify-center">
{profile.avatar_url ? ( {profile.avatar_url ? (
<AppImage src={profile.avatar_url} alt={profile.display_name} width={96} height={96} className="w-full h-full object-cover" /> <AppImage src={profile.avatar_url} alt={profile.display_name} width={96} height={96} className="w-full h-full object-cover" />
) : ( ) : (
<span className="font-heading text-2xl font-bold text-[#ffb800]"> <span className="font-heading text-2xl font-bold text-[#F0A623]">
{profile.display_name.charAt(0).toUpperCase()} {profile.display_name.charAt(0).toUpperCase()}
</span> </span>
)} )}
@@ -156,19 +156,19 @@ export default function ForumUserPage() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-3 flex-wrap"> <div className="flex items-center justify-between gap-3 flex-wrap">
<h1 className="font-heading text-2xl md:text-3xl font-bold text-[#e8e8e8] leading-tight"> <h1 className="font-heading text-2xl md:text-3xl font-bold text-[#FBEFE0] leading-tight">
{profile.display_name} {profile.display_name}
</h1> </h1>
{isOwnProfile && ( {isOwnProfile && (
<Link <Link
href="/forum/profile/edit" href="/forum/profile/edit"
className="text-xs font-body font-bold uppercase tracking-wider bg-[#ffb800] hover:bg-[#d99700] text-white px-3 py-1.5 rounded-sm transition-colors" className="text-xs font-body font-bold uppercase tracking-wider bg-[#F0A623] hover:bg-[#BA7517] text-white px-3 py-1.5 rounded-sm transition-colors"
> >
Edit Profile Edit Profile
</Link> </Link>
)} )}
</div> </div>
<p className="font-body text-sm text-[#ffb800] mt-0.5">@{profile.username}</p> <p className="font-body text-sm text-[#F0A623] mt-0.5">@{profile.username}</p>
<div className="flex flex-wrap items-center gap-2 mt-3"> <div className="flex flex-wrap items-center gap-2 mt-3">
{archetypeLabel && ( {archetypeLabel && (
@@ -198,7 +198,7 @@ export default function ForumUserPage() {
</div> </div>
{profile.bio && ( {profile.bio && (
<p className="font-body text-sm text-[#bfbfbf] leading-relaxed mt-5 pt-5 border-t border-[#2a2a2a]"> <p className="font-body text-sm text-[#bfbfbf] leading-relaxed mt-5 pt-5 border-t border-[#3a322b]">
{profile.bio} {profile.bio}
</p> </p>
)} )}
@@ -206,7 +206,7 @@ export default function ForumUserPage() {
{profile.expertise_tags && profile.expertise_tags.length > 0 && ( {profile.expertise_tags && profile.expertise_tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4"> <div className="flex flex-wrap gap-2 mt-4">
{profile.expertise_tags.map((tag) => ( {profile.expertise_tags.map((tag) => (
<span key={tag} className="font-body text-[11px] text-[#9ca3af] bg-[#222] border border-[#2a2a2a] px-2 py-0.5 rounded-sm"> <span key={tag} className="font-body text-[11px] text-[#9ca3af] bg-[#222] border border-[#3a322b] px-2 py-0.5 rounded-sm">
#{tag} #{tag}
</span> </span>
))} ))}
@@ -217,7 +217,7 @@ export default function ForumUserPage() {
{/* Activity grid */} {/* Activity grid */}
<div className="grid grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6"> <div className="grid grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6">
<div> <div>
<h2 className="font-heading text-lg font-bold text-[#e8e8e8] mb-3"> <h2 className="font-heading text-lg font-bold text-[#FBEFE0] mb-3">
Threads started ({profile.thread_count}) Threads started ({profile.thread_count})
</h2> </h2>
{threads.length === 0 ? ( {threads.length === 0 ? (
@@ -225,8 +225,8 @@ export default function ForumUserPage() {
) : ( ) : (
<ul className="space-y-2 mb-8"> <ul className="space-y-2 mb-8">
{threads.map((t) => ( {threads.map((t) => (
<li key={t.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-3 hover:border-[#ffb800]/50 transition-colors"> <li key={t.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-3 hover:border-[#F0A623]/50 transition-colors">
<Link href={`/forum/thread/${t.id}`} className="font-body text-sm font-semibold text-[#e0e0e0] hover:text-[#ffb800] block"> <Link href={`/forum/thread/${t.id}`} className="font-body text-sm font-semibold text-[#e0e0e0] hover:text-[#F0A623] block">
{t.title} {t.title}
</Link> </Link>
<div className="flex items-center gap-3 mt-1 text-[11px] text-[#666]"> <div className="flex items-center gap-3 mt-1 text-[11px] text-[#666]">
@@ -243,7 +243,7 @@ export default function ForumUserPage() {
</ul> </ul>
)} )}
<h2 className="font-heading text-lg font-bold text-[#e8e8e8] mb-3"> <h2 className="font-heading text-lg font-bold text-[#FBEFE0] mb-3">
Recent replies Recent replies
</h2> </h2>
{replies.length === 0 ? ( {replies.length === 0 ? (
@@ -251,8 +251,8 @@ export default function ForumUserPage() {
) : ( ) : (
<ul className="space-y-2"> <ul className="space-y-2">
{replies.map((r) => ( {replies.map((r) => (
<li key={r.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-3"> <li key={r.id} className="bg-[#231d18] border border-[#3a322b] rounded-sm p-3">
<Link href={`/forum/thread/${r.thread_id}`} className="font-body text-xs font-semibold text-[#ffb800] hover:underline"> <Link href={`/forum/thread/${r.thread_id}`} className="font-body text-xs font-semibold text-[#F0A623] hover:underline">
{r.forum_threads?.title || "(thread)"} {r.forum_threads?.title || "(thread)"}
</Link> </Link>
<p className="font-body text-sm text-[#bfbfbf] leading-relaxed mt-1 line-clamp-3">{r.body}</p> <p className="font-body text-sm text-[#bfbfbf] leading-relaxed mt-1 line-clamp-3">{r.body}</p>
@@ -266,7 +266,7 @@ export default function ForumUserPage() {
</div> </div>
<aside> <aside>
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-4 sticky top-4"> <div className="bg-[#231d18] border border-[#3a322b] rounded-sm p-4 sticky top-4">
<h3 className="font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-2">Profile facts</h3> <h3 className="font-body text-xs font-bold uppercase tracking-wider text-[#888] mb-2">Profile facts</h3>
<dl className="text-sm space-y-2"> <dl className="text-sm space-y-2">
<div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Role</dt><dd className="text-[#cccccc]">{profile.role_title || "—"}</dd></div> <div><dt className="text-[#666] text-[11px] uppercase tracking-wider">Role</dt><dd className="text-[#cccccc]">{profile.role_title || "—"}</dd></div>

View File

@@ -34,7 +34,7 @@ export default async function GearPage() {
<div className="max-w-container mx-auto px-4"> <div className="max-w-container mx-auto px-4">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<span className="section-label">Gear &amp; Reviews</span> <span className="section-label">Gear &amp; Reviews</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<h1 className="font-heading text-white text-3xl font-bold">Production Gear &amp; Reviews</h1> <h1 className="font-heading text-white text-3xl font-bold">Production Gear &amp; Reviews</h1>
<p className="text-[#777] font-body text-sm mt-2">In-depth reviews and coverage of cameras, audio, infrastructure, and production technology</p> <p className="text-[#777] font-body text-sm mt-2">In-depth reviews and coverage of cameras, audio, infrastructure, and production technology</p>
@@ -49,7 +49,7 @@ export default async function GearPage() {
<Link <Link
key={article.slug} key={article.slug}
href={`/articles/${article.slug}`} href={`/articles/${article.slug}`}
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#ffb800] transition-colors"> className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#F0A623] transition-colors">
<div className="relative h-[200px] overflow-hidden"> <div className="relative h-[200px] overflow-hidden">
<AppImage <AppImage
src={article.image} src={article.image}
@@ -59,7 +59,7 @@ export default async function GearPage() {
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw" sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
/> />
<div className="absolute top-2 left-2"> <div className="absolute top-2 left-2">
<span className="bg-[#ffb800] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase"> <span className="bg-[#F0A623] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase">
{article.category} {article.category}
</span> </span>
</div> </div>
@@ -74,13 +74,13 @@ export default async function GearPage() {
)} )}
<span className="text-[#555] font-body text-[11px]">{article.readTime}</span> <span className="text-[#555] font-body text-[11px]">{article.readTime}</span>
</div> </div>
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#ffb800] transition-colors line-clamp-3"> <h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#F0A623] transition-colors line-clamp-3">
{article.title} {article.title}
</h2> </h2>
<p className="text-[#666] font-body text-xs leading-relaxed line-clamp-2">{article.excerpt}</p> <p className="text-[#666] font-body text-xs leading-relaxed line-clamp-2">{article.excerpt}</p>
<div className="mt-3 flex flex-wrap gap-1"> <div className="mt-3 flex flex-wrap gap-1">
{article.tags.slice(0, 3).map((tag) => ( {article.tags.slice(0, 3).map((tag) => (
<span key={tag} className="px-1.5 py-0.5 bg-[#1a1a1a] border border-[#2a2a2a] text-[#666] text-[10px] font-body rounded-sm"> <span key={tag} className="px-1.5 py-0.5 bg-[#231d18] border border-[#3a322b] text-[#666] text-[10px] font-body rounded-sm">
{tag} {tag}
</span> </span>
))} ))}

View File

@@ -13,7 +13,7 @@ export default function GlobalError({
}, [error]); }, [error]);
return ( return (
<html> <html>
<body style={{ background: "#0d0d0d", color: "#e0e0e0", fontFamily: "system-ui", padding: 24, minHeight: "100vh" }}> <body style={{ background: "#1c1815", color: "#e0e0e0", fontFamily: "system-ui", padding: 24, minHeight: "100vh" }}>
<div style={{ maxWidth: 480, margin: "10vh auto", textAlign: "center" }}> <div style={{ maxWidth: 480, margin: "10vh auto", textAlign: "center" }}>
<h2 style={{ fontSize: 22, marginBottom: 12 }}>Something went wrong</h2> <h2 style={{ fontSize: 22, marginBottom: 12 }}>Something went wrong</h2>
<p style={{ marginBottom: 12, color: "#888" }}>AV Beat hit an unexpected error.</p> <p style={{ marginBottom: 12, color: "#888" }}>AV Beat hit an unexpected error.</p>
@@ -22,7 +22,7 @@ export default function GlobalError({
)} )}
<button <button
onClick={() => reset()} onClick={() => reset()}
style={{ padding: "8px 16px", background: "#ffb800", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }} style={{ padding: "8px 16px", background: "#F0A623", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }}
> >
Try again Try again
</button> </button>

View File

@@ -305,7 +305,7 @@ export default function ArticleFeed() {
{/* Section header */} {/* Section header */}
<div className="flex items-center gap-3 mb-4 md:mb-5 flex-wrap"> <div className="flex items-center gap-3 mb-4 md:mb-5 flex-wrap">
<span className="section-label">Industry News</span> <span className="section-label">Industry News</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
{/* Sort dropdown */} {/* Sort dropdown */}
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="font-body text-[10px] text-[#555] uppercase tracking-wide hidden sm:inline">Sort:</span> <span className="font-body text-[10px] text-[#555] uppercase tracking-wide hidden sm:inline">Sort:</span>
@@ -314,7 +314,7 @@ export default function ArticleFeed() {
value={sortOrder} value={sortOrder}
onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }} onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }}
aria-label="Sort articles" aria-label="Sort articles"
className="bg-[#111] border border-[#2a2a2a] text-[#aaa] font-body text-[11px] font-semibold uppercase tracking-wide rounded-sm pl-2.5 pr-7 py-1 appearance-none focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800]/30 transition-colors [color-scheme:dark] cursor-pointer hover:border-[#3a3a3a]" className="bg-[#111] border border-[#3a322b] text-[#aaa] font-body text-[11px] font-semibold uppercase tracking-wide rounded-sm pl-2.5 pr-7 py-1 appearance-none focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623]/30 transition-colors [color-scheme:dark] cursor-pointer hover:border-[#3a3a3a]"
> >
<option value="recent">Recent</option> <option value="recent">Recent</option>
<option value="trending">Trending</option> <option value="trending">Trending</option>
@@ -328,13 +328,13 @@ export default function ArticleFeed() {
{/* Feed mode toggle */} {/* Feed mode toggle */}
<div className="flex items-center gap-1.5" role="group" aria-label="Feed display mode"> <div className="flex items-center gap-1.5" role="group" aria-label="Feed display mode">
<span className="font-body text-[10px] text-[#555] uppercase tracking-wide hidden sm:inline">View:</span> <span className="font-body text-[10px] text-[#555] uppercase tracking-wide hidden sm:inline">View:</span>
<div className="flex items-center bg-[#111] border border-[#2a2a2a] rounded-sm p-0.5"> <div className="flex items-center bg-[#111] border border-[#3a322b] rounded-sm p-0.5">
<button <button
onClick={() => handleFeedModeChange("pagination")} onClick={() => handleFeedModeChange("pagination")}
aria-pressed={feedMode === "pagination"} aria-pressed={feedMode === "pagination"}
title="Pagination mode" title="Pagination mode"
className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
feedMode === "pagination" ?"bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/40" :"text-[#666] hover:text-[#aaa] border border-transparent" feedMode === "pagination" ?"bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
}`}> }`}>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="1" y="1" width="10" height="2.5" rx="0.5" fill="currentColor" opacity="0.9"/> <rect x="1" y="1" width="10" height="2.5" rx="0.5" fill="currentColor" opacity="0.9"/>
@@ -347,8 +347,8 @@ export default function ArticleFeed() {
onClick={() => handleFeedModeChange("infinite")} onClick={() => handleFeedModeChange("infinite")}
aria-pressed={feedMode === "infinite"} aria-pressed={feedMode === "infinite"}
title="Infinite scroll mode" title="Infinite scroll mode"
className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
feedMode === "infinite" ?"bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/40" :"text-[#666] hover:text-[#aaa] border border-transparent" feedMode === "infinite" ?"bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
}`}> }`}>
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<rect x="1" y="1" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.9"/> <rect x="1" y="1" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.9"/>
@@ -372,7 +372,7 @@ export default function ArticleFeed() {
value={dateFrom} value={dateFrom}
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }} onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
max={dateTo || undefined} max={dateTo || undefined}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#ffb800] transition-colors [color-scheme:dark]" className="bg-[#231d18] border border-[#3a322b] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#F0A623] transition-colors [color-scheme:dark]"
title="From date" title="From date"
aria-label="Filter articles from date" aria-label="Filter articles from date"
/> />
@@ -382,7 +382,7 @@ export default function ArticleFeed() {
value={dateTo} value={dateTo}
onChange={(e) => { setDateTo(e.target.value); setPage(1); }} onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
min={dateFrom || undefined} min={dateFrom || undefined}
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#ffb800] transition-colors [color-scheme:dark]" className="bg-[#231d18] border border-[#3a322b] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#F0A623] transition-colors [color-scheme:dark]"
title="To date" title="To date"
aria-label="Filter articles to date" aria-label="Filter articles to date"
/> />
@@ -401,17 +401,17 @@ export default function ArticleFeed() {
onClick={() => setShowAdvanced((v) => !v)} onClick={() => setShowAdvanced((v) => !v)}
aria-expanded={showAdvanced} aria-expanded={showAdvanced}
aria-controls={`${feedId}-advanced`} aria-controls={`${feedId}-advanced`}
className={`flex items-center gap-1.5 px-3 py-2 rounded-sm border font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] whitespace-nowrap ${ className={`flex items-center gap-1.5 px-3 py-2 rounded-sm border font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] whitespace-nowrap ${
showAdvanced || hasAdvancedActive showAdvanced || hasAdvancedActive
? "bg-[#ffb800]/15 text-[#ffb800] border-[#ffb800]/40" ? "bg-[#F0A623]/15 text-[#F0A623] border-[#F0A623]/40"
: "bg-[#111] text-[#666] border-[#2a2a2a] hover:text-[#aaa] hover:border-[#3a3a3a]" : "bg-[#111] text-[#666] border-[#3a322b] hover:text-[#aaa] hover:border-[#3a3a3a]"
}`}> }`}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true"> <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
<path d="M1 3h10M3 6h6M5 9h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/> <path d="M1 3h10M3 6h6M5 9h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
</svg> </svg>
More More
{hasAdvancedActive && ( {hasAdvancedActive && (
<span className="w-1.5 h-1.5 rounded-full bg-[#ffb800] inline-block ml-0.5" aria-label="Active filters" /> <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623] inline-block ml-0.5" aria-label="Active filters" />
)} )}
</button> </button>
</div> </div>
@@ -428,10 +428,10 @@ export default function ArticleFeed() {
type="button" type="button"
onClick={() => { setActiveCategory(cat); setPage(1); }} onClick={() => { setActiveCategory(cat); setPage(1); }}
aria-pressed={isActive} aria-pressed={isActive}
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
isActive isActive
? "bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/50" ? "bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/50"
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc] hover:border-[#3a3a3a]" : "bg-[#111] text-[#888] border border-[#3a322b] hover:text-[#ccc] hover:border-[#3a3a3a]"
}`}> }`}>
{cat} {cat}
</button> </button>
@@ -444,7 +444,7 @@ export default function ArticleFeed() {
{showAdvanced && ( {showAdvanced && (
<div <div
id={`${feedId}-advanced`} id={`${feedId}-advanced`}
className="bg-[#0d0d0d] border border-[#2a2a2a] rounded-sm p-3 space-y-3" className="bg-[#1c1815] border border-[#3a322b] rounded-sm p-3 space-y-3"
role="group" role="group"
aria-label="Advanced filters"> aria-label="Advanced filters">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -458,7 +458,7 @@ export default function ArticleFeed() {
id={`${feedId}-author`} id={`${feedId}-author`}
value={authorFilter} value={authorFilter}
onChange={(e) => { setAuthorFilter(e.target.value); setPage(1); }} onChange={(e) => { setAuthorFilter(e.target.value); setPage(1); }}
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 pr-8 appearance-none focus:outline-none focus:border-[#ffb800] focus:ring-1 focus:ring-[#ffb800]/30 transition-colors [color-scheme:dark]" className="w-full bg-[#111] border border-[#3a322b] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 pr-8 appearance-none focus:outline-none focus:border-[#F0A623] focus:ring-1 focus:ring-[#F0A623]/30 transition-colors [color-scheme:dark]"
aria-label="Filter by author"> aria-label="Filter by author">
<option value="All">All Authors</option> <option value="All">All Authors</option>
{uniqueAuthors.map((author) => ( {uniqueAuthors.map((author) => (
@@ -475,7 +475,7 @@ export default function ArticleFeed() {
{/* Source filter inside advanced panel */} {/* Source filter inside advanced panel */}
<div className="space-y-1"> <div className="space-y-1">
<span className="font-body text-[10px] text-[#555] uppercase tracking-wide block">Source</span> <span className="font-body text-[10px] text-[#555] uppercase tracking-wide block">Source</span>
<div className="flex items-center gap-1 bg-[#111] border border-[#2a2a2a] rounded-sm p-0.5 w-fit" role="group" aria-label="Filter by source"> <div className="flex items-center gap-1 bg-[#111] border border-[#3a322b] rounded-sm p-0.5 w-fit" role="group" aria-label="Filter by source">
{SOURCE_FILTERS.map((sf) => { {SOURCE_FILTERS.map((sf) => {
const isActive = sourceFilter === sf; const isActive = sourceFilter === sf;
return ( return (
@@ -483,10 +483,10 @@ export default function ArticleFeed() {
key={sf} key={sf}
onClick={() => { setSourceFilter(sf); setPage(1); }} onClick={() => { setSourceFilter(sf); setPage(1); }}
aria-pressed={isActive} aria-pressed={isActive}
className={`px-3 py-1 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`px-3 py-1 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
isActive isActive
? sf === "Imported" ?"bg-amber-500/20 text-amber-400 border border-amber-500/40" ? sf === "Imported" ?"bg-amber-500/20 text-amber-400 border border-amber-500/40"
: sf === "Live" ?"bg-emerald-500/20 text-emerald-400 border border-emerald-500/40" :"bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/40" :"text-[#666] hover:text-[#aaa] border border-transparent" : sf === "Live" ?"bg-emerald-500/20 text-emerald-400 border border-emerald-500/40" :"bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
}`}> }`}>
{sf} {sf}
{sf === "Imported" && importedCount > 0 && ( {sf === "Imported" && importedCount > 0 && (
@@ -507,7 +507,7 @@ export default function ArticleFeed() {
<button <button
type="button" type="button"
onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }} onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }}
className="font-body text-[11px] text-[#ffb800] hover:underline focus:outline-none focus-visible:underline"> className="font-body text-[11px] text-[#F0A623] hover:underline focus:outline-none focus-visible:underline">
Clear advanced filters Clear advanced filters
</button> </button>
</div> </div>
@@ -537,7 +537,7 @@ export default function ArticleFeed() {
{sortedFiltered.length === 0 && ( {sortedFiltered.length === 0 && (
<button <button
onClick={clearAllFilters} onClick={clearAllFilters}
className="ml-1 text-[#ffb800] hover:underline focus:outline-none focus-visible:underline"> className="ml-1 text-[#F0A623] hover:underline focus:outline-none focus-visible:underline">
Clear filters Clear filters
</button> </button>
)} )}
@@ -557,7 +557,7 @@ export default function ArticleFeed() {
</p> </p>
<button <button
onClick={clearAllFilters} onClick={clearAllFilters}
className="mt-4 font-body text-sm text-[#ffb800] hover:underline focus:outline-none focus-visible:underline"> className="mt-4 font-body text-sm text-[#F0A623] hover:underline focus:outline-none focus-visible:underline">
Clear filters Clear filters
</button> </button>
</div> </div>
@@ -585,7 +585,7 @@ export default function ArticleFeed() {
const promotedClass = promoted === 'premium' const promotedClass = promoted === 'premium'
? 'border-l-4 border-amber-400 bg-gradient-to-r from-amber-500/10 to-transparent pl-3 -ml-2 mr-0' ? 'border-l-4 border-amber-400 bg-gradient-to-r from-amber-500/10 to-transparent pl-3 -ml-2 mr-0'
: promoted === 'client' : promoted === 'client'
? 'border-l-4 border-[#ffb800] bg-gradient-to-r from-[#ffb800]/8 to-transparent pl-3 -ml-2 mr-0' ? 'border-l-4 border-[#F0A623] bg-gradient-to-r from-[#F0A623]/8 to-transparent pl-3 -ml-2 mr-0'
: ''; : '';
return ( return (
<Fragment key={`feed-${i}`}> <Fragment key={`feed-${i}`}>
@@ -593,7 +593,7 @@ export default function ArticleFeed() {
<Link <Link
href={articleHref} href={articleHref}
{...linkProps} {...linkProps}
className="flex-shrink-0 img-zoom overflow-hidden w-[100px] h-[70px] sm:w-[140px] sm:h-[95px] md:w-[190px] md:h-[128px] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]" className="flex-shrink-0 img-zoom overflow-hidden w-[100px] h-[70px] sm:w-[140px] sm:h-[95px] md:w-[190px] md:h-[128px] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]"
tabIndex={-1} tabIndex={-1}
aria-hidden="true" aria-hidden="true"
> >
@@ -614,21 +614,21 @@ export default function ArticleFeed() {
</span> </span>
)} )}
{promoted === 'client' && ( {promoted === 'client' && (
<span className="font-body text-[9px] font-bold text-[#7aa7d4] bg-[#ffb800]/15 border border-[#ffb800]/40 px-1.5 py-0.5 rounded uppercase tracking-widest" title={article?.promoted_advertiser ? `Advertiser coverage: ${article.promoted_advertiser}` : 'Advertiser coverage'}> <span className="font-body text-[9px] font-bold text-[#7aa7d4] bg-[#F0A623]/15 border border-[#F0A623]/40 px-1.5 py-0.5 rounded uppercase tracking-widest" title={article?.promoted_advertiser ? `Advertiser coverage: ${article.promoted_advertiser}` : 'Advertiser coverage'}>
Advertiser Advertiser
</span> </span>
)} )}
{article?.category && article.category.toLowerCase() !== "legacy" && ( {article?.category && article.category.toLowerCase() !== "legacy" && (
<span className="font-body text-[10px] font-bold text-[#ffb800] uppercase tracking-wide"> <span className="font-body text-[10px] font-bold text-[#F0A623] uppercase tracking-wide">
{article.category} {article.category}
</span> </span>
)} )}
</div> </div>
<h3 className="font-heading text-[#e0e0e0] font-bold text-[0.875rem] md:text-[1rem] leading-snug mb-1 md:mb-1.5 group-hover:text-[#ffb800] transition-colors duration-200 line-clamp-2"> <h3 className="font-heading text-[#e0e0e0] font-bold text-[0.875rem] md:text-[1rem] leading-snug mb-1 md:mb-1.5 group-hover:text-[#F0A623] transition-colors duration-200 line-clamp-2">
<Link <Link
href={articleHref} href={articleHref}
{...linkProps} {...linkProps}
className="focus:outline-none focus-visible:text-[#ffb800] focus-visible:underline"> className="focus:outline-none focus-visible:text-[#F0A623] focus-visible:underline">
{article?.title} {article?.title}
</Link> </Link>
</h3> </h3>
@@ -667,42 +667,42 @@ export default function ArticleFeed() {
const shareUrl = `https://avbeat.com${articleHref.startsWith('/') ? articleHref : `/articles/${article?.slug}`}`; const shareUrl = `https://avbeat.com${articleHref.startsWith('/') ? articleHref : `/articles/${article?.slug}`}`;
const shareTitle = article?.title || ''; const shareTitle = article?.title || '';
return ( return (
<div className="flex items-center gap-1.5 mt-2 pt-2 border-t border-[#1a1a1a]"> <div className="flex items-center gap-1.5 mt-2 pt-2 border-t border-[#231d18]">
<span className="text-[#444] text-[9px] font-body uppercase tracking-wider mr-1">Share:</span> <span className="text-[#444] text-[9px] font-body uppercase tracking-wider mr-1">Share:</span>
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`} <a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn" target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#0a66c2] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
</a> </a>
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}&via=BroadcastBeat`} <a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}&via=BroadcastBeat`}
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)" target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-white hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-white hover:bg-[#231d18] transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a> </a>
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`} <a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook" target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#1877f2] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#1877f2] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M24 12.073c0-6.627-5.373-12-12-12S0 5.446 0 12.073C0 18.062 4.388 23.027 10.125 23.927v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>
</a> </a>
<a href={`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(shareTitle)}`} <a href={`https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(shareTitle)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit" target="_blank" rel="noopener noreferrer" aria-label="Share on Reddit" title="Reddit"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#ff4500] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#ff4500] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249z"/></svg>
</a> </a>
<a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(shareTitle + ' ' + shareUrl)}`} <a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(shareTitle + ' ' + shareUrl)}`}
target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp" target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#25d366] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#25d366] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981z"/></svg> <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M.057 24l1.687-6.163a11.867 11.867 0 0 1-1.587-5.945C.16 5.335 5.495 0 12.05 0a11.817 11.817 0 0 1 8.413 3.488 11.824 11.824 0 0 1 3.48 8.414c-.003 6.557-5.338 11.892-11.893 11.892a11.9 11.9 0 0 1-5.688-1.448L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981z"/></svg>
</a> </a>
<a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`} <a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
aria-label="Share via email" title="Email" aria-label="Share via email" title="Email"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#ffb800] hover:bg-[#1a1a1a] transition-colors"> className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#F0A623] hover:bg-[#231d18] transition-colors">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" /> <rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
</svg> </svg>
@@ -735,9 +735,9 @@ export default function ArticleFeed() {
{isLoadingMore && ( {isLoadingMore && (
<div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles"> <div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles">
<div className="flex gap-1"> <div className="flex gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-[#ffb800] animate-bounce" style={{ animationDelay: "0ms" }} /> <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623] animate-bounce" style={{ animationDelay: "0ms" }} />
<span className="w-1.5 h-1.5 rounded-full bg-[#ffb800] animate-bounce" style={{ animationDelay: "150ms" }} /> <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623] animate-bounce" style={{ animationDelay: "150ms" }} />
<span className="w-1.5 h-1.5 rounded-full bg-[#ffb800] animate-bounce" style={{ animationDelay: "300ms" }} /> <span className="w-1.5 h-1.5 rounded-full bg-[#F0A623] animate-bounce" style={{ animationDelay: "300ms" }} />
</div> </div>
<span className="font-body text-xs text-[#555]">Loading more articles</span> <span className="font-body text-xs text-[#555]">Loading more articles</span>
</div> </div>
@@ -762,7 +762,7 @@ export default function ArticleFeed() {
</p> </p>
<div className="flex items-center gap-1" role="navigation" aria-label="Pagination"> <div className="flex items-center gap-1" role="navigation" aria-label="Pagination">
<button <button
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]" className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]"
disabled={page === 1} disabled={page === 1}
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }} onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
aria-label="Previous page"> aria-label="Previous page">
@@ -786,7 +786,7 @@ export default function ArticleFeed() {
) : ( ) : (
<button <button
key={p} key={p}
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${page === p ? "active" : ""}`} className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${page === p ? "active" : ""}`}
onClick={() => { setPage(p as number); window.scrollTo({ top: 0, behavior: "smooth" }); }} onClick={() => { setPage(p as number); window.scrollTo({ top: 0, behavior: "smooth" }); }}
aria-label={`Page ${p}`} aria-label={`Page ${p}`}
aria-current={page === p ? "page" : undefined}> aria-current={page === p ? "page" : undefined}>
@@ -796,7 +796,7 @@ export default function ArticleFeed() {
); );
})()} })()}
<button <button
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]" className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]"
disabled={page === totalPages} disabled={page === totalPages}
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }} onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
aria-label="Next page"> aria-label="Next page">

View File

@@ -157,7 +157,7 @@ export default function FeaturedBento() {
{/* Section header */} {/* Section header */}
<div className="flex items-center gap-3 mb-4 md:mb-5"> <div className="flex items-center gap-3 mb-4 md:mb-5">
<span className="section-label">Featured</span> <span className="section-label">Featured</span>
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
{/* Navigation arrows */} {/* Navigation arrows */}
<div <div
className="flex items-center gap-1.5" className="flex items-center gap-1.5"
@@ -173,7 +173,7 @@ export default function FeaturedBento() {
type="button" type="button"
onClick={() => { goPrev(); setPaused(true); }} onClick={() => { goPrev(); setPaused(true); }}
aria-label="Previous stories" aria-label="Previous stories"
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#ffb800] hover:border-[#ffb800] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ffb800]"> className="w-8 h-8 flex items-center justify-center bg-[#231d18] border border-[#3a3a3a] hover:bg-[#F0A623] hover:border-[#F0A623] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#F0A623]">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<polyline points="15 18 9 12 15 6" /> <polyline points="15 18 9 12 15 6" />
</svg> </svg>
@@ -182,7 +182,7 @@ export default function FeaturedBento() {
type="button" type="button"
onClick={() => { goNext(); setPaused(true); }} onClick={() => { goNext(); setPaused(true); }}
aria-label="Next stories" aria-label="Next stories"
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#ffb800] hover:border-[#ffb800] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#ffb800]"> className="w-8 h-8 flex items-center justify-center bg-[#231d18] border border-[#3a3a3a] hover:bg-[#F0A623] hover:border-[#F0A623] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#F0A623]">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
<polyline points="9 18 15 12 9 6" /> <polyline points="9 18 15 12 9 6" />
</svg> </svg>
@@ -210,7 +210,7 @@ export default function FeaturedBento() {
/> />
<div className="hero-overlay absolute inset-0" /> <div className="hero-overlay absolute inset-0" />
<div className="absolute inset-0 flex flex-col justify-end p-3 sm:p-4 md:p-5"> <div className="absolute inset-0 flex flex-col justify-end p-3 sm:p-4 md:p-5">
<span className="inline-block bg-[#ffb800] text-white font-body text-[10px] font-bold px-2 py-0.5 tracking-wider uppercase mb-2 w-fit"> <span className="inline-block bg-[#F0A623] text-white font-body text-[10px] font-bold px-2 py-0.5 tracking-wider uppercase mb-2 w-fit">
{hero?.category} {hero?.category}
</span> </span>
<h2 className="font-heading text-white text-lg sm:text-xl md:text-2xl font-bold leading-tight mb-2 max-w-xl"> <h2 className="font-heading text-white text-lg sm:text-xl md:text-2xl font-bold leading-tight mb-2 max-w-xl">
@@ -236,7 +236,7 @@ export default function FeaturedBento() {
<Link <Link
key={`${article?.slug}-${offset}-${i}`} key={`${article?.slug}-${offset}-${i}`}
href={`/articles/${article?.slug}`} href={`/articles/${article?.slug}`}
className="bg-[#1a1a1a] border border-[#2a2a2a] overflow-hidden group cursor-pointer card-accent-hover flex flex-col hover:border-[#ffb800] transition-colors"> className="bg-[#231d18] border border-[#3a322b] overflow-hidden group cursor-pointer card-accent-hover flex flex-col hover:border-[#F0A623] transition-colors">
<div className="relative overflow-hidden img-zoom h-[90px] sm:h-[110px] md:h-[120px]"> <div className="relative overflow-hidden img-zoom h-[90px] sm:h-[110px] md:h-[120px]">
<AppImage <AppImage
src={article?.image} src={article?.image}
@@ -246,13 +246,13 @@ export default function FeaturedBento() {
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 50vw, 22vw" sizes="(max-width: 640px) 50vw, (max-width: 1024px) 50vw, 22vw"
/> />
<div className="absolute top-0 left-0"> <div className="absolute top-0 left-0">
<span className="bg-[#ffb800] text-white font-body text-[9px] font-bold px-1.5 py-0.5 tracking-wider uppercase"> <span className="bg-[#F0A623] text-white font-body text-[9px] font-bold px-1.5 py-0.5 tracking-wider uppercase">
{article?.category} {article?.category}
</span> </span>
</div> </div>
</div> </div>
<div className="p-2 md:p-2.5 flex-1 flex flex-col justify-between"> <div className="p-2 md:p-2.5 flex-1 flex flex-col justify-between">
<h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-3 group-hover:text-[#ffb800] transition-colors duration-200 mb-1.5 md:mb-2"> <h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-3 group-hover:text-[#F0A623] transition-colors duration-200 mb-1.5 md:mb-2">
{article?.title} {article?.title}
</h3> </h3>
<span className="read-more inline-block text-[11px]">Read More »</span> <span className="read-more inline-block text-[11px]">Read More »</span>
@@ -270,7 +270,7 @@ export default function FeaturedBento() {
aria-label={`Go to position ${i + 1}`} aria-label={`Go to position ${i + 1}`}
className={`rounded-full transition-all focus:outline-none ${ className={`rounded-full transition-all focus:outline-none ${
i === offset i === offset
? "w-4 h-1.5 bg-[#ffb800]" ? "w-4 h-1.5 bg-[#F0A623]"
: "w-1.5 h-1.5 bg-[#333] hover:bg-[#555]" : "w-1.5 h-1.5 bg-[#333] hover:bg-[#555]"
}`} }`}
/> />

View File

@@ -17,7 +17,7 @@ export default function NewsTicker() {
const doubled = [...headlines, ...headlines]; const doubled = [...headlines, ...headlines];
return ( return (
<div className="bg-[#d60701] overflow-hidden border-b border-[#a30501]"> <div className="bg-[#D85A30] overflow-hidden border-b border-[#E24B4A]">
<div className="flex items-stretch h-8"> <div className="flex items-stretch h-8">
{/* Label */} {/* Label */}
<div className="flex-shrink-0 bg-[#080808] flex items-center px-4 z-10"> <div className="flex-shrink-0 bg-[#080808] flex items-center px-4 z-10">

View File

@@ -60,13 +60,13 @@ export default function NewsletterSignup() {
return ( return (
<section className="max-w-container mx-auto px-4" aria-labelledby={`${formId}-heading`}> <section className="max-w-container mx-auto px-4" aria-labelledby={`${formId}-heading`}>
<div className="bg-[#0d1520] border border-[#4a3500] py-8 md:py-10"> <div className="bg-[#0d1520] border border-[#412402] py-8 md:py-10">
<div className="max-w-2xl mx-auto text-center px-4"> <div className="max-w-2xl mx-auto text-center px-4">
{/* Header */} {/* Header */}
<div className="flex items-center justify-center gap-3 mb-3"> <div className="flex items-center justify-center gap-3 mb-3">
<div className="flex-1 h-px bg-[#4a3500] max-w-[80px]" /> <div className="flex-1 h-px bg-[#412402] max-w-[80px]" />
<span className="section-label" id={`${formId}-heading`}>Newsletter</span> <span className="section-label" id={`${formId}-heading`}>Newsletter</span>
<div className="flex-1 h-px bg-[#4a3500] max-w-[80px]" /> <div className="flex-1 h-px bg-[#412402] max-w-[80px]" />
</div> </div>
<p className="font-heading text-[#e0e0e0] text-xl md:text-2xl font-bold mb-1.5"> <p className="font-heading text-[#e0e0e0] text-xl md:text-2xl font-bold mb-1.5">
Stay Ahead of the Pro AV Industry Stay Ahead of the Pro AV Industry
@@ -77,12 +77,12 @@ export default function NewsletterSignup() {
{submitted ? ( {submitted ? (
<div <div
className="bg-[#1a1208] border border-[#ffb800] rounded-sm px-6 py-8 text-center" className="bg-[#231d18] border border-[#F0A623] rounded-sm px-6 py-8 text-center"
role="status" role="status"
aria-live="polite"> aria-live="polite">
<div className="w-10 h-10 rounded-full bg-[#ffb800]/20 flex items-center justify-center mx-auto mb-3"> <div className="w-10 h-10 rounded-full bg-[#F0A623]/20 flex items-center justify-center mx-auto mb-3">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true"> <svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M4 10l4 4 8-8" stroke="#ffb800" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> <path d="M4 10l4 4 8-8" stroke="#F0A623" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg> </svg>
</div> </div>
<p className="font-heading text-[#e0e0e0] font-bold text-lg mb-1">You&apos;re subscribed!</p> <p className="font-heading text-[#e0e0e0] font-bold text-lg mb-1">You&apos;re subscribed!</p>
@@ -111,7 +111,7 @@ export default function NewsletterSignup() {
className="sr-only" className="sr-only"
aria-label={`Subscribe to ${cat} news`} aria-label={`Subscribe to ${cat} news`}
/> />
<span aria-hidden="true" className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#ffb800] border-[#ffb800]" : "border-[#444]"}`}> <span aria-hidden="true" className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#F0A623] border-[#F0A623]" : "border-[#444]"}`}>
{checked && ( {checked && (
<svg viewBox="0 0 12 12" fill="none" className="w-full h-full" aria-hidden="true"> <svg viewBox="0 0 12 12" fill="none" className="w-full h-full" aria-hidden="true">
<path d="M2 6l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> <path d="M2 6l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
@@ -147,7 +147,7 @@ export default function NewsletterSignup() {
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className={`btn-subscribe py-2.5 px-6 text-sm whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1520] ${loading ? "opacity-70 cursor-not-allowed" : ""}`}> className={`btn-subscribe py-2.5 px-6 text-sm whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1520] ${loading ? "opacity-70 cursor-not-allowed" : ""}`}>
{loading ? "Subscribing..." : "Subscribe Free"} {loading ? "Subscribing..." : "Subscribe Free"}
</button> </button>
</div> </div>
@@ -164,7 +164,7 @@ export default function NewsletterSignup() {
<p className="font-body text-[#555] text-xs mt-3"> <p className="font-body text-[#555] text-xs mt-3">
No spam. Unsubscribe anytime. By subscribing you agree to our{" "} No spam. Unsubscribe anytime. By subscribing you agree to our{" "}
<a href="/privacy" className="text-[#ffb800] hover:underline focus:outline-none focus-visible:underline"> <a href="/privacy" className="text-[#F0A623] hover:underline focus:outline-none focus-visible:underline">
Privacy Policy Privacy Policy
</a>. </a>.
</p> </p>

View File

@@ -141,7 +141,7 @@ export default function SpotlightCarousel() {
return ( return (
<section <section
suppressHydrationWarning suppressHydrationWarning
className="bg-[#0d0d0d] py-6 md:py-8 border-t border-b border-[#222]" className="bg-[#1c1815] py-6 md:py-8 border-t border-b border-[#222]"
aria-label="Spotlight carousel" aria-label="Spotlight carousel"
onKeyDown={handleCarouselKeyDown}> onKeyDown={handleCarouselKeyDown}>
<div suppressHydrationWarning className="max-w-container mx-auto px-4"> <div suppressHydrationWarning className="max-w-container mx-auto px-4">
@@ -156,14 +156,14 @@ export default function SpotlightCarousel() {
ref={prevBtnRef} ref={prevBtnRef}
onClick={() => go(-1)} onClick={() => go(-1)}
aria-label="Previous spotlight (Left arrow key)" aria-label="Previous spotlight (Left arrow key)"
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#ffb800] hover:text-[#ffb800] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]"> className="carousel-btn w-7 h-7 border border-[#333] bg-[#231d18] hover:border-[#F0A623] hover:text-[#F0A623] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
<ChevronLeftIcon size={13} /> <ChevronLeftIcon size={13} />
</button> </button>
<button <button
ref={nextBtnRef} ref={nextBtnRef}
onClick={() => go(1)} onClick={() => go(1)}
aria-label="Next spotlight (Right arrow key)" aria-label="Next spotlight (Right arrow key)"
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#ffb800] hover:text-[#ffb800] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800]"> className="carousel-btn w-7 h-7 border border-[#333] bg-[#231d18] hover:border-[#F0A623] hover:text-[#F0A623] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623]">
<ChevronRightIcon size={13} /> <ChevronRightIcon size={13} />
</button> </button>
</div> </div>
@@ -181,7 +181,7 @@ export default function SpotlightCarousel() {
key={`${startIndex}-${i}`} key={`${startIndex}-${i}`}
href={item.href} href={item.href}
role="listitem" role="listitem"
className={`spotlight-card group block spotlight-slide-in focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`spotlight-card group block spotlight-slide-in focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
i >= 3 ? "hidden lg:block" : i >= 2 ? "hidden md:block" : "" i >= 3 ? "hidden lg:block" : i >= 2 ? "hidden md:block" : ""
}`} }`}
style={{ animationDelay: `${i * 50}ms` }} style={{ animationDelay: `${i * 50}ms` }}
@@ -200,11 +200,11 @@ export default function SpotlightCarousel() {
</span> </span>
</div> </div>
</div> </div>
<div className="p-2.5 md:p-3 border-t border-[#2a2a2a]"> <div className="p-2.5 md:p-3 border-t border-[#3a322b]">
<p className="font-body text-[11px] font-bold text-[#ffb800] uppercase tracking-wide mb-1"> <p className="font-body text-[11px] font-bold text-[#F0A623] uppercase tracking-wide mb-1">
{item.name} {item.name}
</p> </p>
<h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-2 group-hover:text-[#ffb800] transition-colors duration-200"> <h3 className="font-heading text-[#e0e0e0] text-[12px] md:text-[13px] font-bold leading-snug line-clamp-2 group-hover:text-[#F0A623] transition-colors duration-200">
{item.title} {item.title}
</h3> </h3>
</div> </div>
@@ -221,7 +221,7 @@ export default function SpotlightCarousel() {
role="tab" role="tab"
aria-selected={Math.floor(startIndex / VISIBLE) === i} aria-selected={Math.floor(startIndex / VISIBLE) === i}
aria-label={`Go to spotlight page ${i + 1}`} aria-label={`Go to spotlight page ${i + 1}`}
className={`h-1.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb800] ${ className={`h-1.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#F0A623] ${
Math.floor(startIndex / VISIBLE) === i Math.floor(startIndex / VISIBLE) === i
? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]" ? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]"
}`} }`}

View File

@@ -54,7 +54,7 @@ function BentoSkeleton() {
<section className="max-w-container mx-auto px-4 py-4 md:py-6"> <section className="max-w-container mx-auto px-4 py-4 md:py-6">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="skeleton h-4 w-20 rounded-sm" /> <div className="skeleton h-4 w-20 rounded-sm" />
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-3 md:gap-4"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-3 md:gap-4">
<div className="lg:col-span-7 skeleton rounded-sm h-[260px] sm:h-[320px] md:h-[380px]" /> <div className="lg:col-span-7 skeleton rounded-sm h-[260px] sm:h-[320px] md:h-[380px]" />
@@ -81,7 +81,7 @@ function CarouselSkeleton() {
<div className="max-w-container mx-auto"> <div className="max-w-container mx-auto">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="skeleton h-4 w-24 rounded-sm" /> <div className="skeleton h-4 w-24 rounded-sm" />
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{[...Array(4)]?.map((_, i) => ( {[...Array(4)]?.map((_, i) => (
@@ -107,7 +107,7 @@ function FeedSkeleton() {
<div className="lg:col-span-8 space-y-1"> <div className="lg:col-span-8 space-y-1">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="skeleton h-4 w-20 rounded-sm" /> <div className="skeleton h-4 w-20 rounded-sm" />
<div className="flex-1 h-px bg-[#2a2a2a]" /> <div className="flex-1 h-px bg-[#3a322b]" />
</div> </div>
{[...Array(5)]?.map((_, i) => ( {[...Array(5)]?.map((_, i) => (
<div key={i} className="flex gap-4 py-4 border-b border-[#222]"> <div key={i} className="flex gap-4 py-4 border-b border-[#222]">

View File

@@ -144,7 +144,7 @@ export default function RootLayout({
{/* Skip to main content — WCAG 2.4.1 */} {/* Skip to main content — WCAG 2.4.1 */}
<a <a
href="#main-content" href="#main-content"
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[10000] focus:px-4 focus:py-2 focus:bg-[#ffb800] focus:text-white focus:rounded focus:font-bold focus:text-sm focus:outline-none" className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[10000] focus:px-4 focus:py-2 focus:bg-[#F0A623] focus:text-white focus:rounded focus:font-bold focus:text-sm focus:outline-none"
> >
Skip to main content Skip to main content
</a> </a>

View File

@@ -50,7 +50,7 @@ function LoginInner() {
return ( return (
<> <>
<Header /> <Header />
<main className="min-h-screen bg-[#111111]"> <main className="min-h-screen bg-[#1c1815]">
<div className="max-w-md mx-auto px-4 py-12"> <div className="max-w-md mx-auto px-4 py-12">
<div className="text-center mb-6"> <div className="text-center mb-6">
<h1 className="text-2xl font-heading font-bold text-white">Sign in to AV Beat</h1> <h1 className="text-2xl font-heading font-bold text-white">Sign in to AV Beat</h1>
@@ -60,10 +60,10 @@ function LoginInner() {
</div> </div>
<form <form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4" className="bg-[#231d18] border border-[#3a322b] rounded-lg p-6 space-y-4"
> >
{error && ( {error && (
<div className="bg-[#2a0a0a] border border-[#d60701] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded"> <div className="bg-[#2a0a0a] border border-[#D85A30] text-[#ff8a8a] font-body text-sm px-3 py-2 rounded">
{error} {error}
</div> </div>
)} )}
@@ -77,7 +77,7 @@ function LoginInner() {
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
autoComplete="email" autoComplete="email"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<div> <div>
@@ -90,19 +90,19 @@ function LoginInner() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password" autoComplete="current-password"
required required
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#ffb800]" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#F0A623]"
/> />
</div> </div>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="w-full bg-[#ffb800] hover:bg-[#d99700] disabled:opacity-50 text-white font-body font-semibold py-2 rounded-lg transition-colors" className="w-full bg-[#F0A623] hover:bg-[#BA7517] disabled:opacity-50 text-white font-body font-semibold py-2 rounded-lg transition-colors"
> >
{loading ? 'Signing in…' : 'Sign in'} {loading ? 'Signing in…' : 'Sign in'}
</button> </button>
<p className="text-xs text-[#666] font-body text-center pt-2"> <p className="text-xs text-[#666] font-body text-center pt-2">
PR firm / contributor?{' '} PR firm / contributor?{' '}
<Link href="/client-login" className="text-[#ffb800] hover:underline"> <Link href="/client-login" className="text-[#F0A623] hover:underline">
Use the contributor portal Use the contributor portal
</Link> </Link>
</p> </p>

View File

@@ -77,14 +77,14 @@ export default function ManufacturerDirectoryClient({
return ( return (
<div> <div>
<div className="bg-[#111] border border-[#252525] rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4"> <div className="bg-[#111] border border-[#3a322b] rounded-xl p-4 mb-6 flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<input <input
type="search" type="search"
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
placeholder="Search by company or product…" placeholder="Search by company or product…"
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#ffb800]/40" className="w-full bg-[#1c1815] border border-[#3a322b] rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-[#F0A623]/40"
/> />
</div> </div>
</div> </div>
@@ -97,7 +97,7 @@ export default function ManufacturerDirectoryClient({
type="button" type="button"
onClick={() => setShowFilter("all")} onClick={() => setShowFilter("all")}
className={`px-3 py-1.5 rounded text-xs font-semibold ${ className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === "all" ? "bg-[#ffb800] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]" showFilter === "all" ? "bg-[#F0A623] text-white" : "bg-[#1c1815] border border-[#3a322b] text-[#aaa] hover:bg-[#231d18]"
}`} }`}
> >
All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span> All shows <span className="ml-1 opacity-60">({manufacturers.length.toLocaleString()})</span>
@@ -106,7 +106,7 @@ export default function ManufacturerDirectoryClient({
type="button" type="button"
onClick={() => setShowFilter("both-major")} onClick={() => setShowFilter("both-major")}
className={`px-3 py-1.5 rounded text-xs font-semibold ${ className={`px-3 py-1.5 rounded text-xs font-semibold ${
showFilter === "both-major" ? "bg-[#ffb800] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]" showFilter === "both-major" ? "bg-[#F0A623] text-white" : "bg-[#1c1815] border border-[#3a322b] text-[#aaa] hover:bg-[#231d18]"
}`} }`}
> >
NAB + IBC <span className="ml-1 opacity-60">({manufacturers.filter((m) => m.exhibits_nab && m.exhibits_ibc).length})</span> NAB + IBC <span className="ml-1 opacity-60">({manufacturers.filter((m) => m.exhibits_nab && m.exhibits_ibc).length})</span>
@@ -121,7 +121,7 @@ export default function ManufacturerDirectoryClient({
type="button" type="button"
onClick={() => setShowFilter(s.slug)} onClick={() => setShowFilter(s.slug)}
className={`px-3 py-1.5 rounded text-xs font-semibold ${ className={`px-3 py-1.5 rounded text-xs font-semibold ${
active ? "bg-[#ffb800] text-white" : "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]" active ? "bg-[#F0A623] text-white" : "bg-[#1c1815] border border-[#3a322b] text-[#aaa] hover:bg-[#231d18]"
}`} }`}
title={`${s.name} · ${s.start_date}${count === 0 ? " · exhibitor list coming soon" : ""}`} title={`${s.name} · ${s.start_date}${count === 0 ? " · exhibitor list coming soon" : ""}`}
> >
@@ -144,8 +144,8 @@ export default function ManufacturerDirectoryClient({
onClick={() => setLetter(l)} onClick={() => setLetter(l)}
className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${ className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${
letter === l letter === l
? "bg-[#ffb800] text-white" ? "bg-[#F0A623] text-white"
: "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]" : "bg-[#1c1815] border border-[#3a322b] text-[#aaa] hover:bg-[#231d18]"
}`} }`}
> >
{l} {l}
@@ -162,9 +162,9 @@ export default function ManufacturerDirectoryClient({
<Link <Link
key={m.slug} key={m.slug}
href={`/manufacturers/${m.slug}`} href={`/manufacturers/${m.slug}`}
className="group bg-[#0d0d0d] border border-[#252525] rounded-xl p-4 hover:border-[#ffb800] hover:shadow-sm transition-all flex gap-3" className="group bg-[#1c1815] border border-[#3a322b] rounded-xl p-4 hover:border-[#F0A623] hover:shadow-sm transition-all flex gap-3"
> >
<div className="w-14 h-14 rounded-lg bg-[#1a1a1a] flex items-center justify-center overflow-hidden flex-shrink-0"> <div className="w-14 h-14 rounded-lg bg-[#231d18] flex items-center justify-center overflow-hidden flex-shrink-0">
{m.logo_url ? ( {m.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
@@ -180,7 +180,7 @@ export default function ManufacturerDirectoryClient({
)} )}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="font-semibold text-[#e0e0e0] truncate group-hover:text-[#ffb800] transition-colors"> <h3 className="font-semibold text-[#e0e0e0] truncate group-hover:text-[#F0A623] transition-colors">
{m.company_name} {m.company_name}
</h3> </h3>
<div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap"> <div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap">
@@ -195,7 +195,7 @@ export default function ManufacturerDirectoryClient({
</span> </span>
)} )}
{m.featured && ( {m.featured && (
<span className="text-[10px] font-semibold uppercase tracking-wider bg-[#ffb800]/15 text-[#ffb800] px-1.5 py-0.5 rounded"> <span className="text-[10px] font-semibold uppercase tracking-wider bg-[#F0A623]/15 text-[#F0A623] px-1.5 py-0.5 rounded">
Featured Featured
</span> </span>
)} )}

View File

@@ -29,7 +29,7 @@ export default function BackLink() {
} }
} }
return ( return (
<Link href="/manufacturers" onClick={onClick} className="hover:text-[#ffb800]"> <Link href="/manufacturers" onClick={onClick} className="hover:text-[#F0A623]">
Back Back
</Link> </Link>
); );

View File

@@ -159,7 +159,7 @@ export default async function ManufacturerProfile({
}; };
return ( return (
<div className="min-h-screen bg-[#0d0d0d]"> <div className="min-h-screen bg-[#1c1815]">
<Header /> <Header />
<script <script
type="application/ld+json" type="application/ld+json"
@@ -170,8 +170,8 @@ export default async function ManufacturerProfile({
<BackLink /> <BackLink />
</nav> </nav>
<header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-[#252525]"> <header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-[#3a322b]">
<div className="w-24 h-24 rounded-xl bg-[#1a1a1a] flex items-center justify-center overflow-hidden flex-shrink-0"> <div className="w-24 h-24 rounded-xl bg-[#231d18] flex items-center justify-center overflow-hidden flex-shrink-0">
{company.logo_url ? ( {company.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
@@ -206,7 +206,7 @@ export default async function ManufacturerProfile({
</span> </span>
)} )}
{company.phone && ( {company.phone && (
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#ffb800]"> <a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#F0A623]">
{company.phone} {company.phone}
</a> </a>
)} )}
@@ -217,7 +217,7 @@ export default async function ManufacturerProfile({
href={company.company_website} href={company.company_website}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
Website Website
</a> </a>
@@ -227,7 +227,7 @@ export default async function ManufacturerProfile({
href={company.press_url} href={company.press_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
Newsroom Newsroom
</a> </a>
@@ -237,7 +237,7 @@ export default async function ManufacturerProfile({
href={company.contact_url} href={company.contact_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
Contact Contact
</a> </a>
@@ -247,7 +247,7 @@ export default async function ManufacturerProfile({
href={company.linkedin_url} href={company.linkedin_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#888] hover:text-[#ffb800]" className="text-[#888] hover:text-[#F0A623]"
> >
LinkedIn LinkedIn
</a> </a>
@@ -257,7 +257,7 @@ export default async function ManufacturerProfile({
href={`https://twitter.com/${company.twitter_handle}`} href={`https://twitter.com/${company.twitter_handle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[#888] hover:text-[#ffb800]" className="text-[#888] hover:text-[#F0A623]"
> >
X / Twitter X / Twitter
</a> </a>
@@ -283,17 +283,17 @@ export default async function ManufacturerProfile({
<h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-4">Executive leadership</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0] mb-4">Executive leadership</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{executives.map((e: any) => ( {executives.map((e: any) => (
<div key={e.id} className="bg-[#111] border border-[#252525] rounded-lg p-4 flex gap-3"> <div key={e.id} className="bg-[#111] border border-[#3a322b] rounded-lg p-4 flex gap-3">
{e.photo_url ? ( {e.photo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img <img
src={e.photo_url} src={e.photo_url}
alt={e.name} alt={e.name}
className="w-16 h-16 rounded-full object-cover bg-[#1a1a1a] flex-shrink-0" className="w-16 h-16 rounded-full object-cover bg-[#231d18] flex-shrink-0"
loading="lazy" loading="lazy"
/> />
) : ( ) : (
<div className="w-16 h-16 rounded-full bg-[#1a1a1a] flex items-center justify-center text-[#666] font-bold flex-shrink-0"> <div className="w-16 h-16 rounded-full bg-[#231d18] flex items-center justify-center text-[#666] font-bold flex-shrink-0">
{(e.name || "?").trim().charAt(0).toUpperCase()} {(e.name || "?").trim().charAt(0).toUpperCase()}
</div> </div>
)} )}
@@ -306,7 +306,7 @@ export default async function ManufacturerProfile({
href={e.linkedin_url} href={e.linkedin_url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="text-[11px] text-[#ffb800] hover:underline" className="text-[11px] text-[#F0A623] hover:underline"
> >
LinkedIn LinkedIn
</a> </a>
@@ -328,14 +328,14 @@ export default async function ManufacturerProfile({
)} )}
{company.phone && ( {company.phone && (
<div className="mt-1"> <div className="mt-1">
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#ffb800] hover:underline"> <a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#F0A623] hover:underline">
{company.phone} {company.phone}
</a> </a>
</div> </div>
)} )}
{company.contact_email && ( {company.contact_email && (
<div> <div>
<a href={`mailto:${company.contact_email}`} className="text-[#ffb800] hover:underline"> <a href={`mailto:${company.contact_email}`} className="text-[#F0A623] hover:underline">
{company.contact_email} {company.contact_email}
</a> </a>
</div> </div>
@@ -356,7 +356,7 @@ export default async function ManufacturerProfile({
{cleanCategories.map((c: string) => ( {cleanCategories.map((c: string) => (
<span <span
key={c} key={c}
className="text-xs bg-[#1a1a1a] text-[#aaa] px-2.5 py-1 rounded-full" className="text-xs bg-[#231d18] text-[#aaa] px-2.5 py-1 rounded-full"
> >
{c} {c}
</span> </span>
@@ -382,7 +382,7 @@ export default async function ManufacturerProfile({
return ( return (
<li <li
key={`${s.show}-${s.show_year}-${s.booth || "?"}`} key={`${s.show}-${s.show_year}-${s.booth || "?"}`}
className="bg-[#111] border border-[#252525] rounded-md px-4 py-3 flex items-center justify-between gap-4" className="bg-[#111] border border-[#3a322b] rounded-md px-4 py-3 flex items-center justify-between gap-4"
> >
<div className="min-w-0"> <div className="min-w-0">
<span className="font-semibold text-[#e0e0e0]">{label}</span> <span className="font-semibold text-[#e0e0e0]">{label}</span>
@@ -401,7 +401,7 @@ export default async function ManufacturerProfile({
<section className="mb-10"> <section className="mb-10">
<header className="flex items-baseline justify-between mb-3"> <header className="flex items-baseline justify-between mb-3">
<h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2> <h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2>
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#ffb800] hover:underline"> <Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#F0A623] hover:underline">
All stories All stories
</Link> </Link>
</header> </header>
@@ -410,7 +410,7 @@ export default async function ManufacturerProfile({
<Link <Link
key={r.wp_slug} key={r.wp_slug}
href={`/news/${r.wp_slug}`} href={`/news/${r.wp_slug}`}
className="group bg-[#0d0d0d] border border-[#252525] rounded-md p-3 hover:border-[#ffb800] hover:shadow-sm transition-all flex gap-3" className="group bg-[#1c1815] border border-[#3a322b] rounded-md p-3 hover:border-[#F0A623] hover:shadow-sm transition-all flex gap-3"
> >
{/* Image fallback cascade — Ryan rule: never show an empty {/* Image fallback cascade — Ryan rule: never show an empty
gray box on a coverage card. Falls through: gray box on a coverage card. Falls through:
@@ -419,11 +419,11 @@ export default async function ManufacturerProfile({
<img <img
src={r.featured_image || company.logo_url || '/assets/images/article-placeholder.svg'} src={r.featured_image || company.logo_url || '/assets/images/article-placeholder.svg'}
alt="" alt=""
className="w-20 h-14 rounded object-cover bg-[#1a1a1a] flex-shrink-0" className="w-20 h-14 rounded object-cover bg-[#231d18] flex-shrink-0"
loading="lazy" loading="lazy"
/> />
<div className="min-w-0"> <div className="min-w-0">
<h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#ffb800] line-clamp-3"> <h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#F0A623] line-clamp-3">
{r.title} {r.title}
</h3> </h3>
{r.wp_published_at && ( {r.wp_published_at && (
@@ -447,7 +447,7 @@ export default async function ManufacturerProfile({
{p.slug ? ( {p.slug ? (
<Link <Link
href={`/news/${p.slug}`} href={`/news/${p.slug}`}
className="text-[#ffb800] hover:underline" className="text-[#F0A623] hover:underline"
> >
{p.title} {p.title}
</Link> </Link>
@@ -465,7 +465,7 @@ export default async function ManufacturerProfile({
</section> </section>
)} )}
<footer className="text-xs text-[#666] mt-12 pt-6 border-t border-[#252525]"> <footer className="text-xs text-[#666] mt-12 pt-6 border-t border-[#3a322b]">
Listing maintained by AV Beat editorial. Listing maintained by AV Beat editorial.
{company.last_mentioned && ( {company.last_mentioned && (
<> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</> <> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</>

View File

@@ -72,7 +72,7 @@ export default async function ManufacturersIndex() {
const ibcCount = list.filter((m) => m.exhibits_ibc).length; const ibcCount = list.filter((m) => m.exhibits_ibc).length;
return ( return (
<div className="min-h-screen bg-[#0d0d0d]"> <div className="min-h-screen bg-[#1c1815]">
<Header /> <Header />
<div className="max-w-6xl mx-auto px-4 py-10"> <div className="max-w-6xl mx-auto px-4 py-10">
<header className="mb-8"> <header className="mb-8">
@@ -94,7 +94,7 @@ export default async function ManufacturersIndex() {
<ManufacturerDirectoryClient manufacturers={list} shows={events} /> <ManufacturerDirectoryClient manufacturers={list} shows={events} />
<section className="mt-16 border-t border-[#252525] pt-8 text-sm text-[#888] leading-relaxed"> <section className="mt-16 border-t border-[#3a322b] pt-8 text-sm text-[#888] leading-relaxed">
<p> <p>
Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list, Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list,
manufacturer-direct press releases, and reporting tracked on AV Beat. manufacturer-direct press releases, and reporting tracked on AV Beat.
@@ -102,7 +102,7 @@ export default async function ManufacturersIndex() {
</p> </p>
<p className="mt-2"> <p className="mt-2">
Are you a manufacturer and want to update your listing?{" "} Are you a manufacturer and want to update your listing?{" "}
<a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#ffb800] hover:underline"> <a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#F0A623] hover:underline">
Contact our editors Contact our editors
</a> </a>
. .

View File

@@ -8,7 +8,7 @@ export const metadata = {
export default function BillingPage() { export default function BillingPage() {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-3xl mx-auto px-4 py-24"> <div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
@@ -84,7 +84,7 @@ export default function BillingPage() {
className={`rounded-xl p-6 border relative ${ className={`rounded-xl p-6 border relative ${
plan?.highlight plan?.highlight
? 'bg-[#CC0000]/10 border-[#CC0000]' ? 'bg-[#CC0000]/10 border-[#CC0000]'
: 'bg-[#1a1a1a] border-white/10' : 'bg-[#231d18] border-white/10'
}`} }`}
> >
{plan?.highlight && ( {plan?.highlight && (
@@ -120,7 +120,7 @@ export default function BillingPage() {
</div> </div>
{/* Job post pricing */} {/* Job post pricing */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6 mb-6"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-6 mb-6">
<h3 className="font-bold text-lg mb-4">One-Time Listings (Coming Soon)</h3> <h3 className="font-bold text-lg mb-4">One-Time Listings (Coming Soon)</h3>
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-sm">
<div className="flex justify-between py-2 border-b border-white/5"> <div className="flex justify-between py-2 border-b border-white/5">

View File

@@ -173,14 +173,14 @@ export default function AccountPage() {
if (loading || profileLoading) { if (loading || profileLoading) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
} }
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-4xl mx-auto px-4 py-24"> <div className="max-w-4xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-6"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-6">
@@ -195,7 +195,7 @@ export default function AccountPage() {
{/* No profile yet */} {/* No profile yet */}
{!profile && ( {!profile && (
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-8 text-center"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-8 text-center">
<div className="text-4xl mb-4">👤</div> <div className="text-4xl mb-4">👤</div>
<h2 className="text-xl font-bold mb-3">No Profile Yet</h2> <h2 className="text-xl font-bold mb-3">No Profile Yet</h2>
<p className="text-gray-400 mb-6"> <p className="text-gray-400 mb-6">
@@ -213,7 +213,7 @@ export default function AccountPage() {
{/* Profile status card */} {/* Profile status card */}
{profile && ( {profile && (
<div className="grid md:grid-cols-3 gap-6 mb-8"> <div className="grid md:grid-cols-3 gap-6 mb-8">
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Status</div> <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Status</div>
<div <div
className={`text-lg font-bold capitalize ${ className={`text-lg font-bold capitalize ${
@@ -234,7 +234,7 @@ export default function AccountPage() {
)} )}
</div> </div>
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Plan</div> <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Plan</div>
<div className="text-lg font-bold capitalize text-white"> <div className="text-lg font-bold capitalize text-white">
{profile.subscription_tier === 'featured' ?'⭐ Featured' {profile.subscription_tier === 'featured' ?'⭐ Featured'
@@ -248,7 +248,7 @@ export default function AccountPage() {
</Link> </Link>
</div> </div>
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-6"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-6">
<div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Type</div> <div className="text-xs text-gray-400 uppercase tracking-wide mb-1">Profile Type</div>
<div className="text-lg font-bold capitalize text-white"> <div className="text-lg font-bold capitalize text-white">
{profile.profile_type === 'crew' ? '🎬 Crew Member' : '🏢 Vendor / Company'} {profile.profile_type === 'crew' ? '🎬 Crew Member' : '🏢 Vendor / Company'}
@@ -272,7 +272,7 @@ export default function AccountPage() {
{/* Subscription gate notice */} {/* Subscription gate notice */}
{!hasActiveSubscription && ( {!hasActiveSubscription && (
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-xl p-6"> <div className="bg-[#231d18] border border-yellow-500/30 rounded-xl p-6">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<div className="text-2xl">💳</div> <div className="text-2xl">💳</div>
<div> <div>
@@ -293,7 +293,7 @@ export default function AccountPage() {
)} )}
{/* Job post form */} {/* Job post form */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl overflow-hidden"> <div className="bg-[#231d18] border border-white/10 rounded-xl overflow-hidden">
<button <button
onClick={() => setShowJobForm((p) => !p)} onClick={() => setShowJobForm((p) => !p)}
disabled={!hasActiveSubscription} disabled={!hasActiveSubscription}
@@ -315,7 +315,7 @@ export default function AccountPage() {
value={jobTitle} value={jobTitle}
onChange={(e) => setJobTitle(e.target.value)} onChange={(e) => setJobTitle(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. Senior Broadcast Engineer" placeholder="e.g. Senior Broadcast Engineer"
/> />
</div> </div>
@@ -325,7 +325,7 @@ export default function AccountPage() {
value={jobCompany} value={jobCompany}
onChange={(e) => setJobCompany(e.target.value)} onChange={(e) => setJobCompany(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/> />
</div> </div>
<div> <div>
@@ -334,7 +334,7 @@ export default function AccountPage() {
value={jobLocation} value={jobLocation}
onChange={(e) => setJobLocation(e.target.value)} onChange={(e) => setJobLocation(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="City, State or Remote" placeholder="City, State or Remote"
/> />
</div> </div>
@@ -343,7 +343,7 @@ export default function AccountPage() {
<select <select
value={jobType} value={jobType}
onChange={(e) => setJobType(e.target.value)} onChange={(e) => setJobType(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
> >
{JOB_TYPES.map((t) => ( {JOB_TYPES.map((t) => (
<option key={t.value} value={t.value}> <option key={t.value} value={t.value}>
@@ -359,14 +359,14 @@ export default function AccountPage() {
type="number" type="number"
value={jobSalaryMin} value={jobSalaryMin}
onChange={(e) => setJobSalaryMin(e.target.value)} onChange={(e) => setJobSalaryMin(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Min $" placeholder="Min $"
/> />
<input <input
type="number" type="number"
value={jobSalaryMax} value={jobSalaryMax}
onChange={(e) => setJobSalaryMax(e.target.value)} onChange={(e) => setJobSalaryMax(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Max $" placeholder="Max $"
/> />
</div> </div>
@@ -378,7 +378,7 @@ export default function AccountPage() {
onChange={(e) => setJobDescription(e.target.value)} onChange={(e) => setJobDescription(e.target.value)}
required required
rows={5} rows={5}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Job description, requirements, responsibilities..." placeholder="Job description, requirements, responsibilities..."
/> />
</div> </div>
@@ -387,7 +387,7 @@ export default function AccountPage() {
<input <input
value={jobApplyUrl} value={jobApplyUrl}
onChange={(e) => setJobApplyUrl(e.target.value)} onChange={(e) => setJobApplyUrl(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="https://..." placeholder="https://..."
/> />
</div> </div>
@@ -397,7 +397,7 @@ export default function AccountPage() {
type="email" type="email"
value={jobApplyEmail} value={jobApplyEmail}
onChange={(e) => setJobApplyEmail(e.target.value)} onChange={(e) => setJobApplyEmail(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="jobs@company.com" placeholder="jobs@company.com"
/> />
</div> </div>
@@ -424,7 +424,7 @@ export default function AccountPage() {
</div> </div>
{/* Gig post form */} {/* Gig post form */}
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl overflow-hidden"> <div className="bg-[#231d18] border border-white/10 rounded-xl overflow-hidden">
<button <button
onClick={() => setShowGigForm((p) => !p)} onClick={() => setShowGigForm((p) => !p)}
disabled={!hasActiveSubscription} disabled={!hasActiveSubscription}
@@ -446,7 +446,7 @@ export default function AccountPage() {
value={gigTitle} value={gigTitle}
onChange={(e) => setGigTitle(e.target.value)} onChange={(e) => setGigTitle(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. DP needed for 2-day commercial shoot" placeholder="e.g. DP needed for 2-day commercial shoot"
/> />
</div> </div>
@@ -456,7 +456,7 @@ export default function AccountPage() {
value={gigCompany} value={gigCompany}
onChange={(e) => setGigCompany(e.target.value)} onChange={(e) => setGigCompany(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/> />
</div> </div>
<div> <div>
@@ -465,7 +465,7 @@ export default function AccountPage() {
value={gigLocation} value={gigLocation}
onChange={(e) => setGigLocation(e.target.value)} onChange={(e) => setGigLocation(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/> />
</div> </div>
<div> <div>
@@ -474,7 +474,7 @@ export default function AccountPage() {
value={gigType} value={gigType}
onChange={(e) => setGigType(e.target.value)} onChange={(e) => setGigType(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
> >
<option value="">Select type...</option> <option value="">Select type...</option>
{GIG_TYPES.map((t) => ( {GIG_TYPES.map((t) => (
@@ -489,7 +489,7 @@ export default function AccountPage() {
<input <input
value={gigRate} value={gigRate}
onChange={(e) => setGigRate(e.target.value)} onChange={(e) => setGigRate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. $750/day or negotiable" placeholder="e.g. $750/day or negotiable"
/> />
</div> </div>
@@ -499,7 +499,7 @@ export default function AccountPage() {
type="date" type="date"
value={gigStartDate} value={gigStartDate}
onChange={(e) => setGigStartDate(e.target.value)} onChange={(e) => setGigStartDate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/> />
</div> </div>
<div> <div>
@@ -508,7 +508,7 @@ export default function AccountPage() {
type="date" type="date"
value={gigEndDate} value={gigEndDate}
onChange={(e) => setGigEndDate(e.target.value)} onChange={(e) => setGigEndDate(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
/> />
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
@@ -518,7 +518,7 @@ export default function AccountPage() {
onChange={(e) => setGigDescription(e.target.value)} onChange={(e) => setGigDescription(e.target.value)}
required required
rows={4} rows={4}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Project details, crew needed, requirements..." placeholder="Project details, crew needed, requirements..."
/> />
</div> </div>
@@ -527,7 +527,7 @@ export default function AccountPage() {
<input <input
value={gigContact} value={gigContact}
onChange={(e) => setGigContact(e.target.value)} onChange={(e) => setGigContact(e.target.value)}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Email address, phone, or instructions" placeholder="Email address, phone, or instructions"
/> />
</div> </div>

View File

@@ -173,7 +173,7 @@ export default function AdminQueuePage() {
if (loading || checkingAdmin) { if (loading || checkingAdmin) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
@@ -181,7 +181,7 @@ export default function AdminQueuePage() {
if (!isAdmin) { if (!isAdmin) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="text-4xl mb-4">🔒</div> <div className="text-4xl mb-4">🔒</div>
<h1 className="text-2xl font-bold mb-2">Access Denied</h1> <h1 className="text-2xl font-bold mb-2">Access Denied</h1>
@@ -197,7 +197,7 @@ export default function AdminQueuePage() {
const pendingCount = profiles.filter((p) => p.status === 'pending').length; const pendingCount = profiles.filter((p) => p.status === 'pending').length;
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-6xl mx-auto px-4 py-24"> <div className="max-w-6xl mx-auto px-4 py-24">
{/* Header */} {/* Header */}
@@ -235,7 +235,7 @@ export default function AdminQueuePage() {
className={`px-4 py-2 rounded-lg text-sm font-bold capitalize transition-colors ${ className={`px-4 py-2 rounded-lg text-sm font-bold capitalize transition-colors ${
filter === f filter === f
? 'bg-[#CC0000] text-white' ? 'bg-[#CC0000] text-white'
: 'bg-[#1a1a1a] text-gray-400 hover:text-white border border-white/10' : 'bg-[#231d18] text-gray-400 hover:text-white border border-white/10'
}`} }`}
> >
{f} {f}
@@ -266,7 +266,7 @@ export default function AdminQueuePage() {
return ( return (
<div <div
key={profile.id} key={profile.id}
className={`bg-[#1a1a1a] rounded-xl border overflow-hidden ${ className={`bg-[#231d18] rounded-xl border overflow-hidden ${
isPending ? 'border-yellow-500/30' : 'border-white/10' isPending ? 'border-yellow-500/30' : 'border-white/10'
}`} }`}
> >
@@ -377,7 +377,7 @@ export default function AdminQueuePage() {
[profile.id]: e.target.value, [profile.id]: e.target.value,
})) }))
} }
className="flex-1 bg-[#0d0d0d] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-red-500" className="flex-1 bg-[#1c1815] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-red-500"
placeholder="Rejection reason (required)..." placeholder="Rejection reason (required)..."
/> />
<button <button

View File

@@ -31,7 +31,7 @@ export default function GigDetailPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
@@ -39,7 +39,7 @@ export default function GigDetailPage() {
if (!gig) { if (!gig) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold mb-2">Gig Not Found</h1> <h1 className="text-2xl font-bold mb-2">Gig Not Found</h1>
<Link href="/marketplace/gigs" className="text-[#CC0000] hover:underline">Back to Gigs</Link> <Link href="/marketplace/gigs" className="text-[#CC0000] hover:underline">Back to Gigs</Link>
@@ -49,7 +49,7 @@ export default function GigDetailPage() {
} }
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-3xl mx-auto px-4 py-24"> <div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
@@ -60,7 +60,7 @@ export default function GigDetailPage() {
<span className="text-white truncate">{gig.title}</span> <span className="text-white truncate">{gig.title}</span>
</div> </div>
<div className="bg-[#1a1a1a] rounded-2xl border border-white/10 overflow-hidden"> <div className="bg-[#231d18] rounded-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8"> <div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<h1 className="text-2xl font-black mb-2">{gig.title}</h1> <h1 className="text-2xl font-black mb-2">{gig.title}</h1>
<div className="text-[#CC0000] font-bold text-lg mb-4">{gig.company}</div> <div className="text-[#CC0000] font-bold text-lg mb-4">{gig.company}</div>
@@ -85,7 +85,7 @@ export default function GigDetailPage() {
<div className="border-t border-white/10 pt-6"> <div className="border-t border-white/10 pt-6">
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-4">How to Respond</h2> <h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-4">How to Respond</h2>
{gig.contact_method ? ( {gig.contact_method ? (
<div className="bg-[#0d0d0d] rounded-lg p-4 text-sm text-gray-300">{gig.contact_method}</div> <div className="bg-[#1c1815] rounded-lg p-4 text-sm text-gray-300">{gig.contact_method}</div>
) : ( ) : (
<p className="text-gray-400 text-sm">Contact the poster directly through the platform.</p> <p className="text-gray-400 text-sm">Contact the poster directly through the platform.</p>
)} )}

View File

@@ -26,7 +26,7 @@ export default function GigsPage() {
}, []); }, []);
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-5xl mx-auto px-4 py-24"> <div className="max-w-5xl mx-auto px-4 py-24">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
@@ -49,7 +49,7 @@ export default function GigsPage() {
</Link> </Link>
</div> </div>
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8"> <div className="bg-[#231d18] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8">
💳 <strong>Gig posting requires a Pro subscription.</strong> Billing is coming soon 💳 <strong>Gig posting requires a Pro subscription.</strong> Billing is coming soon
create your free profile now and post gigs when payments launch. create your free profile now and post gigs when payments launch.
</div> </div>
@@ -57,7 +57,7 @@ export default function GigsPage() {
{loading ? ( {loading ? (
<div className="space-y-4"> <div className="space-y-4">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-xl p-6 animate-pulse h-28" /> <div key={i} className="bg-[#231d18] rounded-xl p-6 animate-pulse h-28" />
))} ))}
</div> </div>
) : gigs.length === 0 ? ( ) : gigs.length === 0 ? (
@@ -72,7 +72,7 @@ export default function GigsPage() {
<Link <Link
key={gig.id} key={gig.id}
href={`/marketplace/gigs/${gig.id}`} href={`/marketplace/gigs/${gig.id}`}
className="block bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all" className="block bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all"
> >
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">

View File

@@ -31,7 +31,7 @@ export default function JobDetailPage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
@@ -39,7 +39,7 @@ export default function JobDetailPage() {
if (!job) { if (!job) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold mb-2">Job Not Found</h1> <h1 className="text-2xl font-bold mb-2">Job Not Found</h1>
<Link href="/marketplace/jobs" className="text-[#CC0000] hover:underline">Back to Jobs</Link> <Link href="/marketplace/jobs" className="text-[#CC0000] hover:underline">Back to Jobs</Link>
@@ -49,7 +49,7 @@ export default function JobDetailPage() {
} }
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-3xl mx-auto px-4 py-24"> <div className="max-w-3xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
@@ -60,7 +60,7 @@ export default function JobDetailPage() {
<span className="text-white truncate">{job.title}</span> <span className="text-white truncate">{job.title}</span>
</div> </div>
<div className="bg-[#1a1a1a] rounded-2xl border border-white/10 overflow-hidden"> <div className="bg-[#231d18] rounded-2xl border border-white/10 overflow-hidden">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8"> <div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<h1 className="text-2xl font-black mb-2">{job.title}</h1> <h1 className="text-2xl font-black mb-2">{job.title}</h1>
<div className="text-[#CC0000] font-bold text-lg mb-4">{job.company}</div> <div className="text-[#CC0000] font-bold text-lg mb-4">{job.company}</div>

View File

@@ -26,7 +26,7 @@ export default function JobsPage() {
}, []); }, []);
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-5xl mx-auto px-4 py-24"> <div className="max-w-5xl mx-auto px-4 py-24">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-8">
@@ -49,7 +49,7 @@ export default function JobsPage() {
</Link> </Link>
</div> </div>
<div className="bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8"> <div className="bg-[#231d18] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400 mb-8">
💳 <strong>Job posting requires a Pro subscription.</strong> Billing is coming soon 💳 <strong>Job posting requires a Pro subscription.</strong> Billing is coming soon
create your free profile now and post jobs when payments launch. create your free profile now and post jobs when payments launch.
</div> </div>
@@ -57,7 +57,7 @@ export default function JobsPage() {
{loading ? ( {loading ? (
<div className="space-y-4"> <div className="space-y-4">
{[...Array(4)].map((_, i) => ( {[...Array(4)].map((_, i) => (
<div key={i} className="bg-[#1a1a1a] rounded-xl p-6 animate-pulse h-28" /> <div key={i} className="bg-[#231d18] rounded-xl p-6 animate-pulse h-28" />
))} ))}
</div> </div>
) : jobs.length === 0 ? ( ) : jobs.length === 0 ? (
@@ -72,7 +72,7 @@ export default function JobsPage() {
<Link <Link
key={job.id} key={job.id}
href={`/marketplace/jobs/${job.id}`} href={`/marketplace/jobs/${job.id}`}
className="block bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all" className="block bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-6 transition-all"
> >
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4"> <div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex-1"> <div className="flex-1">

View File

@@ -243,7 +243,7 @@ export default function JoinMarketplacePage() {
}; };
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-2xl mx-auto px-4 py-24"> <div className="max-w-2xl mx-auto px-4 py-24">
{/* Header */} {/* Header */}
@@ -261,7 +261,7 @@ export default function JoinMarketplacePage() {
{/* Auth gate */} {/* Auth gate */}
{!user && ( {!user && (
<div className="bg-[#1a1a1a] border border-white/10 rounded-xl p-8 mb-8"> <div className="bg-[#231d18] border border-white/10 rounded-xl p-8 mb-8">
<h2 className="text-lg font-bold mb-6 text-center"> <h2 className="text-lg font-bold mb-6 text-center">
{authMode === 'signup' ? 'Create an Account' : 'Sign In to Continue'} {authMode === 'signup' ? 'Create an Account' : 'Sign In to Continue'}
</h2> </h2>
@@ -273,7 +273,7 @@ export default function JoinMarketplacePage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="you@example.com" placeholder="you@example.com"
/> />
</div> </div>
@@ -285,7 +285,7 @@ export default function JoinMarketplacePage() {
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
minLength={6} minLength={6}
className="w-full bg-[#0d0d0d] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#1c1815] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="••••••••" placeholder="••••••••"
/> />
</div> </div>
@@ -325,7 +325,7 @@ export default function JoinMarketplacePage() {
setProfileType('crew'); setProfileType('crew');
setStep('details'); setStep('details');
}} }}
className="w-full bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group" className="w-full bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group"
> >
<div className="text-2xl mb-2">🎬</div> <div className="text-2xl mb-2">🎬</div>
<div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors"> <div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
@@ -340,7 +340,7 @@ export default function JoinMarketplacePage() {
setProfileType('vendor'); setProfileType('vendor');
setStep('details'); setStep('details');
}} }}
className="w-full bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group" className="w-full bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/50 rounded-xl p-6 text-left transition-all group"
> >
<div className="text-2xl mb-2">🏢</div> <div className="text-2xl mb-2">🏢</div>
<div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors"> <div className="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
@@ -376,7 +376,7 @@ export default function JoinMarketplacePage() {
value={crewName} value={crewName}
onChange={(e) => setCrewName(e.target.value)} onChange={(e) => setCrewName(e.target.value)}
required required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="Your professional name" placeholder="Your professional name"
/> />
</div> </div>
@@ -388,7 +388,7 @@ export default function JoinMarketplacePage() {
value={crewLocation} value={crewLocation}
onChange={(e) => setCrewLocation(e.target.value)} onChange={(e) => setCrewLocation(e.target.value)}
required required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="City, State / Country" placeholder="City, State / Country"
/> />
</div> </div>
@@ -397,7 +397,7 @@ export default function JoinMarketplacePage() {
<select <select
value={crewUnion} value={crewUnion}
onChange={(e) => setCrewUnion(e.target.value)} onChange={(e) => setCrewUnion(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
> >
<option value="non_union">Non-Union</option> <option value="non_union">Non-Union</option>
<option value="iatse">IATSE</option> <option value="iatse">IATSE</option>
@@ -412,7 +412,7 @@ export default function JoinMarketplacePage() {
type="number" type="number"
value={crewRate} value={crewRate}
onChange={(e) => setCrewRate(e.target.value)} onChange={(e) => setCrewRate(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. 750" placeholder="e.g. 750"
min="0" min="0"
/> />
@@ -426,7 +426,7 @@ export default function JoinMarketplacePage() {
onChange={(e) => setCrewBio(e.target.value)} onChange={(e) => setCrewBio(e.target.value)}
rows={4} rows={4}
maxLength={500} maxLength={500}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Brief professional bio (500 chars max)" placeholder="Brief professional bio (500 chars max)"
/> />
<div className="text-xs text-gray-500 text-right mt-1">{crewBio.length}/500</div> <div className="text-xs text-gray-500 text-right mt-1">{crewBio.length}/500</div>
@@ -457,7 +457,7 @@ export default function JoinMarketplacePage() {
<input <input
value={crewEquipment} value={crewEquipment}
onChange={(e) => setCrewEquipment(e.target.value)} onChange={(e) => setCrewEquipment(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="e.g. Sony FX9, DJI Ronin, Sennheiser MKH416 (comma-separated)" placeholder="e.g. Sony FX9, DJI Ronin, Sennheiser MKH416 (comma-separated)"
/> />
</div> </div>
@@ -467,7 +467,7 @@ export default function JoinMarketplacePage() {
<input <input
value={crewReel} value={crewReel}
onChange={(e) => setCrewReel(e.target.value)} onChange={(e) => setCrewReel(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="https://vimeo.com/..." placeholder="https://vimeo.com/..."
/> />
</div> </div>
@@ -488,7 +488,7 @@ export default function JoinMarketplacePage() {
type="file" type="file"
accept="application/pdf,.doc,.docx" accept="application/pdf,.doc,.docx"
onChange={(e) => setResumeFile(e.target.files?.[0] || null)} onChange={(e) => setResumeFile(e.target.files?.[0] || null)}
className="w-full text-sm text-gray-400 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-[#1a1a1a] file:border file:border-white/20 file:text-white file:cursor-pointer" className="w-full text-sm text-gray-400 file:mr-3 file:py-2 file:px-4 file:rounded-lg file:border-0 file:bg-[#231d18] file:border file:border-white/20 file:text-white file:cursor-pointer"
/> />
</div> </div>
</div> </div>
@@ -532,7 +532,7 @@ export default function JoinMarketplacePage() {
value={vendorName} value={vendorName}
onChange={(e) => setVendorName(e.target.value)} onChange={(e) => setVendorName(e.target.value)}
required required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="Your company name" placeholder="Your company name"
/> />
</div> </div>
@@ -546,7 +546,7 @@ export default function JoinMarketplacePage() {
value={vendorLocation} value={vendorLocation}
onChange={(e) => setVendorLocation(e.target.value)} onChange={(e) => setVendorLocation(e.target.value)}
required required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="City, State / Country" placeholder="City, State / Country"
/> />
</div> </div>
@@ -558,7 +558,7 @@ export default function JoinMarketplacePage() {
value={vendorCategory} value={vendorCategory}
onChange={(e) => setVendorCategory(e.target.value)} onChange={(e) => setVendorCategory(e.target.value)}
required required
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
> >
<option value="">Select category...</option> <option value="">Select category...</option>
{VENDOR_CATEGORIES.map((c) => ( {VENDOR_CATEGORIES.map((c) => (
@@ -577,7 +577,7 @@ export default function JoinMarketplacePage() {
onChange={(e) => setVendorDescription(e.target.value)} onChange={(e) => setVendorDescription(e.target.value)}
rows={4} rows={4}
maxLength={1000} maxLength={1000}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000] resize-none"
placeholder="Describe your services, specialties, and what makes you stand out" placeholder="Describe your services, specialties, and what makes you stand out"
/> />
</div> </div>
@@ -588,7 +588,7 @@ export default function JoinMarketplacePage() {
<input <input
value={vendorWebsite} value={vendorWebsite}
onChange={(e) => setVendorWebsite(e.target.value)} onChange={(e) => setVendorWebsite(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="https://yourcompany.com" placeholder="https://yourcompany.com"
/> />
</div> </div>
@@ -598,7 +598,7 @@ export default function JoinMarketplacePage() {
type="email" type="email"
value={vendorEmail} value={vendorEmail}
onChange={(e) => setVendorEmail(e.target.value)} onChange={(e) => setVendorEmail(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-4 py-3 text-white focus:outline-none focus:border-[#CC0000]"
placeholder="contact@yourcompany.com" placeholder="contact@yourcompany.com"
/> />
</div> </div>

View File

@@ -26,7 +26,7 @@ const STATS = [
export default function MarketplacePage() { export default function MarketplacePage() {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
{/* Hero */} {/* Hero */}
<section className="relative pt-24 pb-20 px-4 overflow-hidden"> <section className="relative pt-24 pb-20 px-4 overflow-hidden">
@@ -83,7 +83,7 @@ export default function MarketplacePage() {
<Link <Link
key={cat?.label} key={cat?.label}
href={`/marketplace/search?category=${encodeURIComponent(cat?.label)}`} href={`/marketplace/search?category=${encodeURIComponent(cat?.label)}`}
className="bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/40 rounded-xl p-6 transition-all group" className="bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/40 rounded-xl p-6 transition-all group"
> >
<div className="text-3xl mb-3">{cat?.icon}</div> <div className="text-3xl mb-3">{cat?.icon}</div>
<div className="font-bold text-white group-hover:text-[#CC0000] transition-colors"> <div className="font-bold text-white group-hover:text-[#CC0000] transition-colors">
@@ -212,7 +212,7 @@ export default function MarketplacePage() {
className={`rounded-xl p-6 border ${ className={`rounded-xl p-6 border ${
plan?.highlight plan?.highlight
? 'bg-[#CC0000]/10 border-[#CC0000] relative' ? 'bg-[#CC0000]/10 border-[#CC0000] relative'
: 'bg-[#1a1a1a] border-white/10' : 'bg-[#231d18] border-white/10'
}`} }`}
> >
{plan?.highlight && ( {plan?.highlight && (
@@ -247,7 +247,7 @@ export default function MarketplacePage() {
</div> </div>
{/* Stripe coming soon notice */} {/* Stripe coming soon notice */}
<div className="mt-8 bg-[#1a1a1a] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400"> <div className="mt-8 bg-[#231d18] border border-yellow-500/30 rounded-lg p-4 text-sm text-yellow-400">
💳 <strong>Billing coming soon</strong> Pro and Studio plans are available now. 💳 <strong>Billing coming soon</strong> Pro and Studio plans are available now.
Payment processing will be enabled shortly. Create your free profile today and upgrade Payment processing will be enabled shortly. Create your free profile today and upgrade
when billing launches. when billing launches.

View File

@@ -52,7 +52,7 @@ export default function ProfilePage() {
if (loading) { if (loading) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
); );
@@ -60,7 +60,7 @@ export default function ProfilePage() {
if (notFoundState || !profile || !details) { if (notFoundState || !profile || !details) {
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="text-4xl mb-4">👤</div> <div className="text-4xl mb-4">👤</div>
<h1 className="text-2xl font-bold mb-2">Profile Not Found</h1> <h1 className="text-2xl font-bold mb-2">Profile Not Found</h1>
@@ -76,7 +76,7 @@ export default function ProfilePage() {
const photoUrl = isCrew ? details.headshot_url : details.logo_url; const photoUrl = isCrew ? details.headshot_url : details.logo_url;
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-4xl mx-auto px-4 py-24"> <div className="max-w-4xl mx-auto px-4 py-24">
<div className="flex items-center gap-2 text-sm text-gray-500 mb-8"> <div className="flex items-center gap-2 text-sm text-gray-500 mb-8">
@@ -87,7 +87,7 @@ export default function ProfilePage() {
<span className="text-white">{name}</span> <span className="text-white">{name}</span>
</div> </div>
<div className="bg-[#1a1a1a] rounded-2xl overflow-hidden border border-white/10"> <div className="bg-[#231d18] rounded-2xl overflow-hidden border border-white/10">
<div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8"> <div className="bg-gradient-to-r from-[#CC0000]/20 to-transparent p-8">
<div className="flex flex-col sm:flex-row items-start gap-6"> <div className="flex flex-col sm:flex-row items-start gap-6">
<div className="w-24 h-24 rounded-full bg-[#CC0000]/20 flex-shrink-0 overflow-hidden flex items-center justify-center text-3xl font-black text-[#CC0000] border-2 border-[#CC0000]/30"> <div className="w-24 h-24 rounded-full bg-[#CC0000]/20 flex-shrink-0 overflow-hidden flex items-center justify-center text-3xl font-black text-[#CC0000] border-2 border-[#CC0000]/30">

View File

@@ -57,7 +57,7 @@ export default function MarketplaceSearchPage() {
return ( return (
<Suspense <Suspense
fallback={ fallback={
<div className="min-h-screen bg-[#0d0d0d] flex items-center justify-center"> <div className="min-h-screen bg-[#1c1815] flex items-center justify-center">
<div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" /> <div className="w-8 h-8 border-4 border-[#CC0000] border-t-transparent rounded-full animate-spin" />
</div> </div>
} }
@@ -246,7 +246,7 @@ function SearchPageInner() {
}; };
return ( return (
<div className="min-h-screen bg-[#0d0d0d] text-white"> <div className="min-h-screen bg-[#1c1815] text-white">
<Header /> <Header />
<div className="max-w-7xl mx-auto px-4 py-24"> <div className="max-w-7xl mx-auto px-4 py-24">
{/* Breadcrumb */} {/* Breadcrumb */}
@@ -280,7 +280,7 @@ function SearchPageInner() {
<input <input
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Name, skill, company..." placeholder="Name, skill, company..."
/> />
</div> </div>
@@ -314,7 +314,7 @@ function SearchPageInner() {
<input <input
value={filterLocation} value={filterLocation}
onChange={(e) => setFilterLocation(e.target.value)} onChange={(e) => setFilterLocation(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="City, State..." placeholder="City, State..."
/> />
</div> </div>
@@ -328,7 +328,7 @@ function SearchPageInner() {
<select <select
value={filterUnion} value={filterUnion}
onChange={(e) => setFilterUnion(e.target.value)} onChange={(e) => setFilterUnion(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
> >
<option value="">Any</option> <option value="">Any</option>
<option value="non_union">Non-Union</option> <option value="non_union">Non-Union</option>
@@ -346,14 +346,14 @@ function SearchPageInner() {
type="number" type="number"
value={filterRateMin} value={filterRateMin}
onChange={(e) => setFilterRateMin(e.target.value)} onChange={(e) => setFilterRateMin(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Min $" placeholder="Min $"
/> />
<input <input
type="number" type="number"
value={filterRateMax} value={filterRateMax}
onChange={(e) => setFilterRateMax(e.target.value)} onChange={(e) => setFilterRateMax(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
placeholder="Max $" placeholder="Max $"
/> />
</div> </div>
@@ -369,7 +369,7 @@ function SearchPageInner() {
<select <select
value={filterCategory} value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)} onChange={(e) => setFilterCategory(e.target.value)}
className="w-full bg-[#1a1a1a] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]" className="w-full bg-[#231d18] border border-white/20 rounded-lg px-3 py-2.5 text-white text-sm focus:outline-none focus:border-[#CC0000]"
> >
<option value="">All Categories</option> <option value="">All Categories</option>
{VENDOR_CATEGORIES.map((c) => ( {VENDOR_CATEGORIES.map((c) => (
@@ -428,7 +428,7 @@ function SearchPageInner() {
{[...Array(6)].map((_, i) => ( {[...Array(6)].map((_, i) => (
<div <div
key={i} key={i}
className="bg-[#1a1a1a] rounded-xl p-5 animate-pulse h-40" className="bg-[#231d18] rounded-xl p-5 animate-pulse h-40"
/> />
))} ))}
</div> </div>
@@ -449,7 +449,7 @@ function SearchPageInner() {
<Link <Link
key={profile.id} key={profile.id}
href={`/marketplace/profile/${profile.slug}`} href={`/marketplace/profile/${profile.slug}`}
className="bg-[#1a1a1a] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-5 transition-all block" className="bg-[#231d18] hover:bg-[#222] border border-white/10 hover:border-[#CC0000]/30 rounded-xl p-5 transition-all block"
> >
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
{/* Avatar */} {/* Avatar */}
@@ -510,7 +510,7 @@ function SearchPageInner() {
<button <button
onClick={() => setPage((p) => Math.max(1, p - 1))} onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1} disabled={page === 1}
className="px-4 py-2 bg-[#1a1a1a] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors" className="px-4 py-2 bg-[#231d18] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors"
> >
Prev Prev
</button> </button>
@@ -518,7 +518,7 @@ function SearchPageInner() {
<button <button
onClick={() => setPage((p) => p + 1)} onClick={() => setPage((p) => p + 1)}
disabled={results.length < 20} disabled={results.length < 20}
className="px-4 py-2 bg-[#1a1a1a] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors" className="px-4 py-2 bg-[#231d18] border border-white/20 rounded-lg text-sm disabled:opacity-40 hover:border-white/40 transition-colors"
> >
Next Next
</button> </button>

View File

@@ -87,12 +87,12 @@ export default async function NabHub() {
const articles = (rawArticles || []) as ArticleRow[]; const articles = (rawArticles || []) as ArticleRow[];
return ( return (
<div className="min-h-screen bg-[#0d0d0d]"> <div className="min-h-screen bg-[#1c1815]">
<Header /> <Header />
<CompanyMentionsHover /> <CompanyMentionsHover />
{/* Hero */} {/* Hero */}
<section className="bg-gradient-to-br from-amber-500/10 to-[#0d0d0d] border-b border-[#252525]"> <section className="bg-gradient-to-br from-amber-500/10 to-[#1c1815] border-b border-[#3a322b]">
<div className="max-w-6xl mx-auto px-4 py-10"> <div className="max-w-6xl mx-auto px-4 py-10">
<div className="flex items-baseline gap-3 mb-2"> <div className="flex items-baseline gap-3 mb-2">
<span className="text-[10px] font-bold uppercase tracking-widest bg-amber-500/20 text-amber-300 px-2 py-0.5 rounded"> <span className="text-[10px] font-bold uppercase tracking-widest bg-amber-500/20 text-amber-300 px-2 py-0.5 rounded">
@@ -128,9 +128,9 @@ export default async function NabHub() {
key={e.slug} key={e.slug}
href={`/manufacturers/${e.slug}`} href={`/manufacturers/${e.slug}`}
data-company-slug={e.slug} data-company-slug={e.slug}
className="bg-[#0d0d0d] border-2 border-emerald-500/40 rounded-lg p-3 hover:border-emerald-500/70 hover:shadow-md transition-all flex flex-col items-center text-center gap-2" className="bg-[#1c1815] border-2 border-emerald-500/40 rounded-lg p-3 hover:border-emerald-500/70 hover:shadow-md transition-all flex flex-col items-center text-center gap-2"
> >
<div className="w-full h-16 flex items-center justify-center bg-[#1a1a1a] rounded"> <div className="w-full h-16 flex items-center justify-center bg-[#231d18] rounded">
{e.logo_url ? ( {e.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={e.logo_url} alt={`${e.company_name} logo`} className="max-h-12 max-w-full object-contain" loading="lazy" /> <img src={e.logo_url} alt={`${e.company_name} logo`} className="max-h-12 max-w-full object-contain" loading="lazy" />
@@ -153,7 +153,7 @@ export default async function NabHub() {
<h2 className="text-xl font-display font-bold text-[#e0e0e0]"> <h2 className="text-xl font-display font-bold text-[#e0e0e0]">
Latest from the show floor Latest from the show floor
</h2> </h2>
<Link href="/news?search=NAB+2026" className="text-xs text-[#ffb800] hover:underline"> <Link href="/news?search=NAB+2026" className="text-xs text-[#F0A623] hover:underline">
See all NAB coverage See all NAB coverage
</Link> </Link>
</header> </header>
@@ -162,7 +162,7 @@ export default async function NabHub() {
<Link <Link
key={a.wp_slug} key={a.wp_slug}
href={`/news/${a.wp_slug}`} href={`/news/${a.wp_slug}`}
className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#ffb800] hover:shadow-sm transition-all" className="group bg-[#1c1815] border border-[#3a322b] rounded-lg overflow-hidden hover:border-[#F0A623] hover:shadow-sm transition-all"
> >
{a.featured_image && ( {a.featured_image && (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
@@ -174,7 +174,7 @@ export default async function NabHub() {
/> />
)} )}
<div className="p-3"> <div className="p-3">
<h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#ffb800] line-clamp-3 mb-1"> <h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#F0A623] line-clamp-3 mb-1">
{a.title} {a.title}
</h3> </h3>
{a.wp_published_at && ( {a.wp_published_at && (
@@ -195,7 +195,7 @@ export default async function NabHub() {
<h2 className="text-xl font-display font-bold text-[#e0e0e0]"> <h2 className="text-xl font-display font-bold text-[#e0e0e0]">
All 2026 NAB Show exhibitors All 2026 NAB Show exhibitors
</h2> </h2>
<Link href="/manufacturers" className="text-xs text-[#ffb800] hover:underline"> <Link href="/manufacturers" className="text-xs text-[#F0A623] hover:underline">
Full directory Full directory
</Link> </Link>
</header> </header>
@@ -205,13 +205,13 @@ export default async function NabHub() {
key={e.slug} key={e.slug}
href={`/manufacturers/${e.slug}`} href={`/manufacturers/${e.slug}`}
data-company-slug={e.slug} data-company-slug={e.slug}
className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#ffb800] transition-all flex items-center gap-2.5" className="bg-[#1c1815] border border-[#3a322b] rounded-md p-2.5 hover:border-[#F0A623] transition-all flex items-center gap-2.5"
> >
{e.logo_url ? ( {e.logo_url ? (
/* eslint-disable-next-line @next/next/no-img-element */ /* eslint-disable-next-line @next/next/no-img-element */
<img src={e.logo_url} alt="" className="w-10 h-10 rounded bg-[#1a1a1a] object-contain flex-shrink-0" loading="lazy" /> <img src={e.logo_url} alt="" className="w-10 h-10 rounded bg-[#231d18] object-contain flex-shrink-0" loading="lazy" />
) : ( ) : (
<div className="w-10 h-10 rounded bg-[#1a1a1a] flex items-center justify-center text-[#666] font-bold text-sm flex-shrink-0"> <div className="w-10 h-10 rounded bg-[#231d18] flex items-center justify-center text-[#666] font-bold text-sm flex-shrink-0">
{e.company_name.charAt(0).toUpperCase()} {e.company_name.charAt(0).toUpperCase()}
</div> </div>
)} )}
@@ -222,7 +222,7 @@ export default async function NabHub() {
{otherRows.length > 200 && ( {otherRows.length > 200 && (
<p className="text-center text-xs text-[#888] mt-4"> <p className="text-center text-xs text-[#888] mt-4">
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "} Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
<Link href="/manufacturers" className="text-[#ffb800] hover:underline">Browse the full directory </Link> <Link href="/manufacturers" className="text-[#F0A623] hover:underline">Browse the full directory </Link>
</p> </p>
)} )}
</section> </section>

Some files were not shown because too many files have changed in this diff Show More