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:
@@ -51,7 +51,7 @@ export default function ContactPage() {
|
||||
<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">
|
||||
<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">
|
||||
AV Beat
|
||||
</div>
|
||||
@@ -63,7 +63,7 @@ export default function ContactPage() {
|
||||
|
||||
<ul className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{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">
|
||||
{c.title}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function AboutPage() {
|
||||
<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">
|
||||
<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">
|
||||
AV Beat
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export default function AboutPage() {
|
||||
|
||||
function Stat({ label, value }: { label: string; value: string }) {
|
||||
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="mt-1 text-[10px] font-mono uppercase tracking-[0.16em] text-[#6b7280]">
|
||||
{label}
|
||||
|
||||
@@ -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="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">
|
||||
<span className="w-6 h-px bg-[#d60701]" />
|
||||
<span className="w-6 h-px bg-[#D85A30]" />
|
||||
AV Beat · editorial
|
||||
</div>
|
||||
<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 (
|
||||
<li
|
||||
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 */}
|
||||
<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">
|
||||
{p.avatar_url ? (
|
||||
@@ -142,7 +142,7 @@ export default async function TeamPage() {
|
||||
alt={p.name}
|
||||
width={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>
|
||||
) : (
|
||||
@@ -155,7 +155,7 @@ export default async function TeamPage() {
|
||||
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3>
|
||||
</a>
|
||||
{(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)}
|
||||
</div>
|
||||
)}
|
||||
@@ -209,7 +209,7 @@ export default async function TeamPage() {
|
||||
{ops.map((p) => (
|
||||
<li
|
||||
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 ? (
|
||||
<img
|
||||
@@ -226,7 +226,7 @@ export default async function TeamPage() {
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
<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}
|
||||
</div>
|
||||
<div className="text-xs text-[#aab1bd] mt-3 space-y-1">
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function AccountPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#111111]">
|
||||
<div className="min-h-screen bg-[#1c1815]">
|
||||
<Header />
|
||||
<div className="max-w-container mx-auto px-4 py-12">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
@@ -298,21 +298,21 @@ export default function AccountPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#111111]">
|
||||
<div className="min-h-screen bg-[#1c1815]">
|
||||
<Header />
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<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 ${
|
||||
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" ? (
|
||||
<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" />
|
||||
</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" />
|
||||
</svg>
|
||||
)}
|
||||
@@ -321,10 +321,10 @@ export default function AccountPage() {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<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 className="text-[#888]">Account</span>
|
||||
</nav>
|
||||
@@ -336,25 +336,25 @@ export default function AccountPage() {
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={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 ${
|
||||
activeTab === tab.id
|
||||
? "border-[#ffb800] text-[#ffb800]"
|
||||
? "border-[#F0A623] text-[#F0A623]"
|
||||
: "border-transparent text-[#666] hover:text-[#aaa]"
|
||||
}`}>
|
||||
{tab.label}
|
||||
@@ -365,8 +365,8 @@ export default function AccountPage() {
|
||||
{/* Profile Tab */}
|
||||
{activeTab === "profile" && (
|
||||
<form onSubmit={handleSaveProfile} className="space-y-5">
|
||||
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6">
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]">
|
||||
<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-[#3a322b]">
|
||||
Public Profile
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
@@ -379,7 +379,7 @@ export default function AccountPage() {
|
||||
value={fullName}
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
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>
|
||||
@@ -391,7 +391,7 @@ export default function AccountPage() {
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
placeholder="Tell us a bit about yourself..."
|
||||
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 className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
@@ -404,7 +404,7 @@ export default function AccountPage() {
|
||||
value={website}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
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>
|
||||
@@ -416,15 +416,15 @@ export default function AccountPage() {
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
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 className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6">
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-3 pb-3 border-b border-[#2a2a2a]">
|
||||
<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-[#3a322b]">
|
||||
Account Info
|
||||
</h2>
|
||||
<div>
|
||||
@@ -435,7 +435,7 @@ export default function AccountPage() {
|
||||
type="email"
|
||||
value={user?.email || ""}
|
||||
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>
|
||||
</div>
|
||||
@@ -445,7 +445,7 @@ export default function AccountPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -456,8 +456,8 @@ export default function AccountPage() {
|
||||
{activeTab === "security" && (
|
||||
<div className="space-y-5">
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6">
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]">
|
||||
<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-[#3a322b]">
|
||||
Change Password
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
@@ -471,7 +471,7 @@ export default function AccountPage() {
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
placeholder="Enter new password"
|
||||
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>
|
||||
@@ -484,7 +484,7 @@ export default function AccountPage() {
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Confirm new password"
|
||||
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>
|
||||
@@ -492,7 +492,7 @@ export default function AccountPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -506,7 +506,7 @@ export default function AccountPage() {
|
||||
</p>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@@ -520,12 +520,12 @@ export default function AccountPage() {
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0]">
|
||||
Saved Articles
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
</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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -533,37 +533,37 @@ export default function AccountPage() {
|
||||
{bookmarksLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[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="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="h-3 bg-[#252525] rounded w-1/4" />
|
||||
<div className="h-4 bg-[#252525] rounded w-3/4" />
|
||||
<div className="h-3 bg-[#3a322b] rounded w-1/4" />
|
||||
<div className="h-4 bg-[#3a322b] rounded w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : 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">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<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 →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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">
|
||||
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
|
||||
{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" />
|
||||
) : (
|
||||
<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">
|
||||
<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>
|
||||
@@ -572,10 +572,10 @@ export default function AccountPage() {
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
{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}`}>
|
||||
<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}
|
||||
</h3>
|
||||
</Link>
|
||||
@@ -605,7 +605,7 @@ export default function AccountPage() {
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0]">
|
||||
Reading History
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -622,37 +622,37 @@ export default function AccountPage() {
|
||||
{historyLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[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="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="h-3 bg-[#252525] rounded w-1/4" />
|
||||
<div className="h-4 bg-[#252525] rounded w-3/4" />
|
||||
<div className="h-3 bg-[#3a322b] rounded w-1/4" />
|
||||
<div className="h-4 bg-[#3a322b] rounded w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : 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">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<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 →
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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">
|
||||
<Link href={`/articles/${item.article_slug}`} className="flex-shrink-0 w-20 h-14 overflow-hidden rounded-sm">
|
||||
{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" />
|
||||
) : (
|
||||
<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">
|
||||
<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>
|
||||
@@ -661,10 +661,10 @@ export default function AccountPage() {
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
{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}`}>
|
||||
<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}
|
||||
</h3>
|
||||
</Link>
|
||||
@@ -693,17 +693,17 @@ export default function AccountPage() {
|
||||
{topicsLoading ? (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||||
{[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>
|
||||
) : 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">
|
||||
<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>
|
||||
<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>
|
||||
<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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -714,18 +714,18 @@ export default function AccountPage() {
|
||||
const maxCount = topics[0]?.view_count || 1;
|
||||
const pct = Math.round((t.view_count / maxCount) * 100);
|
||||
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">
|
||||
<span className="font-body text-xs font-bold uppercase tracking-wider text-[#e0e0e0] capitalize leading-tight">
|
||||
{t.topic}
|
||||
</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}×
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#252525] rounded-full h-1">
|
||||
<div className="w-full bg-[#3a322b] rounded-full h-1">
|
||||
<div
|
||||
className="bg-[#ffb800] h-1 rounded-full transition-all"
|
||||
className="bg-[#F0A623] h-1 rounded-full transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -744,13 +744,13 @@ export default function AccountPage() {
|
||||
{/* Preferences Tab */}
|
||||
{activeTab === "preferences" && (
|
||||
<div className="space-y-5">
|
||||
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6">
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]">
|
||||
<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-[#3a322b]">
|
||||
Forum Reputation
|
||||
</h2>
|
||||
<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]">
|
||||
<span className="font-heading text-3xl font-bold text-[#ffb800]">
|
||||
<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-[#F0A623]">
|
||||
{profile?.reputation ?? 0}
|
||||
</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">
|
||||
Earn reputation by posting helpful threads and replies in the community forum.
|
||||
</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 →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-6">
|
||||
<h2 className="font-heading text-base font-bold text-[#e0e0e0] mb-5 pb-3 border-b border-[#2a2a2a]">
|
||||
<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-[#3a322b]">
|
||||
Reading Preferences
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
@@ -778,7 +778,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -789,7 +789,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -808,8 +808,8 @@ export default function AccountPage() {
|
||||
onClick={() => handleToggleDigest(!weeklyDigest)}
|
||||
aria-pressed={weeklyDigest}
|
||||
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 ${
|
||||
weeklyDigest ? "bg-[#ffb800] border-[#ffb800]" : "bg-[#2a2a2a] border-[#333]"
|
||||
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-[#F0A623] border-[#F0A623]" : "bg-[#3a322b] border-[#333]"
|
||||
}`}>
|
||||
<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 ${
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function ClientsPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -102,7 +102,7 @@ export default function ClientsPage() {
|
||||
<h1 className="text-xl font-bold">Clients</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -116,14 +116,14 @@ export default function ClientsPage() {
|
||||
{/* Filters */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{['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>
|
||||
|
||||
{/* New Client Form */}
|
||||
{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>
|
||||
|
||||
{errors._form && (
|
||||
@@ -148,7 +148,7 @@ export default function ClientsPage() {
|
||||
type={f.key === 'contact_email' ? 'email' : 'text'}
|
||||
value={(form as any)[f.key]}
|
||||
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>}
|
||||
</div>
|
||||
@@ -156,26 +156,26 @@ export default function ClientsPage() {
|
||||
<div className="col-span-2">
|
||||
<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)}
|
||||
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 className="col-span-2">
|
||||
<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]" />
|
||||
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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -187,17 +187,17 @@ export default function ClientsPage() {
|
||||
) : clients.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No clients found</td></tr>
|
||||
) : 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-[#ccc]">{c.contact_name}</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-green-400">{fmt(c.total_revenue ?? 0)}</td>
|
||||
<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 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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function CommissionsPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -53,10 +53,10 @@ export default function CommissionsPage() {
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</div>
|
||||
<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>)}
|
||||
</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>
|
||||
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
|
||||
</select>
|
||||
@@ -68,7 +68,7 @@ export default function CommissionsPage() {
|
||||
{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;
|
||||
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">
|
||||
<p className="text-sm font-semibold text-white">{s.full_name}</p>
|
||||
<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">
|
||||
<span>Tier progress</span><span>{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#ffb800] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
<div className="h-1.5 bg-[#231d18] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#F0A623] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -94,10 +94,10 @@ export default function CommissionsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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 => (
|
||||
<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 ? (
|
||||
<tr><td colSpan={8} className="px-4 py-8 text-center text-[#555] text-xs">No commission records</td></tr>
|
||||
) : 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-[#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-[#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>
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function DocumentsPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -54,70 +54,70 @@ export default function DocumentsPage() {
|
||||
<h1 className="text-xl font-bold">Document Vault</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Year tabs */}
|
||||
<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 => (
|
||||
<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 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>
|
||||
{DOC_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
{form.document_type === 'sales_agreement' && (
|
||||
<>
|
||||
<div>
|
||||
<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>
|
||||
<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 className="col-span-2">
|
||||
<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 className="col-span-2">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -129,14 +129,14 @@ export default function DocumentsPage() {
|
||||
) : docs.length === 0 ? (
|
||||
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No documents found</td></tr>
|
||||
) : 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-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-[#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 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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -95,7 +95,7 @@ export default function ExpensesPage() {
|
||||
<h1 className="text-xl font-bold">Expenses</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -107,11 +107,11 @@ export default function ExpensesPage() {
|
||||
)}
|
||||
|
||||
<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>
|
||||
{CATEGORIES.map(c => <option key={c} value={c}>{c.replace(/_/g, ' ')}</option>)}
|
||||
</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="true">Reconciled</option>
|
||||
<option value="false">Unreconciled</option>
|
||||
@@ -119,7 +119,7 @@ export default function ExpensesPage() {
|
||||
</div>
|
||||
|
||||
{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>
|
||||
|
||||
{errors._form && (
|
||||
@@ -132,40 +132,40 @@ export default function ExpensesPage() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
<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>}
|
||||
</div>
|
||||
<div>
|
||||
<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>}
|
||||
</div>
|
||||
<div>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -177,7 +177,7 @@ export default function ExpensesPage() {
|
||||
) : expenses.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No expenses found</td></tr>
|
||||
) : 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-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>
|
||||
|
||||
@@ -10,7 +10,7 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
paid: 'bg-green-400/10 text-green-400',
|
||||
pending: 'bg-yellow-400/10 text-yellow-400',
|
||||
overdue: 'bg-red-400/10 text-red-400',
|
||||
cancelled: 'bg-[#1a1a1a] text-[#555]',
|
||||
cancelled: 'bg-[#231d18] text-[#555]',
|
||||
};
|
||||
|
||||
function InvoicesContent() {
|
||||
@@ -83,7 +83,7 @@ function InvoicesContent() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -94,7 +94,7 @@ function InvoicesContent() {
|
||||
<h1 className="text-xl font-bold">Invoices</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -108,14 +108,14 @@ function InvoicesContent() {
|
||||
{/* Filters */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{['','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>
|
||||
|
||||
{/* Mark Paid Modal */}
|
||||
{markPaidId && (
|
||||
<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>
|
||||
|
||||
{payErrors._form && (
|
||||
@@ -127,35 +127,35 @@ function InvoicesContent() {
|
||||
|
||||
<div>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>}
|
||||
</div>
|
||||
{payForm.method === 'wire' && (
|
||||
<div>
|
||||
<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>}
|
||||
</div>
|
||||
)}
|
||||
<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="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>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -167,14 +167,14 @@ function InvoicesContent() {
|
||||
) : invoices.length === 0 ? (
|
||||
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No invoices found</td></tr>
|
||||
) : invoices.map((inv: any) => (
|
||||
<tr key={inv.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{inv.invoice_number}</td>
|
||||
<tr key={inv.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
|
||||
<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-[#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-green-400 text-xs">{inv.amount_cents ? fmt(inv.amount_cents) : '—'}</td>
|
||||
<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 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>
|
||||
@@ -195,7 +195,7 @@ function InvoicesContent() {
|
||||
|
||||
export default function InvoicesPage() {
|
||||
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 />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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: '' });
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -115,7 +115,7 @@ function OrdersContent() {
|
||||
<h1 className="text-xl font-bold">Orders</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -128,20 +128,20 @@ function OrdersContent() {
|
||||
|
||||
{/* Filters */}
|
||||
<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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</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>
|
||||
{STATUSES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</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>
|
||||
|
||||
{/* New Order Form */}
|
||||
{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>
|
||||
|
||||
{errors._form && (
|
||||
@@ -154,7 +154,7 @@ function OrdersContent() {
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
|
||||
</select>
|
||||
@@ -162,7 +162,7 @@ function OrdersContent() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
@@ -170,67 +170,67 @@ function OrdersContent() {
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
{products.map((p: any) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>}
|
||||
</div>
|
||||
<div>
|
||||
<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>}
|
||||
</div>
|
||||
<div>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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 className="col-span-2 md:col-span-3">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<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 => (
|
||||
<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 ? (
|
||||
<tr><td colSpan={10} className="px-4 py-8 text-center text-[#555] text-xs">No orders found</td></tr>
|
||||
) : 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-[#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-[#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 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">
|
||||
<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 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>
|
||||
</tr>
|
||||
))}
|
||||
@@ -269,7 +269,7 @@ function OrdersContent() {
|
||||
|
||||
export default function OrdersPage() {
|
||||
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 />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function AccountingDashboard() {
|
||||
|
||||
if (loading || loadingData) return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-[#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>
|
||||
);
|
||||
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>
|
||||
</div>
|
||||
<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/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/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/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/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/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-[#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-[#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-[#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-[#231d18] border border-[#2a3a50] rounded text-xs text-[#F0A623] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stat Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
|
||||
{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-xs text-[#888] mt-1">{c.label}</p>
|
||||
</Link>
|
||||
@@ -78,13 +78,13 @@ export default function AccountingDashboard() {
|
||||
</div>
|
||||
|
||||
{/* Site Revenue Table */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#252525]">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#3a322b]">
|
||||
<h2 className="text-sm font-semibold text-white">Revenue by Site</h2>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -94,7 +94,7 @@ export default function AccountingDashboard() {
|
||||
{siteRevenue.length === 0 ? (
|
||||
<tr><td colSpan={5} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
|
||||
) : 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-green-400">{fmt(row.revenue_mtd ?? 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>
|
||||
|
||||
{/* Top Clients */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#252525]">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#3a322b]">
|
||||
<h2 className="text-sm font-semibold text-white">Top 5 Clients YTD</h2>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -123,7 +123,7 @@ export default function AccountingDashboard() {
|
||||
{topClients.length === 0 ? (
|
||||
<tr><td colSpan={3} className="px-4 py-6 text-center text-[#555] text-xs">No data yet</td></tr>
|
||||
) : 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-green-400">{fmt(c.revenue_ytd ?? 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: 'Documents', href: '/admin/accounting/documents' },
|
||||
].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}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ReconciliationPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -59,7 +59,7 @@ export default function ReconciliationPage() {
|
||||
<h1 className="text-xl font-bold">Bank Reconciliation</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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'}
|
||||
<input type="file" accept=".csv" className="hidden" onChange={handleUpload} disabled={uploading} />
|
||||
</label>
|
||||
@@ -78,10 +78,10 @@ export default function ReconciliationPage() {
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -93,7 +93,7 @@ export default function ReconciliationPage() {
|
||||
) : 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>
|
||||
) : 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-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>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function ReportsPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -40,41 +40,41 @@ export default function ReportsPage() {
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</div>
|
||||
<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>
|
||||
<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" />
|
||||
<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>
|
||||
<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-[#F0A623] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{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 ? (
|
||||
<div className="space-y-4">
|
||||
{/* P&L Summary */}
|
||||
<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-2xl font-bold text-green-400 mt-1">{fmt(report.total_revenue_cents ?? 0)}</p>
|
||||
</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-2xl font-bold text-red-400 mt-1">{fmt(report.total_expenses_cents ?? 0)}</p>
|
||||
</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-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>
|
||||
|
||||
{/* Revenue by Site */}
|
||||
<div className="bg-[#111] border border-[#252525] 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="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<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">
|
||||
<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>
|
||||
{(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-green-400 text-xs">{fmt(r.revenue_cents ?? 0)}</td>
|
||||
</tr>
|
||||
@@ -84,13 +84,13 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
|
||||
{/* Top Clients */}
|
||||
<div className="bg-[#111] border border-[#252525] 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="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<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">
|
||||
<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>
|
||||
{(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-green-400 text-xs">{fmt(c.revenue_cents ?? 0)}</td>
|
||||
</tr>
|
||||
@@ -100,13 +100,13 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
|
||||
{/* Expenses by Category */}
|
||||
<div className="bg-[#111] border border-[#252525] 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="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<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">
|
||||
<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>
|
||||
{(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-red-400 text-xs">{fmt(c.amount_cents ?? 0)}</td>
|
||||
</tr>
|
||||
|
||||
@@ -97,7 +97,7 @@ export default function StaffPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -108,7 +108,7 @@ export default function StaffPage() {
|
||||
<h1 className="text-xl font-bold">Sales Staff</h1>
|
||||
<Link href="/admin/accounting" className="text-xs text-[#555] hover:text-[#888]">← Accounting</Link>
|
||||
</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>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -120,7 +120,7 @@ export default function StaffPage() {
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
{errors._form && (
|
||||
@@ -140,7 +140,7 @@ export default function StaffPage() {
|
||||
type={f.key === 'email' ? 'email' : 'text'}
|
||||
value={(form as any)[f.key]}
|
||||
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>}
|
||||
</div>
|
||||
@@ -149,34 +149,34 @@ export default function StaffPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<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>
|
||||
{tiers.map((tier, i) => (
|
||||
<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>
|
||||
<div className="flex-1">
|
||||
<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 className="flex-1">
|
||||
<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>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -188,16 +188,16 @@ export default function StaffPage() {
|
||||
) : staff.length === 0 ? (
|
||||
<tr><td colSpan={6} className="px-4 py-8 text-center text-[#555] text-xs">No staff found</td></tr>
|
||||
) : 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-[#888] text-xs">{s.email}</td>
|
||||
<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 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">
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
|
||||
scheduled: 'bg-blue-400/10 text-blue-400',
|
||||
paused: 'bg-gray-600/20 text-gray-400',
|
||||
expired: 'bg-red-400/10 text-red-400',
|
||||
cancelled: 'bg-[#1a1a1a] text-[#555]',
|
||||
cancelled: 'bg-[#231d18] text-[#555]',
|
||||
};
|
||||
|
||||
export default function AdOpsIOsPage() {
|
||||
@@ -73,7 +73,7 @@ export default function AdOpsIOsPage() {
|
||||
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;
|
||||
|
||||
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>
|
||||
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]">← Ad Operations</Link>
|
||||
</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 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>
|
||||
{['scheduled','active','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</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>
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
{clients.map((c: any) => <option key={c.id} value={c.id}>{c.company_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
{staff.map((s: any) => <option key={s.id} value={s.id}>{s.full_name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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 className="flex items-center gap-3 pt-4">
|
||||
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
|
||||
@@ -159,47 +159,47 @@ export default function AdOpsIOsPage() {
|
||||
{form.is_comp && (
|
||||
<div>
|
||||
<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>
|
||||
)}
|
||||
{isEmailProduct && (
|
||||
<>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
{isAdvertorial && (
|
||||
<div>
|
||||
<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 className="col-span-2 md:col-span-3">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<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 => (
|
||||
<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 ? (
|
||||
<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) => (
|
||||
<tr key={io.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-[#ffb800] font-mono text-xs">{io.existing_io_id}</td>
|
||||
<tr key={io.id} className="border-b border-[#231d18] hover:bg-[#231d18]">
|
||||
<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-[#ccc] text-xs">{io.rmp_products?.name ?? '—'}</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>
|
||||
</td>
|
||||
<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' && (
|
||||
<button onClick={() => { const r = prompt('Reactivation reason?'); if (r) updateStatus(io.id, 'active', r); }} className="text-xs text-green-400 hover:underline">Reactivate</button>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function AdOpsNotificationsPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -33,10 +33,10 @@ export default function AdOpsNotificationsPage() {
|
||||
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]">← Ad Operations</Link>
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -48,9 +48,9 @@ export default function AdOpsNotificationsPage() {
|
||||
) : notifications.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No notifications yet</td></tr>
|
||||
) : 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-[#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-[#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>
|
||||
|
||||
@@ -9,7 +9,7 @@ const FLIGHT_COLORS: Record<string, string> = {
|
||||
scheduled: 'bg-blue-400/10 text-blue-400 border-blue-400/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',
|
||||
cancelled: 'bg-[#1a1a1a] text-[#555] border-[#252525]',
|
||||
cancelled: 'bg-[#231d18] text-[#555] border-[#3a322b]',
|
||||
};
|
||||
|
||||
export default function AdOpsDashboardPage() {
|
||||
@@ -43,7 +43,7 @@ export default function AdOpsDashboardPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -64,7 +64,7 @@ export default function AdOpsDashboardPage() {
|
||||
{ label: 'Unpaid Active', key: 'unpaid_active', color: 'text-orange-400' },
|
||||
{ label: 'Email Blasts Scheduled', key: 'email_scheduled', color: 'text-purple-400' },
|
||||
].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-xs text-[#888] mt-1">{c.label}</p>
|
||||
</div>
|
||||
@@ -73,25 +73,25 @@ export default function AdOpsDashboardPage() {
|
||||
|
||||
{/* Filters */}
|
||||
<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>
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</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>
|
||||
{['active','scheduled','paused','expired','cancelled'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Active Flights Table */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-x-auto">
|
||||
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-x-auto">
|
||||
<div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
|
||||
<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>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -103,7 +103,7 @@ export default function AdOpsDashboardPage() {
|
||||
) : flights.length === 0 ? (
|
||||
<tr><td colSpan={9} className="px-4 py-8 text-center text-[#555] text-xs">No flights found</td></tr>
|
||||
) : 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-[#ccc] text-xs">{f.rmp_products?.name ?? '—'}</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>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
@@ -126,13 +126,13 @@ export default function AdOpsDashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Email Schedule */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#252525]">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#3a322b]">
|
||||
<h2 className="text-sm font-semibold">Email Distribution Schedule</h2>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -142,7 +142,7 @@ export default function AdOpsDashboardPage() {
|
||||
{emailSchedule.length === 0 ? (
|
||||
<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) => (
|
||||
<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-[#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>
|
||||
@@ -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>
|
||||
</td>
|
||||
<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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function AdOpsProductsPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -60,62 +60,62 @@ export default function AdOpsProductsPage() {
|
||||
<h1 className="text-xl font-bold">Product Catalog</h1>
|
||||
<Link href="/admin/adops" className="text-xs text-[#555] hover:text-[#888]">← Ad Operations</Link>
|
||||
</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 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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
{PRODUCT_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>
|
||||
<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 className="col-span-2 md:col-span-3">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -127,17 +127,17 @@ export default function AdOpsProductsPage() {
|
||||
) : products.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No products found</td></tr>
|
||||
) : 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-[#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-[#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">
|
||||
<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 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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -64,7 +64,7 @@ interface MonthlyTrend {
|
||||
// ─── Mock / derived data helpers ──────────────────────────────────────────────
|
||||
|
||||
const CATEGORY_COLORS = [
|
||||
'#ffb800', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#F0A623', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#ef4444', '#06b6d4', '#f97316', '#84cc16',
|
||||
];
|
||||
|
||||
@@ -78,9 +78,9 @@ interface StatCardProps {
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, accent = '#ffb800', icon }: StatCardProps) {
|
||||
function StatCard({ label, value, sub, accent = '#F0A623', icon }: StatCardProps) {
|
||||
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
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: `${accent}18` }}
|
||||
@@ -112,7 +112,7 @@ function SectionHeader({ title, subtitle }: { title: string; subtitle?: string }
|
||||
function ChartTooltip({ active, payload, label }: any) {
|
||||
if (!active || !payload?.length) return null;
|
||||
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>}
|
||||
{payload.map((entry: any, i: number) => (
|
||||
<p key={i} style={{ color: entry.color }} className="leading-5">
|
||||
@@ -252,7 +252,7 @@ export default function AdminAnalyticsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,7 +266,7 @@ export default function AdminAnalyticsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||||
{/* ── 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>
|
||||
<h1 className="text-white text-xl font-bold font-heading uppercase tracking-wider">Analytics</h1>
|
||||
@@ -275,20 +275,20 @@ export default function AdminAnalyticsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<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
|
||||
</Link>
|
||||
<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
|
||||
</Link>
|
||||
<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
|
||||
</Link>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -312,7 +312,7 @@ export default function AdminAnalyticsPage() {
|
||||
label="Total Articles"
|
||||
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>}
|
||||
accent="#ffb800"
|
||||
accent="#F0A623"
|
||||
/>
|
||||
<StatCard
|
||||
label="Published"
|
||||
@@ -361,7 +361,7 @@ export default function AdminAnalyticsPage() {
|
||||
value={`${publishRate}%`}
|
||||
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>}
|
||||
accent="#ffb800"
|
||||
accent="#F0A623"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -369,7 +369,7 @@ export default function AdminAnalyticsPage() {
|
||||
{/* ── Monthly Trend ────────────────────────────────────────────── */}
|
||||
<section>
|
||||
<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 ? (
|
||||
<ResponsiveContainer width="100%" height={240}>
|
||||
<LineChart data={monthlyTrends} margin={{ top: 4, right: 8, left: -20, bottom: 0 }}>
|
||||
@@ -395,7 +395,7 @@ export default function AdminAnalyticsPage() {
|
||||
{/* Author Contributions */}
|
||||
<section>
|
||||
<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 ? (
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<BarChart
|
||||
@@ -429,7 +429,7 @@ export default function AdminAnalyticsPage() {
|
||||
{/* Top Categories */}
|
||||
<section>
|
||||
<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 ? (
|
||||
<>
|
||||
<ResponsiveContainer width="100%" height={200}>
|
||||
@@ -481,7 +481,7 @@ export default function AdminAnalyticsPage() {
|
||||
<SectionHeader title="Import Success Rates" subtitle="Publishing success across content sources" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{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">
|
||||
<p className="text-white text-sm font-bold font-heading uppercase tracking-wider">{stat.label}</p>
|
||||
<span
|
||||
@@ -526,7 +526,7 @@ export default function AdminAnalyticsPage() {
|
||||
{/* ── Author Detail Table ──────────────────────────────────────── */}
|
||||
<section>
|
||||
<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">
|
||||
<table className="w-full text-sm font-body">
|
||||
<thead>
|
||||
@@ -550,7 +550,7 @@ export default function AdminAnalyticsPage() {
|
||||
return (
|
||||
<tr
|
||||
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-right text-[#888]">{a.total}</td>
|
||||
|
||||
@@ -33,7 +33,7 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
published: 'text-green-400 bg-green-400/10',
|
||||
draft: 'text-yellow-400 bg-yellow-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> = {
|
||||
@@ -384,7 +384,7 @@ export default function AdminArticlesPage() {
|
||||
if (loading || (!user && !loading)) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -406,10 +406,10 @@ export default function AdminArticlesPage() {
|
||||
)}
|
||||
|
||||
{/* 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="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-[#888] text-sm">Admin</span>
|
||||
<span className="text-[#333]">/</span>
|
||||
@@ -423,13 +423,13 @@ export default function AdminArticlesPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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>
|
||||
New Article
|
||||
</button>
|
||||
<Link
|
||||
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>
|
||||
WP Import
|
||||
</Link>
|
||||
@@ -471,7 +471,7 @@ export default function AdminArticlesPage() {
|
||||
{ label: 'Drafts', value: counts.draft, color: 'text-yellow-400' },
|
||||
{ label: 'Archived', value: counts.archived, color: 'text-[#555]' },
|
||||
].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-[#555] text-xs mt-0.5 uppercase tracking-wider">{s.label}</p>
|
||||
</div>
|
||||
@@ -479,7 +479,7 @@ export default function AdminArticlesPage() {
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={tab.key}
|
||||
@@ -492,7 +492,7 @@ export default function AdminArticlesPage() {
|
||||
{tab.label}
|
||||
{tab.key !== 'all' && (
|
||||
<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]}
|
||||
</span>
|
||||
@@ -512,13 +512,13 @@ export default function AdminArticlesPage() {
|
||||
placeholder="Search articles..."
|
||||
value={searchQuery}
|
||||
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>
|
||||
<select
|
||||
value={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="imported">Imported (WP)</option>
|
||||
<option value="native">Native</option>
|
||||
@@ -527,13 +527,13 @@ export default function AdminArticlesPage() {
|
||||
|
||||
{/* Bulk Actions Bar */}
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#4a3500] rounded-lg px-4 py-3">
|
||||
<span className="text-[#ffb800] text-sm font-bold">{selectedIds.size} selected</span>
|
||||
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#412402] rounded-lg px-4 py-3">
|
||||
<span className="text-[#F0A623] text-sm font-bold">{selectedIds.size} selected</span>
|
||||
<div className="flex-1" />
|
||||
<select
|
||||
value={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="publish">Publish</option>
|
||||
<option value="pending">Submit for Review</option>
|
||||
@@ -544,7 +544,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleBulkAction}
|
||||
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 ? (
|
||||
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : null}
|
||||
@@ -568,15 +568,15 @@ export default function AdminArticlesPage() {
|
||||
<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>
|
||||
</div>
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{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' : ''}`}>
|
||||
{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" />
|
||||
</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]">
|
||||
<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>
|
||||
@@ -587,9 +587,9 @@ export default function AdminArticlesPage() {
|
||||
<div className="flex items-center gap-3 mt-1">
|
||||
<span className="text-[11px] text-[#555]">{article.author_name || 'Unknown author'}</span>
|
||||
{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'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -606,13 +606,13 @@ export default function AdminArticlesPage() {
|
||||
</button>
|
||||
<button
|
||||
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>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
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">
|
||||
<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" />
|
||||
@@ -627,14 +627,14 @@ export default function AdminArticlesPage() {
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<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
|
||||
type="checkbox"
|
||||
checked={articles.length > 0 && selectedIds.size === articles.length}
|
||||
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 w-24 hidden md:block">Category</span>
|
||||
@@ -646,7 +646,7 @@ export default function AdminArticlesPage() {
|
||||
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : articles.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -656,7 +656,7 @@ export default function AdminArticlesPage() {
|
||||
<p className="text-[#555] text-sm">No articles found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{articles.map((article) => (
|
||||
<div
|
||||
key={article.id}
|
||||
@@ -668,13 +668,13 @@ export default function AdminArticlesPage() {
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(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 */}
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{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
|
||||
src={article.featured_image}
|
||||
alt={article.featured_image_alt || article.title}
|
||||
@@ -684,7 +684,7 @@ export default function AdminArticlesPage() {
|
||||
/>
|
||||
</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]">
|
||||
<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>
|
||||
@@ -699,7 +699,7 @@ export default function AdminArticlesPage() {
|
||||
{/* Category */}
|
||||
<div className="w-24 hidden md:block">
|
||||
{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}
|
||||
</span>
|
||||
) : (
|
||||
@@ -710,7 +710,7 @@ export default function AdminArticlesPage() {
|
||||
{/* Source */}
|
||||
<div className="w-20 hidden lg:block">
|
||||
<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'}
|
||||
</span>
|
||||
@@ -730,7 +730,7 @@ export default function AdminArticlesPage() {
|
||||
|
||||
{/* Status */}
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -741,7 +741,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => openEdit(article)}
|
||||
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">
|
||||
<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" />
|
||||
@@ -753,7 +753,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleStatusChange(article, 'pending')}
|
||||
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">
|
||||
<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>
|
||||
@@ -765,7 +765,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleStatusChange(article, 'published')}
|
||||
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">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
@@ -777,7 +777,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleStatusChange(article, '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">
|
||||
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
@@ -789,7 +789,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleStatusChange(article, 'published')}
|
||||
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">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" /><polyline points="22 4 12 14.01 9 11.01" />
|
||||
</svg>
|
||||
@@ -801,7 +801,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleStatusChange(article, 'archived')}
|
||||
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">
|
||||
<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>
|
||||
@@ -812,7 +812,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => handleDelete(article)}
|
||||
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">
|
||||
<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" />
|
||||
@@ -834,12 +834,12 @@ export default function AdminArticlesPage() {
|
||||
{/* New Article Modal */}
|
||||
{showNewArticle && (
|
||||
<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 */}
|
||||
<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">
|
||||
<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>
|
||||
<button
|
||||
onClick={() => setShowNewArticle(false)}
|
||||
@@ -859,7 +859,7 @@ export default function AdminArticlesPage() {
|
||||
value={newTitle}
|
||||
onChange={(e) => setNewTitle(e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -871,7 +871,7 @@ export default function AdminArticlesPage() {
|
||||
onChange={(e) => setNewExcerpt(e.target.value)}
|
||||
placeholder="Brief summary shown in article listings..."
|
||||
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>
|
||||
|
||||
@@ -894,7 +894,7 @@ export default function AdminArticlesPage() {
|
||||
value={newAuthor}
|
||||
onChange={(e) => setNewAuthor(e.target.value)}
|
||||
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>
|
||||
@@ -904,7 +904,7 @@ export default function AdminArticlesPage() {
|
||||
value={newCategory}
|
||||
onChange={(e) => setNewCategory(e.target.value)}
|
||||
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>
|
||||
@@ -912,7 +912,7 @@ export default function AdminArticlesPage() {
|
||||
<select
|
||||
value={newStatus}
|
||||
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="pending">Submit for Review</option>
|
||||
<option value="published">Publish Now</option>
|
||||
@@ -929,7 +929,7 @@ export default function AdminArticlesPage() {
|
||||
value={newFeaturedImage}
|
||||
onChange={(e) => setNewFeaturedImage(e.target.value)}
|
||||
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>
|
||||
@@ -939,21 +939,21 @@ export default function AdminArticlesPage() {
|
||||
value={newFeaturedImageAlt}
|
||||
onChange={(e) => setNewFeaturedImageAlt(e.target.value)}
|
||||
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>
|
||||
|
||||
{/* Featured image preview */}
|
||||
{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" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 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]">
|
||||
{newStatus === 'draft' ? 'Saved as draft — not publicly visible' : newStatus === 'pending' ? 'Submitted for editorial review' : 'Will be published immediately'}
|
||||
</p>
|
||||
@@ -966,7 +966,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleNewArticleSave}
|
||||
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 ? (
|
||||
<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 */}
|
||||
{editingArticle && (
|
||||
<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="flex items-center justify-between px-5 py-4 border-b border-[#252525]">
|
||||
<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-[#3a322b]">
|
||||
<h2 className="text-base font-bold text-white font-heading">Edit Article</h2>
|
||||
<button
|
||||
onClick={() => setEditingArticle(null)}
|
||||
@@ -1002,7 +1002,7 @@ export default function AdminArticlesPage() {
|
||||
type="text"
|
||||
value={editTitle}
|
||||
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 className="grid grid-cols-2 gap-4">
|
||||
@@ -1012,7 +1012,7 @@ export default function AdminArticlesPage() {
|
||||
type="text"
|
||||
value={editAuthor}
|
||||
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>
|
||||
@@ -1021,7 +1021,7 @@ export default function AdminArticlesPage() {
|
||||
type="text"
|
||||
value={editCategory}
|
||||
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>
|
||||
@@ -1030,7 +1030,7 @@ export default function AdminArticlesPage() {
|
||||
<select
|
||||
value={editStatus}
|
||||
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="pending">Pending Review</option>
|
||||
<option value="published">Published</option>
|
||||
@@ -1040,19 +1040,19 @@ export default function AdminArticlesPage() {
|
||||
|
||||
{/* Scheduling — only shown for draft status */}
|
||||
{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">
|
||||
<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" />
|
||||
</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>
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={editScheduledAt}
|
||||
onChange={(e) => setEditScheduledAt(e.target.value)}
|
||||
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 && (
|
||||
<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">
|
||||
<span className="text-xs text-[#555]">Source:</span>
|
||||
<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'}
|
||||
</span>
|
||||
</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
|
||||
onClick={() => setEditingArticle(null)}
|
||||
className="text-[#666] hover:text-[#999] text-sm transition-colors px-4 py-2">
|
||||
@@ -1090,7 +1090,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleEditSave}
|
||||
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 ? (
|
||||
<div className="w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
) : null}
|
||||
|
||||
@@ -50,13 +50,13 @@ const ACTION_COLORS: Record<string, string> = {
|
||||
article_published: 'text-green-400 bg-green-400/10',
|
||||
article_unpublished: 'text-yellow-400 bg-yellow-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',
|
||||
user_role_changed: 'text-orange-400 bg-orange-400/10',
|
||||
user_activated: 'text-green-400 bg-green-400/10',
|
||||
user_deactivated: 'text-red-400 bg-red-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_failed: 'text-red-400 bg-red-400/10',
|
||||
};
|
||||
@@ -150,7 +150,7 @@ interface StatCardProps {
|
||||
|
||||
function StatCard({ label, value, accent, icon }: StatCardProps) {
|
||||
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}`}>
|
||||
{icon}
|
||||
</div>
|
||||
@@ -170,16 +170,16 @@ interface 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 performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
|
||||
|
||||
return (
|
||||
<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="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 */}
|
||||
<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={`w-8 h-8 rounded-lg flex items-center justify-center ${actionColor}`}>
|
||||
{ACTION_ICONS[log.action] || getDefaultIcon(log.action)}
|
||||
@@ -247,7 +247,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
|
||||
{log.metadata && Object.keys(log.metadata).length > 0 && (
|
||||
<div>
|
||||
<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">
|
||||
{JSON.stringify(log.metadata, null, 2)}
|
||||
</pre>
|
||||
@@ -340,7 +340,7 @@ export default function AuditLogPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -368,7 +368,7 @@ export default function AuditLogPage() {
|
||||
</div>
|
||||
<button
|
||||
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">
|
||||
<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
|
||||
label="Total Events"
|
||||
value={logs.length}
|
||||
accent="bg-[#ffb800]/10 text-[#ffb800]"
|
||||
accent="bg-[#F0A623]/10 text-[#F0A623]"
|
||||
icon={
|
||||
<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" />
|
||||
@@ -433,19 +433,19 @@ export default function AuditLogPage() {
|
||||
placeholder="Search by user, item, or action…"
|
||||
value={searchQuery}
|
||||
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>
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setActionFilter(f)}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
|
||||
actionFilter === f
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#555] hover:text-[#888]'
|
||||
}`}
|
||||
>
|
||||
@@ -466,9 +466,9 @@ export default function AuditLogPage() {
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<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">Performed By</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 && (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -489,14 +489,14 @@ export default function AuditLogPage() {
|
||||
{/* Empty */}
|
||||
{!loadingData && filteredLogs.length === 0 && (
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<p className="text-[#555] text-sm font-body">No audit log entries found</p>
|
||||
{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
|
||||
</button>
|
||||
)}
|
||||
@@ -505,7 +505,7 @@ export default function AuditLogPage() {
|
||||
|
||||
{/* Rows */}
|
||||
{!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 icon = ACTION_ICONS[log.action] || getDefaultIcon(log.action);
|
||||
const performerName = log.user_profiles?.full_name || log.performed_by_email || 'System';
|
||||
@@ -513,8 +513,8 @@ export default function AuditLogPage() {
|
||||
return (
|
||||
<div
|
||||
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 ${
|
||||
idx !== filteredLogs.length - 1 ? 'border-b border-[#1a1a1a]' : ''
|
||||
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-[#231d18]' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedLog(log)}
|
||||
>
|
||||
@@ -530,7 +530,7 @@ export default function AuditLogPage() {
|
||||
|
||||
{/* Performed By */}
|
||||
<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">
|
||||
{performerName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -569,7 +569,7 @@ export default function AuditLogPage() {
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<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
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
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
|
||||
</button>
|
||||
@@ -601,7 +601,7 @@ export default function AuditLogPage() {
|
||||
<button
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -162,7 +162,7 @@ export default function BannedTermsPage() {
|
||||
if (loading || loadingData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-[#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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
</div>
|
||||
<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>
|
||||
Export CSV
|
||||
</button>
|
||||
<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>}
|
||||
Scan Published Posts
|
||||
</button>
|
||||
<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>
|
||||
Add Term
|
||||
</button>
|
||||
@@ -209,7 +209,7 @@ export default function BannedTermsPage() {
|
||||
|
||||
{/* Retroactive scan results */}
|
||||
{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">
|
||||
<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>
|
||||
@@ -234,18 +234,18 @@ export default function BannedTermsPage() {
|
||||
|
||||
{/* Add/Edit Form */}
|
||||
{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>
|
||||
<form onSubmit={handleSave} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<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"
|
||||
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>
|
||||
<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' }))}
|
||||
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="hard">Hard Ban — Block submission entirely</option>
|
||||
</select>
|
||||
@@ -253,7 +253,7 @@ export default function BannedTermsPage() {
|
||||
<div>
|
||||
<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 }))}
|
||||
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="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">
|
||||
<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..."
|
||||
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 className="md:col-span-2 flex gap-3">
|
||||
<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'}
|
||||
</button>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -281,22 +281,22 @@ export default function BannedTermsPage() {
|
||||
{/* Filters */}
|
||||
<div className="mb-4 flex flex-wrap gap-3">
|
||||
<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)}
|
||||
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="soft">Soft Ban</option>
|
||||
<option value="hard">Hard Ban</option>
|
||||
</select>
|
||||
<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="avbeat">AV Beat</option>
|
||||
<option value="avbeat">AV Beat</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
<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="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
@@ -304,8 +304,8 @@ export default function BannedTermsPage() {
|
||||
</div>
|
||||
|
||||
{/* Terms Table */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<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>
|
||||
</div>
|
||||
{terms.length === 0 ? (
|
||||
@@ -316,15 +316,15 @@ export default function BannedTermsPage() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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 => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#1a1a1a]">
|
||||
<tbody className="divide-y divide-[#231d18]">
|
||||
{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">
|
||||
<span className="text-sm font-body font-semibold text-white">{term.term}</span>
|
||||
</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>
|
||||
</td>
|
||||
<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'}
|
||||
</span>
|
||||
</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">
|
||||
<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}
|
||||
className="text-xs text-[#888] hover:text-white font-body transition-colors disabled:opacity-50">
|
||||
{term.is_active ? 'Deactivate' : 'Reactivate'}
|
||||
@@ -366,7 +366,7 @@ export default function BannedTermsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
|
||||
@@ -54,7 +54,7 @@ function StatBlock({ label, b }: { label: string; b: StatBucket }) {
|
||||
<span className="font-semibold">{fmtNum(b.clicks)}</span>
|
||||
<span className="text-[#6b7280]"> clk</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>
|
||||
);
|
||||
@@ -140,7 +140,7 @@ export default function BannerRows({
|
||||
<input
|
||||
value={current.name}
|
||||
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>
|
||||
|
||||
@@ -150,7 +150,7 @@ export default function BannerRows({
|
||||
value={current.click_url || ""}
|
||||
onChange={(e) => field(b.id, "click_url", e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -160,7 +160,7 @@ export default function BannerRows({
|
||||
type="date"
|
||||
value={fmtDate(current.start_date)}
|
||||
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>
|
||||
|
||||
@@ -170,7 +170,7 @@ export default function BannerRows({
|
||||
type="date"
|
||||
value={fmtDate(current.end_date)}
|
||||
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>
|
||||
|
||||
@@ -179,7 +179,7 @@ export default function BannerRows({
|
||||
<select
|
||||
value={current.status}
|
||||
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="scheduled">scheduled</option>
|
||||
@@ -210,7 +210,7 @@ export default function BannerRows({
|
||||
<button
|
||||
onClick={() => save(b.id)}
|
||||
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
|
||||
</button>
|
||||
@@ -219,7 +219,7 @@ export default function BannerRows({
|
||||
</div>
|
||||
<Link
|
||||
href={`/admin/banners/${b.id}/analytics`}
|
||||
className="text-sm text-[#ffd25a] hover:underline"
|
||||
className="text-sm text-[#F0A623] hover:underline"
|
||||
>
|
||||
View detailed analytics →
|
||||
</Link>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function AnalyticsCharts({
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</section>
|
||||
|
||||
@@ -121,7 +121,7 @@ function TopListCard({ title, rows }: { title: string; rows: TopRow[] }) {
|
||||
{rows.map((r) => (
|
||||
<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 text-right text-[#ffd25a]">{r.count.toLocaleString()}</td>
|
||||
<td className="py-2 text-right text-[#F0A623]">{r.count.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
@@ -142,7 +142,7 @@ export default async function BannerAnalyticsPage({
|
||||
|
||||
return (
|
||||
<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
|
||||
</Link>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default async function BannerAnalyticsPage({
|
||||
<p className="text-sm text-[#9ca3af]">
|
||||
{b.size} · {b.status} ·{" "}
|
||||
{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 ? "…" : ""}
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -113,7 +113,7 @@ export default async function AdminBannersPage() {
|
||||
after the next refresh.
|
||||
</p>
|
||||
</div>
|
||||
<Link href="/admin" className="text-sm text-[#ffd25a] hover:underline">
|
||||
<Link href="/admin" className="text-sm text-[#F0A623] hover:underline">
|
||||
← Admin home
|
||||
</Link>
|
||||
</header>
|
||||
|
||||
@@ -171,7 +171,7 @@ export default function CompanyCoveragePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +218,7 @@ export default function CompanyCoveragePage() {
|
||||
<button
|
||||
onClick={handleScan}
|
||||
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 ? (
|
||||
<>
|
||||
@@ -255,14 +255,14 @@ export default function CompanyCoveragePage() {
|
||||
<div className="lg:col-span-2">
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -272,7 +272,7 @@ export default function CompanyCoveragePage() {
|
||||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<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">
|
||||
@@ -282,13 +282,13 @@ export default function CompanyCoveragePage() {
|
||||
{activeTab === 'queued' ? 'No stories queued — run a scan to detect companies' : `No ${activeTab} stories`}
|
||||
</p>
|
||||
{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 →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{queue.map((item) => (
|
||||
<div key={item.id} className="p-4">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
@@ -332,7 +332,7 @@ export default function CompanyCoveragePage() {
|
||||
</button>
|
||||
<button
|
||||
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'}
|
||||
</button>
|
||||
@@ -349,7 +349,7 @@ export default function CompanyCoveragePage() {
|
||||
|
||||
{/* Expanded Draft Preview */}
|
||||
{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>
|
||||
<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>
|
||||
@@ -370,11 +370,11 @@ export default function CompanyCoveragePage() {
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
{/* 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>
|
||||
<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" />
|
||||
</svg>
|
||||
<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>
|
||||
</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" />
|
||||
<p className="text-xs text-[#666] font-body">Manual review required before publishing</p>
|
||||
</div>
|
||||
@@ -413,7 +413,7 @@ export default function CompanyCoveragePage() {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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" />
|
||||
@@ -421,14 +421,14 @@ export default function CompanyCoveragePage() {
|
||||
<h3 className="text-sm font-display font-bold text-white">Reminder Email</h3>
|
||||
</div>
|
||||
<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">
|
||||
<p className="text-xs text-[#ffb800] font-body font-mono break-all">{settings.reminder_email}</p>
|
||||
<div className="bg-[#1c1815] border border-[#231d18] rounded-md px-3 py-2">
|
||||
<p className="text-xs text-[#F0A623] font-body font-mono break-all">{settings.reminder_email}</p>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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" />
|
||||
@@ -456,7 +456,7 @@ export default function CompanyCoveragePage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<ol className="space-y-2">
|
||||
{[
|
||||
@@ -467,7 +467,7 @@ export default function CompanyCoveragePage() {
|
||||
'Email reminder sent to coverage contact',
|
||||
].map((step, i) => (
|
||||
<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}
|
||||
</span>
|
||||
<span className="text-xs text-[#666] font-body leading-relaxed">{step}</span>
|
||||
|
||||
@@ -245,7 +245,7 @@ export default function CompanyTrackerPage() {
|
||||
if (loading || loadingData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-[#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>
|
||||
);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export default function CompanyTrackerPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 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 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>
|
||||
@@ -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 }) });
|
||||
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'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<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 ? 'Scanning…' : 'Scan Articles Now'}
|
||||
</button>
|
||||
@@ -313,7 +313,7 @@ export default function CompanyTrackerPage() {
|
||||
{ label: 'Companies Found', value: settings.last_scan_companies_found },
|
||||
{ label: 'Tracked Companies', value: companies.length || '—' },
|
||||
].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-xs text-[#555] font-body">{stat.label}</div>
|
||||
</div>
|
||||
@@ -322,10 +322,10 @@ export default function CompanyTrackerPage() {
|
||||
)}
|
||||
|
||||
{/* 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 => (
|
||||
<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}
|
||||
</button>
|
||||
))}
|
||||
@@ -337,12 +337,12 @@ export default function CompanyTrackerPage() {
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
{(['generated','pending','approved','published','rejected'] as QueueStatus[]).map(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}
|
||||
</button>
|
||||
))}
|
||||
<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="1">AV Beat</option>
|
||||
<option value="2">AV Beat</option>
|
||||
@@ -350,18 +350,18 @@ export default function CompanyTrackerPage() {
|
||||
</div>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{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="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<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 && (
|
||||
<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
|
||||
@@ -398,7 +398,7 @@ export default function CompanyTrackerPage() {
|
||||
</div>
|
||||
</div>
|
||||
{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"
|
||||
dangerouslySetInnerHTML={{ __html: item.generated_content }} />
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
@@ -430,30 +430,30 @@ export default function CompanyTrackerPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<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>
|
||||
Add Company
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<form onSubmit={handleAddCompany} className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<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
|
||||
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>
|
||||
<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"
|
||||
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>
|
||||
<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 }))}
|
||||
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="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
@@ -462,51 +462,51 @@ export default function CompanyTrackerPage() {
|
||||
<div>
|
||||
<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 }))}
|
||||
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="high">High (every 3 days)</option>
|
||||
<option value="critical">Critical (daily)</option>
|
||||
</select>
|
||||
</div>
|
||||
<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="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="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-[#231d18] border border-[#333] text-[#888] hover:text-white text-sm font-body rounded-lg transition-colors">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<table className="w-full">
|
||||
<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 => (
|
||||
<th key={h} className="px-4 py-3 text-left text-xs font-body font-bold text-[#555] uppercase tracking-wider">{h}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#1a1a1a]">
|
||||
<tbody className="divide-y divide-[#231d18]">
|
||||
{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>
|
||||
) : 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-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-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<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'}`} />
|
||||
</button>
|
||||
</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">
|
||||
<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'); }}
|
||||
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}
|
||||
className="text-xs text-red-500 hover:text-red-400 font-body transition-colors disabled:opacity-50">Remove</button>
|
||||
</div>
|
||||
@@ -522,21 +522,21 @@ export default function CompanyTrackerPage() {
|
||||
|
||||
{/* ── Crawl Log Tab ── */}
|
||||
{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">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#1a1a1a]">
|
||||
<tbody className="divide-y divide-[#231d18]">
|
||||
{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>
|
||||
) : 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-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>
|
||||
@@ -558,7 +558,7 @@ export default function CompanyTrackerPage() {
|
||||
{/* ── Settings Tab ── */}
|
||||
{activeTab === 'settings' && (
|
||||
<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>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<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'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<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 }))}
|
||||
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 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>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<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'}`} />
|
||||
</button>
|
||||
</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>
|
||||
<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'}`} />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<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) }))}
|
||||
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>
|
||||
<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) }))}
|
||||
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>
|
||||
|
||||
<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'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -97,7 +97,7 @@ function StatCard({
|
||||
good?: boolean;
|
||||
}) {
|
||||
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-sm text-[#888] font-body">{label}</p>
|
||||
{sub && <p className="text-xs text-[#555] font-body">{sub}</p>}
|
||||
@@ -173,7 +173,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
|
||||
};
|
||||
|
||||
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>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div>
|
||||
@@ -181,7 +181,7 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
|
||||
<select
|
||||
value={site}
|
||||
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]) => (
|
||||
<option key={key} value={key}>{val.label}</option>
|
||||
@@ -195,21 +195,21 @@ function GeneratePanel({ onGenerated }: { onGenerated: () => void }) {
|
||||
value={topic}
|
||||
onChange={(e) => setTopic(e.target.value)}
|
||||
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 className="flex gap-2">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSEOBatch}
|
||||
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'}
|
||||
</button>
|
||||
@@ -279,7 +279,7 @@ export default function ContentStatusPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,7 +296,7 @@ export default function ContentStatusPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||||
{/* 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="flex items-center gap-3">
|
||||
<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
|
||||
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
|
||||
</button>
|
||||
@@ -395,7 +395,7 @@ export default function ContentStatusPage() {
|
||||
{Object.entries(SITE_LABELS).map(([key, cfg]) => {
|
||||
const count = status?.siteBreakdown?.[key] ?? 0;
|
||||
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-3xl font-display font-bold text-white">{count}</p>
|
||||
<p className="text-xs text-[#555] font-body">Target: {cfg.target}</p>
|
||||
@@ -484,7 +484,7 @@ export default function ContentStatusPage() {
|
||||
{/* API Reference */}
|
||||
<section>
|
||||
<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/pipeline-status', desc: 'Live pipeline + node status' },
|
||||
|
||||
@@ -90,7 +90,7 @@ function StatCard({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accent = '#ffb800',
|
||||
accent = '#F0A623',
|
||||
icon,
|
||||
trend,
|
||||
}: {
|
||||
@@ -102,7 +102,7 @@ function StatCard({
|
||||
trend?: { value: number; label: string };
|
||||
}) {
|
||||
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="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) {
|
||||
if (!active || !payload?.length) return null;
|
||||
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>}
|
||||
{payload.map((entry: any, i: number) => (
|
||||
<p key={i} style={{ color: entry.color }} className="leading-5">
|
||||
@@ -252,7 +252,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,7 +262,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||||
{/* 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="flex items-center gap-3">
|
||||
<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" />
|
||||
</svg>
|
||||
</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>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -280,17 +280,17 @@ export default function EmailAnalyticsDashboard() {
|
||||
onClick={() => setTimeRange(r)}
|
||||
className={`px-3 py-1.5 text-xs font-semibold font-body rounded-lg transition-colors ${
|
||||
timeRange === r
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#555] hover:text-white border border-[#3a322b] hover:border-[#444]'
|
||||
}`}
|
||||
>
|
||||
{r === '3m' ? '3 Mo' : r === '6m' ? '6 Mo' : 'All'}
|
||||
</button>
|
||||
))}
|
||||
<div className="w-px h-6 bg-[#252525] mx-1" />
|
||||
<div className="w-px h-6 bg-[#3a322b] mx-1" />
|
||||
<Link
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M3 14h18M10 3v18M14 3v18" />
|
||||
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label="Active Subscribers"
|
||||
value={fmt(stats.activeSubscribers)}
|
||||
sub={`${fmt(stats.unsubscribed)} unsubscribed`}
|
||||
accent="#ffb800"
|
||||
accent="#F0A623"
|
||||
trend={{ value: 12, label: 'vs last month' }}
|
||||
icon={
|
||||
<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>
|
||||
|
||||
{/* 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
|
||||
title="Send Volume Over Time"
|
||||
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)} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<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="clicks" name="Clicks" fill="#f59e0b20" stroke="#f59e0b" strokeWidth={1} radius={[3, 3, 0, 0]} />
|
||||
</ComposedChart>
|
||||
@@ -387,7 +387,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
title="Open Rate & Click-Through Rate Trend"
|
||||
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 }}>
|
||||
<defs>
|
||||
<linearGradient id="openGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#ffb800" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#ffb800" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#F0A623" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#F0A623" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
<linearGradient id="clickGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#10b981" stopOpacity={0.2} />
|
||||
@@ -418,10 +418,10 @@ export default function EmailAnalyticsDashboard() {
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="Open Rate"
|
||||
stroke="#ffb800"
|
||||
stroke="#F0A623"
|
||||
strokeWidth={2}
|
||||
fill="url(#openGrad)"
|
||||
dot={{ r: 3, fill: '#ffb800' }}
|
||||
dot={{ r: 3, fill: '#F0A623' }}
|
||||
activeDot={{ r: 5 }}
|
||||
/>
|
||||
<Area
|
||||
@@ -441,7 +441,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
{/* Subscriber Growth */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* 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
|
||||
title="Subscriber Growth"
|
||||
subtitle="Total subscribers and monthly net change"
|
||||
@@ -463,14 +463,14 @@ export default function EmailAnalyticsDashboard() {
|
||||
fill="#8b5cf618"
|
||||
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>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
title="New Subscribers vs Unsubscribes"
|
||||
subtitle="Monthly acquisition vs churn"
|
||||
@@ -492,7 +492,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
title="Performance vs Industry Benchmarks"
|
||||
subtitle="How your metrics compare to email marketing industry averages"
|
||||
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label: 'Open Rate',
|
||||
yours: stats.avgOpenRate,
|
||||
benchmark: 21.5,
|
||||
color: '#ffb800',
|
||||
color: '#F0A623',
|
||||
unit: '%',
|
||||
},
|
||||
{
|
||||
@@ -584,7 +584,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
<div className="flex flex-wrap gap-3 pb-4">
|
||||
<Link
|
||||
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">
|
||||
<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
|
||||
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">
|
||||
<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
|
||||
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">
|
||||
<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" />
|
||||
|
||||
@@ -75,7 +75,7 @@ function DomainsContent() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -86,41 +86,41 @@ function DomainsContent() {
|
||||
<h1 className="text-xl font-bold">Sending Domains</h1>
|
||||
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]">← Email</Link>
|
||||
</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 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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{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>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
<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">
|
||||
<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>
|
||||
@@ -142,12 +142,12 @@ function DomainsContent() {
|
||||
|
||||
<div className="space-y-2">
|
||||
{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 ? (
|
||||
<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) => (
|
||||
<div key={d.id} className="bg-[#111] border border-[#252525] 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 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-[#231d18]" onClick={() => setExpanded(expanded === d.id ? null : d.id)}>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<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>
|
||||
{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>}
|
||||
<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 className="flex gap-3 mt-1 text-xs text-[#555]">
|
||||
<span>Health: {d.health_score}/100</span>
|
||||
@@ -167,7 +167,7 @@ function DomainsContent() {
|
||||
</div>
|
||||
|
||||
{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 */}
|
||||
<div className="grid grid-cols-2 gap-x-6">
|
||||
<DnsRow label="SPF" valid={d.spf_valid} checkedAt={d.spf_checked_at} />
|
||||
@@ -178,7 +178,7 @@ function DomainsContent() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
@@ -197,10 +197,10 @@ function DomainsContent() {
|
||||
|
||||
{/* Actions */}
|
||||
<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'}
|
||||
</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'}
|
||||
</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">
|
||||
@@ -219,7 +219,7 @@ function DomainsContent() {
|
||||
|
||||
export default function EmailDomainsPage() {
|
||||
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 />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
@@ -42,7 +42,7 @@ export default function EmailDashboard() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -61,7 +61,7 @@ export default function EmailDashboard() {
|
||||
{ label: 'Total Suppressions', value: stats?.total_suppressions ?? 0, color: 'text-red-400' },
|
||||
{ label: 'Blacklisted Domains', value: stats?.blacklisted ?? 0, color: 'text-red-400' },
|
||||
].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-xs text-[#888] mt-1">{c.label}</p>
|
||||
</div>
|
||||
@@ -74,10 +74,10 @@ export default function EmailDashboard() {
|
||||
const primary = siteDomains.find((d: any) => d.is_primary);
|
||||
const suppCount = stats?.suppressions_by_site?.[site] ?? 0;
|
||||
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">
|
||||
<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 className="text-xs text-[#888]">
|
||||
<span className="text-white">{primary?.domain ?? 'No primary'}</span>
|
||||
@@ -113,7 +113,7 @@ export default function EmailDashboard() {
|
||||
{ label: 'Suppressions', href: '/admin/email/suppressions' },
|
||||
{ label: 'Send Log', href: '/admin/email/send-log' },
|
||||
].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}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function SendLogPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -41,20 +41,20 @@ export default function SendLogPage() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</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>
|
||||
{['queued','sent','failed','suppressed'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</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">
|
||||
<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 => (
|
||||
<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 ? (
|
||||
<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) => (
|
||||
<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-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-[#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">
|
||||
<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 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>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function SuppressionsPage() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -84,29 +84,29 @@ export default function SuppressionsPage() {
|
||||
<Link href="/admin/email" className="text-xs text-[#555] hover:text-[#888]">← Email</Link>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
{/* Site selector */}
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{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>
|
||||
|
||||
{/* Stats */}
|
||||
{stats && (
|
||||
<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-xs text-[#888] mt-0.5">Total Suppressed</p>
|
||||
</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-xs text-[#888] mt-0.5">Added This Month</p>
|
||||
</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-xs text-[#888] mt-0.5">Hostile Replies</p>
|
||||
</div>
|
||||
@@ -115,44 +115,44 @@ export default function SuppressionsPage() {
|
||||
|
||||
{/* Filters */}
|
||||
<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>
|
||||
{SUPP_TYPES.map(t => <option key={t} value={t}>{t.replace(/_/g, ' ')}</option>)}
|
||||
</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>
|
||||
|
||||
{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>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<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>
|
||||
<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>)}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<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 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="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="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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888] hover:text-white">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<thead>
|
||||
<tr className="border-b border-[#1a1a1a]">
|
||||
<tr className="border-b border-[#231d18]">
|
||||
{['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>
|
||||
))}
|
||||
@@ -164,10 +164,10 @@ export default function SuppressionsPage() {
|
||||
) : suppressions.length === 0 ? (
|
||||
<tr><td colSpan={7} className="px-4 py-8 text-center text-[#555] text-xs">No suppressions found</td></tr>
|
||||
) : 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">
|
||||
<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 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>
|
||||
@@ -183,40 +183,40 @@ export default function SuppressionsPage() {
|
||||
</div>
|
||||
|
||||
{/* Keyword Management */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#252525] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#3a322b] flex items-center justify-between">
|
||||
<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>
|
||||
{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>
|
||||
<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>
|
||||
<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="hostile">hostile</option>
|
||||
</select>
|
||||
</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="button" onClick={() => setShowKeywordForm(false)} className="px-3 py-1.5 bg-[#1a1a1a] border border-[#252525] rounded text-xs text-[#888]">Cancel</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-[#231d18] border border-[#3a322b] rounded text-xs text-[#888]">Cancel</button>
|
||||
</form>
|
||||
)}
|
||||
<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>
|
||||
{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-[#888] text-xs">{k.keyword_type}</td>
|
||||
<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 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>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -91,7 +91,7 @@ interface StatCardProps {
|
||||
|
||||
function StatCard({ label, value, sub, accentColor, bgColor, icon, badge }: StatCardProps) {
|
||||
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={`w-10 h-10 rounded-lg flex items-center justify-center ${bgColor}`}>
|
||||
<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) {
|
||||
if (!active || !payload?.length) return null;
|
||||
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>}
|
||||
{payload.map((entry: any, i: number) => (
|
||||
<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() {
|
||||
const { user, loading } = useAuth();
|
||||
@@ -204,7 +204,7 @@ export default function ForumDashboardPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,7 +236,7 @@ export default function ForumDashboardPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
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">
|
||||
<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>
|
||||
<Link
|
||||
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">
|
||||
<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 */}
|
||||
<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
|
||||
title="Thread Growth"
|
||||
subtitle="Weekly new threads and replies over the past 8 weeks"
|
||||
@@ -398,7 +398,7 @@ export default function ForumDashboardPage() {
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={240}>
|
||||
<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} />
|
||||
<YAxis tick={{ fill: '#555', fontSize: 11 }} axisLine={false} tickLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
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" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -419,33 +419,33 @@ export default function ForumDashboardPage() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
|
||||
{/* Top Categories */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
</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>
|
||||
{topCategories.length === 0 ? (
|
||||
<div className="px-5 py-10 text-center">
|
||||
<p className="text-[#555] text-sm font-body">No categories found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{topCategories.map((cat, idx) => {
|
||||
const maxThreads = topCategories[0]?.thread_count ?? 1;
|
||||
const pct = maxThreads > 0 ? Math.round((cat.thread_count / maxThreads) * 100) : 0;
|
||||
const color = CATEGORY_COLORS[idx % CATEGORY_COLORS.length];
|
||||
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">
|
||||
<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-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>
|
||||
</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
|
||||
className="h-full rounded-full transition-all duration-500"
|
||||
style={{ width: `${pct}%`, backgroundColor: color }}
|
||||
@@ -459,8 +459,8 @@ export default function ForumDashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* Active Users */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a]">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#231d18]">
|
||||
<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>
|
||||
</div>
|
||||
@@ -469,13 +469,13 @@ export default function ForumDashboardPage() {
|
||||
<p className="text-[#555] text-sm font-body">No active users found</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{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 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 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-[#231d18] flex items-center justify-center flex-shrink-0 text-xs font-bold font-body text-[#555]">
|
||||
{idx + 1}
|
||||
</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">
|
||||
{u.author_name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -498,14 +498,14 @@ export default function ForumDashboardPage() {
|
||||
{/* Category Bar Chart */}
|
||||
{topCategories.length > 0 && (
|
||||
<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
|
||||
title="Category Activity"
|
||||
subtitle="Thread and post distribution across categories"
|
||||
/>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<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
|
||||
dataKey="name"
|
||||
tick={{ fill: '#555', fontSize: 10 }}
|
||||
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
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" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -533,22 +533,22 @@ export default function ForumDashboardPage() {
|
||||
|
||||
{/* Moderation Stats */}
|
||||
<section className="mb-8">
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#231d18] flex items-center justify-between">
|
||||
<div>
|
||||
<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>
|
||||
</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 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 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: '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: '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 }) => (
|
||||
<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>
|
||||
|
||||
@@ -62,7 +62,7 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
|
||||
const [reason, setReason] = useState('');
|
||||
return (
|
||||
<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>
|
||||
<p className="text-[#888] font-body text-sm mb-4">Provide a reason for flagging this content (optional).</p>
|
||||
<textarea
|
||||
@@ -70,10 +70,10 @@ function FlagModal({ onConfirm, onCancel }: FlagModalProps) {
|
||||
onChange={e => setReason(e.target.value)}
|
||||
rows={3}
|
||||
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">
|
||||
<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
|
||||
</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">
|
||||
@@ -119,14 +119,14 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
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-1 min-w-0">
|
||||
{/* Title + badges */}
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
{thread.title}
|
||||
@@ -159,7 +159,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
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 ${
|
||||
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'}
|
||||
@@ -169,7 +169,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
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 ${
|
||||
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'}
|
||||
@@ -179,7 +179,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
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 ${
|
||||
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'}
|
||||
@@ -199,7 +199,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
<button
|
||||
disabled={isLoading}
|
||||
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
|
||||
</button>
|
||||
@@ -235,7 +235,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
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-1 min-w-0">
|
||||
{/* Thread link + badges */}
|
||||
@@ -243,13 +243,13 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
{reply.forum_threads && (
|
||||
<Link
|
||||
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"
|
||||
>
|
||||
↗ {reply.forum_threads.title}
|
||||
</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" />}
|
||||
{reply.is_flagged && !isAiFlagged && <StatusBadge label="🚩 Flagged" color="text-red-400 bg-red-400/10" />}
|
||||
</div>
|
||||
@@ -273,8 +273,8 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
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 ${
|
||||
reply.is_hidden
|
||||
? 'bg-[#252525] text-[#aaa] hover:bg-[#333]'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white'
|
||||
? 'bg-[#3a322b] text-[#aaa] hover:bg-[#333]'
|
||||
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{reply.is_hidden ? 'Unhide' : 'Hide'}
|
||||
@@ -294,7 +294,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
<button
|
||||
disabled={isLoading}
|
||||
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
|
||||
</button>
|
||||
@@ -398,7 +398,7 @@ export default function ForumModerationPage() {
|
||||
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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -435,10 +435,10 @@ export default function ForumModerationPage() {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<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="text-[#aab4c4]">Forum Moderation</span>
|
||||
</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">
|
||||
🤖 Claude AI Auto-Moderation Active
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
@@ -463,14 +463,14 @@ export default function ForumModerationPage() {
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => { setActiveTab(tab); setPage(1); }}
|
||||
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
|
||||
activeTab === tab
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -488,8 +488,8 @@ export default function ForumModerationPage() {
|
||||
onClick={() => { setThreadFilter(f.value); setPage(1); }}
|
||||
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
|
||||
threadFilter === f.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -501,8 +501,8 @@ export default function ForumModerationPage() {
|
||||
onClick={() => { setReplyFilter(f.value); setPage(1); }}
|
||||
className={`px-3 py-1.5 text-xs font-body font-semibold rounded-lg transition-colors ${
|
||||
replyFilter === f.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'bg-[#231d18] text-[#888] hover:bg-[#3a322b] hover:text-white border border-[#3a322b]'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -515,7 +515,7 @@ export default function ForumModerationPage() {
|
||||
{/* Content */}
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : activeTab === 'threads' ? (
|
||||
<>
|
||||
@@ -559,7 +559,7 @@ export default function ForumModerationPage() {
|
||||
<button
|
||||
disabled={page === 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
|
||||
</button>
|
||||
@@ -567,7 +567,7 @@ export default function ForumModerationPage() {
|
||||
<button
|
||||
disabled={page === totalPages}
|
||||
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 →
|
||||
</button>
|
||||
|
||||
@@ -136,13 +136,13 @@ export default function AdminForumSeedPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-[#111111]">
|
||||
<div className="bg-[#1a1208] border-b border-[#2a3a50]">
|
||||
<main className="min-h-screen bg-[#1c1815]">
|
||||
<div className="bg-[#231d18] border-b border-[#2a3a50]">
|
||||
<div className="max-w-container mx-auto px-4 py-6">
|
||||
<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>
|
||||
<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="text-[#aab4c4]">Seed Tool</span>
|
||||
</nav>
|
||||
@@ -157,17 +157,17 @@ export default function AdminForumSeedPage() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left: Editor */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-[#1a1a1a] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="flex border-b border-[#252525]">
|
||||
<div className="bg-[#231d18] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="flex border-b border-[#3a322b]">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -182,20 +182,20 @@ export default function AdminForumSeedPage() {
|
||||
value={seedData}
|
||||
onChange={e => setSeedData(e.target.value)}
|
||||
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}
|
||||
/>
|
||||
<div className="flex gap-3 mt-3">
|
||||
<button
|
||||
onClick={handleImport}
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -206,7 +206,7 @@ export default function AdminForumSeedPage() {
|
||||
{activeTab === 'json' && (
|
||||
<div className="p-4">
|
||||
<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",
|
||||
@@ -226,7 +226,7 @@ export default function AdminForumSeedPage() {
|
||||
<h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4>
|
||||
<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 => (
|
||||
<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>
|
||||
@@ -239,7 +239,7 @@ export default function AdminForumSeedPage() {
|
||||
<div className="space-y-4">
|
||||
{/* Import Results */}
|
||||
{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>
|
||||
<div className="flex gap-4 mb-3">
|
||||
<div className="text-center">
|
||||
@@ -265,20 +265,20 @@ export default function AdminForumSeedPage() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<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
|
||||
</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
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function WPImportPage() {
|
||||
if (loading || (!user && !loading)) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -142,10 +142,10 @@ export default function WPImportPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-[#cccccc]">
|
||||
{/* 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="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
|
||||
</Link>
|
||||
<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>
|
||||
<p className="text-[#666] text-sm mt-1">
|
||||
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
|
||||
</a>{' '}
|
||||
via WordPress REST API
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-[#555] bg-[#1a1a1a] border border-[#252525] rounded px-3 py-2">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#ffb800]">
|
||||
<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-[#F0A623]">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
{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">
|
||||
{/* 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">
|
||||
<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" />
|
||||
</svg>
|
||||
Run Import
|
||||
@@ -191,7 +191,7 @@ export default function WPImportPage() {
|
||||
value={maxPages}
|
||||
onChange={(e) => setMaxPages(Number(e.target.value))}
|
||||
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={3}>3 pages (~60 posts)</option>
|
||||
<option value={5}>5 pages (~100 posts)</option>
|
||||
@@ -204,7 +204,7 @@ export default function WPImportPage() {
|
||||
<button
|
||||
onClick={handleImport}
|
||||
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 ? (
|
||||
<>
|
||||
<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 */}
|
||||
{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="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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#1a1208] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#ffb800] rounded-full animate-pulse w-2/3" />
|
||||
<div className="mt-3 h-1.5 bg-[#231d18] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#F0A623] rounded-full animate-pulse w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -261,7 +261,7 @@ export default function WPImportPage() {
|
||||
{[
|
||||
{ label: 'Fetched', value: importResult.totalFetched, color: 'text-[#cccccc]' },
|
||||
{ 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]' },
|
||||
].map((stat) => (
|
||||
<div key={stat.label} className="bg-[#111] rounded p-2.5 text-center">
|
||||
@@ -277,14 +277,14 @@ export default function WPImportPage() {
|
||||
</div>
|
||||
|
||||
{/* 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) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
|
||||
activeTab === tab
|
||||
? 'border-[#ffb800] text-[#ffb800]'
|
||||
? 'border-[#F0A623] text-[#F0A623]'
|
||||
: 'border-transparent text-[#666] hover:text-[#999]'
|
||||
}`}>
|
||||
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
|
||||
@@ -306,17 +306,17 @@ export default function WPImportPage() {
|
||||
placeholder="Search posts..."
|
||||
value={searchQuery}
|
||||
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>
|
||||
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : 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">
|
||||
<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>
|
||||
@@ -327,10 +327,10 @@ export default function WPImportPage() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{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 */}
|
||||
{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
|
||||
src={post.featured_image}
|
||||
alt={post.featured_image_alt || post.title}
|
||||
@@ -340,7 +340,7 @@ export default function WPImportPage() {
|
||||
/>
|
||||
</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]">
|
||||
<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>
|
||||
@@ -354,7 +354,7 @@ export default function WPImportPage() {
|
||||
<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">
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -368,7 +368,7 @@ export default function WPImportPage() {
|
||||
href={`https://www.avbeat.com/${post.wp_slug}`}
|
||||
target="_blank"
|
||||
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">
|
||||
<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" />
|
||||
@@ -402,16 +402,16 @@ export default function WPImportPage() {
|
||||
<div>
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : 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>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{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-center gap-2">
|
||||
{log.status === 'completed' ? (
|
||||
@@ -439,7 +439,7 @@ export default function WPImportPage() {
|
||||
{ label: 'Skipped', value: log.total_skipped },
|
||||
{ label: 'Errors', value: log.total_errors },
|
||||
].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-[10px] text-[#444] uppercase tracking-wider">{stat.label}</p>
|
||||
</div>
|
||||
|
||||
@@ -67,11 +67,11 @@ function fmtDate(dateStr: string): string {
|
||||
|
||||
// ─── 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;
|
||||
}) {
|
||||
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
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center flex-shrink-0"
|
||||
style={{ background: `${accent}18` }}
|
||||
@@ -90,7 +90,7 @@ function StatCard({ label, value, sub, accent = '#ffb800', icon }: {
|
||||
function ChartTooltip({ active, payload, label }: any) {
|
||||
if (!active || !payload?.length) return null;
|
||||
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>}
|
||||
{payload.map((entry: any, i: number) => (
|
||||
<p key={i} style={{ color: entry.color }} className="leading-5">
|
||||
@@ -243,7 +243,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
if (loading || loadingData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<div className="w-8 h-8 border-2 border-[#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>
|
||||
);
|
||||
}
|
||||
@@ -258,7 +258,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-white">
|
||||
{/* 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="flex items-center gap-3">
|
||||
<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" />
|
||||
</svg>
|
||||
</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">
|
||||
Newsletter
|
||||
</Link>
|
||||
@@ -278,14 +278,14 @@ export default function NewsletterAnalyticsPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
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">
|
||||
<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>
|
||||
Templates
|
||||
</Link>
|
||||
<div className="w-px h-8 bg-[#252525]" />
|
||||
<div className="w-px h-8 bg-[#3a322b]" />
|
||||
<div className="text-right">
|
||||
<p className="text-white text-sm font-bold font-heading">{fmt(totalSubscribers)}</p>
|
||||
<p className="text-[#555] text-xs font-body">active subscribers</p>
|
||||
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
label="Avg Open Rate"
|
||||
value={pct(avgOpenRate)}
|
||||
sub={`across ${digestData.length} digests`}
|
||||
accent="#ffb800"
|
||||
accent="#F0A623"
|
||||
icon={
|
||||
<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" />
|
||||
@@ -352,7 +352,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
<div className="h-64">
|
||||
<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}%`} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<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 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -370,7 +370,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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" />
|
||||
<div className="h-64">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -380,7 +380,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
<YAxis tick={{ fill: '#555', fontSize: 11 }} tickLine={false} axisLine={false} />
|
||||
<Tooltip content={<ChartTooltip />} />
|
||||
<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]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -388,7 +388,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
</div>
|
||||
|
||||
{/* 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]">
|
||||
<SectionHeader title="Per-Digest Performance" subtitle="Click column headers to sort" />
|
||||
</div>
|
||||
@@ -421,7 +421,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
{sortedDigests.map((digest, i) => (
|
||||
<tr
|
||||
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">
|
||||
<p className="truncate font-medium" title={digest.subject}>{digest.subject}</p>
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
|
||||
article_blocks: payload.article_blocks,
|
||||
layout: 'featured',
|
||||
style: 'dark',
|
||||
accent_color: '#ffb800',
|
||||
accent_color: '#F0A623',
|
||||
is_preset: false,
|
||||
}),
|
||||
});
|
||||
@@ -296,7 +296,7 @@ export default function CampaignEditorPage() {
|
||||
)}
|
||||
|
||||
{/* ── 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="flex items-center gap-3 min-w-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"
|
||||
/>
|
||||
<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}
|
||||
@@ -339,7 +339,7 @@ export default function CampaignEditorPage() {
|
||||
<button
|
||||
onClick={() => handleSave('draft')}
|
||||
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'}
|
||||
</button>
|
||||
@@ -393,9 +393,9 @@ export default function CampaignEditorPage() {
|
||||
<div className="space-y-2">
|
||||
{loadingArticles ? (
|
||||
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-2.5 bg-[#1a1a1a] rounded w-1/2" />
|
||||
<div className="h-2.5 bg-[#231d18] rounded w-1/2" />
|
||||
</div>
|
||||
))
|
||||
) : articles.length === 0 ? (
|
||||
@@ -409,7 +409,7 @@ export default function CampaignEditorPage() {
|
||||
onClick={() => toggleArticle(article)}
|
||||
className={`group relative bg-[#111] border rounded-xl p-4 cursor-pointer transition-all ${
|
||||
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">
|
||||
@@ -427,7 +427,7 @@ export default function CampaignEditorPage() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -454,7 +454,7 @@ export default function CampaignEditorPage() {
|
||||
{(activePanel === 'settings' || activePanel === 'articles') && (
|
||||
<>
|
||||
{/* 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">
|
||||
<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]'}`}>
|
||||
@@ -477,7 +477,7 @@ export default function CampaignEditorPage() {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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]'}`}>
|
||||
@@ -497,7 +497,7 @@ export default function CampaignEditorPage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="space-y-2">
|
||||
{SEGMENTS.map((seg) => {
|
||||
@@ -509,7 +509,7 @@ export default function CampaignEditorPage() {
|
||||
onClick={() => setCampaign((p) => ({ ...p, segment: seg.id }))}
|
||||
className={`w-full text-left px-3 py-2.5 rounded-lg border transition-all ${
|
||||
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">
|
||||
@@ -529,7 +529,7 @@ export default function CampaignEditorPage() {
|
||||
})}
|
||||
</div>
|
||||
{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">
|
||||
<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>
|
||||
@@ -543,7 +543,7 @@ export default function CampaignEditorPage() {
|
||||
)}
|
||||
|
||||
{/* ── 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">
|
||||
<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>
|
||||
@@ -559,7 +559,7 @@ export default function CampaignEditorPage() {
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{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">
|
||||
{/* Order controls */}
|
||||
<div className="flex flex-col gap-0.5 flex-shrink-0 mt-0.5">
|
||||
@@ -618,16 +618,16 @@ export default function CampaignEditorPage() {
|
||||
|
||||
{/* ── Preview Panel ── */}
|
||||
{activePanel === 'preview' && (
|
||||
<div className="bg-[#111] border border-[#1a1a1a] rounded-xl overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[#1a1a1a] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#231d18] rounded-xl overflow-hidden">
|
||||
<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>
|
||||
<span className="text-[10px] text-[#555]">Inbox view</span>
|
||||
</div>
|
||||
|
||||
{/* 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="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>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -644,7 +644,7 @@ export default function CampaignEditorPage() {
|
||||
{/* Email body preview */}
|
||||
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{/* 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>
|
||||
{campaign.previewText && (
|
||||
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
|
||||
@@ -657,7 +657,7 @@ export default function CampaignEditorPage() {
|
||||
) : (
|
||||
campaign.articles.map((article, idx) => (
|
||||
<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 && (
|
||||
<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 */}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -687,7 +687,7 @@ export default function CampaignEditorPage() {
|
||||
)}
|
||||
|
||||
{/* ── 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>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
@@ -710,7 +710,7 @@ export default function CampaignEditorPage() {
|
||||
<button
|
||||
onClick={() => handleSave('draft')}
|
||||
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'}
|
||||
</button>
|
||||
|
||||
@@ -356,7 +356,7 @@ export default function AdminNewsletterPage() {
|
||||
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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -373,7 +373,7 @@ export default function AdminNewsletterPage() {
|
||||
)}
|
||||
|
||||
{/* 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="flex items-center gap-3">
|
||||
<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">
|
||||
<Link
|
||||
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">
|
||||
<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>
|
||||
@@ -397,7 +397,7 @@ export default function AdminNewsletterPage() {
|
||||
</Link>
|
||||
<Link
|
||||
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">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
@@ -405,18 +405,18 @@ export default function AdminNewsletterPage() {
|
||||
</Link>
|
||||
<Link
|
||||
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">
|
||||
<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>
|
||||
Analytics
|
||||
</Link>
|
||||
<div className="w-px h-8 bg-[#252525]" />
|
||||
<div className="w-px h-8 bg-[#3a322b]" />
|
||||
<div className="text-right">
|
||||
<p className="text-white font-display font-bold text-xl">{activeCount}</p>
|
||||
<p className="text-[#555] text-xs font-body">active subscribers</p>
|
||||
</div>
|
||||
<div className="w-px h-8 bg-[#252525]" />
|
||||
<div className="w-px h-8 bg-[#3a322b]" />
|
||||
<div className="text-right">
|
||||
<p className="text-white font-display font-bold text-xl">{totalCount}</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">
|
||||
{/* 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
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@@ -452,12 +452,12 @@ export default function AdminNewsletterPage() {
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
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) => (
|
||||
<button
|
||||
key={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}
|
||||
</button>
|
||||
@@ -467,7 +467,7 @@ export default function AdminNewsletterPage() {
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
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">
|
||||
<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 */}
|
||||
{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">
|
||||
<span className="text-sm font-body font-semibold text-[#ffb800]">
|
||||
<span className="text-sm font-body font-semibold text-[#F0A623]">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<button
|
||||
@@ -512,17 +512,17 @@ export default function AdminNewsletterPage() {
|
||||
Delete
|
||||
</button>
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* Table */}
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : subscribers.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -534,13 +534,13 @@ export default function AdminNewsletterPage() {
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-[#252525]">
|
||||
<tr className="border-b border-[#3a322b]">
|
||||
<th className="px-4 py-3 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
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 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) => (
|
||||
<tr
|
||||
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">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(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 className="px-4 py-3 text-sm font-body text-[#e0e0e0]">{sub.email}</td>
|
||||
<td className="px-4 py-3 hidden md:table-cell">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{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 && (
|
||||
<span className="text-xs text-[#555] font-body">+{sub.topics.length - 3}</span>
|
||||
@@ -576,7 +576,7 @@ export default function AdminNewsletterPage() {
|
||||
</div>
|
||||
</td>
|
||||
<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}
|
||||
</span>
|
||||
</td>
|
||||
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
|
||||
{activeTab === 'send-digest' && (
|
||||
<div className="max-w-2xl">
|
||||
{/* SMTP warning if not configured */}
|
||||
<div className="bg-[#1a1208] border border-[#4a3500] 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">
|
||||
<div className="bg-[#231d18] border border-[#412402] rounded-lg p-4 mb-6 flex gap-3">
|
||||
<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" />
|
||||
</svg>
|
||||
<div>
|
||||
<p className="text-[#e0e0e0] text-sm font-body font-semibold">SMTP Required</p>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Applied template badge */}
|
||||
{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="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 }}>
|
||||
@@ -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>
|
||||
<button
|
||||
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">
|
||||
<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
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{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">
|
||||
<span className="text-xs font-body text-[#555]">Article {index + 1}</span>
|
||||
{digestArticles.length > 1 && (
|
||||
@@ -803,11 +803,11 @@ export default function AdminNewsletterPage() {
|
||||
{/* Template Picker Drawer */}
|
||||
{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="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="flex items-center justify-between px-6 py-4 border-b border-[#252525] flex-shrink-0">
|
||||
<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-[#3a322b] flex-shrink-0">
|
||||
<h2 className="font-display font-bold text-white text-base">Choose a Template</h2>
|
||||
<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 →
|
||||
</Link>
|
||||
<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">
|
||||
{loadingTemplates ? (
|
||||
<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>
|
||||
) : templates.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -835,7 +835,7 @@ export default function AdminNewsletterPage() {
|
||||
<button
|
||||
key={tpl.id}
|
||||
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' }}>
|
||||
<span style={{ color: tpl.accent_color }}>
|
||||
@@ -851,8 +851,8 @@ export default function AdminNewsletterPage() {
|
||||
</div>
|
||||
{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">
|
||||
<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-[#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 capitalize">{tpl.layout}</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>
|
||||
</button>
|
||||
|
||||
@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
|
||||
];
|
||||
|
||||
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> = {
|
||||
@@ -83,7 +83,7 @@ const emptyForm = (): Omit<NewsletterTemplate, 'id' | 'is_preset' | 'created_at'
|
||||
subject_prefix: '',
|
||||
header_text: '',
|
||||
footer_text: '',
|
||||
accent_color: '#ffb800',
|
||||
accent_color: '#F0A623',
|
||||
article_blocks: [{ title: '', excerpt: '', url: '', category: '' }],
|
||||
});
|
||||
|
||||
@@ -226,7 +226,7 @@ export default function NewsletterTemplatesPage() {
|
||||
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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export default function NewsletterTemplatesPage() {
|
||||
)}
|
||||
|
||||
{/* 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="flex items-center gap-3">
|
||||
<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="flex items-center gap-2 mb-4">
|
||||
<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>
|
||||
{loadingData ? (
|
||||
<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 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 className="flex items-center gap-2 mb-4">
|
||||
<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>
|
||||
{!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">
|
||||
<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>
|
||||
<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 →
|
||||
</button>
|
||||
</div>
|
||||
@@ -330,8 +330,8 @@ export default function NewsletterTemplatesPage() {
|
||||
{/* Template Form Modal */}
|
||||
{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="bg-[#111] border border-[#252525] rounded-xl w-full max-w-2xl">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
|
||||
<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-[#3a322b]">
|
||||
<h2 className="font-display font-bold text-white text-base">
|
||||
{editingId ? 'Edit Template' : 'New Template'}
|
||||
</h2>
|
||||
@@ -376,8 +376,8 @@ export default function NewsletterTemplatesPage() {
|
||||
key={opt.value}
|
||||
type="button"
|
||||
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]'}`}>
|
||||
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#ffb800]' : 'text-[#555]'}`}>
|
||||
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-[#F0A623]' : 'text-[#555]'}`}>
|
||||
{LAYOUT_ICONS[opt.value]}
|
||||
</span>
|
||||
<div>
|
||||
@@ -398,7 +398,7 @@ export default function NewsletterTemplatesPage() {
|
||||
key={opt.value}
|
||||
type="button"
|
||||
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}
|
||||
</button>
|
||||
))}
|
||||
@@ -469,14 +469,14 @@ export default function NewsletterTemplatesPage() {
|
||||
<div>
|
||||
<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>
|
||||
<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
|
||||
</button>
|
||||
</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>
|
||||
<div className="space-y-2">
|
||||
{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">
|
||||
<span className="text-xs font-body text-[#555]">Slot {i + 1}</span>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -486,7 +486,7 @@ export default function NewsletterTemplatesPage() {
|
||||
type="checkbox"
|
||||
checked={!!block.featured}
|
||||
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>
|
||||
</label>
|
||||
@@ -527,7 +527,7 @@ export default function NewsletterTemplatesPage() {
|
||||
</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">
|
||||
Cancel
|
||||
</button>
|
||||
@@ -545,8 +545,8 @@ export default function NewsletterTemplatesPage() {
|
||||
{/* Preview Modal */}
|
||||
{previewTemplate && (
|
||||
<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="flex items-center justify-between px-6 py-4 border-b border-[#252525]">
|
||||
<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-[#3a322b]">
|
||||
<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">
|
||||
<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 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-sm font-body font-semibold text-white capitalize">{previewTemplate.layout}</p>
|
||||
</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-sm font-body font-semibold text-white capitalize">{previewTemplate.style}</p>
|
||||
</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-sm font-body font-semibold text-white">{previewTemplate.article_blocks?.length || 0}</p>
|
||||
</div>
|
||||
</div>
|
||||
{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-sm font-body text-[#e0e0e0]">{previewTemplate.subject_prefix}</p>
|
||||
</div>
|
||||
)}
|
||||
{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-sm font-body text-[#e0e0e0]">{previewTemplate.header_text}</p>
|
||||
</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>
|
||||
<div className="space-y-1.5">
|
||||
{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>
|
||||
{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.title && <span className="text-xs text-[#e0e0e0] font-body truncate">{b.title}</span>}
|
||||
</div>
|
||||
@@ -614,7 +614,7 @@ export default function NewsletterTemplatesPage() {
|
||||
{!previewTemplate.is_preset && (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
@@ -637,7 +637,7 @@ interface TemplateCardProps {
|
||||
|
||||
function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: TemplateCardProps) {
|
||||
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 */}
|
||||
<div className="h-1" style={{ backgroundColor: template.accent_color }} />
|
||||
<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">
|
||||
<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-[#1a1a1a] 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 capitalize">{template.layout}</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-[#231d18] text-[#888] px-2 py-0.5 rounded-full font-body">{template.article_blocks?.length || 0} slots</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
{!template.is_preset && (
|
||||
<>
|
||||
<button
|
||||
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">
|
||||
<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>
|
||||
@@ -688,7 +688,7 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
|
||||
<button
|
||||
onClick={() => onDelete(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">
|
||||
<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>
|
||||
|
||||
@@ -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 ${
|
||||
active
|
||||
? `${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">
|
||||
@@ -191,7 +191,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
|
||||
return (
|
||||
<div
|
||||
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 */}
|
||||
@@ -202,7 +202,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
<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} />
|
||||
</div>
|
||||
@@ -244,7 +244,7 @@ function NotificationRow({ notification: n, onMarkRead, onDismiss }: Notificatio
|
||||
{n.actionUrl && (
|
||||
<Link
|
||||
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 →'}
|
||||
</Link>
|
||||
@@ -474,7 +474,7 @@ export default function NotificationsPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -512,7 +512,7 @@ export default function NotificationsPage() {
|
||||
{stats.unread > 0 && (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -520,7 +520,7 @@ export default function NotificationsPage() {
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
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' : ''}`} />
|
||||
Refresh
|
||||
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
|
||||
label="Unread"
|
||||
value={stats.unread}
|
||||
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'}
|
||||
onClick={() => setFilter(filter === 'unread' ? 'all' : 'unread')}
|
||||
/>
|
||||
@@ -580,7 +580,7 @@ export default function NotificationsPage() {
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1.5 text-xs font-body font-bold rounded transition-colors ${
|
||||
filter === f
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#555] hover:text-[#888] hover:bg-[#111]'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -44,8 +44,8 @@ function timeAgo(dateStr: string): string {
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
published: 'text-green-400 bg-green-400/10',
|
||||
pending: 'text-yellow-400 bg-yellow-400/10',
|
||||
draft: 'text-[#888] bg-[#1a1a1a]',
|
||||
archived: 'text-[#555] bg-[#1a1a1a]',
|
||||
draft: 'text-[#888] bg-[#231d18]',
|
||||
archived: 'text-[#555] bg-[#231d18]',
|
||||
imported: 'text-blue-400 bg-blue-400/10',
|
||||
};
|
||||
|
||||
@@ -91,7 +91,7 @@ interface MetricCardProps {
|
||||
|
||||
function MetricCard({ label, value, sub, accent, icon, href, badge }: MetricCardProps) {
|
||||
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={`w-10 h-10 rounded-lg flex items-center justify-center ${accent}`}>
|
||||
{icon}
|
||||
@@ -125,15 +125,15 @@ interface QuickActionProps {
|
||||
|
||||
function QuickAction({ label, description, href, icon, accent }: QuickActionProps) {
|
||||
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`}>
|
||||
{icon}
|
||||
</div>
|
||||
<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>
|
||||
</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" />
|
||||
</svg>
|
||||
</Link>
|
||||
@@ -239,7 +239,7 @@ export default function AdminDashboardPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -345,7 +345,7 @@ export default function AdminDashboardPage() {
|
||||
<MetricCard
|
||||
label="Drafts"
|
||||
value={metrics.draftArticles}
|
||||
accent="bg-[#1a1a1a]"
|
||||
accent="bg-[#231d18]"
|
||||
href="/admin/articles"
|
||||
icon={
|
||||
<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
|
||||
label="Archived"
|
||||
value={metrics.archivedArticles}
|
||||
accent="bg-[#1a1a1a]"
|
||||
accent="bg-[#231d18]"
|
||||
href="/admin/articles"
|
||||
icon={
|
||||
<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 */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a] flex items-center justify-between">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<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>
|
||||
<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 className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{recentActivity.length === 0 ? (
|
||||
<div className="px-5 py-10 text-center">
|
||||
<p className="text-[#555] text-sm font-body">No recent activity found</p>
|
||||
</div>
|
||||
) : (
|
||||
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 className="w-6 h-6 rounded-full bg-[#1a1a1a] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<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-[#231d18] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
{ACTIVITY_ICONS[item.type] || ACTIVITY_ICONS['draft']}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -412,8 +412,8 @@ export default function AdminDashboardPage() {
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#1a1a1a]">
|
||||
<div className="bg-[#111] border border-[#3a322b] rounded-lg overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-[#231d18]">
|
||||
<h2 className="text-sm font-display font-bold text-white">Quick Actions</h2>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col gap-2">
|
||||
@@ -587,21 +587,21 @@ export default function AdminDashboardPage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ 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: '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 }) => (
|
||||
<div key={label}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-[#888] font-body">{label}</span>
|
||||
<span className="text-xs text-white font-body font-semibold">{value}</span>
|
||||
</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 rounded-full ${color} transition-all duration-500`}
|
||||
style={{ width: total > 0 ? `${Math.round((value / total) * 100)}%` : '0%' }}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ReviewActions({ id, currentStatus }: Props) {
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
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}
|
||||
/>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-sm">
|
||||
|
||||
@@ -41,12 +41,12 @@ export default async function ReviewDetailPage({ params }: { params: Promise<{ i
|
||||
|
||||
return (
|
||||
<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
|
||||
</Link>
|
||||
|
||||
<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 className="text-[#4b5563]">·</span>
|
||||
<span>{art.category}</span>
|
||||
|
||||
@@ -70,7 +70,7 @@ export default async function ReviewQueuePage() {
|
||||
return (
|
||||
<li
|
||||
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">
|
||||
{persona?.avatar_url ? (
|
||||
@@ -85,7 +85,7 @@ export default async function ReviewQueuePage() {
|
||||
<div className="h-14 w-14 rounded-full bg-[#1f2937]" />
|
||||
)}
|
||||
<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}
|
||||
</div>
|
||||
<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">
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function RmpHubPage() {
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
@@ -68,7 +68,7 @@ export default function RmpHubPage() {
|
||||
],
|
||||
},
|
||||
]?.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">
|
||||
<span className="text-2xl">{section?.icon}</span>
|
||||
<div>
|
||||
@@ -78,19 +78,19 @@ export default function RmpHubPage() {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{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}
|
||||
</Link>
|
||||
))}
|
||||
</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}
|
||||
</Link>
|
||||
</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>
|
||||
<RmpAdminNav />
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ const ROLE_COLORS: Record<string, string> = {
|
||||
editor: 'text-purple-400 bg-purple-400/10 border-purple-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',
|
||||
reader: 'text-[#888] bg-[#1a1a1a] border-[#333]',
|
||||
reader: 'text-[#888] bg-[#231d18] border-[#333]',
|
||||
};
|
||||
|
||||
const ROLE_ICONS: Record<string, React.ReactNode> = {
|
||||
@@ -324,7 +324,7 @@ export default function AdminRolesPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +376,7 @@ export default function AdminRolesPage() {
|
||||
key={role}
|
||||
onClick={() => { setSelectedRole(role === selectedRole ? 'all' : role); setActiveTab('assign'); }}
|
||||
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}`}>
|
||||
@@ -391,7 +391,7 @@ export default function AdminRolesPage() {
|
||||
</div>
|
||||
|
||||
{/* 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: '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)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-md text-sm font-body font-medium transition-all ${
|
||||
activeTab === tab.id
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -416,9 +416,9 @@ export default function AdminRolesPage() {
|
||||
|
||||
{/* ── Tab: Assign Roles ── */}
|
||||
{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 */}
|
||||
<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">
|
||||
<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" />
|
||||
@@ -428,13 +428,13 @@ export default function AdminRolesPage() {
|
||||
placeholder="Search users…"
|
||||
value={searchQuery}
|
||||
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>
|
||||
<select
|
||||
value={selectedRole}
|
||||
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>
|
||||
{ROLES.map((r) => (
|
||||
@@ -447,7 +447,7 @@ export default function AdminRolesPage() {
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<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">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>
|
||||
@@ -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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#1a1a1a]">
|
||||
<tbody className="divide-y divide-[#231d18]">
|
||||
{filteredUsers.length === 0 ? (
|
||||
<tr>
|
||||
<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>
|
||||
) : (
|
||||
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">
|
||||
<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
|
||||
? <img src={u.avatar_url} alt={u.full_name} className="w-8 h-8 rounded-full object-cover" />
|
||||
: getInitials(u.full_name || u.email)
|
||||
@@ -484,7 +484,7 @@ export default function AdminRolesPage() {
|
||||
</td>
|
||||
<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 ${
|
||||
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'}
|
||||
</span>
|
||||
@@ -500,14 +500,14 @@ export default function AdminRolesPage() {
|
||||
defaultValue={u.role}
|
||||
onChange={(e) => handleAssignRole(u.id, e.target.value)}
|
||||
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) => (
|
||||
<option key={r} value={r} className="capitalize">{r.charAt(0).toUpperCase() + r.slice(1)}</option>
|
||||
))}
|
||||
</select>
|
||||
{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>
|
||||
</td>
|
||||
@@ -519,7 +519,7 @@ export default function AdminRolesPage() {
|
||||
</div>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
@@ -535,9 +535,9 @@ export default function AdminRolesPage() {
|
||||
const color = ROLE_COLORS[perm.role] || ROLE_COLORS['reader'];
|
||||
|
||||
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 */}
|
||||
<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={`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]}
|
||||
@@ -557,7 +557,7 @@ export default function AdminRolesPage() {
|
||||
<button
|
||||
onClick={handleSavePermissions}
|
||||
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" />}
|
||||
Save Changes
|
||||
@@ -593,12 +593,12 @@ export default function AdminRolesPage() {
|
||||
{isEditing ? (
|
||||
<button
|
||||
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'}`} />
|
||||
</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 ? (
|
||||
<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" />
|
||||
@@ -631,9 +631,9 @@ export default function AdminRolesPage() {
|
||||
|
||||
{/* ── Tab: Activity Logs ── */}
|
||||
{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 */}
|
||||
<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>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-[#555] font-body">Filter by role:</span>
|
||||
@@ -644,8 +644,8 @@ export default function AdminRolesPage() {
|
||||
onClick={() => setActivityRoleFilter(r)}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-body font-medium transition-colors capitalize ${
|
||||
activityRoleFilter === r
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'bg-[#231d18] text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
{r}
|
||||
@@ -656,7 +656,7 @@ export default function AdminRolesPage() {
|
||||
</div>
|
||||
|
||||
{/* Log List */}
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
<div className="divide-y divide-[#231d18]">
|
||||
{activityLogs.length === 0 ? (
|
||||
<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">
|
||||
@@ -666,13 +666,13 @@ export default function AdminRolesPage() {
|
||||
</div>
|
||||
) : (
|
||||
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 performer = log.user_profiles?.full_name || log.performed_by_email || 'System';
|
||||
const performerRole = log.user_profiles?.role;
|
||||
|
||||
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 */}
|
||||
<div className={`mt-0.5 px-2 py-0.5 rounded text-xs font-body font-semibold flex-shrink-0 ${actionColor}`}>
|
||||
{actionLabel}
|
||||
@@ -720,7 +720,7 @@ export default function AdminRolesPage() {
|
||||
</div>
|
||||
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -223,7 +223,7 @@ export default function AdminSuspensionsPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export default function AdminSuspensionsPage() {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab
|
||||
? 'text-[#ffb800] border-[#ffb800]'
|
||||
? 'text-[#F0A623] border-[#F0A623]'
|
||||
: 'text-[#555] border-transparent hover:text-[#888]'
|
||||
}`}
|
||||
>
|
||||
@@ -304,7 +304,7 @@ export default function AdminSuspensionsPage() {
|
||||
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
@@ -336,7 +336,7 @@ export default function AdminSuspensionsPage() {
|
||||
<tr key={s.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
|
||||
<td className="px-5 py-3.5">
|
||||
<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 ? (
|
||||
<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">
|
||||
<td className="px-5 py-3.5">
|
||||
<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()}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
@@ -465,7 +465,7 @@ export default function AdminSuspensionsPage() {
|
||||
placeholder="Search users..."
|
||||
value={userSearch}
|
||||
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>
|
||||
@@ -488,7 +488,7 @@ export default function AdminSuspensionsPage() {
|
||||
<tr key={u.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
|
||||
<td className="px-5 py-3.5">
|
||||
<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 ? (
|
||||
<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">
|
||||
{/* User info */}
|
||||
<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 ? (
|
||||
<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
|
||||
value={suspendDuration ?? ''}
|
||||
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) => (
|
||||
<option key={d.value} value={d.value!}>{d.label}</option>
|
||||
@@ -657,7 +657,7 @@ export default function AdminSuspensionsPage() {
|
||||
onChange={(e) => setSuspendReason(e.target.value)}
|
||||
placeholder="Explain the reason for this action..."
|
||||
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>
|
||||
|
||||
@@ -729,7 +729,7 @@ export default function AdminSuspensionsPage() {
|
||||
onChange={(e) => setLiftReason(e.target.value)}
|
||||
placeholder="Reason for lifting this restriction..."
|
||||
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 className="flex items-center gap-3 pt-1">
|
||||
|
||||
@@ -31,14 +31,14 @@ interface Invitation {
|
||||
type Tab = 'users' | 'invitations';
|
||||
|
||||
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: 'editor', label: 'Editor', color: 'text-purple-400 bg-purple-400/10' },
|
||||
{ value: 'admin', label: 'Admin', color: 'text-red-400 bg-red-400/10' },
|
||||
];
|
||||
|
||||
const ROLE_COLORS: Record<string, string> = {
|
||||
reader: 'text-[#888] bg-[#1a1a1a]',
|
||||
reader: 'text-[#888] bg-[#231d18]',
|
||||
contributor: 'text-blue-400 bg-blue-400/10',
|
||||
editor: 'text-purple-400 bg-purple-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> = {
|
||||
pending: 'text-yellow-400 bg-yellow-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',
|
||||
email_failed: 'text-red-400 bg-red-400/10',
|
||||
};
|
||||
@@ -246,7 +246,7 @@ export default function AdminUsersPage() {
|
||||
if (loading) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -276,7 +276,7 @@ export default function AdminUsersPage() {
|
||||
</div>
|
||||
<button
|
||||
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">
|
||||
<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)}
|
||||
className={`px-4 py-2.5 text-sm font-semibold capitalize transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab
|
||||
? 'text-[#ffb800] border-[#ffb800]'
|
||||
? 'text-[#F0A623] border-[#F0A623]'
|
||||
: 'text-[#555] border-transparent hover:text-[#888]'
|
||||
}`}
|
||||
>
|
||||
@@ -331,14 +331,14 @@ export default function AdminUsersPage() {
|
||||
placeholder={activeTab === 'users' ? 'Search users...' : 'Search by email...'}
|
||||
value={searchQuery}
|
||||
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>
|
||||
{activeTab === 'users' && (
|
||||
<select
|
||||
value={roleFilter}
|
||||
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>
|
||||
{ROLE_OPTIONS.map((r) => (
|
||||
@@ -353,7 +353,7 @@ export default function AdminUsersPage() {
|
||||
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : filteredUsers.length === 0 ? (
|
||||
<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">
|
||||
<td className="px-5 py-3.5">
|
||||
<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 ? (
|
||||
<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>
|
||||
</td>
|
||||
<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}
|
||||
</span>
|
||||
</td>
|
||||
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
onClick={() => openEditRole(u)}
|
||||
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
|
||||
</button>
|
||||
@@ -455,7 +455,7 @@ export default function AdminUsersPage() {
|
||||
<div className="bg-[#111827] border border-[#1f2937] rounded-xl overflow-hidden">
|
||||
{loadingData ? (
|
||||
<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>
|
||||
) : filteredInvitations.length === 0 ? (
|
||||
<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>
|
||||
<button
|
||||
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 →
|
||||
</button>
|
||||
@@ -491,19 +491,19 @@ export default function AdminUsersPage() {
|
||||
<tr key={inv.id} className="border-b border-[#1a2030] hover:bg-[#0d1117]/50 transition-colors">
|
||||
<td className="px-5 py-3.5">
|
||||
<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()}
|
||||
</div>
|
||||
<span className="text-white text-sm">{inv.email}</span>
|
||||
</div>
|
||||
</td>
|
||||
<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}
|
||||
</span>
|
||||
</td>
|
||||
<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('_', ' ')}
|
||||
</span>
|
||||
</td>
|
||||
@@ -564,7 +564,7 @@ export default function AdminUsersPage() {
|
||||
value={inviteEmail}
|
||||
onChange={(e) => setInviteEmail(e.target.value)}
|
||||
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>
|
||||
@@ -574,7 +574,7 @@ export default function AdminUsersPage() {
|
||||
<select
|
||||
value={inviteRole}
|
||||
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="editor">Editor — can publish and manage articles</option>
|
||||
@@ -590,7 +590,7 @@ export default function AdminUsersPage() {
|
||||
onChange={(e) => setInviteMessage(e.target.value)}
|
||||
placeholder="Add a personal note to the invitation email..."
|
||||
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 className="bg-[#0d1117] border border-[#1f2937] rounded-lg p-3">
|
||||
@@ -609,7 +609,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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 ? (
|
||||
<>
|
||||
@@ -644,7 +644,7 @@ export default function AdminUsersPage() {
|
||||
</div>
|
||||
<div className="px-6 py-5 space-y-4">
|
||||
<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 ? (
|
||||
<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}
|
||||
className={`flex items-center gap-3 p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
editRole === r.value
|
||||
? 'border-[#ffb800] bg-[#ffb800]/10'
|
||||
? 'border-[#F0A623] bg-[#F0A623]/10'
|
||||
: 'border-[#1f2937] hover:border-[#2a3a50]'
|
||||
}`}
|
||||
>
|
||||
@@ -679,9 +679,9 @@ export default function AdminUsersPage() {
|
||||
className="sr-only"
|
||||
/>
|
||||
<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 className={`text-xs font-semibold uppercase tracking-wide px-2 py-0.5 rounded ${r.color}`}>
|
||||
{r.label}
|
||||
@@ -700,7 +700,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
onClick={handleEditSave}
|
||||
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 ? (
|
||||
<>
|
||||
|
||||
@@ -223,7 +223,7 @@ export default function WPUsersAdminPage() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{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">
|
||||
<span className="text-white font-medium">{u.wp_username}</span>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function AdvertisePage() {
|
||||
|
||||
{/* Hero */}
|
||||
{/* 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">
|
||||
<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 & Live Production Professionals</h1>
|
||||
@@ -64,12 +64,12 @@ export default function AdvertisePage() {
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<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 className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat) => (
|
||||
<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>
|
||||
))}
|
||||
@@ -80,25 +80,25 @@ export default function AdvertisePage() {
|
||||
<section className="mb-12">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<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 className="overflow-x-auto">
|
||||
<table className="w-full text-sm font-body">
|
||||
<thead>
|
||||
<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-[#ffb800] 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-[#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">Placement</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-[#F0A623] 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">Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{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-[#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-[#ffb800] font-medium">{zone.cpm}</td>
|
||||
<td className="py-3 px-4 text-[#F0A623] font-medium">{zone.cpm}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -114,7 +114,7 @@ export default function AdvertisePage() {
|
||||
<section id="contact" className="mb-12">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<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 className="grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||||
{/* 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>
|
||||
<div className="space-y-4">
|
||||
<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>
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
<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">Relevant Media Properties</p>
|
||||
</div>
|
||||
<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">
|
||||
{["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">
|
||||
<span className="text-[#ffb800] mt-0.5">✓</span>
|
||||
<span className="text-[#F0A623] mt-0.5">✓</span>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
@@ -152,16 +152,16 @@ export default function AdvertisePage() {
|
||||
{/* Form */}
|
||||
<div className="lg:col-span-8">
|
||||
{submitted ? (
|
||||
<div className="bg-[#111] border border-[#ffb800] 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="bg-[#111] border border-[#F0A623] p-8 text-center">
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
|
||||
<p className="font-body text-[#777] text-sm">
|
||||
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>
|
||||
</div>
|
||||
) : (
|
||||
@@ -219,7 +219,7 @@ export default function AdvertisePage() {
|
||||
<select
|
||||
value={formData.budget}
|
||||
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="under-5k">Under $5,000</option>
|
||||
<option value="5k-15k">$5,000 – $15,000</option>
|
||||
@@ -245,7 +245,7 @@ export default function AdvertisePage() {
|
||||
{loading ? "Sending..." : "Send Inquiry"}
|
||||
</button>
|
||||
<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>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function POST(req: NextRequest) {
|
||||
subject_prefix: subject_prefix || '',
|
||||
header_text: header_text || '',
|
||||
footer_text: footer_text || '',
|
||||
accent_color: accent_color || '#ffb800',
|
||||
accent_color: accent_color || '#F0A623',
|
||||
article_blocks: article_blocks || [],
|
||||
is_preset: false,
|
||||
})
|
||||
|
||||
@@ -227,11 +227,11 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
|
||||
{/* Breadcrumb 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]">
|
||||
<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>
|
||||
<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 className="text-[#888] truncate max-w-[300px]">{article.title}</span>
|
||||
</div>
|
||||
@@ -245,7 +245,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{/* Article Header */}
|
||||
<div className="mb-8">
|
||||
<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}
|
||||
</Link>
|
||||
<span className="text-xs text-[#777]">{article.date}</span>
|
||||
@@ -271,7 +271,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div>
|
||||
<Link
|
||||
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}
|
||||
</Link>
|
||||
<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)}`}
|
||||
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">
|
||||
<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" />
|
||||
@@ -294,39 +294,39 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
</a>
|
||||
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(articleUrl)}`}
|
||||
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>
|
||||
</a>
|
||||
<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)"
|
||||
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>
|
||||
</a>
|
||||
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(articleUrl)}`}
|
||||
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>
|
||||
</a>
|
||||
<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"
|
||||
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>
|
||||
</a>
|
||||
<a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(article.title + " " + articleUrl)}`}
|
||||
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>
|
||||
</a>
|
||||
<button type="button"
|
||||
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
|
||||
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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</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}
|
||||
disabled={savingState !== "idle"}
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{article.tags.map((tag) => (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
))}
|
||||
@@ -380,7 +380,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div className="pt-6 border-t border-[#222]">
|
||||
<Link
|
||||
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">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
@@ -398,7 +398,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{/* Related Articles */}
|
||||
{/* DO NOT OVERRIDE — related articles sidebar */}
|
||||
<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
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -417,10 +417,10 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
/>
|
||||
</div>
|
||||
<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}
|
||||
</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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -435,19 +435,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
recently-active threads so the box is never empty. */}
|
||||
{relatedForumThreads.length > 0 && (
|
||||
<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
|
||||
</h3>
|
||||
<ul className="space-y-3.5">
|
||||
{relatedForumThreads.map((t) => (
|
||||
<li key={t.id}>
|
||||
<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}
|
||||
</p>
|
||||
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
@@ -461,7 +461,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
</ul>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
@@ -479,7 +479,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div
|
||||
role="status"
|
||||
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}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="mt-10 pt-8 border-t border-[#2a2a2a]">
|
||||
<section className="mt-10 pt-8 border-t border-[#3a322b]">
|
||||
{/* Section Header */}
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<span className="section-label">Comments</span>
|
||||
@@ -127,15 +127,15 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
{comments.length} {comments.length === 1 ? "comment" : "comments"}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
<div className="flex-1 h-px bg-[#3a322b]" />
|
||||
</div>
|
||||
|
||||
{/* Comment Form */}
|
||||
{user ? (
|
||||
<form onSubmit={handleSubmit} className="mb-8">
|
||||
<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">
|
||||
<span className="font-body text-[10px] font-bold text-[#ffb800]">
|
||||
<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-[#F0A623]">
|
||||
{getInitials(user.user_metadata?.full_name || user.email || "U")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -146,10 +146,10 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
placeholder="Share your thoughts on this article..."
|
||||
rows={3}
|
||||
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 && (
|
||||
<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">
|
||||
<span className="font-body text-[11px] text-[#555]">
|
||||
@@ -158,7 +158,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -166,14 +166,14 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
</div>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
</div>
|
||||
@@ -208,7 +208,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
const isOwn = user?.id === comment.user_id;
|
||||
return (
|
||||
<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 ? (
|
||||
<img
|
||||
src={comment.user_profiles.avatar_url}
|
||||
@@ -216,7 +216,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
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)}
|
||||
</span>
|
||||
)}
|
||||
@@ -230,7 +230,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
onClick={() => handleDelete(comment.id)}
|
||||
disabled={deletingId === comment.id}
|
||||
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"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function ClientReadout() {
|
||||
}, []);
|
||||
|
||||
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)}
|
||||
</pre>
|
||||
);
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function AuthorProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
{/* Cover Image */}
|
||||
<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"
|
||||
/>
|
||||
)}
|
||||
<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 className="max-w-container mx-auto px-4">
|
||||
{/* Author Header */}
|
||||
<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="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 && (
|
||||
<AppImage
|
||||
src={profile.avatar_url}
|
||||
@@ -145,10 +145,10 @@ export default function AuthorProfilePage() {
|
||||
)}
|
||||
</div>
|
||||
<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"}
|
||||
</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"}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-2">
|
||||
@@ -180,7 +180,7 @@ export default function AuthorProfilePage() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
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">
|
||||
<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"
|
||||
rel="noopener noreferrer"
|
||||
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">
|
||||
<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
|
||||
href={`mailto:${profile.email}`}
|
||||
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">
|
||||
<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) => (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
@@ -232,12 +232,12 @@ export default function AuthorProfilePage() {
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-0 border-b border-[#2a2a2a] mb-6">
|
||||
<div className="flex gap-0 border-b border-[#3a322b] mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab("articles")}
|
||||
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
|
||||
activeTab === "articles"
|
||||
? "border-[#ffb800] text-[#ffb800]"
|
||||
? "border-[#F0A623] text-[#F0A623]"
|
||||
: "border-transparent text-[#666] hover:text-[#aaa]"
|
||||
}`}
|
||||
>
|
||||
@@ -247,7 +247,7 @@ export default function AuthorProfilePage() {
|
||||
onClick={() => setActiveTab("about")}
|
||||
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-3 border-b-2 transition-colors ${
|
||||
activeTab === "about"
|
||||
? "border-[#ffb800] text-[#ffb800]"
|
||||
? "border-[#F0A623] text-[#F0A623]"
|
||||
: "border-transparent text-[#666] hover:text-[#aaa]"
|
||||
}`}
|
||||
>
|
||||
@@ -271,7 +271,7 @@ export default function AuthorProfilePage() {
|
||||
<Link
|
||||
key={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">
|
||||
<AppImage
|
||||
@@ -284,13 +284,13 @@ export default function AuthorProfilePage() {
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<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}
|
||||
</span>
|
||||
<span className="font-body text-[10px] text-[#555]">·</span>
|
||||
<span className="font-body text-[10px] text-[#555]">{article.readTime}</span>
|
||||
</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}
|
||||
</h3>
|
||||
<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 */}
|
||||
{activeTab === "about" && profile && (
|
||||
<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>
|
||||
{profile.bio ? (
|
||||
<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.city ||
|
||||
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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
{addressLines.length > 0 && (
|
||||
@@ -353,7 +353,7 @@ export default function AuthorProfilePage() {
|
||||
</p>
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
@@ -366,7 +366,7 @@ export default function AuthorProfilePage() {
|
||||
</p>
|
||||
<a
|
||||
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}
|
||||
</a>
|
||||
@@ -381,7 +381,7 @@ export default function AuthorProfilePage() {
|
||||
href={profile.website}
|
||||
target="_blank"
|
||||
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(/\/$/, "")}
|
||||
</a>
|
||||
@@ -392,7 +392,7 @@ export default function AuthorProfilePage() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{locationLine && (
|
||||
|
||||
@@ -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 ${
|
||||
view === item.id
|
||||
? '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>
|
||||
@@ -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">
|
||||
+ New Post
|
||||
</button>
|
||||
<button onClick={() => setView('my-posts')} className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] text-white rounded-lg text-sm hover:bg-[#222] transition-colors">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
@@ -453,7 +453,7 @@ export default function ContributorDashboard() {
|
||||
{submittingPost ? 'Submitting...' : profile?.role === 'contributor' ? 'Submit for Review' : 'Publish Post'}
|
||||
</button>
|
||||
<button type="button" onClick={() => setView('overview')}
|
||||
className="px-5 py-2.5 bg-[#1a1a1a] border border-[#333] text-[#888] rounded-lg text-sm hover:text-white hover:bg-[#222] transition-colors">
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -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(/\*\*(.+?)\*\*/g, '<strong class="text-white font-semibold">$1</strong>')
|
||||
.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(/^\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">')
|
||||
@@ -294,7 +294,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
|
||||
</button>
|
||||
<button
|
||||
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 →
|
||||
</button>
|
||||
@@ -353,7 +353,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
|
||||
onClick={() => { setSelectedVariant(v.prompt); setCustomPrompt(''); }}
|
||||
className={`text-left text-xs px-3 py-2 rounded border transition-colors ${
|
||||
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]'
|
||||
}`}
|
||||
>
|
||||
@@ -370,7 +370,7 @@ function RegenerateModal({ onClose, onRegenerate }: RegenerateModalProps) {
|
||||
onChange={e => { setCustomPrompt(e.target.value); setSelectedVariant(null); }}
|
||||
placeholder="e.g. Rewrite this focusing on the European market impact..."
|
||||
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>
|
||||
|
||||
@@ -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={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 ↺
|
||||
</button>
|
||||
@@ -418,7 +418,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
const found = PEN_NAME_PROFILES.find(p => p.name === e.target.value);
|
||||
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 => (
|
||||
<option key={p.name} value={p.name}>{p.name} ({p.site})</option>
|
||||
@@ -434,7 +434,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
</span>
|
||||
<span className="text-white font-semibold text-sm">{profile.name}</span>
|
||||
</div>
|
||||
<p className="text-[#ffb800] text-xs">{profile.title}</p>
|
||||
<p className="text-[#F0A623] text-xs">{profile.title}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -458,7 +458,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<button
|
||||
@@ -485,7 +485,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
<button
|
||||
key={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'}
|
||||
</button>
|
||||
@@ -514,7 +514,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
>
|
||||
<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-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>
|
||||
<p className="text-[#555] text-[10px]">{p.title}</p>
|
||||
</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"
|
||||
>
|
||||
<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'}`} />
|
||||
</div>
|
||||
<p className="text-[#aaa] text-xs line-clamp-3 leading-relaxed">{preview}…</p>
|
||||
<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]">{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>
|
||||
</button>
|
||||
);
|
||||
@@ -927,7 +927,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
{/* Header */}
|
||||
<div className="bg-[#111827] border-b border-[#1e2a3a] px-4 py-3 flex items-center justify-between">
|
||||
<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>
|
||||
<h1 className="text-white font-bold text-sm tracking-wide">AI Console</h1>
|
||||
</div>
|
||||
@@ -943,18 +943,18 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
onClick={() => setShowHistorySidebar(v => !v)}
|
||||
className={`text-xs px-2.5 py-1.5 rounded border transition-colors ${
|
||||
showHistorySidebar
|
||||
? 'bg-[#4a3500] border-[#ffb800]/40 text-[#ffb800]'
|
||||
? 'bg-[#412402] border-[#F0A623]/40 text-[#F0A623]'
|
||||
: 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]'
|
||||
}`}
|
||||
title="Toggle response history sidebar"
|
||||
>
|
||||
History
|
||||
{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>
|
||||
<Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#ffb800] 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/health" className="text-[#555] hover:text-[#F0A623] text-xs transition-colors">Health</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">
|
||||
+ New Session
|
||||
</button>
|
||||
@@ -1002,7 +1002,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
key={session.id}
|
||||
className={`group p-2.5 rounded border cursor-pointer transition-colors ${
|
||||
currentSessionId === session.id
|
||||
? 'border-[#ffb800]/40 bg-[#1e2a3a]'
|
||||
? 'border-[#F0A623]/40 bg-[#1e2a3a]'
|
||||
: '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={() => {
|
||||
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
|
||||
</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 →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -1066,7 +1066,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
<select
|
||||
value={selectedSite}
|
||||
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={2}>AV Beat</option>
|
||||
@@ -1077,7 +1077,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
<select
|
||||
value={selectedPenName}
|
||||
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 => (
|
||||
<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>
|
||||
<button
|
||||
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 →
|
||||
</button>
|
||||
@@ -1123,7 +1123,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
</div>
|
||||
)}
|
||||
<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' ? (
|
||||
<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
|
||||
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
|
||||
</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="bg-[#111827] border border-[#1e2a3a] rounded-lg px-4 py-3">
|
||||
<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-[#ffb800] 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: '0ms' }} />
|
||||
<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-[#F0A623] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
</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)}
|
||||
onKeyDown={e => e.key === 'Enter' && handleFetchUrl()}
|
||||
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
|
||||
/>
|
||||
<button
|
||||
onClick={handleFetchUrl}
|
||||
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'}
|
||||
</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">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</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..."
|
||||
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">
|
||||
<button
|
||||
onClick={() => handleSend()}
|
||||
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 ? (
|
||||
<span className="flex items-center gap-1">
|
||||
|
||||
@@ -117,7 +117,7 @@ export default function AIConsoleSettingsPage() {
|
||||
if (loading || loadingSettings) {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -127,14 +127,14 @@ export default function AIConsoleSettingsPage() {
|
||||
{/* Header */}
|
||||
<div className="bg-[#111827] border-b border-[#1e2a3a] px-6 py-4 flex items-center justify-between">
|
||||
<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>
|
||||
<h1 className="text-white font-bold text-sm">AI Console Settings</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
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'}
|
||||
</button>
|
||||
@@ -177,7 +177,7 @@ export default function AIConsoleSettingsPage() {
|
||||
value={settings.ollama_endpoint}
|
||||
onChange={e => update('ollama_endpoint', e.target.value)}
|
||||
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">
|
||||
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
|
||||
value={settings.ollama_model_default}
|
||||
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>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
@@ -223,7 +223,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.ollama_model_translation}
|
||||
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>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
@@ -234,7 +234,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.ollama_model_background}
|
||||
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>
|
||||
{connectionResult.models.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
@@ -250,7 +250,7 @@ export default function AIConsoleSettingsPage() {
|
||||
</div>
|
||||
<button
|
||||
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'}`} />
|
||||
</button>
|
||||
@@ -263,7 +263,7 @@ export default function AIConsoleSettingsPage() {
|
||||
value={settings.failover_timeout_seconds}
|
||||
onChange={e => update('failover_timeout_seconds', Number(e.target.value))}
|
||||
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>
|
||||
@@ -301,7 +301,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.default_site_id}
|
||||
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={2}>AV Beat</option>
|
||||
@@ -312,7 +312,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.default_pen_name_bb}
|
||||
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>)}
|
||||
</select>
|
||||
@@ -322,7 +322,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.default_pen_name_av}
|
||||
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>)}
|
||||
</select>
|
||||
@@ -335,7 +335,7 @@ export default function AIConsoleSettingsPage() {
|
||||
</div>
|
||||
<button
|
||||
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'}`} />
|
||||
</button>
|
||||
@@ -346,7 +346,7 @@ export default function AIConsoleSettingsPage() {
|
||||
<select
|
||||
value={settings.history_retention_days}
|
||||
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={60}>60 days</option>
|
||||
@@ -361,7 +361,7 @@ export default function AIConsoleSettingsPage() {
|
||||
</div>
|
||||
<button
|
||||
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'}`} />
|
||||
</button>
|
||||
@@ -380,7 +380,7 @@ export default function AIConsoleSettingsPage() {
|
||||
</div>
|
||||
<button
|
||||
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'}`} />
|
||||
</button>
|
||||
@@ -393,7 +393,7 @@ export default function AIConsoleSettingsPage() {
|
||||
value={settings.background_timeout_seconds}
|
||||
onChange={e => update('background_timeout_seconds', Number(e.target.value))}
|
||||
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>
|
||||
|
||||
@@ -404,7 +404,7 @@ export default function AIConsoleSettingsPage() {
|
||||
</div>
|
||||
<button
|
||||
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'}`} />
|
||||
</button>
|
||||
@@ -422,7 +422,7 @@ export default function AIConsoleSettingsPage() {
|
||||
'Classification and extraction tasks',
|
||||
].map((item, i) => (
|
||||
<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}
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -130,9 +130,9 @@ export default function OverviewTab({ companyId, selectedCompany }: Props) {
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
stroke={selectedCompany === 'rmp' ? '#ffb800' : '#a855f7'}
|
||||
stroke={selectedCompany === 'rmp' ? '#F0A623' : '#a855f7'}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: selectedCompany === 'rmp' ? '#ffb800' : '#a855f7', r: 3 }}
|
||||
dot={{ fill: selectedCompany === 'rmp' ? '#F0A623' : '#a855f7', r: 3 }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -269,13 +269,13 @@ export default function EditorialDashboard() {
|
||||
|
||||
function SortIcon({ col }: { col: typeof sortBy }) {
|
||||
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) {
|
||||
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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ export default function EditorialDashboard() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0a0a0a] text-[#e0e0e0]">
|
||||
{/* 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>
|
||||
<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">
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
@@ -325,7 +325,7 @@ export default function EditorialDashboard() {
|
||||
<button
|
||||
key={card.label}
|
||||
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-[11px] text-[#666] mt-1 uppercase tracking-wider">{card.label}</div>
|
||||
@@ -344,7 +344,7 @@ export default function EditorialDashboard() {
|
||||
onClick={() => setStatusFilter(s)}
|
||||
className={`px-3 py-1.5 text-xs font-semibold capitalize transition-colors ${
|
||||
statusFilter === s
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -360,7 +360,7 @@ export default function EditorialDashboard() {
|
||||
placeholder="Search articles, clients, campaigns..."
|
||||
value={searchQuery}
|
||||
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 && (
|
||||
<button
|
||||
@@ -376,10 +376,10 @@ export default function EditorialDashboard() {
|
||||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<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>
|
||||
) : filtered.length === 0 ? (
|
||||
<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">
|
||||
<table className="w-full text-xs">
|
||||
<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-3 py-3 text-[#555] font-semibold uppercase tracking-wider">Status</th>
|
||||
<th
|
||||
@@ -437,7 +437,7 @@ export default function EditorialDashboard() {
|
||||
<span className="text-[#555]">{article.author_name}</span>
|
||||
)}
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -256,7 +256,7 @@ export default function ShowCalendarPage() {
|
||||
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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -281,7 +281,7 @@ export default function ShowCalendarPage() {
|
||||
)}
|
||||
|
||||
{/* 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="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -294,13 +294,13 @@ export default function ShowCalendarPage() {
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -316,8 +316,8 @@ export default function ShowCalendarPage() {
|
||||
onClick={() => setSiteFilter(s)}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
siteFilter === s
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'bg-[#231d18] text-[#888] hover:text-white border border-[#333]'
|
||||
}`}
|
||||
>
|
||||
{s === '' ? 'All Sites' : s === '1' ? '🔵 AV Beat' : '🟠 AV Beat'}
|
||||
@@ -326,14 +326,14 @@ export default function ShowCalendarPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab.id
|
||||
? 'border-[#ffb800] text-white'
|
||||
? 'border-[#F0A623] 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">
|
||||
{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 className="w-8 h-8 border-2 border-[#F0A623] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -383,7 +383,7 @@ export default function ShowCalendarPage() {
|
||||
<span>Engine: {formatDate(event.story_engine_start_date)} – {formatDate(event.story_engine_end_date)}</span>
|
||||
{event.official_site && (
|
||||
<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 className="flex items-center gap-3 mt-2 text-xs text-[#555]">
|
||||
@@ -400,13 +400,13 @@ export default function ShowCalendarPage() {
|
||||
<button
|
||||
onClick={() => handleExtractStyle(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'}
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@@ -440,8 +440,8 @@ export default function ShowCalendarPage() {
|
||||
onClick={() => setQueueStatus(s)}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
queueStatus === s
|
||||
? 'bg-[#ffb800] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'bg-[#231d18] text-[#888] hover:text-white border border-[#333]'
|
||||
}`}
|
||||
>
|
||||
{s.replace('_', ' ')}
|
||||
@@ -460,7 +460,7 @@ export default function ShowCalendarPage() {
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<SiteBadge siteId={item.site_id} />
|
||||
{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 && (
|
||||
<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 ${
|
||||
item.status === 'published' ? 'bg-green-900/20 text-green-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.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'}`}>
|
||||
@@ -490,7 +490,7 @@ export default function ShowCalendarPage() {
|
||||
|
||||
{/* Expanded 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
|
||||
className="text-[#aaa] text-xs prose prose-invert max-w-none"
|
||||
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">
|
||||
<button
|
||||
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'}
|
||||
</button>
|
||||
@@ -541,7 +541,7 @@ export default function ShowCalendarPage() {
|
||||
<select
|
||||
value={showFilter}
|
||||
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>
|
||||
{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>
|
||||
)}
|
||||
{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-[#888]">{ex.booth_number || '—'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<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 === '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>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -613,14 +613,14 @@ export default function ShowCalendarPage() {
|
||||
Updated: {formatDate(profile.last_updated)}
|
||||
</p>
|
||||
{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>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
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'}
|
||||
</button>
|
||||
@@ -637,24 +637,24 @@ export default function ShowCalendarPage() {
|
||||
<div className="bg-[#111] border border-[#1e1e1e] rounded-xl p-6">
|
||||
<h2 className="text-white font-semibold mb-4">Show Engine Settings</h2>
|
||||
<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>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 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="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>
|
||||
</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>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 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>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 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>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>
|
||||
@@ -779,14 +779,14 @@ export default function ShowCalendarPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -857,14 +857,14 @@ export default function ShowCalendarPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function Error({
|
||||
console.error("[route error]", error);
|
||||
}, [error]);
|
||||
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">
|
||||
<h2 className="font-heading text-2xl mb-3">Something went wrong</h2>
|
||||
<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">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
@@ -74,27 +74,27 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
|
||||
|
||||
const getMetricValue = (thread: ForumThread) => {
|
||||
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 (
|
||||
<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 */}
|
||||
<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">
|
||||
<span className="text-lg">🏆</span>
|
||||
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2>
|
||||
</div>
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={m.value}
|
||||
onClick={() => setActiveMetric(m.value)}
|
||||
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||
activeMetric === m.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -111,12 +111,12 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
|
||||
<>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<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="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>
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
@@ -142,11 +142,11 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
|
||||
</span>
|
||||
{/* Thread info */}
|
||||
<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}
|
||||
</p>
|
||||
<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)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -277,12 +277,12 @@ export default function ForumCategoryPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-[#111111]">
|
||||
<main className="min-h-screen bg-[#1c1815]">
|
||||
{/* 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">
|
||||
<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="text-[#aab4c4]">{category?.name || slug}</span>
|
||||
</nav>
|
||||
@@ -299,14 +299,14 @@ export default function ForumCategoryPage() {
|
||||
{isAuthenticated ? (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
@@ -318,7 +318,7 @@ export default function ForumCategoryPage() {
|
||||
<div className="max-w-container mx-auto px-4 py-6">
|
||||
{/* New Thread Form — auth-gated */}
|
||||
{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>
|
||||
<div className="space-y-3">
|
||||
<input
|
||||
@@ -327,7 +327,7 @@ export default function ForumCategoryPage() {
|
||||
value={newTitle}
|
||||
onChange={e => setNewTitle(e.target.value)}
|
||||
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
|
||||
placeholder="What would you like to discuss? *"
|
||||
@@ -335,14 +335,14 @@ export default function ForumCategoryPage() {
|
||||
onChange={e => setNewBody(e.target.value)}
|
||||
required
|
||||
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">
|
||||
<ImageAttachButton onInsert={(md) => setNewBody((b) => (b || "") + md)} />
|
||||
<span className="text-[10px] text-[#666]">Auto-optimized, never > 20 MB</span>
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@@ -350,14 +350,14 @@ export default function ForumCategoryPage() {
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -378,7 +378,7 @@ export default function ForumCategoryPage() {
|
||||
placeholder="Search threads in this category..."
|
||||
value={search}
|
||||
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 && (
|
||||
<button
|
||||
@@ -394,14 +394,14 @@ export default function ForumCategoryPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setSort(opt.value)}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||
sort === opt.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -414,7 +414,7 @@ export default function ForumCategoryPage() {
|
||||
{(loading || threadsLoading) && (
|
||||
<div className="space-y-2">
|
||||
{[...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>
|
||||
)}
|
||||
@@ -445,12 +445,12 @@ export default function ForumCategoryPage() {
|
||||
<Link
|
||||
key={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 items-center gap-2 flex-wrap">
|
||||
{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' && (
|
||||
<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 && (
|
||||
<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}
|
||||
</h3>
|
||||
</div>
|
||||
<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)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -30,10 +30,10 @@ function ForgotInner() {
|
||||
return (
|
||||
<>
|
||||
<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="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>
|
||||
<p className="text-[#888] font-body text-sm mt-2">
|
||||
Enter your email — we’ll send a link to set a new password.
|
||||
@@ -41,10 +41,10 @@ function ForgotInner() {
|
||||
</div>
|
||||
<form
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
@@ -63,19 +63,19 @@ function ForgotInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</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?{" "}
|
||||
<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
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -51,10 +51,10 @@ function ForumLoginInner() {
|
||||
return (
|
||||
<>
|
||||
<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="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>
|
||||
<p className="text-[#888] font-body text-sm mt-2">
|
||||
Your account covers the forum, newsletter preferences, and saved articles.
|
||||
@@ -62,10 +62,10 @@ function ForumLoginInner() {
|
||||
</div>
|
||||
<form
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
@@ -79,7 +79,7 @@ function ForumLoginInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
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>
|
||||
@@ -92,20 +92,20 @@ function ForumLoginInner() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<div className="text-right text-xs font-body">
|
||||
<Link
|
||||
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?
|
||||
</Link>
|
||||
@@ -115,7 +115,7 @@ function ForumLoginInner() {
|
||||
No account yet?{' '}
|
||||
<Link
|
||||
href={`/forum/register?next=${encodeURIComponent(next)}`}
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
Create one
|
||||
</Link>
|
||||
|
||||
@@ -103,27 +103,27 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
|
||||
const getMetricValue = (thread: ForumThread) => {
|
||||
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 (
|
||||
<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 */}
|
||||
<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">
|
||||
<span className="text-lg">🏆</span>
|
||||
<h2 className="text-white font-heading font-bold text-base">Top Performing Threads</h2>
|
||||
</div>
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={m.value}
|
||||
onClick={() => setActiveMetric(m.value)}
|
||||
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||
activeMetric === m.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -140,12 +140,12 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
<>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<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="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>
|
||||
<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>
|
||||
))}
|
||||
</>
|
||||
@@ -171,13 +171,13 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
</span>
|
||||
{/* Thread info */}
|
||||
<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}
|
||||
</p>
|
||||
<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 && (
|
||||
<> · <span className="text-[#ffb800]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#F0A623]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -283,10 +283,10 @@ export default function ForumIndexPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-[#111111]">
|
||||
<main className="min-h-screen bg-[#1c1815]">
|
||||
{/* Hero — width-aligned with content below + featured-box pattern */}
|
||||
<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-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
@@ -301,7 +301,7 @@ export default function ForumIndexPage() {
|
||||
{currentUserId ? (
|
||||
<Link
|
||||
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">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
@@ -311,7 +311,7 @@ export default function ForumIndexPage() {
|
||||
) : (
|
||||
<Link
|
||||
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">
|
||||
<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>
|
||||
|
||||
{/* 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
|
||||
onClick={() => setFeedMode('discover')}
|
||||
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">
|
||||
@@ -344,7 +344,7 @@ export default function ForumIndexPage() {
|
||||
<button
|
||||
onClick={() => setFeedMode('following')}
|
||||
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">
|
||||
@@ -359,11 +359,11 @@ export default function ForumIndexPage() {
|
||||
{feedMode === 'following' && (
|
||||
<div className="mb-8">
|
||||
{!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>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
@@ -372,19 +372,19 @@ export default function ForumIndexPage() {
|
||||
{currentUserId && followingLoading && (
|
||||
<div className="space-y-2">
|
||||
{[...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>
|
||||
)}
|
||||
{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>
|
||||
<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>
|
||||
<Link
|
||||
href="/forum"
|
||||
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
|
||||
</Link>
|
||||
@@ -398,10 +398,10 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={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">
|
||||
<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}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
@@ -409,7 +409,7 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
href={`/profile/${thread.author_id}`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
{thread.author_name}
|
||||
</Link>
|
||||
@@ -450,7 +450,7 @@ export default function ForumIndexPage() {
|
||||
placeholder="Search threads..."
|
||||
value={search}
|
||||
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 && (
|
||||
<button
|
||||
@@ -470,7 +470,7 @@ export default function ForumIndexPage() {
|
||||
<select
|
||||
value={selectedCategory}
|
||||
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>
|
||||
{categories.map(cat => (
|
||||
@@ -483,14 +483,14 @@ export default function ForumIndexPage() {
|
||||
</div>
|
||||
|
||||
{/* 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 => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setSort(opt.value)}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||
sort === opt.value
|
||||
? 'bg-[#ffb800] text-white'
|
||||
? 'bg-[#F0A623] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -506,7 +506,7 @@ export default function ForumIndexPage() {
|
||||
{threadsLoading && (
|
||||
<div className="space-y-2">
|
||||
{[...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>
|
||||
)}
|
||||
@@ -523,16 +523,16 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={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">
|
||||
<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}
|
||||
</h3>
|
||||
<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 && (
|
||||
<> · <span className="text-[#ffb800]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#F0A623]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -560,7 +560,7 @@ export default function ForumIndexPage() {
|
||||
{loading && (
|
||||
<div className="space-y-3 mb-8">
|
||||
{[...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>
|
||||
)}
|
||||
@@ -584,11 +584,11 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={cat.id}
|
||||
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>
|
||||
<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}
|
||||
</h2>
|
||||
<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-[#666] font-body text-xs">posts</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" />
|
||||
</svg>
|
||||
</Link>
|
||||
@@ -624,12 +624,12 @@ export default function ForumIndexPage() {
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
<ul className="text-[#aab4c4] font-body text-sm space-y-1">
|
||||
<li>• Be respectful and professional — this is a community of peers</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>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function EditForumProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
</main>
|
||||
<Footer />
|
||||
@@ -85,7 +85,7 @@ export default function EditForumProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
Profile unavailable.
|
||||
</div>
|
||||
@@ -98,13 +98,13 @@ export default function EditForumProfilePage() {
|
||||
return (
|
||||
<>
|
||||
<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="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-heading font-bold text-white">Edit Profile</h1>
|
||||
<Link
|
||||
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 →
|
||||
</Link>
|
||||
@@ -112,10 +112,10 @@ export default function EditForumProfilePage() {
|
||||
|
||||
<form
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
@@ -132,7 +132,7 @@ export default function EditForumProfilePage() {
|
||||
onChange={(e) => update('username', e.target.value)}
|
||||
pattern="[A-Za-z0-9_]{3,24}"
|
||||
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">
|
||||
3-24 characters. Letters, numbers, underscore.
|
||||
@@ -145,7 +145,7 @@ export default function EditForumProfilePage() {
|
||||
value={profile.display_name}
|
||||
onChange={(e) => update('display_name', e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function EditForumProfilePage() {
|
||||
value={profile.bio || ''}
|
||||
onChange={(e) => update('bio', e.target.value)}
|
||||
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>
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function EditForumProfilePage() {
|
||||
type="text"
|
||||
value={profile.role_title || ''}
|
||||
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 label="Company">
|
||||
@@ -172,7 +172,7 @@ export default function EditForumProfilePage() {
|
||||
type="text"
|
||||
value={profile.company || ''}
|
||||
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>
|
||||
</div>
|
||||
@@ -183,7 +183,7 @@ export default function EditForumProfilePage() {
|
||||
type="text"
|
||||
value={profile.location_city || ''}
|
||||
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 label="Country">
|
||||
@@ -191,7 +191,7 @@ export default function EditForumProfilePage() {
|
||||
type="text"
|
||||
value={profile.location_country || ''}
|
||||
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>
|
||||
</div>
|
||||
@@ -202,14 +202,14 @@ export default function EditForumProfilePage() {
|
||||
value={profile.avatar_url || ''}
|
||||
onChange={(e) => update('avatar_url', e.target.value)}
|
||||
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>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
|
||||
@@ -101,11 +101,11 @@ function ForumRegisterInner() {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
{/* Left rail — what membership gets you */}
|
||||
<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">
|
||||
Join AV Beat
|
||||
</h1>
|
||||
@@ -128,10 +128,10 @@ function ForumRegisterInner() {
|
||||
</li>
|
||||
))}
|
||||
</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 & 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>
|
||||
</aside>
|
||||
|
||||
@@ -145,14 +145,14 @@ function ForumRegisterInner() {
|
||||
|
||||
<form
|
||||
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="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>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
@@ -171,7 +171,7 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
autoComplete="name"
|
||||
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>
|
||||
@@ -184,7 +184,7 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
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>
|
||||
@@ -198,7 +198,7 @@ function ForumRegisterInner() {
|
||||
autoComplete="new-password"
|
||||
required
|
||||
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>
|
||||
@@ -211,29 +211,29 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
|
||||
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{' '}
|
||||
<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’ll never sell your email.
|
||||
</p>
|
||||
<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?{' '}
|
||||
<Link
|
||||
href={`/forum/login?next=${encodeURIComponent(next)}`}
|
||||
className="text-[#ffb800] hover:underline font-semibold"
|
||||
className="text-[#F0A623] hover:underline font-semibold"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
|
||||
@@ -40,20 +40,20 @@ function ResetInner() {
|
||||
return (
|
||||
<>
|
||||
<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="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>
|
||||
</div>
|
||||
{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">
|
||||
This reset link is expired or invalid.
|
||||
</p>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
@@ -61,10 +61,10 @@ function ResetInner() {
|
||||
) : (
|
||||
<form
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
@@ -84,7 +84,7 @@ function ResetInner() {
|
||||
autoComplete="new-password"
|
||||
minLength={8}
|
||||
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>
|
||||
@@ -97,13 +97,13 @@ function ResetInner() {
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
|
||||
@@ -67,13 +67,13 @@ function timeAgo(dateStr: string): string {
|
||||
function Avatar({ name, isAI }: { name: string; isAI: boolean }) {
|
||||
if (isAI) {
|
||||
return (
|
||||
<div className="w-9 h-9 rounded-full bg-[#4a3500] border border-[#ffb800] flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-[#ffb800] text-xs font-bold">AI</span>
|
||||
<div className="w-9 h-9 rounded-full bg-[#412402] border border-[#F0A623] flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-[#F0A623] text-xs font-bold">AI</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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;
|
||||
return (
|
||||
<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 (
|
||||
<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'}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
|
||||
currentVote === 1
|
||||
? 'bg-[#ffb800]/20 border-[#ffb800] text-[#ffb800]'
|
||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#ffb800] hover:text-[#ffb800]'
|
||||
? 'bg-[#F0A623]/20 border-[#F0A623] text-[#F0A623]'
|
||||
: 'bg-transparent border-[#3a322b] text-[#666] hover:border-[#F0A623] hover:text-[#F0A623]'
|
||||
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
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'}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
|
||||
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`}
|
||||
aria-label="Downvote"
|
||||
>
|
||||
@@ -178,7 +178,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
</button>
|
||||
|
||||
{!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
|
||||
</Link>
|
||||
)}
|
||||
@@ -300,12 +300,12 @@ export default function ForumThreadPage() {
|
||||
return (
|
||||
<>
|
||||
<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>
|
||||
<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-32 bg-[#1a1a1a] rounded animate-pulse" />
|
||||
<div className="h-20 bg-[#1a1a1a] rounded animate-pulse" />
|
||||
<div className="h-8 bg-[#231d18] rounded animate-pulse w-2/3" />
|
||||
<div className="h-32 bg-[#231d18] rounded animate-pulse" />
|
||||
<div className="h-20 bg-[#231d18] rounded animate-pulse" />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
@@ -317,12 +317,12 @@ export default function ForumThreadPage() {
|
||||
return (
|
||||
<>
|
||||
<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="bg-red-900/20 border border-red-800 rounded-lg p-4 text-red-400 font-body">
|
||||
{error || 'Thread not found.'}
|
||||
</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
|
||||
</Link>
|
||||
</div>
|
||||
@@ -337,16 +337,16 @@ export default function ForumThreadPage() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="min-h-screen bg-[#111111]">
|
||||
<main className="min-h-screen bg-[#1c1815]">
|
||||
{/* 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">
|
||||
<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>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
@@ -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>
|
||||
|
||||
{/* 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">
|
||||
<Avatar name={thread.author_name} isAI={false} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<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">{timeAgo(thread.created_at)}</span>
|
||||
<span className="text-[#555] font-body text-xs">·</span>
|
||||
@@ -395,17 +395,17 @@ export default function ForumThreadPage() {
|
||||
<div
|
||||
className={`border rounded-lg p-5 ${
|
||||
reply.is_ai_response
|
||||
? 'bg-[#0d1f35] border-[#4a3500]'
|
||||
: 'bg-[#1a1a1a] border-[#252525]'
|
||||
? 'bg-[#0d1f35] border-[#412402]'
|
||||
: 'bg-[#231d18] border-[#3a322b]'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Avatar name={reply.author_name} isAI={reply.is_ai_response} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<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 && (
|
||||
<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
|
||||
</span>
|
||||
)}
|
||||
@@ -441,14 +441,14 @@ export default function ForumThreadPage() {
|
||||
)}
|
||||
|
||||
{/* 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="w-7 h-7 rounded-full bg-[#4a3500] border border-[#ffb800] flex items-center justify-center">
|
||||
<span className="text-[#ffb800] text-xs font-bold">AI</span>
|
||||
<div className="w-7 h-7 rounded-full bg-[#412402] border border-[#F0A623] flex items-center justify-center">
|
||||
<span className="text-[#F0A623] text-xs font-bold">AI</span>
|
||||
</div>
|
||||
<div>
|
||||
<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>
|
||||
<p className="text-[#7a9ab8] font-body text-xs mb-3">
|
||||
@@ -461,12 +461,12 @@ export default function ForumThreadPage() {
|
||||
value={aiQuestion}
|
||||
onChange={e => setAiQuestion(e.target.value)}
|
||||
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
|
||||
type="submit"
|
||||
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 ? (
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -486,7 +486,7 @@ export default function ForumThreadPage() {
|
||||
|
||||
{/* Reply Form — auth-gated */}
|
||||
{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>
|
||||
<form onSubmit={handleReply} className="space-y-3">
|
||||
<textarea
|
||||
@@ -495,21 +495,21 @@ export default function ForumThreadPage() {
|
||||
onChange={e => setReplyBody(e.target.value)}
|
||||
required
|
||||
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">
|
||||
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
|
||||
<span className="text-[10px] text-[#666]">Auto-optimized, never > 20 MB</span>
|
||||
</div>
|
||||
{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}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
@@ -518,7 +518,7 @@ export default function ForumThreadPage() {
|
||||
)}
|
||||
|
||||
{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>
|
||||
<p className="text-[#7a9ab8] font-body text-sm mb-4">
|
||||
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">
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
@@ -541,7 +541,7 @@ export default function ForumThreadPage() {
|
||||
)}
|
||||
|
||||
{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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function ForumUserPage() {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<p className="font-body text-sm text-[#666]">Loading profile…</p>
|
||||
</div>
|
||||
@@ -120,11 +120,11 @@ export default function ForumUserPage() {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</main>
|
||||
<Footer />
|
||||
@@ -139,16 +139,16 @@ export default function ForumUserPage() {
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
{/* 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="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 ? (
|
||||
<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()}
|
||||
</span>
|
||||
)}
|
||||
@@ -156,19 +156,19 @@ export default function ForumUserPage() {
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<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}
|
||||
</h1>
|
||||
{isOwnProfile && (
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
)}
|
||||
</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">
|
||||
{archetypeLabel && (
|
||||
@@ -198,7 +198,7 @@ export default function ForumUserPage() {
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</p>
|
||||
)}
|
||||
@@ -206,7 +206,7 @@ export default function ForumUserPage() {
|
||||
{profile.expertise_tags && profile.expertise_tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
@@ -217,7 +217,7 @@ export default function ForumUserPage() {
|
||||
{/* Activity grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6">
|
||||
<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})
|
||||
</h2>
|
||||
{threads.length === 0 ? (
|
||||
@@ -225,8 +225,8 @@ export default function ForumUserPage() {
|
||||
) : (
|
||||
<ul className="space-y-2 mb-8">
|
||||
{threads.map((t) => (
|
||||
<li key={t.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-3 hover:border-[#ffb800]/50 transition-colors">
|
||||
<Link href={`/forum/thread/${t.id}`} className="font-body text-sm font-semibold text-[#e0e0e0] hover:text-[#ffb800] block">
|
||||
<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-[#F0A623] block">
|
||||
{t.title}
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 mt-1 text-[11px] text-[#666]">
|
||||
@@ -243,7 +243,7 @@ export default function ForumUserPage() {
|
||||
</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
|
||||
</h2>
|
||||
{replies.length === 0 ? (
|
||||
@@ -251,8 +251,8 @@ export default function ForumUserPage() {
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{replies.map((r) => (
|
||||
<li key={r.id} className="bg-[#1a1a1a] border border-[#2a2a2a] rounded-sm p-3">
|
||||
<Link href={`/forum/thread/${r.thread_id}`} className="font-body text-xs font-semibold text-[#ffb800] hover:underline">
|
||||
<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-[#F0A623] hover:underline">
|
||||
{r.forum_threads?.title || "(thread)"}
|
||||
</Link>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -34,7 +34,7 @@ export default async function GearPage() {
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="section-label">Gear & Reviews</span>
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
<div className="flex-1 h-px bg-[#3a322b]" />
|
||||
</div>
|
||||
<h1 className="font-heading text-white text-3xl font-bold">Production Gear & 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>
|
||||
@@ -49,7 +49,7 @@ export default async function GearPage() {
|
||||
<Link
|
||||
key={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">
|
||||
<AppImage
|
||||
src={article.image}
|
||||
@@ -59,7 +59,7 @@ export default async function GearPage() {
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,13 +74,13 @@ export default async function GearPage() {
|
||||
)}
|
||||
<span className="text-[#555] font-body text-[11px]">{article.readTime}</span>
|
||||
</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}
|
||||
</h2>
|
||||
<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">
|
||||
{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}
|
||||
</span>
|
||||
))}
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function GlobalError({
|
||||
}, [error]);
|
||||
return (
|
||||
<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" }}>
|
||||
<h2 style={{ fontSize: 22, marginBottom: 12 }}>Something went wrong</h2>
|
||||
<p style={{ marginBottom: 12, color: "#888" }}>AV Beat hit an unexpected error.</p>
|
||||
@@ -22,7 +22,7 @@ export default function GlobalError({
|
||||
)}
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -305,7 +305,7 @@ export default function ArticleFeed() {
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-3 mb-4 md:mb-5 flex-wrap">
|
||||
<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 */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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}
|
||||
onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }}
|
||||
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="trending">Trending</option>
|
||||
@@ -328,13 +328,13 @@ export default function ArticleFeed() {
|
||||
{/* Feed mode toggle */}
|
||||
<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>
|
||||
<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
|
||||
onClick={() => handleFeedModeChange("pagination")}
|
||||
aria-pressed={feedMode === "pagination"}
|
||||
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] ${
|
||||
feedMode === "pagination" ?"bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
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-[#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">
|
||||
<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")}
|
||||
aria-pressed={feedMode === "infinite"}
|
||||
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] ${
|
||||
feedMode === "infinite" ?"bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
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-[#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">
|
||||
<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}
|
||||
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||
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"
|
||||
aria-label="Filter articles from date"
|
||||
/>
|
||||
@@ -382,7 +382,7 @@ export default function ArticleFeed() {
|
||||
value={dateTo}
|
||||
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||
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"
|
||||
aria-label="Filter articles to date"
|
||||
/>
|
||||
@@ -401,17 +401,17 @@ export default function ArticleFeed() {
|
||||
onClick={() => setShowAdvanced((v) => !v)}
|
||||
aria-expanded={showAdvanced}
|
||||
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
|
||||
? "bg-[#ffb800]/15 text-[#ffb800] border-[#ffb800]/40"
|
||||
: "bg-[#111] text-[#666] border-[#2a2a2a] hover:text-[#aaa] hover:border-[#3a3a3a]"
|
||||
? "bg-[#F0A623]/15 text-[#F0A623] border-[#F0A623]/40"
|
||||
: "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">
|
||||
<path d="M1 3h10M3 6h6M5 9h2" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
|
||||
</svg>
|
||||
More
|
||||
{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>
|
||||
</div>
|
||||
@@ -428,10 +428,10 @@ export default function ArticleFeed() {
|
||||
type="button"
|
||||
onClick={() => { setActiveCategory(cat); setPage(1); }}
|
||||
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
|
||||
? "bg-[#ffb800]/20 text-[#ffb800] border border-[#ffb800]/50"
|
||||
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc] hover:border-[#3a3a3a]"
|
||||
? "bg-[#F0A623]/20 text-[#F0A623] border border-[#F0A623]/50"
|
||||
: "bg-[#111] text-[#888] border border-[#3a322b] hover:text-[#ccc] hover:border-[#3a3a3a]"
|
||||
}`}>
|
||||
{cat}
|
||||
</button>
|
||||
@@ -444,7 +444,7 @@ export default function ArticleFeed() {
|
||||
{showAdvanced && (
|
||||
<div
|
||||
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"
|
||||
aria-label="Advanced filters">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
@@ -458,7 +458,7 @@ export default function ArticleFeed() {
|
||||
id={`${feedId}-author`}
|
||||
value={authorFilter}
|
||||
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">
|
||||
<option value="All">All Authors</option>
|
||||
{uniqueAuthors.map((author) => (
|
||||
@@ -475,7 +475,7 @@ export default function ArticleFeed() {
|
||||
{/* Source filter inside advanced panel */}
|
||||
<div className="space-y-1">
|
||||
<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) => {
|
||||
const isActive = sourceFilter === sf;
|
||||
return (
|
||||
@@ -483,10 +483,10 @@ export default function ArticleFeed() {
|
||||
key={sf}
|
||||
onClick={() => { setSourceFilter(sf); setPage(1); }}
|
||||
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
|
||||
? 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 === "Imported" && importedCount > 0 && (
|
||||
@@ -507,7 +507,7 @@ export default function ArticleFeed() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@@ -537,7 +537,7 @@ export default function ArticleFeed() {
|
||||
{sortedFiltered.length === 0 && (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
@@ -557,7 +557,7 @@ export default function ArticleFeed() {
|
||||
</p>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
@@ -585,7 +585,7 @@ export default function ArticleFeed() {
|
||||
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'
|
||||
: 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 (
|
||||
<Fragment key={`feed-${i}`}>
|
||||
@@ -593,7 +593,7 @@ export default function ArticleFeed() {
|
||||
<Link
|
||||
href={articleHref}
|
||||
{...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}
|
||||
aria-hidden="true"
|
||||
>
|
||||
@@ -614,21 +614,21 @@ export default function ArticleFeed() {
|
||||
</span>
|
||||
)}
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
{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}
|
||||
</span>
|
||||
)}
|
||||
</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
|
||||
href={articleHref}
|
||||
{...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}
|
||||
</Link>
|
||||
</h3>
|
||||
@@ -667,42 +667,42 @@ export default function ArticleFeed() {
|
||||
const shareUrl = `https://avbeat.com${articleHref.startsWith('/') ? articleHref : `/articles/${article?.slug}`}`;
|
||||
const shareTitle = article?.title || '';
|
||||
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>
|
||||
<a href={`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(shareUrl)}`}
|
||||
target="_blank" rel="noopener noreferrer" aria-label="Share on LinkedIn" title="LinkedIn"
|
||||
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>
|
||||
</a>
|
||||
<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)"
|
||||
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>
|
||||
</a>
|
||||
<a href={`https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`}
|
||||
target="_blank" rel="noopener noreferrer" aria-label="Share on Facebook" title="Facebook"
|
||||
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>
|
||||
</a>
|
||||
<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"
|
||||
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>
|
||||
</a>
|
||||
<a href={`https://api.whatsapp.com/send?text=${encodeURIComponent(shareTitle + ' ' + shareUrl)}`}
|
||||
target="_blank" rel="noopener noreferrer" aria-label="Share on WhatsApp" title="WhatsApp"
|
||||
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>
|
||||
</a>
|
||||
<a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
||||
aria-label="Share via email" title="Email"
|
||||
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">
|
||||
<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>
|
||||
@@ -735,9 +735,9 @@ export default function ArticleFeed() {
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles">
|
||||
<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-[#ffb800] 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: "0ms" }} />
|
||||
<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-[#F0A623] animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||
</div>
|
||||
<span className="font-body text-xs text-[#555]">Loading more articles…</span>
|
||||
</div>
|
||||
@@ -762,7 +762,7 @@ export default function ArticleFeed() {
|
||||
</p>
|
||||
<div className="flex items-center gap-1" role="navigation" aria-label="Pagination">
|
||||
<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}
|
||||
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Previous page">
|
||||
@@ -786,7 +786,7 @@ export default function ArticleFeed() {
|
||||
) : (
|
||||
<button
|
||||
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" }); }}
|
||||
aria-label={`Page ${p}`}
|
||||
aria-current={page === p ? "page" : undefined}>
|
||||
@@ -796,7 +796,7 @@ export default function ArticleFeed() {
|
||||
);
|
||||
})()}
|
||||
<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}
|
||||
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Next page">
|
||||
|
||||
@@ -157,7 +157,7 @@ export default function FeaturedBento() {
|
||||
{/* Section header */}
|
||||
<div className="flex items-center gap-3 mb-4 md:mb-5">
|
||||
<span className="section-label">Featured</span>
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
<div className="flex-1 h-px bg-[#3a322b]" />
|
||||
{/* Navigation arrows */}
|
||||
<div
|
||||
className="flex items-center gap-1.5"
|
||||
@@ -173,7 +173,7 @@ export default function FeaturedBento() {
|
||||
type="button"
|
||||
onClick={() => { goPrev(); setPaused(true); }}
|
||||
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">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
@@ -182,7 +182,7 @@ export default function FeaturedBento() {
|
||||
type="button"
|
||||
onClick={() => { goNext(); setPaused(true); }}
|
||||
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">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
@@ -210,7 +210,7 @@ export default function FeaturedBento() {
|
||||
/>
|
||||
<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">
|
||||
<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}
|
||||
</span>
|
||||
<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
|
||||
key={`${article?.slug}-${offset}-${i}`}
|
||||
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]">
|
||||
<AppImage
|
||||
src={article?.image}
|
||||
@@ -246,13 +246,13 @@ export default function FeaturedBento() {
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 50vw, 22vw"
|
||||
/>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<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}
|
||||
</h3>
|
||||
<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}`}
|
||||
className={`rounded-full transition-all focus:outline-none ${
|
||||
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]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function NewsTicker() {
|
||||
const doubled = [...headlines, ...headlines];
|
||||
|
||||
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">
|
||||
{/* Label */}
|
||||
<div className="flex-shrink-0 bg-[#080808] flex items-center px-4 z-10">
|
||||
|
||||
@@ -60,13 +60,13 @@ export default function NewsletterSignup() {
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* Header */}
|
||||
<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>
|
||||
<div className="flex-1 h-px bg-[#4a3500] max-w-[80px]" />
|
||||
<div className="flex-1 h-px bg-[#412402] max-w-[80px]" />
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] text-xl md:text-2xl font-bold mb-1.5">
|
||||
Stay Ahead of the Pro AV Industry
|
||||
@@ -77,12 +77,12 @@ export default function NewsletterSignup() {
|
||||
|
||||
{submitted ? (
|
||||
<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"
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] font-bold text-lg mb-1">You're subscribed!</p>
|
||||
@@ -111,7 +111,7 @@ export default function NewsletterSignup() {
|
||||
className="sr-only"
|
||||
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 && (
|
||||
<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" />
|
||||
@@ -147,7 +147,7 @@ export default function NewsletterSignup() {
|
||||
<button
|
||||
type="submit"
|
||||
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"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -164,7 +164,7 @@ export default function NewsletterSignup() {
|
||||
|
||||
<p className="font-body text-[#555] text-xs mt-3">
|
||||
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
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
@@ -141,7 +141,7 @@ export default function SpotlightCarousel() {
|
||||
return (
|
||||
<section
|
||||
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"
|
||||
onKeyDown={handleCarouselKeyDown}>
|
||||
<div suppressHydrationWarning className="max-w-container mx-auto px-4">
|
||||
@@ -156,14 +156,14 @@ export default function SpotlightCarousel() {
|
||||
ref={prevBtnRef}
|
||||
onClick={() => go(-1)}
|
||||
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} />
|
||||
</button>
|
||||
<button
|
||||
ref={nextBtnRef}
|
||||
onClick={() => go(1)}
|
||||
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} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -181,7 +181,7 @@ export default function SpotlightCarousel() {
|
||||
key={`${startIndex}-${i}`}
|
||||
href={item.href}
|
||||
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" : ""
|
||||
}`}
|
||||
style={{ animationDelay: `${i * 50}ms` }}
|
||||
@@ -200,11 +200,11 @@ export default function SpotlightCarousel() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2.5 md:p-3 border-t border-[#2a2a2a]">
|
||||
<p className="font-body text-[11px] font-bold text-[#ffb800] uppercase tracking-wide mb-1">
|
||||
<div className="p-2.5 md:p-3 border-t border-[#3a322b]">
|
||||
<p className="font-body text-[11px] font-bold text-[#F0A623] uppercase tracking-wide mb-1">
|
||||
{item.name}
|
||||
</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}
|
||||
</h3>
|
||||
</div>
|
||||
@@ -221,7 +221,7 @@ export default function SpotlightCarousel() {
|
||||
role="tab"
|
||||
aria-selected={Math.floor(startIndex / VISIBLE) === i}
|
||||
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
|
||||
? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]"
|
||||
}`}
|
||||
|
||||
@@ -54,7 +54,7 @@ function BentoSkeleton() {
|
||||
<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="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 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]" />
|
||||
@@ -81,7 +81,7 @@ function CarouselSkeleton() {
|
||||
<div className="max-w-container mx-auto">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<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 className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{[...Array(4)]?.map((_, i) => (
|
||||
@@ -107,7 +107,7 @@ function FeedSkeleton() {
|
||||
<div className="lg:col-span-8 space-y-1">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<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>
|
||||
{[...Array(5)]?.map((_, i) => (
|
||||
<div key={i} className="flex gap-4 py-4 border-b border-[#222]">
|
||||
|
||||
@@ -144,7 +144,7 @@ export default function RootLayout({
|
||||
{/* Skip to main content — WCAG 2.4.1 */}
|
||||
<a
|
||||
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
|
||||
</a>
|
||||
|
||||
@@ -50,7 +50,7 @@ function LoginInner() {
|
||||
return (
|
||||
<>
|
||||
<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="text-center mb-6">
|
||||
<h1 className="text-2xl font-heading font-bold text-white">Sign in to AV Beat</h1>
|
||||
@@ -60,10 +60,10 @@ function LoginInner() {
|
||||
</div>
|
||||
<form
|
||||
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 && (
|
||||
<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}
|
||||
</div>
|
||||
)}
|
||||
@@ -77,7 +77,7 @@ function LoginInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
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>
|
||||
@@ -90,19 +90,19 @@ function LoginInner() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
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>
|
||||
<button
|
||||
type="submit"
|
||||
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'}
|
||||
</button>
|
||||
<p className="text-xs text-[#666] font-body text-center pt-2">
|
||||
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
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -77,14 +77,14 @@ export default function ManufacturerDirectoryClient({
|
||||
|
||||
return (
|
||||
<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">
|
||||
<input
|
||||
type="search"
|
||||
value={q}
|
||||
onChange={(e) => setQ(e.target.value)}
|
||||
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>
|
||||
@@ -97,7 +97,7 @@ export default function ManufacturerDirectoryClient({
|
||||
type="button"
|
||||
onClick={() => setShowFilter("all")}
|
||||
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>
|
||||
@@ -106,7 +106,7 @@ export default function ManufacturerDirectoryClient({
|
||||
type="button"
|
||||
onClick={() => setShowFilter("both-major")}
|
||||
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>
|
||||
@@ -121,7 +121,7 @@ export default function ManufacturerDirectoryClient({
|
||||
type="button"
|
||||
onClick={() => setShowFilter(s.slug)}
|
||||
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" : ""}`}
|
||||
>
|
||||
@@ -144,8 +144,8 @@ export default function ManufacturerDirectoryClient({
|
||||
onClick={() => setLetter(l)}
|
||||
className={`min-w-[28px] h-7 px-1.5 rounded font-semibold ${
|
||||
letter === l
|
||||
? "bg-[#ffb800] text-white"
|
||||
: "bg-[#0d0d0d] border border-[#252525] text-[#aaa] hover:bg-[#1a1a1a]"
|
||||
? "bg-[#F0A623] text-white"
|
||||
: "bg-[#1c1815] border border-[#3a322b] text-[#aaa] hover:bg-[#231d18]"
|
||||
}`}
|
||||
>
|
||||
{l}
|
||||
@@ -162,9 +162,9 @@ export default function ManufacturerDirectoryClient({
|
||||
<Link
|
||||
key={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 ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
@@ -180,7 +180,7 @@ export default function ManufacturerDirectoryClient({
|
||||
)}
|
||||
</div>
|
||||
<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}
|
||||
</h3>
|
||||
<div className="flex items-center gap-1.5 mt-1 mb-1.5 flex-wrap">
|
||||
@@ -195,7 +195,7 @@ export default function ManufacturerDirectoryClient({
|
||||
</span>
|
||||
)}
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function BackLink() {
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Link href="/manufacturers" onClick={onClick} className="hover:text-[#ffb800]">
|
||||
<Link href="/manufacturers" onClick={onClick} className="hover:text-[#F0A623]">
|
||||
← Back
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -159,7 +159,7 @@ export default async function ManufacturerProfile({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d]">
|
||||
<div className="min-h-screen bg-[#1c1815]">
|
||||
<Header />
|
||||
<script
|
||||
type="application/ld+json"
|
||||
@@ -170,8 +170,8 @@ export default async function ManufacturerProfile({
|
||||
<BackLink />
|
||||
</nav>
|
||||
|
||||
<header className="flex flex-col sm:flex-row gap-6 mb-8 pb-8 border-b border-[#252525]">
|
||||
<div className="w-24 h-24 rounded-xl bg-[#1a1a1a] flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||
<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-[#231d18] flex items-center justify-center overflow-hidden flex-shrink-0">
|
||||
{company.logo_url ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
@@ -206,7 +206,7 @@ export default async function ManufacturerProfile({
|
||||
</span>
|
||||
)}
|
||||
{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}
|
||||
</a>
|
||||
)}
|
||||
@@ -217,7 +217,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.company_website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
Website ↗
|
||||
</a>
|
||||
@@ -227,7 +227,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.press_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
Newsroom ↗
|
||||
</a>
|
||||
@@ -237,7 +237,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.contact_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
Contact ↗
|
||||
</a>
|
||||
@@ -247,7 +247,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#888] hover:text-[#ffb800]"
|
||||
className="text-[#888] hover:text-[#F0A623]"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
@@ -257,7 +257,7 @@ export default async function ManufacturerProfile({
|
||||
href={`https://twitter.com/${company.twitter_handle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#888] hover:text-[#ffb800]"
|
||||
className="text-[#888] hover:text-[#F0A623]"
|
||||
>
|
||||
X / Twitter
|
||||
</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>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{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 ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img
|
||||
src={e.photo_url}
|
||||
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"
|
||||
/>
|
||||
) : (
|
||||
<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()}
|
||||
</div>
|
||||
)}
|
||||
@@ -306,7 +306,7 @@ export default async function ManufacturerProfile({
|
||||
href={e.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[11px] text-[#ffb800] hover:underline"
|
||||
className="text-[11px] text-[#F0A623] hover:underline"
|
||||
>
|
||||
LinkedIn ↗
|
||||
</a>
|
||||
@@ -328,14 +328,14 @@ export default async function ManufacturerProfile({
|
||||
)}
|
||||
{company.phone && (
|
||||
<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}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{company.contact_email && (
|
||||
<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}
|
||||
</a>
|
||||
</div>
|
||||
@@ -356,7 +356,7 @@ export default async function ManufacturerProfile({
|
||||
{cleanCategories.map((c: string) => (
|
||||
<span
|
||||
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}
|
||||
</span>
|
||||
@@ -382,7 +382,7 @@ export default async function ManufacturerProfile({
|
||||
return (
|
||||
<li
|
||||
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">
|
||||
<span className="font-semibold text-[#e0e0e0]">{label}</span>
|
||||
@@ -401,7 +401,7 @@ export default async function ManufacturerProfile({
|
||||
<section className="mb-10">
|
||||
<header className="flex items-baseline justify-between mb-3">
|
||||
<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 →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -410,7 +410,7 @@ export default async function ManufacturerProfile({
|
||||
<Link
|
||||
key={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
|
||||
gray box on a coverage card. Falls through:
|
||||
@@ -419,11 +419,11 @@ export default async function ManufacturerProfile({
|
||||
<img
|
||||
src={r.featured_image || company.logo_url || '/assets/images/article-placeholder.svg'}
|
||||
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"
|
||||
/>
|
||||
<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}
|
||||
</h3>
|
||||
{r.wp_published_at && (
|
||||
@@ -447,7 +447,7 @@ export default async function ManufacturerProfile({
|
||||
{p.slug ? (
|
||||
<Link
|
||||
href={`/news/${p.slug}`}
|
||||
className="text-[#ffb800] hover:underline"
|
||||
className="text-[#F0A623] hover:underline"
|
||||
>
|
||||
{p.title}
|
||||
</Link>
|
||||
@@ -465,7 +465,7 @@ export default async function ManufacturerProfile({
|
||||
</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.
|
||||
{company.last_mentioned && (
|
||||
<> Last mentioned in coverage on {new Date(company.last_mentioned).toISOString().slice(0, 10)}.</>
|
||||
|
||||
@@ -72,7 +72,7 @@ export default async function ManufacturersIndex() {
|
||||
const ibcCount = list.filter((m) => m.exhibits_ibc).length;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d]">
|
||||
<div className="min-h-screen bg-[#1c1815]">
|
||||
<Header />
|
||||
<div className="max-w-6xl mx-auto px-4 py-10">
|
||||
<header className="mb-8">
|
||||
@@ -94,7 +94,7 @@ export default async function ManufacturersIndex() {
|
||||
|
||||
<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>
|
||||
Sources: 2026 NAB Show exhibitor list (Map Your Show), IBC exhibitor list,
|
||||
manufacturer-direct press releases, and reporting tracked on AV Beat.
|
||||
@@ -102,7 +102,7 @@ export default async function ManufacturersIndex() {
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
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
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -8,7 +8,7 @@ export const metadata = {
|
||||
|
||||
export default function BillingPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -84,7 +84,7 @@ export default function BillingPage() {
|
||||
className={`rounded-xl p-6 border relative ${
|
||||
plan?.highlight
|
||||
? 'bg-[#CC0000]/10 border-[#CC0000]'
|
||||
: 'bg-[#1a1a1a] border-white/10'
|
||||
: 'bg-[#231d18] border-white/10'
|
||||
}`}
|
||||
>
|
||||
{plan?.highlight && (
|
||||
@@ -120,7 +120,7 @@ export default function BillingPage() {
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex justify-between py-2 border-b border-white/5">
|
||||
|
||||
@@ -173,14 +173,14 @@ export default function AccountPage() {
|
||||
|
||||
if (loading || profileLoading) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -195,7 +195,7 @@ export default function AccountPage() {
|
||||
|
||||
{/* No profile yet */}
|
||||
{!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>
|
||||
<h2 className="text-xl font-bold mb-3">No Profile Yet</h2>
|
||||
<p className="text-gray-400 mb-6">
|
||||
@@ -213,7 +213,7 @@ export default function AccountPage() {
|
||||
{/* Profile status card */}
|
||||
{profile && (
|
||||
<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-lg font-bold capitalize ${
|
||||
@@ -234,7 +234,7 @@ export default function AccountPage() {
|
||||
)}
|
||||
</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-lg font-bold capitalize text-white">
|
||||
{profile.subscription_tier === 'featured' ?'⭐ Featured'
|
||||
@@ -248,7 +248,7 @@ export default function AccountPage() {
|
||||
</Link>
|
||||
</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-lg font-bold capitalize text-white">
|
||||
{profile.profile_type === 'crew' ? '🎬 Crew Member' : '🏢 Vendor / Company'}
|
||||
@@ -272,7 +272,7 @@ export default function AccountPage() {
|
||||
|
||||
{/* Subscription gate notice */}
|
||||
{!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="text-2xl">💳</div>
|
||||
<div>
|
||||
@@ -293,7 +293,7 @@ export default function AccountPage() {
|
||||
)}
|
||||
|
||||
{/* 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
|
||||
onClick={() => setShowJobForm((p) => !p)}
|
||||
disabled={!hasActiveSubscription}
|
||||
@@ -315,7 +315,7 @@ export default function AccountPage() {
|
||||
value={jobTitle}
|
||||
onChange={(e) => setJobTitle(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -325,7 +325,7 @@ export default function AccountPage() {
|
||||
value={jobCompany}
|
||||
onChange={(e) => setJobCompany(e.target.value)}
|
||||
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>
|
||||
@@ -334,7 +334,7 @@ export default function AccountPage() {
|
||||
value={jobLocation}
|
||||
onChange={(e) => setJobLocation(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -343,7 +343,7 @@ export default function AccountPage() {
|
||||
<select
|
||||
value={jobType}
|
||||
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) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
@@ -359,14 +359,14 @@ export default function AccountPage() {
|
||||
type="number"
|
||||
value={jobSalaryMin}
|
||||
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 $"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={jobSalaryMax}
|
||||
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 $"
|
||||
/>
|
||||
</div>
|
||||
@@ -378,7 +378,7 @@ export default function AccountPage() {
|
||||
onChange={(e) => setJobDescription(e.target.value)}
|
||||
required
|
||||
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..."
|
||||
/>
|
||||
</div>
|
||||
@@ -387,7 +387,7 @@ export default function AccountPage() {
|
||||
<input
|
||||
value={jobApplyUrl}
|
||||
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://..."
|
||||
/>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@ export default function AccountPage() {
|
||||
type="email"
|
||||
value={jobApplyEmail}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -424,7 +424,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
onClick={() => setShowGigForm((p) => !p)}
|
||||
disabled={!hasActiveSubscription}
|
||||
@@ -446,7 +446,7 @@ export default function AccountPage() {
|
||||
value={gigTitle}
|
||||
onChange={(e) => setGigTitle(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -456,7 +456,7 @@ export default function AccountPage() {
|
||||
value={gigCompany}
|
||||
onChange={(e) => setGigCompany(e.target.value)}
|
||||
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>
|
||||
@@ -465,7 +465,7 @@ export default function AccountPage() {
|
||||
value={gigLocation}
|
||||
onChange={(e) => setGigLocation(e.target.value)}
|
||||
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>
|
||||
@@ -474,7 +474,7 @@ export default function AccountPage() {
|
||||
value={gigType}
|
||||
onChange={(e) => setGigType(e.target.value)}
|
||||
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>
|
||||
{GIG_TYPES.map((t) => (
|
||||
@@ -489,7 +489,7 @@ export default function AccountPage() {
|
||||
<input
|
||||
value={gigRate}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -499,7 +499,7 @@ export default function AccountPage() {
|
||||
type="date"
|
||||
value={gigStartDate}
|
||||
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>
|
||||
@@ -508,7 +508,7 @@ export default function AccountPage() {
|
||||
type="date"
|
||||
value={gigEndDate}
|
||||
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 className="col-span-2">
|
||||
@@ -518,7 +518,7 @@ export default function AccountPage() {
|
||||
onChange={(e) => setGigDescription(e.target.value)}
|
||||
required
|
||||
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..."
|
||||
/>
|
||||
</div>
|
||||
@@ -527,7 +527,7 @@ export default function AccountPage() {
|
||||
<input
|
||||
value={gigContact}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -173,7 +173,7 @@ export default function AdminQueuePage() {
|
||||
|
||||
if (loading || checkingAdmin) {
|
||||
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>
|
||||
);
|
||||
@@ -181,7 +181,7 @@ export default function AdminQueuePage() {
|
||||
|
||||
if (!isAdmin) {
|
||||
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-4xl mb-4">🔒</div>
|
||||
<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;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<div className="max-w-6xl mx-auto px-4 py-24">
|
||||
{/* Header */}
|
||||
@@ -235,7 +235,7 @@ export default function AdminQueuePage() {
|
||||
className={`px-4 py-2 rounded-lg text-sm font-bold capitalize transition-colors ${
|
||||
filter === f
|
||||
? '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}
|
||||
@@ -266,7 +266,7 @@ export default function AdminQueuePage() {
|
||||
return (
|
||||
<div
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -377,7 +377,7 @@ export default function AdminQueuePage() {
|
||||
[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)..."
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function GigDetailPage() {
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export default function GigDetailPage() {
|
||||
|
||||
if (!gig) {
|
||||
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">
|
||||
<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>
|
||||
@@ -49,7 +49,7 @@ export default function GigDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -60,7 +60,7 @@ export default function GigDetailPage() {
|
||||
<span className="text-white truncate">{gig.title}</span>
|
||||
</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">
|
||||
<h1 className="text-2xl font-black mb-2">{gig.title}</h1>
|
||||
<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">
|
||||
<h2 className="text-sm font-bold uppercase tracking-wide text-gray-400 mb-4">How to Respond</h2>
|
||||
{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>
|
||||
)}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function GigsPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -49,7 +49,7 @@ export default function GigsPage() {
|
||||
</Link>
|
||||
</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 —
|
||||
create your free profile now and post gigs when payments launch.
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export default function GigsPage() {
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[...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>
|
||||
) : gigs.length === 0 ? (
|
||||
@@ -72,7 +72,7 @@ export default function GigsPage() {
|
||||
<Link
|
||||
key={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-1">
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function JobDetailPage() {
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export default function JobDetailPage() {
|
||||
|
||||
if (!job) {
|
||||
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">
|
||||
<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>
|
||||
@@ -49,7 +49,7 @@ export default function JobDetailPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -60,7 +60,7 @@ export default function JobDetailPage() {
|
||||
<span className="text-white truncate">{job.title}</span>
|
||||
</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">
|
||||
<h1 className="text-2xl font-black mb-2">{job.title}</h1>
|
||||
<div className="text-[#CC0000] font-bold text-lg mb-4">{job.company}</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function JobsPage() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -49,7 +49,7 @@ export default function JobsPage() {
|
||||
</Link>
|
||||
</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 —
|
||||
create your free profile now and post jobs when payments launch.
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@ export default function JobsPage() {
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[...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>
|
||||
) : jobs.length === 0 ? (
|
||||
@@ -72,7 +72,7 @@ export default function JobsPage() {
|
||||
<Link
|
||||
key={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-1">
|
||||
|
||||
@@ -243,7 +243,7 @@ export default function JoinMarketplacePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<div className="max-w-2xl mx-auto px-4 py-24">
|
||||
{/* Header */}
|
||||
@@ -261,7 +261,7 @@ export default function JoinMarketplacePage() {
|
||||
|
||||
{/* Auth gate */}
|
||||
{!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">
|
||||
{authMode === 'signup' ? 'Create an Account' : 'Sign In to Continue'}
|
||||
</h2>
|
||||
@@ -273,7 +273,7 @@ export default function JoinMarketplacePage() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -285,7 +285,7 @@ export default function JoinMarketplacePage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
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="••••••••"
|
||||
/>
|
||||
</div>
|
||||
@@ -325,7 +325,7 @@ export default function JoinMarketplacePage() {
|
||||
setProfileType('crew');
|
||||
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="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
|
||||
@@ -340,7 +340,7 @@ export default function JoinMarketplacePage() {
|
||||
setProfileType('vendor');
|
||||
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="font-bold text-lg group-hover:text-[#CC0000] transition-colors">
|
||||
@@ -376,7 +376,7 @@ export default function JoinMarketplacePage() {
|
||||
value={crewName}
|
||||
onChange={(e) => setCrewName(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -388,7 +388,7 @@ export default function JoinMarketplacePage() {
|
||||
value={crewLocation}
|
||||
onChange={(e) => setCrewLocation(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -397,7 +397,7 @@ export default function JoinMarketplacePage() {
|
||||
<select
|
||||
value={crewUnion}
|
||||
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="iatse">IATSE</option>
|
||||
@@ -412,7 +412,7 @@ export default function JoinMarketplacePage() {
|
||||
type="number"
|
||||
value={crewRate}
|
||||
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"
|
||||
min="0"
|
||||
/>
|
||||
@@ -426,7 +426,7 @@ export default function JoinMarketplacePage() {
|
||||
onChange={(e) => setCrewBio(e.target.value)}
|
||||
rows={4}
|
||||
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)"
|
||||
/>
|
||||
<div className="text-xs text-gray-500 text-right mt-1">{crewBio.length}/500</div>
|
||||
@@ -457,7 +457,7 @@ export default function JoinMarketplacePage() {
|
||||
<input
|
||||
value={crewEquipment}
|
||||
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)"
|
||||
/>
|
||||
</div>
|
||||
@@ -467,7 +467,7 @@ export default function JoinMarketplacePage() {
|
||||
<input
|
||||
value={crewReel}
|
||||
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/..."
|
||||
/>
|
||||
</div>
|
||||
@@ -488,7 +488,7 @@ export default function JoinMarketplacePage() {
|
||||
type="file"
|
||||
accept="application/pdf,.doc,.docx"
|
||||
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>
|
||||
@@ -532,7 +532,7 @@ export default function JoinMarketplacePage() {
|
||||
value={vendorName}
|
||||
onChange={(e) => setVendorName(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -546,7 +546,7 @@ export default function JoinMarketplacePage() {
|
||||
value={vendorLocation}
|
||||
onChange={(e) => setVendorLocation(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -558,7 +558,7 @@ export default function JoinMarketplacePage() {
|
||||
value={vendorCategory}
|
||||
onChange={(e) => setVendorCategory(e.target.value)}
|
||||
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>
|
||||
{VENDOR_CATEGORIES.map((c) => (
|
||||
@@ -577,7 +577,7 @@ export default function JoinMarketplacePage() {
|
||||
onChange={(e) => setVendorDescription(e.target.value)}
|
||||
rows={4}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -588,7 +588,7 @@ export default function JoinMarketplacePage() {
|
||||
<input
|
||||
value={vendorWebsite}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -598,7 +598,7 @@ export default function JoinMarketplacePage() {
|
||||
type="email"
|
||||
value={vendorEmail}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ const STATS = [
|
||||
|
||||
export default function MarketplacePage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
{/* Hero */}
|
||||
<section className="relative pt-24 pb-20 px-4 overflow-hidden">
|
||||
@@ -83,7 +83,7 @@ export default function MarketplacePage() {
|
||||
<Link
|
||||
key={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="font-bold text-white group-hover:text-[#CC0000] transition-colors">
|
||||
@@ -212,7 +212,7 @@ export default function MarketplacePage() {
|
||||
className={`rounded-xl p-6 border ${
|
||||
plan?.highlight
|
||||
? 'bg-[#CC0000]/10 border-[#CC0000] relative'
|
||||
: 'bg-[#1a1a1a] border-white/10'
|
||||
: 'bg-[#231d18] border-white/10'
|
||||
}`}
|
||||
>
|
||||
{plan?.highlight && (
|
||||
@@ -247,7 +247,7 @@ export default function MarketplacePage() {
|
||||
</div>
|
||||
|
||||
{/* 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.
|
||||
Payment processing will be enabled shortly. Create your free profile today and upgrade
|
||||
when billing launches.
|
||||
|
||||
@@ -52,7 +52,7 @@ export default function ProfilePage() {
|
||||
|
||||
if (loading) {
|
||||
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>
|
||||
);
|
||||
@@ -60,7 +60,7 @@ export default function ProfilePage() {
|
||||
|
||||
if (notFoundState || !profile || !details) {
|
||||
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-4xl mb-4">👤</div>
|
||||
<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;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<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">
|
||||
@@ -87,7 +87,7 @@ export default function ProfilePage() {
|
||||
<span className="text-white">{name}</span>
|
||||
</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="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">
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function MarketplaceSearchPage() {
|
||||
return (
|
||||
<Suspense
|
||||
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>
|
||||
}
|
||||
@@ -246,7 +246,7 @@ function SearchPageInner() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d] text-white">
|
||||
<div className="min-h-screen bg-[#1c1815] text-white">
|
||||
<Header />
|
||||
<div className="max-w-7xl mx-auto px-4 py-24">
|
||||
{/* Breadcrumb */}
|
||||
@@ -280,7 +280,7 @@ function SearchPageInner() {
|
||||
<input
|
||||
value={query}
|
||||
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..."
|
||||
/>
|
||||
</div>
|
||||
@@ -314,7 +314,7 @@ function SearchPageInner() {
|
||||
<input
|
||||
value={filterLocation}
|
||||
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..."
|
||||
/>
|
||||
</div>
|
||||
@@ -328,7 +328,7 @@ function SearchPageInner() {
|
||||
<select
|
||||
value={filterUnion}
|
||||
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="non_union">Non-Union</option>
|
||||
@@ -346,14 +346,14 @@ function SearchPageInner() {
|
||||
type="number"
|
||||
value={filterRateMin}
|
||||
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 $"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
value={filterRateMax}
|
||||
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 $"
|
||||
/>
|
||||
</div>
|
||||
@@ -369,7 +369,7 @@ function SearchPageInner() {
|
||||
<select
|
||||
value={filterCategory}
|
||||
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>
|
||||
{VENDOR_CATEGORIES.map((c) => (
|
||||
@@ -428,7 +428,7 @@ function SearchPageInner() {
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<div
|
||||
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>
|
||||
@@ -449,7 +449,7 @@ function SearchPageInner() {
|
||||
<Link
|
||||
key={profile.id}
|
||||
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">
|
||||
{/* Avatar */}
|
||||
@@ -510,7 +510,7 @@ function SearchPageInner() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 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
|
||||
</button>
|
||||
@@ -518,7 +518,7 @@ function SearchPageInner() {
|
||||
<button
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
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 →
|
||||
</button>
|
||||
|
||||
@@ -87,12 +87,12 @@ export default async function NabHub() {
|
||||
const articles = (rawArticles || []) as ArticleRow[];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0d0d0d]">
|
||||
<div className="min-h-screen bg-[#1c1815]">
|
||||
<Header />
|
||||
<CompanyMentionsHover />
|
||||
|
||||
{/* 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="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">
|
||||
@@ -128,9 +128,9 @@ export default async function NabHub() {
|
||||
key={e.slug}
|
||||
href={`/manufacturers/${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 ? (
|
||||
/* 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" />
|
||||
@@ -153,7 +153,7 @@ export default async function NabHub() {
|
||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||
Latest from the show floor
|
||||
</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 →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -162,7 +162,7 @@ export default async function NabHub() {
|
||||
<Link
|
||||
key={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 && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
@@ -174,7 +174,7 @@ export default async function NabHub() {
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
</h3>
|
||||
{a.wp_published_at && (
|
||||
@@ -195,7 +195,7 @@ export default async function NabHub() {
|
||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||
All 2026 NAB Show exhibitors
|
||||
</h2>
|
||||
<Link href="/manufacturers" className="text-xs text-[#ffb800] hover:underline">
|
||||
<Link href="/manufacturers" className="text-xs text-[#F0A623] hover:underline">
|
||||
Full directory →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -205,13 +205,13 @@ export default async function NabHub() {
|
||||
key={e.slug}
|
||||
href={`/manufacturers/${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 ? (
|
||||
/* 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()}
|
||||
</div>
|
||||
)}
|
||||
@@ -222,7 +222,7 @@ export default async function NabHub() {
|
||||
{otherRows.length > 200 && (
|
||||
<p className="text-center text-xs text-[#888] mt-4">
|
||||
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>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user