slice 4: accent palette refinement — blue → teal
Replace the four accent-family hex codes site-wide with the teal palette (keeps the dark theme; only swaps the accent family, not the dark base): #3b82f6 (accent primary CTA) → #5B7C8D (teal) [839×] #2563eb (accent-dark / hover) → #4A6473 (darker teal) [44×] #60a5fa (info link, lighter) → #8FB0C3 (mid teal) [68×] #1e3a5f (accent-muted bg) → #2F4F5F (navy) [44×] tailwind.config.js tokens updated to match (accent/accent-dark/accent-muted). Sweep also covers tailwind.css. 108 files touched. The dark base (#0d0d0d, #111, #161616, #1a1a1a etc.) is intentionally untouched. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,7 +37,7 @@ export default function AdvertisePage() {
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-3">Get in touch</h2>
|
||||
<p className="text-[15px]">
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:advertising@broadcastbeat.com">advertising@broadcastbeat.com</a>
|
||||
<a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="mailto:advertising@broadcastbeat.com">advertising@broadcastbeat.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -7,22 +7,22 @@ export default function ContactPage() {
|
||||
<dl className="space-y-5 text-[15px]">
|
||||
<div>
|
||||
<dt className="text-xs font-mono uppercase tracking-wider text-[#6b7280] mb-1">Editorial</dt>
|
||||
<dd><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@broadcastbeat.com">editor@broadcastbeat.com</a></dd>
|
||||
<dd><a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="mailto:editor@broadcastbeat.com">editor@broadcastbeat.com</a></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-mono uppercase tracking-wider text-[#6b7280] mb-1">Press releases (PR firms)</dt>
|
||||
<dd>
|
||||
Route submissions through{" "}
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="https://distribution.relevantmediaproperties.com">distribution.relevantmediaproperties.com</a>.
|
||||
<a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="https://distribution.relevantmediaproperties.com">distribution.relevantmediaproperties.com</a>.
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-mono uppercase tracking-wider text-[#6b7280] mb-1">Advertising</dt>
|
||||
<dd><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:advertising@broadcastbeat.com">advertising@broadcastbeat.com</a> — see <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/advertise">media kit</a>.</dd>
|
||||
<dd><a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="mailto:advertising@broadcastbeat.com">advertising@broadcastbeat.com</a> — see <a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="/about/advertise">media kit</a>.</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-xs font-mono uppercase tracking-wider text-[#6b7280] mb-1">Publisher</dt>
|
||||
<dd>Ryan Salazar · <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a></dd>
|
||||
<dd>Ryan Salazar · <a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</main>
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function PressKitPage() {
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
|
||||
<ul className="space-y-1">
|
||||
<li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li>
|
||||
<li><a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="/assets/images/logo.png" download>BroadcastBeat logo (PNG)</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
@@ -29,7 +29,7 @@ export default function PressKitPage() {
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
|
||||
<p>
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
<a className="text-[var(--color-text-info,#8FB0C3)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default async function TeamPage() {
|
||||
<div className="flex-1">
|
||||
<h2 className="font-serif text-lg font-semibold">{p.name}</h2>
|
||||
{p.beat && (
|
||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)] mt-0.5">
|
||||
<div className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)] mt-0.5">
|
||||
{p.beat.replace(/-/g, " ")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -305,10 +305,10 @@ export default function AccountPage() {
|
||||
{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-[#1a2535] border-[#3b82f6] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#cc0000] text-[#e0e0e0]"
|
||||
toast.type === "success" ?"bg-[#1a2535] border-[#5B7C8D] text-[#e0e0e0]" :"bg-[#1a0a0a] border-[#cc0000] text-[#e0e0e0]"
|
||||
}`}>
|
||||
{toast.type === "success" ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" aria-hidden="true">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5B7C8D" strokeWidth="2.5" aria-hidden="true">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
) : (
|
||||
@@ -324,7 +324,7 @@ export default function AccountPage() {
|
||||
<div className="bg-[#0d0d0d] 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-[#3b82f6] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">Home</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span className="text-[#888]">Account</span>
|
||||
</nav>
|
||||
@@ -354,7 +354,7 @@ export default function AccountPage() {
|
||||
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-[#3b82f6] text-[#3b82f6]"
|
||||
? "border-[#5B7C8D] text-[#5B7C8D]"
|
||||
: "border-transparent text-[#666] hover:text-[#aaa]"
|
||||
}`}>
|
||||
{tab.label}
|
||||
@@ -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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] 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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444] resize-none"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] 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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] transition-colors placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -416,7 +416,7 @@ 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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] transition-colors placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,7 +445,7 @@ export default function AccountPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] 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-[#3b82f6]">
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#5B7C8D]">
|
||||
{saving ? "Saving..." : "Save Profile"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] 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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] transition-colors placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -492,7 +492,7 @@ export default function AccountPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={changingPassword || !newPassword || !confirmPassword}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] 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-[#3b82f6]">
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#5B7C8D]">
|
||||
{changingPassword ? "Updating..." : "Update Password"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -525,7 +525,7 @@ export default function AccountPage() {
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
<Link href="/reading-list" className="font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Full List →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -550,7 +550,7 @@ export default function AccountPage() {
|
||||
<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-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Browse News →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]">{item.article_category}</span>
|
||||
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#5B7C8D]">{item.article_category}</span>
|
||||
)}
|
||||
<Link href={`/articles/${item.article_slug}`}>
|
||||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#5B7C8D] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||||
{item.article_title}
|
||||
</h3>
|
||||
</Link>
|
||||
@@ -639,7 +639,7 @@ export default function AccountPage() {
|
||||
<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-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Browse News →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]">{item.article_category}</span>
|
||||
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#5B7C8D]">{item.article_category}</span>
|
||||
)}
|
||||
<Link href={`/articles/${item.article_slug}`}>
|
||||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#5B7C8D] transition-colors line-clamp-2 leading-snug mt-0.5">
|
||||
{item.article_title}
|
||||
</h3>
|
||||
</Link>
|
||||
@@ -703,7 +703,7 @@ export default function AccountPage() {
|
||||
</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-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
<Link href="/news" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Browse News →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -719,13 +719,13 @@ export default function AccountPage() {
|
||||
<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-[#3b82f6] font-bold ml-2 flex-shrink-0">
|
||||
<span className="font-body text-[10px] text-[#5B7C8D] font-bold ml-2 flex-shrink-0">
|
||||
{t.view_count}×
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-[#252525] rounded-full h-1">
|
||||
<div
|
||||
className="bg-[#3b82f6] h-1 rounded-full transition-all"
|
||||
className="bg-[#5B7C8D] h-1 rounded-full transition-all"
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -750,7 +750,7 @@ export default function AccountPage() {
|
||||
</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-[#3b82f6]">
|
||||
<span className="font-heading text-3xl font-bold text-[#5B7C8D]">
|
||||
{profile?.reputation ?? 0}
|
||||
</span>
|
||||
<span className="font-body text-xs text-[#666] mt-1 uppercase tracking-wider">Points</span>
|
||||
@@ -759,7 +759,7 @@ 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-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
<Link href="/forum" className="inline-block mt-3 font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Go to Forum →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -778,7 +778,7 @@ export default function AccountPage() {
|
||||
</div>
|
||||
<Link
|
||||
href="/home-page"
|
||||
className="font-body text-xs font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
className="font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] 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-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
className="font-body text-xs font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] 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-[#3b82f6] disabled:opacity-50 ${
|
||||
weeklyDigest ? "bg-[#3b82f6] border-[#3b82f6]" : "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-[#5B7C8D] disabled:opacity-50 ${
|
||||
weeklyDigest ? "bg-[#5B7C8D] border-[#5B7C8D]" : "bg-[#2a2a2a] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500 transition-colors">+ New Client</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -116,9 +116,9 @@ 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] 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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
|
||||
{/* New Client 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-[#3b82f6] ${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-[#5B7C8D] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
|
||||
/>
|
||||
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
|
||||
</div>
|
||||
@@ -156,16 +156,16 @@ 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-[#3b82f6]" />
|
||||
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
className="w-full px-2 py-1.5 bg-[#0a0a0a] border border-[#252525] rounded text-xs text-white focus:outline-none focus:border-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Client'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ export default function ClientsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/clients/${c.id}`} className="text-xs text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -84,7 +84,7 @@ export default function CommissionsPage() {
|
||||
<span>Tier progress</span><span>{pct}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#1a1a1a] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#3b82f6] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
<div className="h-full bg-[#5B7C8D] rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -111,7 +111,7 @@ export default function CommissionsPage() {
|
||||
) : commissions.map((c: any) => (
|
||||
<tr key={c.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-white text-xs">{c.rmp_sales_staff?.full_name ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#3b82f6] text-xs font-mono">{c.rmp_invoices?.invoice_number ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -54,14 +54,14 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Upload Document</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{y}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -84,31 +84,31 @@ export default function DocumentsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Save Document'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -136,7 +136,7 @@ export default function DocumentsPage() {
|
||||
<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-[#3b82f6] hover:underline">Download</a>}
|
||||
{d.file_url && <a href={d.file_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ Log Expense</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -132,16 +132,16 @@ 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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6] ${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-[#5B7C8D] ${errors.amount ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6] ${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-[#5B7C8D] ${errors.expense_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{errors.expense_date && <p className="mt-1 text-xs text-red-400">{errors.expense_date}</p>}
|
||||
</div>
|
||||
<div>
|
||||
@@ -152,11 +152,11 @@ export default function ExpensesPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Log Expense'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ New Invoice (via Order)</Link>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -108,7 +108,7 @@ 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] text-[#888] hover:text-white'}`}>{s || 'All'}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -168,7 +168,7 @@ function InvoicesContent() {
|
||||
<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-[#3b82f6] font-mono text-xs">{inv.invoice_number}</td>
|
||||
<td className="px-3 py-2 text-[#5B7C8D] 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>
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ New Order</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -136,7 +136,7 @@ function OrdersContent() {
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
|
||||
{/* New Order 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-[#3b82f6] ${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-[#5B7C8D] ${errors.client_id ? 'border-red-500/60' : 'border-[#252525]'}`}>
|
||||
<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-[#3b82f6] ${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-[#5B7C8D] ${errors.site ? 'border-red-500/60' : 'border-[#252525]'}`}>
|
||||
<option value="">Select site…</option>
|
||||
{SITES.map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
@@ -170,57 +170,57 @@ 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-[#3b82f6]">
|
||||
<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-[#5B7C8D]">
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6] ${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-[#5B7C8D] ${errors.end_date ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6] ${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-[#5B7C8D] ${errors.total_amount_cents ? 'border-red-500/60' : 'border-[#252525]'}`} />
|
||||
{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-[#3b82f6]">
|
||||
<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-[#5B7C8D]">
|
||||
{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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]">
|
||||
<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-[#5B7C8D]">
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create Order'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -245,7 +245,7 @@ function OrdersContent() {
|
||||
<tr key={o.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<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-[#3b82f6] text-xs font-mono">{o.internal_order_number}</td>
|
||||
<td className="px-3 py-2 text-[#5B7C8D] 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>
|
||||
@@ -255,7 +255,7 @@ function OrdersContent() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/orders/${o.id}`} className="text-xs text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
if (!user) return null;
|
||||
@@ -59,11 +59,11 @@ 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-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
|
||||
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
|
||||
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
|
||||
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
|
||||
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
|
||||
<Link href="/admin/accounting/clients" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">+ New Client</Link>
|
||||
<Link href="/admin/accounting/orders" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">+ New Order</Link>
|
||||
<Link href="/admin/accounting/invoices" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">+ New Invoice</Link>
|
||||
<Link href="/admin/accounting/expenses" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">+ Log Expense</Link>
|
||||
<Link href="/admin/accounting/reconciliation" className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">Upload Bank CSV</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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-[#3b82f6] transition-colors">
|
||||
<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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 cursor-pointer ${uploading ? 'opacity-50' : ''}`}>
|
||||
<label className={`px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -43,12 +43,12 @@ export default function ReportsPage() {
|
||||
<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" />
|
||||
<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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Run Report</button>
|
||||
<button onClick={loadReport} disabled={loadingData} className="px-3 py-1.5 bg-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>
|
||||
) : report ? (
|
||||
<div className="space-y-4">
|
||||
{/* P&L Summary */}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ Add Salesperson</button>
|
||||
</div>
|
||||
|
||||
{/* Success Toast */}
|
||||
@@ -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-[#3b82f6] ${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-[#5B7C8D] ${errors[f.key] ? 'border-red-500/60' : 'border-[#252525]'}`}
|
||||
/>
|
||||
{errors[f.key] && <p className="mt-1 text-xs text-red-400">{errors[f.key]}</p>}
|
||||
</div>
|
||||
@@ -149,7 +149,7 @@ 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-[#3b82f6] hover:underline">+ Add Tier</button>
|
||||
<button type="button" onClick={() => setTiers(t => [...t, { tier_order: t.length + 1, threshold_cents: '', rate: '' }])} className="text-xs text-[#5B7C8D] hover:underline">+ Add Tier</button>
|
||||
</div>
|
||||
{tiers.map((tier, i) => (
|
||||
<div key={i} className="flex gap-2 mb-2 items-start">
|
||||
@@ -167,7 +167,7 @@ export default function StaffPage() {
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ export default function StaffPage() {
|
||||
<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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/accounting/staff/${s.id}`} className="text-xs text-[#5B7C8D] hover:underline">View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
const selectedProduct = products.find((p: any) => p.id === form.product_id);
|
||||
@@ -88,7 +88,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ New IO Extension</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
@@ -108,7 +108,7 @@ export default function AdOpsIOsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Client</label>
|
||||
@@ -140,11 +140,11 @@ export default function AdOpsIOsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-4">
|
||||
<label className="flex items-center gap-2 text-xs text-[#888] cursor-pointer">
|
||||
@@ -159,14 +159,14 @@ 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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Scheduled Send Date</label>
|
||||
@@ -174,23 +174,23 @@ export default function AdOpsIOsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Create IO Extension'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -212,7 +212,7 @@ export default function AdOpsIOsPage() {
|
||||
<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-[#3b82f6] font-mono text-xs">{io.existing_io_id}</td>
|
||||
<td className="px-3 py-2 text-[#5B7C8D] 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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/adops/ios/${io.id}`} className="text-xs text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -50,7 +50,7 @@ export default function AdOpsNotificationsPage() {
|
||||
) : notifications.map((n: any) => (
|
||||
<tr key={n.id} className="border-b border-[#1a1a1a] hover:bg-[#161616]">
|
||||
<td className="px-3 py-2 text-[#888] text-xs">{n.notification_type?.replace(/_/g, ' ')}</td>
|
||||
<td className="px-3 py-2 text-[#3b82f6] font-mono text-xs">{n.io_extension_id?.slice(0, 8) ?? '—'}</td>
|
||||
<td className="px-3 py-2 text-[#5B7C8D] 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>
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -87,7 +87,7 @@ export default function AdOpsDashboardPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-semibold">Active Flights</h2>
|
||||
<Link href="/admin/adops/ios" className="text-xs text-[#3b82f6] hover:underline">View All IOs →</Link>
|
||||
<Link href="/admin/adops/ios" className="text-xs text-[#5B7C8D] hover:underline">View All IOs →</Link>
|
||||
</div>
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
@@ -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-[#3b82f6] hover:underline">View</Link>
|
||||
<Link href={`/admin/adops/ios/${f.id}`} className="text-xs text-[#5B7C8D] hover:underline">View</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -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-[#3b82f6] hover:underline">Preview</a>}
|
||||
{e.email_html_url && <a href={e.email_html_url} target="_blank" rel="noopener noreferrer" className="text-xs text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -60,7 +60,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
|
||||
<button onClick={() => setShowForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ Add Product</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -90,23 +90,23 @@ export default function AdOpsProductsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Product'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -137,7 +137,7 @@ export default function AdOpsProductsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#3b82f6] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
|
||||
<button onClick={() => toggleActive(p.id, p.active)} className="text-xs text-[#5B7C8D] hover:underline">{p.active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -64,7 +64,7 @@ interface MonthlyTrend {
|
||||
// ─── Mock / derived data helpers ──────────────────────────────────────────────
|
||||
|
||||
const CATEGORY_COLORS = [
|
||||
'#3b82f6', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#5B7C8D', '#f59e0b', '#10b981', '#8b5cf6',
|
||||
'#ef4444', '#06b6d4', '#f97316', '#84cc16',
|
||||
];
|
||||
|
||||
@@ -78,7 +78,7 @@ interface StatCardProps {
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: StatCardProps) {
|
||||
function StatCard({ label, value, sub, accent = '#5B7C8D', icon }: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg p-5 flex items-start gap-4">
|
||||
<div
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading analytics…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/articles" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/users" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] text-xs font-body uppercase tracking-wider transition-colors">
|
||||
<Link href="/admin/import" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] text-[#888] hover:text-[#3b82f6] text-xs font-body rounded transition-colors"
|
||||
className="ml-2 px-3 py-1.5 bg-[#1a1a1a] border border-[#333] hover:border-[#5B7C8D] text-[#888] hover:text-[#5B7C8D] 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="#3b82f6"
|
||||
accent="#5B7C8D"
|
||||
/>
|
||||
<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="#3b82f6"
|
||||
accent="#5B7C8D"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -409,7 +409,7 @@ export default function AdminArticlesPage() {
|
||||
<div className="bg-[#111111] border-b border-[#252525]">
|
||||
<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-[#3b82f6] transition-colors text-sm">Home</Link>
|
||||
<Link href="/home-page" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] rounded px-3 py-2 transition-colors">
|
||||
className="flex items-center gap-1.5 text-xs font-bold text-white bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] border border-[#252525] hover:border-[#3b82f6] rounded px-3 py-2 transition-colors">
|
||||
className="flex items-center gap-1.5 text-xs text-[#666] hover:text-[#5B7C8D] border border-[#252525] hover:border-[#5B7C8D] 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>
|
||||
@@ -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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]">
|
||||
<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-[#1e3a5f] rounded-lg px-4 py-3">
|
||||
<span className="text-[#3b82f6] text-sm font-bold">{selectedIds.size} selected</span>
|
||||
<div className="flex items-center gap-3 bg-[#0d1a2e] border border-[#2F4F5F] rounded-lg px-4 py-3">
|
||||
<span className="text-[#5B7C8D] 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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-1.5 focus:outline-none focus:border-[#5B7C8D]">
|
||||
<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-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] 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-[#5B7C8D] hover:bg-[#4A6473] disabled:bg-[#2F4F5F] 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}
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">{article.category}</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#5B7C8D] 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-[#3b82f6] bg-[#3b82f6]/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-[#5B7C8D] bg-[#5B7C8D]/10'}`}>
|
||||
{article.source === 'imported' ? 'WP' : 'Native'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -612,7 +612,7 @@ export default function AdminArticlesPage() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openEdit(article)}
|
||||
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]"
|
||||
className="p-1.5 text-[#555] hover:text-[#5B7C8D] transition-colors rounded hover:bg-[#1a1a1a]"
|
||||
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" />
|
||||
@@ -634,7 +634,7 @@ export default function AdminArticlesPage() {
|
||||
type="checkbox"
|
||||
checked={articles.length > 0 && selectedIds.size === articles.length}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : articles.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -668,7 +668,7 @@ 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-[#3b82f6] cursor-pointer flex-shrink-0"
|
||||
className="w-4 h-4 rounded border-[#333] bg-[#1a1a1a] accent-[#5B7C8D] cursor-pointer flex-shrink-0"
|
||||
/>
|
||||
|
||||
{/* Thumbnail + Title */}
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#5B7C8D] 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
article.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#5B7C8D] bg-[#5B7C8D]/10'
|
||||
}`}>
|
||||
{article.source === 'imported' ? 'WP' : 'Native'}
|
||||
</span>
|
||||
@@ -741,7 +741,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={() => openEdit(article)}
|
||||
title="Edit"
|
||||
className="p-1.5 text-[#555] hover:text-[#3b82f6] transition-colors rounded hover:bg-[#1a1a1a]">
|
||||
className="p-1.5 text-[#555] hover:text-[#5B7C8D] transition-colors rounded hover:bg-[#1a1a1a]">
|
||||
<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" />
|
||||
@@ -839,7 +839,7 @@ export default function AdminArticlesPage() {
|
||||
<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 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-[#3b82f6] bg-[#3b82f6]/10 px-1.5 py-0.5 rounded">Native</span>
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#5B7C8D] bg-[#5B7C8D]/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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-white text-lg font-bold rounded px-4 py-3 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6] placeholder-[#444] resize-none"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6]">
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]">
|
||||
<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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -939,7 +939,7 @@ 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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -966,7 +966,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleNewArticleSave}
|
||||
disabled={newSaving || !newTitle.trim()}
|
||||
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
className="flex items-center gap-2 bg-[#5B7C8D] hover:bg-[#4A6473] disabled:bg-[#2F4F5F] 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" />
|
||||
) : (
|
||||
@@ -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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]">
|
||||
className="w-full bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D]">
|
||||
<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-[#1e3a5f] rounded-lg p-4">
|
||||
<div className="bg-[#0d1a2e] border border-[#2F4F5F] 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-[#3b82f6]">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#5B7C8D]">
|
||||
<circle cx="12" cy="12" r="10" /><polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<label className="text-xs text-[#3b82f6] font-bold uppercase tracking-wider">Schedule Publish</label>
|
||||
<label className="text-xs text-[#5B7C8D] 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-[#3b82f6] [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-[#5B7C8D] [color-scheme:dark]"
|
||||
/>
|
||||
{editScheduledAt && (
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
@@ -1074,7 +1074,7 @@ 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
editingArticle.source === 'imported' ? 'text-purple-400 bg-purple-400/10' : 'text-[#5B7C8D] bg-[#5B7C8D]/10'
|
||||
}`}>
|
||||
{editingArticle.source === 'imported' ? 'WordPress Import' : 'Native'}
|
||||
</span>
|
||||
@@ -1090,7 +1090,7 @@ export default function AdminArticlesPage() {
|
||||
<button
|
||||
onClick={handleEditSave}
|
||||
disabled={editSaving || !editTitle.trim()}
|
||||
className="flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
className="flex items-center gap-2 bg-[#5B7C8D] hover:bg-[#4A6473] disabled:bg-[#2F4F5F] 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}
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -382,7 +382,7 @@ export default function AuditLogPage() {
|
||||
<StatCard
|
||||
label="Total Events"
|
||||
value={logs.length}
|
||||
accent="bg-[#3b82f6]/10 text-[#3b82f6]"
|
||||
accent="bg-[#5B7C8D]/10 text-[#5B7C8D]"
|
||||
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,7 +433,7 @@ 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-[#3b82f6] font-body"
|
||||
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-[#5B7C8D] font-body"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -445,7 +445,7 @@ export default function AuditLogPage() {
|
||||
onClick={() => setActionFilter(f)}
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold capitalize transition-all ${
|
||||
actionFilter === f
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#555] hover:text-[#888]'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading audit logs…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -496,7 +496,7 @@ export default function AuditLogPage() {
|
||||
</div>
|
||||
<p className="text-[#555] text-sm font-body">No audit log entries found</p>
|
||||
{searchQuery && (
|
||||
<button onClick={() => setSearchQuery('')} className="text-[#3b82f6] text-xs font-body hover:underline">
|
||||
<button onClick={() => setSearchQuery('')} className="text-[#5B7C8D] text-xs font-body hover:underline">
|
||||
Clear search
|
||||
</button>
|
||||
)}
|
||||
@@ -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-[#3b82f6] transition-colors p-1"
|
||||
className="text-[#444] hover:text-[#5B7C8D] 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">
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -200,7 +200,7 @@ export default function BannedTermsPage() {
|
||||
Scan Published Posts
|
||||
</button>
|
||||
<button onClick={() => { setForm(EMPTY_FORM); setEditingId(null); setShowAddForm(true); }}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#3b82f6] 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-[#5B7C8D] 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>
|
||||
@@ -240,12 +240,12 @@ export default function BannedTermsPage() {
|
||||
<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-[#3b82f6]" 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-[#5B7C8D]" 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-[#3b82f6]">
|
||||
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-[#5B7C8D]">
|
||||
<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-[#3b82f6]">
|
||||
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-[#5B7C8D]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
@@ -262,11 +262,11 @@ 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-[#3b82f6] 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-[#5B7C8D] resize-none" />
|
||||
</div>
|
||||
<div className="md:col-span-2 flex gap-3">
|
||||
<button type="submit" disabled={saving}
|
||||
className="px-5 py-2 bg-[#3b82f6] 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-[#5B7C8D] 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); }}
|
||||
@@ -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-[#3b82f6] w-48" />
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-white text-sm font-body focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#5B7C8D]">
|
||||
<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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#5B7C8D]">
|
||||
<option value="">All Sites</option>
|
||||
<option value="broadcastbeat">Broadcast 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-[#3b82f6]">
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#5B7C8D]">
|
||||
<option value="">All Status</option>
|
||||
<option value="true">Active</option>
|
||||
<option value="false">Inactive</option>
|
||||
@@ -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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Edit</button>
|
||||
<button onClick={() => handleEdit(term)} className="text-xs text-[#5B7C8D] 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'}
|
||||
|
||||
@@ -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-[#60a5fa]">{pct(b.clicks, b.impressions)}</span>
|
||||
<span className="text-[#8FB0C3]">{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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#3b82f6] focus:outline-none"
|
||||
className="rounded border border-[#1f2937] bg-[#070a10] p-2 text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#2563eb] disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="rounded bg-[#1d4ed8] px-3 py-1.5 text-sm font-medium text-white hover:bg-[#4A6473] 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-[#60a5fa] hover:underline"
|
||||
className="text-sm text-[#8FB0C3] 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="#60a5fa" />
|
||||
<ChartCard title="Impressions per day" data={imp} stroke="#8FB0C3" />
|
||||
<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-[#60a5fa]">{r.count.toLocaleString()}</td>
|
||||
<td className="py-2 text-right text-[#8FB0C3]">{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-[#60a5fa] hover:underline">
|
||||
<Link href="/admin/banners" className="text-sm text-[#8FB0C3] 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-[#60a5fa] hover:underline" href={b.click_url} target="_blank" rel="noopener noreferrer">
|
||||
<a className="text-[#8FB0C3] 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-[#60a5fa] hover:underline">
|
||||
<Link href="/admin" className="text-sm text-[#8FB0C3] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white text-sm font-body font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
{scanning ? (
|
||||
<>
|
||||
@@ -262,7 +262,7 @@ export default function CompanyCoveragePage() {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-4 py-1.5 rounded-md text-sm font-body font-semibold capitalize transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -282,7 +282,7 @@ 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-[#3b82f6] hover:text-blue-300 font-body underline">
|
||||
<button onClick={handleScan} disabled={scanning} className="mt-3 text-xs text-[#5B7C8D] hover:text-blue-300 font-body underline">
|
||||
Run scan now →
|
||||
</button>
|
||||
)}
|
||||
@@ -374,7 +374,7 @@ export default function CompanyCoveragePage() {
|
||||
<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-[#3b82f6]" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#5B7C8D]" 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>
|
||||
@@ -422,7 +422,7 @@ export default function CompanyCoveragePage() {
|
||||
</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-[#3b82f6] font-body font-mono break-all">{settings.reminder_email}</p>
|
||||
<p className="text-xs text-[#5B7C8D] 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>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</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-[#3b82f6]' : 'bg-[#333]'}`}
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6] 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-[#5B7C8D] 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>
|
||||
@@ -337,7 +337,7 @@ 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] text-[#555] hover:text-white'}`}>
|
||||
{s === 'generated' ? 'Pending Review' : s}
|
||||
</button>
|
||||
))}
|
||||
@@ -430,7 +430,7 @@ 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-[#3b82f6] 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-[#5B7C8D] 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>
|
||||
@@ -443,17 +443,17 @@ export default function CompanyTrackerPage() {
|
||||
<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-[#3b82f6]" />
|
||||
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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]">
|
||||
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-[#5B7C8D]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
@@ -462,14 +462,14 @@ 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-[#3b82f6]">
|
||||
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-[#5B7C8D]">
|
||||
<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-[#3b82f6] hover:bg-blue-500 text-white text-sm font-body font-semibold rounded-lg transition-colors">Add Company</button>
|
||||
<button type="submit" className="px-5 py-2 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -492,12 +492,12 @@ export default function CompanyTrackerPage() {
|
||||
) : companies.map(company => (
|
||||
<tr key={company.id} className="hover:bg-[#0d0d0d] 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-[#3b82f6] 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-[#5B7C8D] 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 === 'broadcastbeat' ? '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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#5B7C8D]' : '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>
|
||||
@@ -506,7 +506,7 @@ export default function CompanyTrackerPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Force Crawl</button>
|
||||
className="text-xs text-[#5B7C8D] 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>
|
||||
@@ -567,14 +567,14 @@ 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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${settings.autopilot_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6]" />
|
||||
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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_story_global_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
className={`relative w-11 h-6 rounded-full transition-colors ${trackerSettings.auto_publish_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6]" />
|
||||
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-[#5B7C8D]" />
|
||||
</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-[#3b82f6]" />
|
||||
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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onClick={handleSaveSettings} disabled={savingSettings}
|
||||
className="px-6 py-2.5 bg-[#3b82f6] 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-[#5B7C8D] 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>
|
||||
|
||||
@@ -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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
>
|
||||
{Object.entries(SITE_LABELS).map(([key, val]) => (
|
||||
<option key={key} value={key}>{val.label}</option>
|
||||
@@ -195,14 +195,14 @@ 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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={loading}
|
||||
className="flex-1 bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
|
||||
className="flex-1 bg-[#5B7C8D] hover:bg-[#4A6473] disabled:opacity-50 text-white text-sm font-body font-semibold py-2 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
{loading ? 'Generating…' : 'Generate Article'}
|
||||
</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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading content status…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ function StatCard({
|
||||
label,
|
||||
value,
|
||||
sub,
|
||||
accent = '#3b82f6',
|
||||
accent = '#5B7C8D',
|
||||
icon,
|
||||
trend,
|
||||
}: {
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading analytics…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -280,7 +280,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#555] hover:text-white border border-[#252525] hover:border-[#444]'
|
||||
}`}
|
||||
>
|
||||
@@ -317,7 +317,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label="Active Subscribers"
|
||||
value={fmt(stats.activeSubscribers)}
|
||||
sub={`${fmt(stats.unsubscribed)} unsubscribed`}
|
||||
accent="#3b82f6"
|
||||
accent="#5B7C8D"
|
||||
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">
|
||||
@@ -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="#3b82f6" strokeWidth={1} radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="sent" name="Sent" fill="#5B7C8D20" stroke="#5B7C8D" 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>
|
||||
@@ -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="#3b82f6" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0} />
|
||||
<stop offset="5%" stopColor="#5B7C8D" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#5B7C8D" 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="#3b82f6"
|
||||
stroke="#5B7C8D"
|
||||
strokeWidth={2}
|
||||
fill="url(#openGrad)"
|
||||
dot={{ r: 3, fill: '#3b82f6' }}
|
||||
dot={{ r: 3, fill: '#5B7C8D' }}
|
||||
activeDot={{ r: 5 }}
|
||||
/>
|
||||
<Area
|
||||
@@ -463,7 +463,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
fill="#8b5cf618"
|
||||
dot={false}
|
||||
/>
|
||||
<Bar dataKey="net" name="Net New" fill="#3b82f6" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="net" name="Net New" fill="#5B7C8D" radius={[3, 3, 0, 0]} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -503,7 +503,7 @@ export default function EmailAnalyticsDashboard() {
|
||||
label: 'Open Rate',
|
||||
yours: stats.avgOpenRate,
|
||||
benchmark: 21.5,
|
||||
color: '#3b82f6',
|
||||
color: '#5B7C8D',
|
||||
unit: '%',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -86,7 +86,7 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
|
||||
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] rounded text-xs font-medium hover:bg-blue-500">+ Add Domain</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
@@ -109,7 +109,7 @@ function DomainsContent() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -119,7 +119,7 @@ function DomainsContent() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add Domain'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -197,7 +197,7 @@ 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-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] disabled:opacity-50">
|
||||
<button onClick={() => runVerify(d.id)} disabled={verifying === d.id} className="px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] 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">
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>}>
|
||||
<DomainsContent />
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -77,7 +77,7 @@ export default function EmailDashboard() {
|
||||
<div key={site} className="bg-[#111] border border-[#252525] 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-[#3b82f6] hover:underline">Manage →</Link>
|
||||
<Link href={`/admin/email/domains?site=${site}`} className="text-xs text-[#5B7C8D] 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-[#3b82f6] transition-colors">
|
||||
<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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -84,14 +84,14 @@ 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-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500">+ Add Suppression</button>
|
||||
<button onClick={() => setShowAddForm(true)} className="px-3 py-1.5 bg-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'bg-[#111] border border-[#252525] text-[#888]'}`}>{s}</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function SuppressionsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
@@ -128,7 +128,7 @@ export default function SuppressionsPage() {
|
||||
<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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -138,11 +138,11 @@ export default function SuppressionsPage() {
|
||||
</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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">{saving ? 'Saving…' : 'Add'}</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</div>
|
||||
</form>
|
||||
@@ -186,13 +186,13 @@ export default function SuppressionsPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-semibold">Suppression Keywords</h2>
|
||||
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#3b82f6] hover:underline">+ Add Keyword</button>
|
||||
<button onClick={() => setShowKeywordForm(true)} className="text-xs text-[#5B7C8D] hover:underline">+ Add Keyword</button>
|
||||
</div>
|
||||
{showKeywordForm && (
|
||||
<form onSubmit={handleAddKeyword} className="px-4 py-3 border-b border-[#1a1a1a] 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-[#3b82f6]" />
|
||||
<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-[#5B7C8D]" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-[#888] block mb-1">Type</label>
|
||||
@@ -201,7 +201,7 @@ export default function SuppressionsPage() {
|
||||
<option value="hostile">hostile</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#3b82f6] rounded text-xs font-medium hover:bg-blue-500 disabled:opacity-50">Add</button>
|
||||
<button type="submit" disabled={saving} className="px-3 py-1.5 bg-[#5B7C8D] 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>
|
||||
</form>
|
||||
)}
|
||||
@@ -216,7 +216,7 @@ export default function SuppressionsPage() {
|
||||
<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>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#3b82f6] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
|
||||
<button onClick={() => toggleKeyword(k.id, k.active)} className="text-xs text-[#5B7C8D] hover:underline">{k.active ? 'Deactivate' : 'Activate'}</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
|
||||
@@ -167,7 +167,7 @@ const EMPTY_STATS: ForumStats = {
|
||||
},
|
||||
};
|
||||
|
||||
const CATEGORY_COLORS = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ef4444', '#06b6d4', '#f97316', '#84cc16'];
|
||||
const CATEGORY_COLORS = ['#5B7C8D', '#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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading forum dashboard…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] 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" />
|
||||
@@ -407,7 +407,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
iconSize={8}
|
||||
/>
|
||||
<Line type="monotone" dataKey="threads" stroke="#3b82f6" strokeWidth={2} dot={{ fill: '#3b82f6', r: 3 }} name="New Threads" />
|
||||
<Line type="monotone" dataKey="threads" stroke="#5B7C8D" strokeWidth={2} dot={{ fill: '#5B7C8D', r: 3 }} name="New Threads" />
|
||||
<Line type="monotone" dataKey="replies" stroke="#10b981" strokeWidth={2} dot={{ fill: '#10b981', r: 3 }} name="New Replies" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -425,7 +425,7 @@ export default function ForumDashboardPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">View forum →</Link>
|
||||
<Link href="/forum" className="text-xs text-[#5B7C8D] hover:text-blue-300 font-body transition-colors">View forum →</Link>
|
||||
</div>
|
||||
{topCategories.length === 0 ? (
|
||||
<div className="px-5 py-10 text-center">
|
||||
@@ -475,7 +475,7 @@ export default function ForumDashboardPage() {
|
||||
<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]">
|
||||
{idx + 1}
|
||||
</div>
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#3b82f6] to-[#8b5cf6] flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#5B7C8D] 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>
|
||||
@@ -523,7 +523,7 @@ export default function ForumDashboardPage() {
|
||||
iconType="circle"
|
||||
iconSize={8}
|
||||
/>
|
||||
<Bar dataKey="thread_count" fill="#3b82f6" radius={[3, 3, 0, 0]} name="Threads" />
|
||||
<Bar dataKey="thread_count" fill="#5B7C8D" radius={[3, 3, 0, 0]} name="Threads" />
|
||||
<Bar dataKey="post_count" fill="#10b981" radius={[3, 3, 0, 0]} name="Posts" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -539,7 +539,7 @@ export default function ForumDashboardPage() {
|
||||
<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-[#3b82f6] hover:text-blue-300 font-body transition-colors">Open moderation →</Link>
|
||||
<Link href="/admin/forum-moderation" className="text-xs text-[#5B7C8D] 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]">
|
||||
{[
|
||||
|
||||
@@ -126,7 +126,7 @@ function ThreadRow({ thread, onAction, loadingId }: ThreadRowProps) {
|
||||
<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-[#3b82f6] transition-colors truncate max-w-xs"
|
||||
className="text-white font-body font-semibold text-sm hover:text-[#5B7C8D] transition-colors truncate max-w-xs"
|
||||
target="_blank"
|
||||
>
|
||||
{thread.title}
|
||||
@@ -243,7 +243,7 @@ function ReplyRow({ reply, onAction, loadingId }: ReplyRowProps) {
|
||||
{reply.forum_threads && (
|
||||
<Link
|
||||
href={`/forum/thread/${reply.forum_threads.id}`}
|
||||
className="text-[#3b82f6] font-body text-xs hover:underline truncate max-w-xs"
|
||||
className="text-[#5B7C8D] font-body text-xs hover:underline truncate max-w-xs"
|
||||
target="_blank"
|
||||
>
|
||||
↗ {reply.forum_threads.title}
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -438,7 +438,7 @@ export default function ForumModerationPage() {
|
||||
<div className="bg-[#1a2535] 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-[#3b82f6] transition-colors">Admin</Link>
|
||||
<Link href="/admin" className="hover:text-[#5B7C8D] transition-colors">Admin</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<span className="text-[#aab4c4]">Forum Moderation</span>
|
||||
</nav>
|
||||
@@ -470,7 +470,7 @@ export default function ForumModerationPage() {
|
||||
onClick={() => { setActiveTab(tab); setPage(1); }}
|
||||
className={`px-5 py-2 text-sm font-body font-semibold rounded-md transition-colors capitalize ${
|
||||
activeTab === tab
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -488,7 +488,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
@@ -501,7 +501,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:bg-[#252525] hover:text-white border border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-7 h-7 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : activeTab === 'threads' ? (
|
||||
<>
|
||||
|
||||
@@ -140,9 +140,9 @@ export default function AdminForumSeedPage() {
|
||||
<div className="bg-[#1a2535] 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-[#3b82f6] transition-colors">Admin</Link>
|
||||
<Link href="/admin" className="hover:text-[#5B7C8D] transition-colors">Admin</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#5B7C8D] transition-colors">Forum</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<span className="text-[#aab4c4]">Seed Tool</span>
|
||||
</nav>
|
||||
@@ -161,13 +161,13 @@ export default function AdminForumSeedPage() {
|
||||
<div className="flex border-b border-[#252525]">
|
||||
<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-[#3b82f6]' : '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-[#5B7C8D]' : '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-[#3b82f6]' : '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-[#5B7C8D]' : 'text-[#666] hover:text-[#aaa]'}`}
|
||||
>
|
||||
JSON Schema
|
||||
</button>
|
||||
@@ -182,14 +182,14 @@ 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-[#3b82f6] resize-none"
|
||||
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-[#5B7C8D] resize-none"
|
||||
spellCheck={false}
|
||||
/>
|
||||
<div className="flex gap-3 mt-3">
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={importing}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
{importing ? 'Importing...' : 'Import Threads'}
|
||||
</button>
|
||||
@@ -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-[#3b82f6] font-mono text-xs px-2 py-1 rounded">{slug}</code>
|
||||
<code key={slug} className="bg-[#0d0d0d] border border-[#333] text-[#5B7C8D] font-mono text-xs px-2 py-1 rounded">{slug}</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,10 +268,10 @@ export default function AdminForumSeedPage() {
|
||||
<div className="bg-[#1a1a1a] border border-[#252525] 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-[#3b82f6] hover:underline font-body text-sm">
|
||||
<Link href="/forum" className="flex items-center gap-2 text-[#5B7C8D] hover:underline font-body text-sm">
|
||||
<span>→</span> View Forum
|
||||
</Link>
|
||||
<Link href="/admin" className="flex items-center gap-2 text-[#3b82f6] hover:underline font-body text-sm">
|
||||
<Link href="/admin" className="flex items-center gap-2 text-[#5B7C8D] hover:underline font-body text-sm">
|
||||
<span>→</span> Admin Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -145,7 +145,7 @@ export default function WPImportPage() {
|
||||
<div className="bg-[#111111] border-b border-[#252525]">
|
||||
<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-[#3b82f6] transition-colors text-sm">
|
||||
<Link href="/home-page" className="text-[#555] hover:text-[#5B7C8D] 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.broadcastbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
||||
<a href="https://www.broadcastbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#5B7C8D] hover:underline">
|
||||
broadcastbeat.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-[#3b82f6]">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#5B7C8D]">
|
||||
<circle cx="12" cy="12" r="10" /><path d="M12 8v4l3 3" />
|
||||
</svg>
|
||||
{posts.length} posts imported
|
||||
@@ -178,7 +178,7 @@ export default function WPImportPage() {
|
||||
{/* Import Controls */}
|
||||
<div className="bg-[#111111] border border-[#252525] 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-[#3b82f6]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-[#5B7C8D]">
|
||||
<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-[#3b82f6] disabled:opacity-50">
|
||||
className="bg-[#1a1a1a] border border-[#333] text-[#cccccc] text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] disabled:bg-[#1e3a5f] disabled:cursor-not-allowed text-white text-sm font-bold px-5 py-2 rounded transition-colors">
|
||||
className="flex items-center gap-2 bg-[#5B7C8D] hover:bg-[#4A6473] disabled:bg-[#2F4F5F] 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-[#1e3a5f] rounded p-4">
|
||||
<div className="mt-4 bg-[#0d1a2e] border border-[#2F4F5F] rounded p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||
<div className="w-5 h-5 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p>
|
||||
<p className="text-[#5B7C8D] text-sm font-bold">Import in progress</p>
|
||||
<p className="text-[#666] text-xs mt-0.5">Fetching posts from broadcastbeat.com and saving to database...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
|
||||
<div className="h-full bg-[#3b82f6] rounded-full animate-pulse w-2/3" />
|
||||
<div className="h-full bg-[#5B7C8D] 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-[#3b82f6]' },
|
||||
{ label: 'Pages', value: importResult.pagesProcessed, color: 'text-[#5B7C8D]' },
|
||||
{ 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">
|
||||
@@ -284,7 +284,7 @@ export default function WPImportPage() {
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-5 py-2.5 text-sm font-bold uppercase tracking-wider border-b-2 transition-colors ${
|
||||
activeTab === tab
|
||||
? 'border-[#3b82f6] text-[#3b82f6]'
|
||||
? 'border-[#5B7C8D] text-[#5B7C8D]'
|
||||
: 'border-transparent text-[#666] hover:text-[#999]'
|
||||
}`}>
|
||||
{tab === 'posts' ? `Imported Posts (${posts.length})` : `Import History (${logs.length})`}
|
||||
@@ -306,14 +306,14 @@ 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-[#3b82f6] placeholder-[#444]"
|
||||
className="w-full bg-[#111] border border-[#252525] text-[#cccccc] text-sm rounded pl-9 pr-4 py-2 focus:outline-none focus:border-[#5B7C8D] placeholder-[#444]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : filteredPosts.length === 0 ? (
|
||||
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
|
||||
@@ -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-[#3b82f6] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-[#5B7C8D] bg-[#0d1a2e] px-1.5 py-0.5 rounded">
|
||||
{post.category}
|
||||
</span>
|
||||
)}
|
||||
@@ -368,7 +368,7 @@ export default function WPImportPage() {
|
||||
href={`https://www.broadcastbeat.com/${post.wp_slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1"
|
||||
className="text-[#555] hover:text-[#5B7C8D] transition-colors p-1"
|
||||
title="View on broadcastbeat.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,7 +402,7 @@ export default function WPImportPage() {
|
||||
<div>
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="text-center py-16 bg-[#111] border border-[#252525] rounded-lg">
|
||||
|
||||
@@ -67,7 +67,7 @@ function fmtDate(dateStr: string): string {
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function StatCard({ label, value, sub, accent = '#3b82f6', icon }: {
|
||||
function StatCard({ label, value, sub, accent = '#5B7C8D', icon }: {
|
||||
label: string; value: string; sub: string; accent?: string; icon: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -308,7 +308,7 @@ export default function NewsletterAnalyticsPage() {
|
||||
label="Avg Open Rate"
|
||||
value={pct(avgOpenRate)}
|
||||
sub={`across ${digestData.length} digests`}
|
||||
accent="#3b82f6"
|
||||
accent="#5B7C8D"
|
||||
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" />
|
||||
@@ -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="#3b82f6" strokeWidth={2} dot={{ r: 3, fill: '#3b82f6' }} activeDot={{ r: 5 }} />
|
||||
<Line type="monotone" dataKey="Open Rate" stroke="#5B7C8D" strokeWidth={2} dot={{ r: 3, fill: '#5B7C8D' }} activeDot={{ r: 5 }} />
|
||||
<Line type="monotone" dataKey="Click Rate" stroke="#10b981" strokeWidth={2} dot={{ r: 3, fill: '#10b981' }} activeDot={{ r: 5 }} />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -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="#3b82f6" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="subscribers" name="Total Subscribers" fill="#5B7C8D" radius={[3, 3, 0, 0]} />
|
||||
<Bar dataKey="unsubscribes" name="Unsubscribes" fill="#ef4444" radius={[3, 3, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
|
||||
@@ -252,7 +252,7 @@ export default function CampaignEditorPage() {
|
||||
article_blocks: payload.article_blocks,
|
||||
layout: 'featured',
|
||||
style: 'dark',
|
||||
accent_color: '#3b82f6',
|
||||
accent_color: '#5B7C8D',
|
||||
is_preset: false,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-[#3b82f6] hover:bg-[#2563eb] 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-[#5B7C8D] hover:bg-[#4A6473] 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>
|
||||
@@ -430,12 +430,12 @@ export default function AdminNewsletterPage() {
|
||||
<div className="flex gap-1 mb-6 bg-[#111] border border-[#252525] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] text-white' : 'text-[#888] hover:text-white'}`}>
|
||||
Send Digest
|
||||
</button>
|
||||
</div>
|
||||
@@ -457,7 +457,7 @@ export default function AdminNewsletterPage() {
|
||||
<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-[#3b82f6] 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-[#5B7C8D] text-white' : 'text-[#888] hover:text-white'}`}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
@@ -478,9 +478,9 @@ export default function AdminNewsletterPage() {
|
||||
|
||||
{/* Bulk Actions Bar */}
|
||||
{someSelected && (
|
||||
<div className="flex items-center justify-between bg-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4">
|
||||
<div className="flex items-center justify-between bg-[#1a2535] border border-[#2F4F5F] rounded-lg px-4 py-3 mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm font-body font-semibold text-[#3b82f6]">
|
||||
<span className="text-sm font-body font-semibold text-[#5B7C8D]">
|
||||
{selectedIds.size} selected
|
||||
</span>
|
||||
<button
|
||||
@@ -512,7 +512,7 @@ export default function AdminNewsletterPage() {
|
||||
Delete
|
||||
</button>
|
||||
{bulkLoading && (
|
||||
<div className="w-4 h-4 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-4 h-4 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -522,7 +522,7 @@ export default function AdminNewsletterPage() {
|
||||
<div className="bg-[#111] border border-[#252525] rounded-lg overflow-hidden">
|
||||
{loadingData ? (
|
||||
<div className="flex items-center justify-center py-16">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
) : subscribers.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
@@ -540,7 +540,7 @@ export default function AdminNewsletterPage() {
|
||||
type="checkbox"
|
||||
checked={allSelected}
|
||||
onChange={toggleSelectAll}
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#5B7C8D] cursor-pointer"
|
||||
/>
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-body font-bold text-[#555] uppercase tracking-wider">Email</th>
|
||||
@@ -561,14 +561,14 @@ export default function AdminNewsletterPage() {
|
||||
type="checkbox"
|
||||
checked={selectedIds.has(sub.id)}
|
||||
onChange={() => toggleSelect(sub.id)}
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#3b82f6] cursor-pointer"
|
||||
className="w-4 h-4 rounded border-[#444] bg-[#1a1a1a] accent-[#5B7C8D] 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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{t}</span>
|
||||
<span key={t} className="text-xs bg-[#1a2535] text-[#5B7C8D] 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>
|
||||
@@ -616,21 +616,21 @@ export default function AdminNewsletterPage() {
|
||||
{activeTab === 'send-digest' && (
|
||||
<div className="max-w-2xl">
|
||||
{/* SMTP warning if not configured */}
|
||||
<div className="bg-[#1a2535] border border-[#1e3a5f] rounded-lg p-4 mb-6 flex gap-3">
|
||||
<svg className="w-5 h-5 text-[#3b82f6] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<div className="bg-[#1a2535] border border-[#2F4F5F] rounded-lg p-4 mb-6 flex gap-3">
|
||||
<svg className="w-5 h-5 text-[#5B7C8D] 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-[#3b82f6]">SMTP_HOST</code>, <code className="text-[#3b82f6]">SMTP_USER</code>, <code className="text-[#3b82f6]">SMTP_PASS</code>, and <code className="text-[#3b82f6]">SMTP_FROM_EMAIL</code> in your environment variables to enable sending.
|
||||
Set <code className="text-[#5B7C8D]">SMTP_HOST</code>, <code className="text-[#5B7C8D]">SMTP_USER</code>, <code className="text-[#5B7C8D]">SMTP_PASS</code>, and <code className="text-[#5B7C8D]">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-[#1a2535] border border-[#1e3a5f] rounded-lg px-4 py-3 mb-4">
|
||||
<div className="flex items-center justify-between bg-[#1a2535] border border-[#2F4F5F] 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-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors"
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-body font-semibold text-[#5B7C8D] border border-[#2F4F5F] rounded-lg hover:bg-[#1a2535] 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,7 +725,7 @@ export default function AdminNewsletterPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={addArticle}
|
||||
className="text-xs text-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors"
|
||||
className="text-xs text-[#5B7C8D] hover:text-blue-400 font-body font-semibold transition-colors"
|
||||
>
|
||||
+ Add Article
|
||||
</button>
|
||||
@@ -807,7 +807,7 @@ export default function AdminNewsletterPage() {
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[#252525] 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-[#3b82f6] font-body hover:text-blue-400 transition-colors">
|
||||
<Link href="/admin/newsletter/templates" className="text-xs text-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] text-sm font-body hover:text-blue-400 mt-2 inline-block">
|
||||
<Link href="/admin/newsletter/templates" className="text-[#5B7C8D] 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-[#3b82f6] hover:bg-[#1a2535] transition-colors group"
|
||||
className="flex items-start gap-3 p-4 bg-[#0d0d0d] border border-[#252525] rounded-xl text-left hover:border-[#5B7C8D] hover:bg-[#1a2535] 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 }}>
|
||||
|
||||
@@ -42,7 +42,7 @@ const STYLE_OPTIONS = [
|
||||
];
|
||||
|
||||
const ACCENT_COLORS = [
|
||||
'#3b82f6', '#ef4444', '#10b981', '#8b5cf6', '#f59e0b', '#ec4899', '#06b6d4', '#f97316',
|
||||
'#5B7C8D', '#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: '#3b82f6',
|
||||
accent_color: '#5B7C8D',
|
||||
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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{presets.length}</span>
|
||||
<span className="text-xs bg-[#1a2535] text-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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,7 +298,7 @@ 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-[#1a2535] text-[#3b82f6] px-2 py-0.5 rounded-full font-body">{custom.length}</span>
|
||||
<span className="text-xs bg-[#1a2535] text-[#5B7C8D] 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">
|
||||
@@ -306,7 +306,7 @@ export default function NewsletterTemplatesPage() {
|
||||
<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-[#3b82f6] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
|
||||
<button onClick={openNew} className="text-[#5B7C8D] text-sm font-body font-semibold hover:text-blue-400 transition-colors">
|
||||
Create your first template →
|
||||
</button>
|
||||
</div>
|
||||
@@ -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-[#3b82f6] bg-[#1a2535]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}>
|
||||
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#3b82f6]' : 'text-[#555]'}`}>
|
||||
className={`flex items-start gap-3 p-3 rounded-lg border text-left transition-colors ${form.layout === opt.value ? 'border-[#5B7C8D] bg-[#1a2535]' : 'border-[#252525] bg-[#0d0d0d] hover:border-[#333]'}`}>
|
||||
<span className={`mt-0.5 ${form.layout === opt.value ? 'text-[#5B7C8D]' : '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-[#3b82f6] bg-[#1a2535] 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-[#5B7C8D] bg-[#1a2535] text-white' : 'border-[#252525] bg-[#0d0d0d] text-[#888] hover:border-[#333]'}`}>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -469,7 +469,7 @@ 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-[#3b82f6] hover:text-blue-400 font-body font-semibold transition-colors">
|
||||
<button type="button" onClick={addBlock} className="text-xs text-[#5B7C8D] hover:text-blue-400 font-body font-semibold transition-colors">
|
||||
+ Add Slot
|
||||
</button>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]"
|
||||
className="w-3 h-3 accent-[#5B7C8D]"
|
||||
/>
|
||||
<span className="text-xs font-body text-[#888]">Featured</span>
|
||||
</label>
|
||||
@@ -597,7 +597,7 @@ export default function NewsletterTemplatesPage() {
|
||||
{previewTemplate.article_blocks.map((b, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-[#0d0d0d] 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-[#3b82f6] font-body bg-[#1a2535] px-1.5 py-0.5 rounded">Featured</span>}
|
||||
{b.featured && <span className="text-xs text-[#5B7C8D] font-body bg-[#1a2535] 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>
|
||||
@@ -668,12 +668,12 @@ function TemplateCard({ template, onEdit, onDelete, onPreview, deletingId }: Tem
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onPreview(template)}
|
||||
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#3b82f6] border border-[#1e3a5f] rounded-lg hover:bg-[#1a2535] transition-colors">
|
||||
className="flex-1 py-1.5 text-xs font-body font-semibold text-[#5B7C8D] border border-[#2F4F5F] rounded-lg hover:bg-[#1a2535] 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-[#1a2535] border border-[#1e3a5f] rounded-lg hover:bg-[#1e3a5f] transition-colors">
|
||||
className="flex-1 py-1.5 text-xs font-body font-semibold text-center text-white bg-[#1a2535] border border-[#2F4F5F] rounded-lg hover:bg-[#2F4F5F] transition-colors">
|
||||
Use
|
||||
</Link>
|
||||
{!template.is_preset && (
|
||||
|
||||
@@ -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-[#3b82f6] hover:text-blue-300 transition-colors"
|
||||
className="text-xs font-body font-bold text-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin mx-auto mb-3" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] 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-[#3b82f6] border border-[#3b82f6]/30 rounded hover:bg-[#3b82f6]/10 transition-colors"
|
||||
className="px-3 py-1.5 text-xs font-body font-bold text-[#5B7C8D] border border-[#5B7C8D]/30 rounded hover:bg-[#5B7C8D]/10 transition-colors"
|
||||
>
|
||||
Mark all read
|
||||
</button>
|
||||
@@ -558,7 +558,7 @@ export default function NotificationsPage() {
|
||||
label="Unread"
|
||||
value={stats.unread}
|
||||
icon={<BellIcon className="w-4 h-4" />}
|
||||
color="text-[#3b82f6] bg-[#3b82f6]/5 border-[#3b82f6]/30"
|
||||
color="text-[#5B7C8D] bg-[#5B7C8D]/5 border-[#5B7C8D]/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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#555] hover:text-[#888] hover:bg-[#111]'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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-[#3b82f6] hover:bg-[#0d1a2d] transition-all group">
|
||||
<Link href={href} className="flex items-center gap-3 p-3 bg-[#111] border border-[#252525] rounded-lg hover:border-[#5B7C8D] 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-[#3b82f6] transition-colors">{label}</p>
|
||||
<p className="text-sm font-body font-semibold text-white group-hover:text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-[#555] text-sm font-body">Loading dashboard…</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -375,7 +375,7 @@ export default function AdminDashboardPage() {
|
||||
<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">
|
||||
<h2 className="text-sm font-display font-bold text-white">Recent Activity</h2>
|
||||
<Link href="/admin/articles" className="text-xs text-[#3b82f6] hover:text-blue-300 font-body transition-colors">View all →</Link>
|
||||
<Link href="/admin/articles" className="text-xs text-[#5B7C8D] hover:text-blue-300 font-body transition-colors">View all →</Link>
|
||||
</div>
|
||||
<div className="divide-y divide-[#1a1a1a]">
|
||||
{recentActivity.length === 0 ? (
|
||||
|
||||
@@ -80,8 +80,8 @@ export default function EventTagPicker({ articleId }: { articleId: string }) {
|
||||
className={
|
||||
"text-xs px-2.5 py-1 rounded-full border transition-colors " +
|
||||
(on
|
||||
? "bg-[var(--color-text-info,#60a5fa)]/15 border-[var(--color-text-info,#60a5fa)]/60 text-[var(--color-text-info,#60a5fa)]"
|
||||
: "border-[#374151] text-[#9ca3af] hover:border-[var(--color-text-info,#60a5fa)] hover:text-[var(--color-text-info,#60a5fa)]")
|
||||
? "bg-[var(--color-text-info,#8FB0C3)]/15 border-[var(--color-text-info,#8FB0C3)]/60 text-[var(--color-text-info,#8FB0C3)]"
|
||||
: "border-[#374151] text-[#9ca3af] hover:border-[var(--color-text-info,#8FB0C3)] hover:text-[var(--color-text-info,#8FB0C3)]")
|
||||
}
|
||||
title={`${ev.start_date} – ${ev.end_date} (${ev.status})`}
|
||||
>
|
||||
|
||||
@@ -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-[#3b82f6] focus:outline-none"
|
||||
className="w-full rounded border border-[#1f2937] bg-[#070a10] p-2 text-sm text-[#e5e7eb] focus:border-[#5B7C8D] 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-[#60a5fa] hover:underline">
|
||||
<Link href="/admin/review-queue" className="text-sm text-[#8FB0C3] 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-[#60a5fa]">
|
||||
<div className="flex items-center gap-3 text-xs uppercase tracking-wider text-[#8FB0C3]">
|
||||
<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-[#3b82f6] transition-colors"
|
||||
className="rounded border border-[#1f2937] bg-[#0b0f17] p-5 hover:border-[#5B7C8D] 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-[#60a5fa]">
|
||||
<div className="text-xs uppercase tracking-wider text-[#8FB0C3]">
|
||||
{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-[#2563eb]"
|
||||
className="rounded bg-[#1d4ed8] px-3 py-1.5 font-medium text-white hover:bg-[#4A6473]"
|
||||
>
|
||||
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-[#3b82f6] 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-[#5B7C8D] border-t-transparent rounded-full animate-spin" /></div>;
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
@@ -83,7 +83,7 @@ export default function RmpHubPage() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#3b82f6] hover:bg-[#0d1a2d] transition-colors">
|
||||
<Link href={section?.href} className="block w-full text-center px-3 py-1.5 bg-[#1a2535] border border-[#2a3a50] rounded text-xs text-[#5B7C8D] hover:bg-[#0d1a2d] transition-colors">
|
||||
Open {section?.title}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] 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-[#3b82f6] bg-[#0d1a2d]' : 'border-[#252525]'
|
||||
selectedRole === role ? 'border-[#5B7C8D] bg-[#0d1a2d]' : 'border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
<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}`}>
|
||||
@@ -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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-3 py-2 text-sm text-white font-body focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<option value="all">All Roles</option>
|
||||
{ROLES.map((r) => (
|
||||
@@ -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-[#3b82f6] disabled:opacity-50"
|
||||
className="bg-[#0d0d0d] border border-[#252525] rounded-lg px-2 py-1.5 text-xs text-white font-body focus:outline-none focus:border-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-4 h-4 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
@@ -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-[#3b82f6] 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-[#5B7C8D] 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,7 +593,7 @@ export default function AdminRolesPage() {
|
||||
{isEditing ? (
|
||||
<button
|
||||
onClick={() => setEditedPermissions((prev) => ({ ...prev, [key]: !val }))}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${val ? 'bg-[#5B7C8D]' : '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>
|
||||
@@ -644,7 +644,7 @@ 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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] border-[#3b82f6]'
|
||||
? 'text-[#5B7C8D] border-[#5B7C8D]'
|
||||
: '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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] text-xs font-bold flex-shrink-0">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#5B7C8D] 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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
{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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
|
||||
@@ -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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
|
||||
className="flex items-center gap-2 bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] border-[#3b82f6]'
|
||||
? 'text-[#5B7C8D] border-[#5B7C8D]'
|
||||
: '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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="bg-[#111827] border border-[#1f2937] text-[#ccc] text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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" />
|
||||
) : (
|
||||
@@ -418,7 +418,7 @@ export default function AdminUsersPage() {
|
||||
<button
|
||||
onClick={() => openEditRole(u)}
|
||||
disabled={processingIds.has(u.id)}
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors text-xs font-medium px-2 py-1 rounded hover:bg-[#1f2937] disabled:opacity-50"
|
||||
className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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-[#3b82f6] hover:text-[#60a5fa] text-sm transition-colors"
|
||||
className="mt-3 text-[#5B7C8D] hover:text-[#8FB0C3] text-sm transition-colors"
|
||||
>
|
||||
Send your first invitation →
|
||||
</button>
|
||||
@@ -491,7 +491,7 @@ 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-[#3b82f6] text-xs font-bold flex-shrink-0">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1f2937] flex items-center justify-center text-[#5B7C8D] text-xs font-bold flex-shrink-0">
|
||||
{inv.email[0].toUpperCase()}
|
||||
</div>
|
||||
<span className="text-white text-sm">{inv.email}</span>
|
||||
@@ -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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#1f2937] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] 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-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] bg-[#3b82f6]/10'
|
||||
? 'border-[#5B7C8D] bg-[#5B7C8D]/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-[#3b82f6]' : 'border-[#444]'
|
||||
editRole === r.value ? 'border-[#5B7C8D]' : 'border-[#444]'
|
||||
}`}>
|
||||
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6]" />}
|
||||
{editRole === r.value && <span className="w-1.5 h-1.5 rounded-full bg-[#5B7C8D]" />}
|
||||
</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-[#3b82f6] hover:bg-[#2563eb] 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-[#5B7C8D] hover:bg-[#4A6473] 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 ? (
|
||||
<>
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function AdvertisePage() {
|
||||
|
||||
{/* Hero */}
|
||||
{/* DO NOT OVERRIDE — Advertise page hero section */}
|
||||
<div className="bg-[#0d1520] border-b border-[#1e3a5f] py-12">
|
||||
<div className="bg-[#0d1520] border-b border-[#2F4F5F] 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 Broadcast Engineering Professionals</h1>
|
||||
@@ -69,7 +69,7 @@ export default function AdvertisePage() {
|
||||
<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-[#3b82f6] text-2xl font-bold mb-1">{stat.value}</div>
|
||||
<div className="font-heading text-[#5B7C8D] text-2xl font-bold mb-1">{stat.value}</div>
|
||||
<div className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -86,10 +86,10 @@ export default function AdvertisePage() {
|
||||
<table className="w-full text-sm font-body">
|
||||
<thead>
|
||||
<tr className="border-b border-[#222]">
|
||||
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Placement</th>
|
||||
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Dimensions</th>
|
||||
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Location</th>
|
||||
<th className="text-left py-3 px-4 text-[#3b82f6] font-bold text-xs uppercase tracking-wider">Rate</th>
|
||||
<th className="text-left py-3 px-4 text-[#5B7C8D] font-bold text-xs uppercase tracking-wider">Placement</th>
|
||||
<th className="text-left py-3 px-4 text-[#5B7C8D] font-bold text-xs uppercase tracking-wider">Dimensions</th>
|
||||
<th className="text-left py-3 px-4 text-[#5B7C8D] font-bold text-xs uppercase tracking-wider">Location</th>
|
||||
<th className="text-left py-3 px-4 text-[#5B7C8D] font-bold text-xs uppercase tracking-wider">Rate</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -98,7 +98,7 @@ export default function AdvertisePage() {
|
||||
<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-[#3b82f6] font-medium">{zone.cpm}</td>
|
||||
<td className="py-3 px-4 text-[#5B7C8D] font-medium">{zone.cpm}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -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-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p>
|
||||
<p className="font-body text-[#5B7C8D] 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@broadcastbeat.com" className="font-body text-[#3b82f6] text-sm hover:underline">
|
||||
<a href="mailto:jeff@broadcastbeat.com" className="font-body text-[#5B7C8D] text-sm hover:underline">
|
||||
jeff@broadcastbeat.com
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-[#222] pt-4">
|
||||
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
|
||||
<p className="font-body text-[#5B7C8D] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
|
||||
<p className="font-body text-[#777] text-sm">BroadcastBeat</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-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p>
|
||||
<p className="font-body text-[#5B7C8D] 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-[#3b82f6] mt-0.5">✓</span>
|
||||
<span className="text-[#5B7C8D] 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-[#3b82f6] p-8 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-[#3b82f6]/20 flex items-center justify-center mx-auto mb-4">
|
||||
<div className="bg-[#111] border border-[#5B7C8D] p-8 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-[#5B7C8D]/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="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 12l4 4 10-10" stroke="#5B7C8D" 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@broadcastbeat.com" className="text-[#3b82f6] hover:underline">jeff@broadcastbeat.com</a>.
|
||||
<a href="mailto:jeff@broadcastbeat.com" className="text-[#5B7C8D] hover:underline">jeff@broadcastbeat.com</a>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -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@broadcastbeat.com" className="text-[#3b82f6] hover:underline">jeff@broadcastbeat.com</a>
|
||||
Or email directly: <a href="mailto:jeff@broadcastbeat.com" className="text-[#5B7C8D] hover:underline">jeff@broadcastbeat.com</a>
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
|
||||
<div style="margin-bottom:24px;">
|
||||
<h1 style="color:#3b82f6;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1>
|
||||
<h1 style="color:#5B7C8D;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1>
|
||||
<p style="color:#555;font-size:12px;margin:0;">Broadcast Beat · AV Beat — Company Story Engine</p>
|
||||
</div>
|
||||
<h2 style="font-size:18px;color:#fff;margin:0 0 12px;">New Company Stories Queued for Review</h2>
|
||||
@@ -32,11 +32,11 @@ export async function POST(request: NextRequest) {
|
||||
${moreText}
|
||||
</div>
|
||||
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastb5322.builtwithrocket.new'}/admin/company-tracker"
|
||||
style="display:inline-block;background:#3b82f6;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-size:14px;font-weight:bold;">
|
||||
style="display:inline-block;background:#5B7C8D;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-size:14px;font-weight:bold;">
|
||||
Review Story Queue →
|
||||
</a>
|
||||
<p style="color:#444;font-size:12px;margin-top:24px;">
|
||||
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastb5322.builtwithrocket.new'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a>
|
||||
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastb5322.builtwithrocket.new'}/admin/company-tracker" style="color:#5B7C8D;">Company Tracker</a>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
|
||||
const noteHtml = note
|
||||
? `<div style="background:#1a1a1a;border-left:3px solid #3b82f6;padding:12px 16px;margin-bottom:20px;border-radius:2px;">
|
||||
? `<div style="background:#1a1a1a;border-left:3px solid #5B7C8D;padding:12px 16px;margin-bottom:20px;border-radius:2px;">
|
||||
<p style="margin:0;color:#aaa;font-size:14px;font-style:italic;">"${note}"</p>
|
||||
</div>`
|
||||
: "";
|
||||
@@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background:#0a0a0a;border-bottom:1px solid #1a1a1a;padding:20px 28px;">
|
||||
<p style="margin:0;color:#3b82f6;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;">BroadcastBeat</p>
|
||||
<p style="margin:0;color:#5B7C8D;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;">BroadcastBeat</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
@@ -53,10 +53,10 @@ export async function POST(req: NextRequest) {
|
||||
<td style="padding:28px;">
|
||||
<p style="margin:0 0 8px;color:#555;font-size:11px;text-transform:uppercase;letter-spacing:2px;">Someone shared an article with you</p>
|
||||
${noteHtml}
|
||||
<p style="margin:0 0 6px;color:#3b82f6;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:2px;">NEWS</p>
|
||||
<p style="margin:0 0 6px;color:#5B7C8D;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:2px;">NEWS</p>
|
||||
<h1 style="margin:0 0 12px;color:#f0f0f0;font-size:22px;font-weight:700;line-height:1.3;">${articleTitle}</h1>
|
||||
<p style="margin:0 0 24px;color:#999;font-size:14px;line-height:1.6;">${articleExcerpt}</p>
|
||||
<a href="${articleUrl}" style="display:inline-block;background:#3b82f6;color:#fff;text-decoration:none;font-size:13px;font-weight:700;padding:12px 24px;border-radius:2px;letter-spacing:1px;text-transform:uppercase;">Read Article</a>
|
||||
<a href="${articleUrl}" style="display:inline-block;background:#5B7C8D;color:#fff;text-decoration:none;font-size:13px;font-weight:700;padding:12px 24px;border-radius:2px;letter-spacing:1px;text-transform:uppercase;">Read Article</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
|
||||
@@ -48,7 +48,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
|
||||
const topicsList = topics && topics.length > 0
|
||||
? `<p style="color:#aaa;font-size:14px;">You'll receive updates on: <strong style="color:#3b82f6;">${topics.join(', ')}</strong></p>`
|
||||
? `<p style="color:#aaa;font-size:14px;">You'll receive updates on: <strong style="color:#5B7C8D;">${topics.join(', ')}</strong></p>`
|
||||
: '';
|
||||
|
||||
await transporter.sendMail({
|
||||
@@ -57,12 +57,12 @@ export async function POST(req: NextRequest) {
|
||||
subject: `Welcome to BroadcastBeat — You're subscribed!`,
|
||||
html: `
|
||||
<div style="background:#0d1520;padding:40px 20px;font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||
<h1 style="color:#3b82f6;font-size:24px;margin-bottom:8px;">You're in!</h1>
|
||||
<h1 style="color:#5B7C8D;font-size:24px;margin-bottom:8px;">You're in!</h1>
|
||||
<p style="color:#e0e0e0;font-size:16px;">Thanks for subscribing to <strong>BroadcastBeat</strong> — the digital platform for broadcast engineering.</p>
|
||||
${topicsList}
|
||||
<p style="color:#aaa;font-size:14px;margin-top:24px;">You'll be the first to know about the latest news, product launches, and industry insights.</p>
|
||||
<hr style="border:none;border-top:1px solid #1e3a5f;margin:32px 0;" />
|
||||
<p style="color:#555;font-size:12px;">You're receiving this because you signed up at broadcastbeat.com. <a href="${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#3b82f6;">Unsubscribe</a></p>
|
||||
<hr style="border:none;border-top:1px solid #2F4F5F;margin:32px 0;" />
|
||||
<p style="color:#555;font-size:12px;">You're receiving this because you signed up at broadcastbeat.com. <a href="${process.env.NEXT_PUBLIC_SITE_URL}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#5B7C8D;">Unsubscribe</a></p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
@@ -124,13 +124,13 @@ export async function POST(req: NextRequest) {
|
||||
const articlesHtml = articles
|
||||
.map(
|
||||
(a: { title: string; excerpt?: string; url?: string; image?: string; category?: string }) => `
|
||||
<div style="border-bottom:1px solid #1e3a5f;padding:20px 0;">
|
||||
${a.category ? `<span style="color:#3b82f6;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''}
|
||||
<div style="border-bottom:1px solid #2F4F5F;padding:20px 0;">
|
||||
${a.category ? `<span style="color:#5B7C8D;font-size:11px;font-weight:bold;text-transform:uppercase;letter-spacing:1px;">${a.category}</span>` : ''}
|
||||
<h2 style="color:#e0e0e0;font-size:18px;margin:8px 0 6px;">
|
||||
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#e0e0e0;text-decoration:none;">${a.title}</a>` : a.title}
|
||||
</h2>
|
||||
${a.excerpt ? `<p style="color:#888;font-size:14px;margin:0 0 10px;">${a.excerpt}</p>` : ''}
|
||||
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#3b82f6;font-size:13px;text-decoration:none;">Read more →</a>` : ''}
|
||||
${a.url ? `<a href="${siteUrl}${a.url}" style="color:#5B7C8D;font-size:13px;text-decoration:none;">Read more →</a>` : ''}
|
||||
</div>
|
||||
`
|
||||
)
|
||||
@@ -139,22 +139,22 @@ export async function POST(req: NextRequest) {
|
||||
const emailHtml = `
|
||||
<div style="background:#0d1520;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||
<!-- Header -->
|
||||
<div style="background:#111;border-bottom:2px solid #3b82f6;padding:24px 32px;">
|
||||
<h1 style="color:#3b82f6;font-size:22px;margin:0;font-weight:bold;">BroadcastBeat</h1>
|
||||
<div style="background:#111;border-bottom:2px solid #5B7C8D;padding:24px 32px;">
|
||||
<h1 style="color:#5B7C8D;font-size:22px;margin:0;font-weight:bold;">BroadcastBeat</h1>
|
||||
<p style="color:#555;font-size:12px;margin:4px 0 0;">News & Intelligence for Broadcast, Post-Production & Streaming Technology</p>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
<div style="padding:32px;">
|
||||
${previewText ? `<p style="color:#aaa;font-size:15px;margin:0 0 24px;">${previewText}</p>` : ''}
|
||||
${customMessage ? `<div style="background:#1a2535;border-left:3px solid #3b82f6;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''}
|
||||
${customMessage ? `<div style="background:#1a2535;border-left:3px solid #5B7C8D;padding:16px;margin-bottom:24px;"><p style="color:#e0e0e0;font-size:14px;margin:0;">${customMessage}</p></div>` : ''}
|
||||
${articlesHtml}
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;">
|
||||
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #2F4F5F;">
|
||||
<p style="color:#555;font-size:12px;margin:0;">You're receiving this because you subscribed at broadcastbeat.com.</p>
|
||||
<p style="color:#555;font-size:12px;margin:8px 0 0;">
|
||||
<a href="${siteUrl}" style="color:#3b82f6;">Visit BroadcastBeat</a> ·
|
||||
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a>
|
||||
<a href="${siteUrl}" style="color:#5B7C8D;">Visit BroadcastBeat</a> ·
|
||||
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#5B7C8D;">Unsubscribe</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 || '#3b82f6',
|
||||
accent_color: accent_color || '#5B7C8D',
|
||||
article_blocks: article_blocks || [],
|
||||
is_preset: false,
|
||||
})
|
||||
|
||||
@@ -224,9 +224,9 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
{/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
|
||||
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]">
|
||||
<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-[#3b82f6] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">Home</Link>
|
||||
<span>/</span>
|
||||
<Link href={sectionHref} className="hover:text-[#3b82f6] transition-colors">{sectionLabel}</Link>
|
||||
<Link href={sectionHref} className="hover:text-[#5B7C8D] transition-colors">{sectionLabel}</Link>
|
||||
<span>/</span>
|
||||
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
|
||||
</div>
|
||||
@@ -240,7 +240,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
{/* 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-[#3b82f6] hover:underline">
|
||||
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#5B7C8D] hover:underline">
|
||||
{article.category}
|
||||
</Link>
|
||||
<span className="text-xs text-[#777]">{article.date}</span>
|
||||
@@ -265,7 +265,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
<div>
|
||||
<Link
|
||||
href={`/authors/${article.authorSlug}`}
|
||||
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
|
||||
className="font-heading font-bold text-sm hover:text-[#5B7C8D] transition-colors text-[#e0e0e0]">
|
||||
{article.author}
|
||||
</Link>
|
||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||
@@ -275,7 +275,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
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-[#3b82f6] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#5B7C8D] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
{savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -302,7 +302,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
[&_ul]:mb-4 [&_ul]:pl-5 [&_ul]:space-y-2
|
||||
[&_li]:text-[#aaa]
|
||||
[&_strong]:text-[#e0e0e0] [&_strong]:font-bold
|
||||
[&_a]:text-[#3b82f6] [&_a]:hover:underline"
|
||||
[&_a]:text-[#5B7C8D] [&_a]:hover:underline"
|
||||
dangerouslySetInnerHTML={{ __html: article.content }}
|
||||
/>
|
||||
|
||||
@@ -314,7 +314,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
{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-[#3b82f6] hover:text-[#3b82f6] transition-colors rounded-sm cursor-default">
|
||||
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D] transition-colors rounded-sm cursor-default">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -326,7 +326,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
<div className="pt-6 border-t border-[#222]">
|
||||
<Link
|
||||
href={sectionHref}
|
||||
className="inline-flex items-center gap-2 text-[#3b82f6] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="inline-flex items-center gap-2 text-[#5B7C8D] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<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>
|
||||
@@ -340,7 +340,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
{/* 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-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Articles
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -359,10 +359,10 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#5B7C8D] 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-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#5B7C8D] transition-colors line-clamp-2 leading-snug">
|
||||
{related.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -372,7 +372,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
</div>
|
||||
|
||||
{/* Newsletter Signup */}
|
||||
<div className="bg-[#0d1520] border border-[#1e3a5f] p-5">
|
||||
<div className="bg-[#0d1520] border border-[#2F4F5F] p-5">
|
||||
<h3 className="font-heading text-[#e0e0e0] font-bold mb-2">Stay Updated</h3>
|
||||
<p className="font-body text-[#777] text-xs mb-4">
|
||||
Get the latest broadcast engineering news delivered to your inbox.
|
||||
@@ -398,7 +398,7 @@ export default function ArticleDetailClient({ article, relatedArticles }: Articl
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#5B7C8D] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
{saveToast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -134,8 +134,8 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
{user ? (
|
||||
<form onSubmit={handleSubmit} className="mb-8">
|
||||
<div className="flex gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-[#1e3a5f] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="font-body text-[10px] font-bold text-[#3b82f6]">
|
||||
<div className="w-8 h-8 rounded-full bg-[#2F4F5F] border border-[#2a3a50] flex items-center justify-center flex-shrink-0 mt-0.5">
|
||||
<span className="font-body text-[10px] font-bold text-[#5B7C8D]">
|
||||
{getInitials(user.user_metadata?.full_name || user.email || "U")}
|
||||
</span>
|
||||
</div>
|
||||
@@ -146,7 +146,7 @@ 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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6] transition-colors placeholder-[#444] resize-none"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D] transition-colors placeholder-[#444] resize-none"
|
||||
/>
|
||||
{error && (
|
||||
<p className="font-body text-xs text-[#cc0000] mt-1.5">{error}</p>
|
||||
@@ -158,7 +158,7 @@ export default function CommentsSection({ articleSlug }: CommentsSectionProps) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !newComment.trim()}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] 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-[#3b82f6]">
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#5B7C8D]">
|
||||
{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-[#1e3a5f] rounded-sm p-5 flex items-center justify-between gap-4">
|
||||
<div className="mb-8 bg-[#0d1520] border border-[#2F4F5F] 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-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors">
|
||||
className="flex-shrink-0 bg-[#5B7C8D] hover:bg-[#4A6473] text-white font-body text-xs font-bold uppercase tracking-wide px-5 py-2 rounded-sm transition-colors">
|
||||
Sign In
|
||||
</Link>
|
||||
</div>
|
||||
@@ -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-[#3b82f6]">
|
||||
<span className="font-body text-[10px] font-bold text-[#5B7C8D]">
|
||||
{getInitials(name)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -298,7 +298,7 @@ export default function AuthorProfilePage() {
|
||||
<h1 className="font-heading text-2xl md:text-3xl font-bold text-[#e8e8e8] leading-tight">
|
||||
{author.name}
|
||||
</h1>
|
||||
<p className="font-body text-sm text-[#3b82f6] mt-0.5">{author.title}</p>
|
||||
<p className="font-body text-sm text-[#5B7C8D] mt-0.5">{author.title}</p>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-2">
|
||||
{author.location &&
|
||||
<span className="font-body text-xs text-[#666] flex items-center gap-1">
|
||||
@@ -323,7 +323,7 @@ export default function AuthorProfilePage() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${author.name} on Twitter`}
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm">
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#5B7C8D] hover:text-[#5B7C8D] 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" />
|
||||
@@ -336,7 +336,7 @@ export default function AuthorProfilePage() {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${author.name} on LinkedIn`}
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm">
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#5B7C8D] hover:text-[#5B7C8D] 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" />
|
||||
@@ -347,7 +347,7 @@ export default function AuthorProfilePage() {
|
||||
<a
|
||||
href={`mailto:${author.social.email}`}
|
||||
aria-label={`Email ${author.name}`}
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] hover:text-[#3b82f6] text-[#888] transition-colors rounded-sm">
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#5B7C8D] hover:text-[#5B7C8D] 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" />
|
||||
@@ -365,7 +365,7 @@ export default function AuthorProfilePage() {
|
||||
{author.specialties.map((spec) =>
|
||||
<span
|
||||
key={spec}
|
||||
className="font-body text-xs text-[#3b82f6] bg-[#1e3a5f]/30 border border-[#1e3a5f] px-2.5 py-1 rounded-sm">
|
||||
className="font-body text-xs text-[#5B7C8D] bg-[#2F4F5F]/30 border border-[#2F4F5F] px-2.5 py-1 rounded-sm">
|
||||
|
||||
{spec}
|
||||
</span>
|
||||
@@ -378,7 +378,7 @@ export default function AuthorProfilePage() {
|
||||
<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-[#3b82f6] text-[#3b82f6]" : "border-transparent text-[#666] hover:text-[#aaa]"}`
|
||||
activeTab === "articles" ? "border-[#5B7C8D] text-[#5B7C8D]" : "border-transparent text-[#666] hover:text-[#aaa]"}`
|
||||
}>
|
||||
|
||||
Articles ({articles.length})
|
||||
@@ -386,7 +386,7 @@ export default function AuthorProfilePage() {
|
||||
<button
|
||||
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-[#3b82f6] text-[#3b82f6]" : "border-transparent text-[#666] hover:text-[#aaa]"}`
|
||||
activeTab === "about" ? "border-[#5B7C8D] text-[#5B7C8D]" : "border-transparent text-[#666] hover:text-[#aaa]"}`
|
||||
}>
|
||||
|
||||
About
|
||||
@@ -407,7 +407,7 @@ export default function AuthorProfilePage() {
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={`/articles/${article.slug}`}
|
||||
className="group bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] transition-colors rounded-sm overflow-hidden article-card">
|
||||
className="group bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#5B7C8D] transition-colors rounded-sm overflow-hidden article-card">
|
||||
|
||||
<div className="img-zoom h-44 overflow-hidden">
|
||||
<AppImage
|
||||
@@ -420,13 +420,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-[#3b82f6]">
|
||||
<span className="font-body text-[9px] font-bold uppercase tracking-widest text-[#5B7C8D]">
|
||||
{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-[#3b82f6] transition-colors leading-snug mb-2 line-clamp-2">
|
||||
<h3 className="font-heading text-sm font-bold text-[#e0e0e0] group-hover:text-[#5B7C8D] 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">
|
||||
@@ -463,7 +463,7 @@ export default function AuthorProfilePage() {
|
||||
<div>
|
||||
<p className="font-body text-xs font-bold uppercase tracking-wider text-[#555] mb-1">Contact</p>
|
||||
{author.social.email ?
|
||||
<a href={`mailto:${author.social.email}`} className="font-body text-sm text-[#3b82f6] hover:underline">
|
||||
<a href={`mailto:${author.social.email}`} className="font-body text-sm text-[#5B7C8D] hover:underline">
|
||||
{author.social.email}
|
||||
</a> :
|
||||
|
||||
|
||||
@@ -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-[#1a2535] text-[#3b82f6] px-1 py-0.5 rounded text-sm font-mono">$1</code>')
|
||||
.replace(/`([^`]+)`/g, '<code class="bg-[#1a2535] text-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] bg-[#1e3a5f] text-white'
|
||||
? 'border-[#5B7C8D] bg-[#2F4F5F] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#1e2a3a] text-white text-sm rounded px-2 py-1.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
{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-[#3b82f6] text-xs">{profile.title}</p>
|
||||
<p className="text-[#5B7C8D] 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-[#3b82f6]/40 pl-3">"{profile.exampleLead}"</p>
|
||||
<p className="text-[#aaa] text-xs italic leading-relaxed border-l-2 border-[#5B7C8D]/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-[#3b82f6] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`}
|
||||
className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#5B7C8D] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`}
|
||||
>
|
||||
{f === 'ALL' ? 'All Writers' : f === 'BB' ? 'Broadcast 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-[#3b82f6] transition-colors">{p.name}</span>
|
||||
<span className="text-white text-xs font-medium group-hover:text-[#5B7C8D] 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-[#3b82f6] font-bold">Response #{idx + 1}</span>
|
||||
<span className="text-[10px] text-[#5B7C8D] 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-[#3b82f6] opacity-0 group-hover:opacity-100 transition-opacity">Jump ↑</span>
|
||||
<span className="text-[10px] text-[#5B7C8D] 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-[#3b82f6] text-sm transition-colors">← Dashboard</Link>
|
||||
<Link href="/admin" className="text-[#555] hover:text-[#5B7C8D] 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-[#1e3a5f] border-[#3b82f6]/40 text-[#3b82f6]'
|
||||
? 'bg-[#2F4F5F] border-[#5B7C8D]/40 text-[#5B7C8D]'
|
||||
: 'bg-[#1e2a3a] border-[#2a3a50] text-[#555] hover:text-[#aaa]'
|
||||
}`}
|
||||
title="Toggle response history sidebar"
|
||||
>
|
||||
History
|
||||
{assistantMessageCount > 0 && (
|
||||
<span className="ml-1.5 bg-[#3b82f6] text-white text-[9px] font-bold px-1 py-0.5 rounded-full">{assistantMessageCount}</span>
|
||||
<span className="ml-1.5 bg-[#5B7C8D] 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-[#3b82f6] text-xs transition-colors">Health</Link>
|
||||
<Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#3b82f6] text-xs transition-colors">Settings</Link>
|
||||
<Link href="/dashboard/ai-console/health" className="text-[#555] hover:text-[#5B7C8D] text-xs transition-colors">Health</Link>
|
||||
<Link href="/dashboard/ai-console/settings" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6]/40 bg-[#1e2a3a]'
|
||||
? 'border-[#5B7C8D]/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-[#3b82f6] transition-colors"
|
||||
className="text-[10px] text-[#555] hover:text-[#5B7C8D] transition-colors"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#3b82f6] transition-colors">
|
||||
<Link href="/dashboard/ai-console/health" className="text-[10px] text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6]"
|
||||
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<option value={1}>Broadcast 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-[#3b82f6]"
|
||||
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
{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-[#3b82f6] hover:text-[#60a5fa] transition-colors ml-auto"
|
||||
className="text-[10px] text-[#5B7C8D] hover:text-[#8FB0C3] 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-[#1e3a5f] text-white text-sm' :'bg-[#111827] border border-[#1e2a3a] text-[#ccc] text-sm'
|
||||
msg.role === 'user' ?'bg-[#2F4F5F] 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-[#3b82f6] hover:text-[#60a5fa] bg-[#111827] border border-[#3b82f6]/30 px-2 py-1 rounded transition-colors font-medium"
|
||||
className="text-[10px] text-[#5B7C8D] hover:text-[#8FB0C3] bg-[#111827] border border-[#5B7C8D]/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-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-[#3b82f6] rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-[#5B7C8D] rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-[#5B7C8D] rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
|
||||
<div className="w-1.5 h-1.5 bg-[#5B7C8D] 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-[#3b82f6] placeholder-[#444]"
|
||||
className="flex-1 bg-[#1e2a3a] border border-[#2a3a50] text-white text-sm rounded px-3 py-2 focus:outline-none focus:border-[#5B7C8D] placeholder-[#444]"
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
onClick={handleFetchUrl}
|
||||
disabled={fetchingUrl}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white text-xs px-3 py-2 rounded transition-colors disabled:opacity-50"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
|
||||
className="text-xs text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
|
||||
className="text-xs text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] bg-[#111827] border border-[#1e2a3a] px-2 py-1 rounded transition-colors"
|
||||
className="text-xs text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] placeholder-[#333] resize-none"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={() => handleSend()}
|
||||
disabled={isStreaming || !input.trim()}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] 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-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6]/30 border-t-[#3b82f6] rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D]/30 border-t-[#5B7C8D] 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-[#3b82f6] text-sm transition-colors">← AI Console</Link>
|
||||
<Link href="/dashboard/ai-console" className="text-[#555] hover:text-[#5B7C8D] 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-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white text-sm px-4 py-2 rounded-lg transition-colors font-medium"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.auto_failover ? 'bg-[#5B7C8D]' : '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-[#3b82f6]"
|
||||
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<option value={1}>Broadcast 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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
{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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
{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-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.save_session_history ? 'bg-[#5B7C8D]' : '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-[#3b82f6]"
|
||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#5B7C8D]"
|
||||
>
|
||||
<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-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.streaming_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.background_use_local ? 'bg-[#5B7C8D]' : '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-[#3b82f6]"
|
||||
className="w-32 bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2 focus:outline-none focus:border-[#5B7C8D]"
|
||||
/>
|
||||
</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-[#3b82f6]' : 'bg-[#2a3a50]'}`}
|
||||
className={`w-10 h-5 rounded-full transition-colors relative ${settings.priority_queue_enabled ? 'bg-[#5B7C8D]' : '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-[#3b82f6] 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-[#5B7C8D] 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' ? '#3b82f6' : '#a855f7'}
|
||||
stroke={selectedCompany === 'rmp' ? '#5B7C8D' : '#a855f7'}
|
||||
strokeWidth={2}
|
||||
dot={{ fill: selectedCompany === 'rmp' ? '#3b82f6' : '#a855f7', r: 3 }}
|
||||
dot={{ fill: selectedCompany === 'rmp' ? '#5B7C8D' : '#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-[#3b82f6] ml-1">{sortDir === 'desc' ? '↓' : '↑'}</span>;
|
||||
return <span className="text-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ 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-[#3b82f6] hover:bg-blue-500 text-white rounded transition-colors"
|
||||
className="px-3 py-1.5 text-xs font-semibold bg-[#5B7C8D] hover:bg-blue-500 text-white rounded transition-colors"
|
||||
>
|
||||
+ New Article
|
||||
</Link>
|
||||
@@ -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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] transition-colors"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
@@ -379,7 +379,7 @@ export default function EditorialDashboard() {
|
||||
<div className="bg-[#0d0d0d] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] 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>
|
||||
@@ -437,7 +437,7 @@ export default function EditorialDashboard() {
|
||||
<span className="text-[#555]">{article.author_name}</span>
|
||||
)}
|
||||
{article.category && (
|
||||
<span className="text-[#3b82f6]/70 bg-[#3b82f6]/10 px-1.5 py-0.5 rounded text-[10px]">
|
||||
<span className="text-[#5B7C8D]/70 bg-[#5B7C8D]/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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -294,7 +294,7 @@ 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-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors"
|
||||
className="px-3 py-1.5 bg-[#5B7C8D] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors"
|
||||
>
|
||||
+ Add Event
|
||||
</button>
|
||||
@@ -316,7 +316,7 @@ export default function ShowCalendarPage() {
|
||||
onClick={() => setSiteFilter(s)}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
siteFilter === s
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||
}`}
|
||||
>
|
||||
@@ -333,7 +333,7 @@ export default function ShowCalendarPage() {
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||
activeTab === tab.id
|
||||
? 'border-[#3b82f6] text-white'
|
||||
? 'border-[#5B7C8D] 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-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#5B7C8D] 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-[#3b82f6] hover:underline">{event.official_site}</a>
|
||||
className="text-[#5B7C8D] hover:underline">{event.official_site}</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
|
||||
@@ -440,7 +440,7 @@ export default function ShowCalendarPage() {
|
||||
onClick={() => setQueueStatus(s)}
|
||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||
queueStatus === s
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||
}`}
|
||||
>
|
||||
@@ -779,7 +779,7 @@ export default function ShowCalendarPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={actionLoading === 'save-event'}
|
||||
className="flex-1 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
className="flex-1 py-2 bg-[#5B7C8D] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === 'save-event' ? 'Saving...' : 'Save Event'}
|
||||
</button>
|
||||
@@ -857,7 +857,7 @@ export default function ShowCalendarPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={actionLoading === 'generate'}
|
||||
className="flex-1 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
className="flex-1 py-2 bg-[#5B7C8D] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === 'generate' ? 'Generating...' : 'Generate Story'}
|
||||
</button>
|
||||
|
||||
@@ -25,13 +25,13 @@ export default function Error({
|
||||
<div className="flex gap-3 justify-center">
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
className="px-4 py-2 bg-[#3b82f6] text-white text-sm rounded-sm hover:bg-[#2563eb] transition-colors"
|
||||
className="px-4 py-2 bg-[#5B7C8D] text-white text-sm rounded-sm hover:bg-[#4A6473] transition-colors"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
<Link
|
||||
href="/home-page"
|
||||
className="px-4 py-2 border border-[#333] text-sm rounded-sm hover:border-[#3b82f6] transition-colors"
|
||||
className="px-4 py-2 border border-[#333] text-sm rounded-sm hover:border-[#5B7C8D] transition-colors"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
|
||||
@@ -72,7 +72,7 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
|
||||
|
||||
const getMetricValue = (thread: ForumThread) => {
|
||||
if (activeMetric === 'upvotes') return { value: thread.upvote_count ?? 0, label: 'votes', color: 'text-[#22c55e]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#5B7C8D]' };
|
||||
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ function TopThreadsWidget({ categoryId }: { categoryId: string }) {
|
||||
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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -140,7 +140,7 @@ 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-[#3b82f6] transition-colors truncate leading-snug">
|
||||
<p className="text-white font-body text-sm font-semibold group-hover:text-[#5B7C8D] transition-colors truncate leading-snug">
|
||||
{thread.title}
|
||||
</p>
|
||||
<p className="text-[#555] font-body text-xs mt-0.5">
|
||||
@@ -268,7 +268,7 @@ export default function ForumCategoryPage() {
|
||||
<div className="bg-[#1a2535] 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-[#3b82f6] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#5B7C8D] transition-colors">Forum</Link>
|
||||
<span className="mx-2">›</span>
|
||||
<span className="text-[#aab4c4]">{category?.name || slug}</span>
|
||||
</nav>
|
||||
@@ -284,7 +284,7 @@ export default function ForumCategoryPage() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewThread(!showNewThread)}
|
||||
className="flex-shrink-0 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
className="flex-shrink-0 bg-[#5B7C8D] hover:bg-[#4A6473] text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
+ New Thread
|
||||
</button>
|
||||
@@ -303,7 +303,7 @@ export default function ForumCategoryPage() {
|
||||
placeholder="Your name (optional)"
|
||||
value={newAuthor}
|
||||
onChange={e => setNewAuthor(e.target.value)}
|
||||
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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
@@ -311,7 +311,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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="What would you like to discuss? *"
|
||||
@@ -319,13 +319,13 @@ 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-[#3b82f6] 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-[#5B7C8D] resize-none"
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
{submitting ? 'Posting...' : 'Post Thread'}
|
||||
</button>
|
||||
@@ -356,7 +356,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-[#3b82f6] 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-[#1a1a1a] border border-[#2a2a2a] focus:border-[#5B7C8D] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
@@ -379,7 +379,7 @@ export default function ForumCategoryPage() {
|
||||
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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -423,12 +423,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-[#3b82f6] rounded-lg px-4 py-3 transition-all group"
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#5B7C8D] 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-[#3b82f6]/20 text-[#3b82f6] px-2 py-0.5 rounded font-body font-semibold">PINNED</span>
|
||||
<span className="text-xs bg-[#5B7C8D]/20 text-[#5B7C8D] 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>
|
||||
@@ -436,7 +436,7 @@ 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-[#3b82f6] transition-colors truncate">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#5B7C8D] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
|
||||
const getMetricValue = (thread: ForumThread) => {
|
||||
if (activeMetric === 'upvotes') return { value: thread.upvote_count ?? 0, label: 'votes', color: 'text-[#22c55e]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#5B7C8D]' };
|
||||
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
|
||||
};
|
||||
|
||||
@@ -95,7 +95,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -143,13 +143,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-[#3b82f6] transition-colors truncate leading-snug">
|
||||
<p className="text-white font-body text-sm font-semibold group-hover:text-[#5B7C8D] transition-colors truncate leading-snug">
|
||||
{thread.title}
|
||||
</p>
|
||||
<p className="text-[#555] font-body text-xs mt-0.5">
|
||||
<span className="text-[#666]">{thread.author_name}</span>
|
||||
{thread.forum_categories && (
|
||||
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#5B7C8D]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -278,7 +278,7 @@ export default function ForumIndexPage() {
|
||||
<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-[#3b82f6] text-white' : 'text-[#888] hover:text-white'
|
||||
feedMode === 'discover' ? 'bg-[#5B7C8D] 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">
|
||||
@@ -289,7 +289,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-[#3b82f6] text-white' : 'text-[#888] hover:text-white'
|
||||
feedMode === 'following' ? 'bg-[#5B7C8D] 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">
|
||||
@@ -308,7 +308,7 @@ export default function ForumIndexPage() {
|
||||
<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-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors"
|
||||
className="inline-block bg-[#5B7C8D] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#4A6473] transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
@@ -329,7 +329,7 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
href="/forum"
|
||||
onClick={() => setFeedMode('discover')}
|
||||
className="inline-block bg-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors"
|
||||
className="inline-block bg-[#5B7C8D] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#4A6473] transition-colors"
|
||||
>
|
||||
Discover Members
|
||||
</Link>
|
||||
@@ -343,10 +343,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-[#3b82f6] rounded-lg px-4 py-3 transition-all group"
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#5B7C8D] 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-[#3b82f6] transition-colors truncate">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#5B7C8D] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
@@ -354,7 +354,7 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
href={`/profile/${thread.author_id}`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
className="text-[#5B7C8D] hover:underline"
|
||||
>
|
||||
{thread.author_name}
|
||||
</Link>
|
||||
@@ -395,7 +395,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-[#3b82f6] 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-[#1a1a1a] border border-[#2a2a2a] focus:border-[#5B7C8D] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
@@ -415,7 +415,7 @@ export default function ForumIndexPage() {
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={e => setSelectedCategory(e.target.value)}
|
||||
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#3b82f6] 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-[#1a1a1a] border border-[#2a2a2a] focus:border-[#5B7C8D] 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 => (
|
||||
@@ -435,7 +435,7 @@ export default function ForumIndexPage() {
|
||||
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-[#3b82f6] text-white'
|
||||
? 'bg-[#5B7C8D] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -468,16 +468,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-[#3b82f6] rounded-lg px-4 py-3 transition-all group"
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#5B7C8D] 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-[#3b82f6] transition-colors truncate">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#5B7C8D] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
by <span className="text-[#888]">{thread.author_name}</span>
|
||||
{thread.forum_categories && (
|
||||
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#5B7C8D]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -527,11 +527,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-[#3b82f6] rounded-lg p-4 transition-all group"
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#5B7C8D] 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-[#3b82f6] transition-colors">
|
||||
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#5B7C8D] transition-colors">
|
||||
{cat.name}
|
||||
</h2>
|
||||
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
|
||||
@@ -544,7 +544,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-[#3b82f6] 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-[#5B7C8D] 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>
|
||||
@@ -563,7 +563,7 @@ export default function ForumIndexPage() {
|
||||
<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-[#3b82f6]">BroadcastBeat AI</span> assistant is available in threads to help answer questions — it is clearly labeled</li>
|
||||
<li>• The <span className="text-[#5B7C8D]">BroadcastBeat 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>
|
||||
|
||||
@@ -47,13 +47,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-[#1e3a5f] border border-[#3b82f6] flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-[#3b82f6] text-xs font-bold">AI</span>
|
||||
<div className="w-9 h-9 rounded-full bg-[#2F4F5F] border border-[#5B7C8D] flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-[#5B7C8D] text-xs font-bold">AI</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const initials = name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
||||
const colors = ['#1e3a5f', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
|
||||
const colors = ['#2F4F5F', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
|
||||
const colorIdx = name.charCodeAt(0) % colors.length;
|
||||
return (
|
||||
<div
|
||||
@@ -113,7 +113,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
}
|
||||
};
|
||||
|
||||
const scoreColor = counts.vote_score > 0 ? 'text-[#3b82f6]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]';
|
||||
const scoreColor = counts.vote_score > 0 ? 'text-[#5B7C8D]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
@@ -124,8 +124,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-[#3b82f6]/20 border-[#3b82f6] text-[#3b82f6]'
|
||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
? 'bg-[#5B7C8D]/20 border-[#5B7C8D] text-[#5B7C8D]'
|
||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#5B7C8D] hover:text-[#5B7C8D]'
|
||||
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
aria-label="Upvote"
|
||||
>
|
||||
@@ -158,7 +158,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
</button>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#3b82f6] ml-1 transition-colors">
|
||||
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#5B7C8D] ml-1 transition-colors">
|
||||
Sign in to vote
|
||||
</Link>
|
||||
)}
|
||||
@@ -295,7 +295,7 @@ export default function ForumThreadPage() {
|
||||
<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-[#3b82f6] hover:underline font-body text-sm">
|
||||
<Link href="/forum" className="mt-4 inline-block text-[#5B7C8D] hover:underline font-body text-sm">
|
||||
← Back to Forum
|
||||
</Link>
|
||||
</div>
|
||||
@@ -315,11 +315,11 @@ export default function ForumThreadPage() {
|
||||
<div className="bg-[#1a2535] 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-[#3b82f6] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#5B7C8D] transition-colors">Forum</Link>
|
||||
<span className="mx-2">›</span>
|
||||
{category && (
|
||||
<>
|
||||
<Link href={`/forum/${category.slug}`} className="hover:text-[#3b82f6] transition-colors">{category.name}</Link>
|
||||
<Link href={`/forum/${category.slug}`} className="hover:text-[#5B7C8D] transition-colors">{category.name}</Link>
|
||||
<span className="mx-2">›</span>
|
||||
</>
|
||||
)}
|
||||
@@ -366,7 +366,7 @@ export default function ForumThreadPage() {
|
||||
key={reply.id}
|
||||
className={`border rounded-lg p-5 ${
|
||||
reply.is_ai_response
|
||||
? 'bg-[#0d1f35] border-[#1e3a5f]'
|
||||
? 'bg-[#0d1f35] border-[#2F4F5F]'
|
||||
: 'bg-[#1a1a1a] border-[#252525]'
|
||||
}`}
|
||||
>
|
||||
@@ -376,7 +376,7 @@ export default function ForumThreadPage() {
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<span className="text-white font-body font-semibold text-sm">{reply.author_name}</span>
|
||||
{reply.is_ai_response && (
|
||||
<span className="text-xs bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/30 px-2 py-0.5 rounded font-body font-semibold">
|
||||
<span className="text-xs bg-[#5B7C8D]/20 text-[#5B7C8D] border border-[#5B7C8D]/30 px-2 py-0.5 rounded font-body font-semibold">
|
||||
AI Assistant
|
||||
</span>
|
||||
)}
|
||||
@@ -404,14 +404,14 @@ export default function ForumThreadPage() {
|
||||
)}
|
||||
|
||||
{/* AI Assistant Panel */}
|
||||
<div className="bg-[#0d1f35] border border-[#1e3a5f] rounded-lg p-5 mb-6">
|
||||
<div className="bg-[#0d1f35] border border-[#2F4F5F] rounded-lg p-5 mb-6">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1e3a5f] border border-[#3b82f6] flex items-center justify-center">
|
||||
<span className="text-[#3b82f6] text-xs font-bold">AI</span>
|
||||
<div className="w-7 h-7 rounded-full bg-[#2F4F5F] border border-[#5B7C8D] flex items-center justify-center">
|
||||
<span className="text-[#5B7C8D] text-xs font-bold">AI</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-white font-body font-semibold text-sm">BroadcastBeat AI Assistant</span>
|
||||
<span className="ml-2 text-xs bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/30 px-2 py-0.5 rounded font-body">AI — Not a human</span>
|
||||
<span className="ml-2 text-xs bg-[#5B7C8D]/20 text-[#5B7C8D] border border-[#5B7C8D]/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">
|
||||
@@ -424,12 +424,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-[#3b82f6] 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-[#5B7C8D] disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={aiLoading || !aiQuestion.trim()}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors flex-shrink-0"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] 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">
|
||||
@@ -457,7 +457,7 @@ export default function ForumThreadPage() {
|
||||
placeholder="Your name (optional)"
|
||||
value={replyAuthor}
|
||||
onChange={e => setReplyAuthor(e.target.value)}
|
||||
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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Share your thoughts, experience, or answer..."
|
||||
@@ -465,12 +465,12 @@ 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-[#3b82f6] 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-[#5B7C8D] resize-none"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !replyBody.trim()}
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
className="bg-[#5B7C8D] hover:bg-[#4A6473] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
{submitting ? 'Posting...' : 'Post Reply'}
|
||||
</button>
|
||||
|
||||
@@ -45,7 +45,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-[#3b82f6] transition-colors">
|
||||
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#5B7C8D] transition-colors">
|
||||
<div className="relative h-[200px] overflow-hidden">
|
||||
<AppImage
|
||||
src={article.image}
|
||||
@@ -55,7 +55,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-[#3b82f6] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase">
|
||||
<span className="bg-[#5B7C8D] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase">
|
||||
{article.category}
|
||||
</span>
|
||||
</div>
|
||||
@@ -66,7 +66,7 @@ export default async function GearPage() {
|
||||
<span className="text-[#444] text-[10px]">·</span>
|
||||
<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-[#3b82f6] transition-colors line-clamp-3">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#5B7C8D] transition-colors line-clamp-3">
|
||||
{article.title}
|
||||
</h2>
|
||||
<p className="text-[#666] font-body text-xs leading-relaxed line-clamp-2">{article.excerpt}</p>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function GlobalError({
|
||||
)}
|
||||
<button
|
||||
onClick={() => reset()}
|
||||
style={{ padding: "8px 16px", background: "#3b82f6", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }}
|
||||
style={{ padding: "8px 16px", background: "#5B7C8D", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
|
||||
@@ -457,7 +457,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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark] cursor-pointer hover:border-[#3a3a3a]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D]/30 transition-colors [color-scheme:dark] cursor-pointer hover:border-[#3a3a3a]"
|
||||
>
|
||||
<option value="recent">Recent</option>
|
||||
<option value="trending">Trending</option>
|
||||
@@ -476,8 +476,8 @@ export default function ArticleFeed() {
|
||||
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-[#3b82f6] ${
|
||||
feedMode === "pagination" ?"bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/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-[#5B7C8D] ${
|
||||
feedMode === "pagination" ?"bg-[#5B7C8D]/20 text-[#5B7C8D] border border-[#5B7C8D]/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"/>
|
||||
@@ -490,8 +490,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-[#3b82f6] ${
|
||||
feedMode === "infinite" ?"bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/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-[#5B7C8D] ${
|
||||
feedMode === "infinite" ?"bg-[#5B7C8D]/20 text-[#5B7C8D] border border-[#5B7C8D]/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"/>
|
||||
@@ -515,9 +515,9 @@ 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-[#3b82f6] 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-[#5B7C8D] whitespace-nowrap ${
|
||||
showAdvanced || hasAdvancedActive
|
||||
? "bg-[#3b82f6]/15 text-[#3b82f6] border-[#3b82f6]/40"
|
||||
? "bg-[#5B7C8D]/15 text-[#5B7C8D] border-[#5B7C8D]/40"
|
||||
: "bg-[#111] text-[#666] border-[#2a2a2a] hover:text-[#aaa] hover:border-[#3a3a3a]"
|
||||
}`}>
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
@@ -525,7 +525,7 @@ export default function ArticleFeed() {
|
||||
</svg>
|
||||
Filters
|
||||
{hasAdvancedActive && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6] inline-block ml-0.5" aria-label="Active filters" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#5B7C8D] inline-block ml-0.5" aria-label="Active filters" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -548,7 +548,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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D]/30 transition-colors [color-scheme:dark]"
|
||||
aria-label="Filter by author">
|
||||
<option value="All">All Authors</option>
|
||||
{uniqueAuthors.map((author) => (
|
||||
@@ -571,7 +571,7 @@ export default function ArticleFeed() {
|
||||
id={`${feedId}-category-select`}
|
||||
value={activeCategory}
|
||||
onChange={(e) => { setActiveCategory(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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark]"
|
||||
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-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D]/30 transition-colors [color-scheme:dark]"
|
||||
aria-label="Filter by category">
|
||||
{ALL_CATEGORIES.map((cat) => (
|
||||
<option key={cat} value={cat}>{cat === "All" ? "All Categories" : cat}</option>
|
||||
@@ -594,7 +594,7 @@ export default function ArticleFeed() {
|
||||
value={dateFrom}
|
||||
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||
max={dateTo || undefined}
|
||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark]"
|
||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 focus:outline-none focus:border-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D]/30 transition-colors [color-scheme:dark]"
|
||||
aria-label="Filter articles from date"
|
||||
/>
|
||||
</div>
|
||||
@@ -610,7 +610,7 @@ export default function ArticleFeed() {
|
||||
value={dateTo}
|
||||
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||
min={dateFrom || undefined}
|
||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 focus:outline-none focus:border-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark]"
|
||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 focus:outline-none focus:border-[#5B7C8D] focus:ring-1 focus:ring-[#5B7C8D]/30 transition-colors [color-scheme:dark]"
|
||||
aria-label="Filter articles to date"
|
||||
/>
|
||||
</div>
|
||||
@@ -627,10 +627,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-[#3b82f6] ${
|
||||
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-[#5B7C8D] ${
|
||||
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-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
: sf === "Live" ?"bg-emerald-500/20 text-emerald-400 border border-emerald-500/40" :"bg-[#5B7C8D]/20 text-[#5B7C8D] border border-[#5B7C8D]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
}`}>
|
||||
{sf}
|
||||
{sf === "Imported" && importedCount > 0 && (
|
||||
@@ -651,7 +651,7 @@ export default function ArticleFeed() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }}
|
||||
className="font-body text-[11px] text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="font-body text-[11px] text-[#5B7C8D] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear advanced filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -674,7 +674,7 @@ export default function ArticleFeed() {
|
||||
onKeyDown={(e) => handleFilterKeyDown(e, i)}
|
||||
aria-pressed={isActive}
|
||||
tabIndex={isActive ? 0 : -1}
|
||||
className={`filter-chip ${isActive ? "filter-chip-active" : ""} focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]`}>
|
||||
className={`filter-chip ${isActive ? "filter-chip-active" : ""} focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]`}>
|
||||
{cat}
|
||||
</button>
|
||||
);
|
||||
@@ -699,7 +699,7 @@ export default function ArticleFeed() {
|
||||
{sortedFiltered.length === 0 && (
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="ml-1 text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="ml-1 text-[#5B7C8D] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
@@ -719,7 +719,7 @@ export default function ArticleFeed() {
|
||||
</p>
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="mt-4 font-body text-sm text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="mt-4 font-body text-sm text-[#5B7C8D] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -737,7 +737,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-[#3b82f6]"
|
||||
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-[#5B7C8D]"
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
>
|
||||
@@ -752,17 +752,17 @@ export default function ArticleFeed() {
|
||||
</Link>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5 md:gap-2 mb-1 md:mb-1.5 flex-wrap">
|
||||
<span className="font-body text-[10px] font-bold text-[#3b82f6] uppercase tracking-wide">
|
||||
<span className="font-body text-[10px] font-bold text-[#5B7C8D] uppercase tracking-wide">
|
||||
{article?.category}
|
||||
</span>
|
||||
<span className="text-[#444] text-xs hidden sm:inline">|</span>
|
||||
<SourceBadge source={article.source} />
|
||||
</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-[#3b82f6] 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-[#5B7C8D] transition-colors duration-200 line-clamp-2">
|
||||
<Link
|
||||
href={articleHref}
|
||||
{...linkProps}
|
||||
className="focus:outline-none focus-visible:text-[#3b82f6] focus-visible:underline">
|
||||
className="focus:outline-none focus-visible:text-[#5B7C8D] focus-visible:underline">
|
||||
{article?.title}
|
||||
</Link>
|
||||
</h3>
|
||||
@@ -797,9 +797,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-[#3b82f6] animate-bounce" style={{ animationDelay: "0ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6] animate-bounce" style={{ animationDelay: "150ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6] animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#5B7C8D] animate-bounce" style={{ animationDelay: "0ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#5B7C8D] animate-bounce" style={{ animationDelay: "150ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#5B7C8D] animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||
</div>
|
||||
<span className="font-body text-xs text-[#555]">Loading more articles…</span>
|
||||
</div>
|
||||
@@ -824,7 +824,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-[#3b82f6]"
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]"
|
||||
disabled={page === 1}
|
||||
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Previous page">
|
||||
@@ -848,7 +848,7 @@ export default function ArticleFeed() {
|
||||
) : (
|
||||
<button
|
||||
key={p}
|
||||
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${page === p ? "active" : ""}`}
|
||||
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${page === p ? "active" : ""}`}
|
||||
onClick={() => { setPage(p as number); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label={`Page ${p}`}
|
||||
aria-current={page === p ? "page" : undefined}>
|
||||
@@ -858,7 +858,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-[#3b82f6]"
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]"
|
||||
disabled={page === totalPages}
|
||||
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Next page">
|
||||
|
||||
@@ -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-[#3b82f6] hover:border-[#3b82f6] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#5B7C8D] hover:border-[#5B7C8D] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<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-[#3b82f6] hover:border-[#3b82f6] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="w-8 h-8 flex items-center justify-center bg-[#1a1a1a] border border-[#3a3a3a] hover:bg-[#5B7C8D] hover:border-[#5B7C8D] hover:text-white text-[#ccc] transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] transition-colors">
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] overflow-hidden group cursor-pointer card-accent-hover flex flex-col hover:border-[#5B7C8D] 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-[#3b82f6] text-white font-body text-[9px] font-bold px-1.5 py-0.5 tracking-wider uppercase">
|
||||
<span className="bg-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6]"
|
||||
? "w-4 h-1.5 bg-[#5B7C8D]"
|
||||
: "w-1.5 h-1.5 bg-[#333] hover:bg-[#555]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
@@ -60,15 +60,15 @@ export default function NewsletterSignup() {
|
||||
|
||||
return (
|
||||
<section
|
||||
className="bg-[#0d1520] border-t border-b border-[#1e3a5f] py-8 md:py-10"
|
||||
className="bg-[#0d1520] border-t border-b border-[#2F4F5F] py-8 md:py-10"
|
||||
aria-labelledby={`${formId}-heading`}>
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-center gap-3 mb-3">
|
||||
<div className="flex-1 h-px bg-[#1e3a5f] max-w-[80px]" />
|
||||
<div className="flex-1 h-px bg-[#2F4F5F] max-w-[80px]" />
|
||||
<span className="section-label" id={`${formId}-heading`}>Newsletter</span>
|
||||
<div className="flex-1 h-px bg-[#1e3a5f] max-w-[80px]" />
|
||||
<div className="flex-1 h-px bg-[#2F4F5F] max-w-[80px]" />
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] text-xl md:text-2xl font-bold mb-1.5">
|
||||
Stay Ahead of the Broadcast Industry
|
||||
@@ -79,12 +79,12 @@ export default function NewsletterSignup() {
|
||||
|
||||
{submitted ? (
|
||||
<div
|
||||
className="bg-[#1a2535] border border-[#3b82f6] rounded-sm px-6 py-8 text-center"
|
||||
className="bg-[#1a2535] border border-[#5B7C8D] rounded-sm px-6 py-8 text-center"
|
||||
role="status"
|
||||
aria-live="polite">
|
||||
<div className="w-10 h-10 rounded-full bg-[#3b82f6]/20 flex items-center justify-center mx-auto mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#5B7C8D]/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="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 10l4 4 8-8" stroke="#5B7C8D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] font-bold text-lg mb-1">You're subscribed!</p>
|
||||
@@ -113,7 +113,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-[#3b82f6] border-[#3b82f6]" : "border-[#444]"}`}>
|
||||
<span aria-hidden="true" className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#5B7C8D] border-[#5B7C8D]" : "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" />
|
||||
@@ -149,7 +149,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-[#3b82f6] 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-[#5B7C8D] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1520] ${loading ? "opacity-70 cursor-not-allowed" : ""}`}>
|
||||
{loading ? "Subscribing..." : "Subscribe Free"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -166,7 +166,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-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
<a href="/privacy" className="text-[#5B7C8D] hover:underline focus:outline-none focus-visible:underline">
|
||||
Privacy Policy
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
@@ -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-[#3b82f6] hover:text-[#3b82f6] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#5B7C8D] hover:text-[#5B7C8D] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
<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-[#3b82f6] hover:text-[#3b82f6] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="carousel-btn w-7 h-7 border border-[#333] bg-[#1a1a1a] hover:border-[#5B7C8D] hover:text-[#5B7C8D] hover:bg-[#1e1e1e] flex items-center justify-center transition-colors text-[#888] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
<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-[#3b82f6] ${
|
||||
className={`spotlight-card group block spotlight-slide-in focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${
|
||||
i >= 3 ? "hidden lg:block" : i >= 2 ? "hidden md:block" : ""
|
||||
}`}
|
||||
style={{ animationDelay: `${i * 50}ms` }}
|
||||
@@ -201,10 +201,10 @@ export default function SpotlightCarousel() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-2.5 md:p-3 border-t border-[#2a2a2a]">
|
||||
<p className="font-body text-[11px] font-bold text-[#3b82f6] uppercase tracking-wide mb-1">
|
||||
<p className="font-body text-[11px] font-bold text-[#5B7C8D] 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-[#3b82f6] 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-[#5B7C8D] 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-[#3b82f6] ${
|
||||
className={`h-1.5 rounded-full transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${
|
||||
Math.floor(startIndex / VISIBLE) === i
|
||||
? "w-5 bg-accent" : "w-1.5 bg-[#333] hover:bg-[#555]"
|
||||
}`}
|
||||
|
||||
@@ -104,7 +104,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-[#3b82f6] 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-[#5B7C8D] focus:text-white focus:rounded focus:font-bold focus:text-sm focus:outline-none"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
placeholder="Search articles, topics, authors…"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] placeholder-[#555] text-sm font-body pl-9 pr-4 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors"
|
||||
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] text-[#e0e0e0] placeholder-[#555] text-sm font-body pl-9 pr-4 py-2.5 rounded-sm focus:outline-none focus:border-[#5B7C8D] transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
@@ -167,7 +167,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
type="date"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#5B7C8D] transition-colors [color-scheme:dark]"
|
||||
title="From date"
|
||||
/>
|
||||
<span className="text-[#444] text-xs">–</span>
|
||||
@@ -175,7 +175,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
type="date"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2.5 rounded-sm focus:outline-none focus:border-[#5B7C8D] transition-colors [color-scheme:dark]"
|
||||
title="To date"
|
||||
/>
|
||||
</div>
|
||||
@@ -185,7 +185,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body pl-3 pr-8 py-2.5 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors cursor-pointer"
|
||||
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body pl-3 pr-8 py-2.5 rounded-sm focus:outline-none focus:border-[#5B7C8D] transition-colors cursor-pointer"
|
||||
>
|
||||
{SORT_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
@@ -207,8 +207,8 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
||||
activeCategory === cat
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||
? "bg-[#5B7C8D] border-[#5B7C8D] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D]"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
@@ -246,7 +246,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<p className="text-[#555] font-body text-sm">No articles match your filters.</p>
|
||||
<button onClick={clearFilters} className="mt-3 text-[#3b82f6] text-xs font-body hover:underline">Clear filters</button>
|
||||
<button onClick={clearFilters} className="mt-3 text-[#5B7C8D] text-xs font-body hover:underline">Clear filters</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0">
|
||||
@@ -268,9 +268,9 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{article.category || "NEWS"}</span>
|
||||
<span className="text-[#5B7C8D] font-body text-[10px] font-bold uppercase tracking-wider">{article.category || "NEWS"}</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#5B7C8D] transition-colors line-clamp-2">
|
||||
{article.title || "Untitled"}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">{article.excerpt || ""}</p>
|
||||
@@ -293,7 +293,7 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
<AISuggestedArticles variant="full" />
|
||||
|
||||
<div className="bg-[#111] border border-[#222] p-4">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
|
||||
Popular Tags
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -303,8 +303,8 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
onClick={() => setActiveCategory(tag)}
|
||||
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
|
||||
activeCategory === tag
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||
? "bg-[#5B7C8D] border-[#5B7C8D] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D]"
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
@@ -312,8 +312,8 @@ export default function NewsPage({ articles }: { articles: Article[] }) {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-2">Newsletter</h3>
|
||||
<div className="bg-[#0d1520] border border-[#2F4F5F] p-4">
|
||||
<h3 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest mb-2">Newsletter</h3>
|
||||
<p className="text-[#777] text-xs font-body mb-3">Get broadcast news delivered to your inbox every week.</p>
|
||||
<Link href="/home-page#newsletter" className="btn-subscribe text-xs py-2 px-4 inline-block">Subscribe Free</Link>
|
||||
</div>
|
||||
|
||||
@@ -271,11 +271,11 @@ export default function NewsArticleDetailClient({
|
||||
{/* Breadcrumb */}
|
||||
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]">
|
||||
<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-[#3b82f6] transition-colors">
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link href="/news" className="hover:text-[#3b82f6] transition-colors">
|
||||
<Link href="/news" className="hover:text-[#5B7C8D] transition-colors">
|
||||
News
|
||||
</Link>
|
||||
<span>/</span>
|
||||
@@ -293,7 +293,7 @@ export default function NewsArticleDetailClient({
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Link
|
||||
href="/news"
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline">
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#5B7C8D] hover:underline">
|
||||
{article.category}
|
||||
</Link>
|
||||
<span className="text-xs text-[#777]">{article.readTime}</span>
|
||||
@@ -319,7 +319,7 @@ export default function NewsArticleDetailClient({
|
||||
<div>
|
||||
<Link
|
||||
href={`/authors/${article.authorSlug}`}
|
||||
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
|
||||
className="font-heading font-bold text-sm hover:text-[#5B7C8D] transition-colors text-[#e0e0e0]">
|
||||
{article.author}
|
||||
</Link>
|
||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||
@@ -332,7 +332,7 @@ export default function NewsArticleDetailClient({
|
||||
<button
|
||||
onClick={() => setEmailShareOpen(true)}
|
||||
aria-label="Share via email"
|
||||
className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-[#888] hover:text-[#3b82f6] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="flex items-center gap-1.5 text-xs font-bold uppercase tracking-widest text-[#888] hover:text-[#5B7C8D] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -352,7 +352,7 @@ export default function NewsArticleDetailClient({
|
||||
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-[#3b82f6] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#5B7C8D] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
{savingState === "saving" ?"Saving..."
|
||||
: savingState === "removing" ?"Removing..."
|
||||
: isSaved
|
||||
@@ -384,7 +384,7 @@ export default function NewsArticleDetailClient({
|
||||
[&_ul]:mb-4 [&_ul]:pl-5 [&_ul]:space-y-2
|
||||
[&_li]:text-[#aaa]
|
||||
[&_strong]:text-[#e0e0e0] [&_strong]:font-bold
|
||||
[&_a]:text-[#3b82f6] [&_a]:hover:underline"
|
||||
[&_a]:text-[#5B7C8D] [&_a]:hover:underline"
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
@@ -395,7 +395,7 @@ export default function NewsArticleDetailClient({
|
||||
{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-[#3b82f6] hover:text-[#3b82f6] transition-colors rounded-sm cursor-default">
|
||||
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D] transition-colors rounded-sm cursor-default">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -409,7 +409,7 @@ export default function NewsArticleDetailClient({
|
||||
<p className="font-body text-sm text-[#777]">Found this article useful?</p>
|
||||
<button
|
||||
onClick={() => setEmailShareOpen(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#3b82f6] text-[#888] hover:text-[#3b82f6] text-xs font-bold uppercase tracking-widest transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="flex items-center gap-2 px-4 py-2 bg-[#1a1a1a] border border-[#2a2a2a] hover:border-[#5B7C8D] text-[#888] hover:text-[#5B7C8D] text-xs font-bold uppercase tracking-widest transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
@@ -430,7 +430,7 @@ export default function NewsArticleDetailClient({
|
||||
<div className="pt-6 border-t border-[#222]">
|
||||
<Link
|
||||
href="/news"
|
||||
className="inline-flex items-center gap-2 text-[#3b82f6] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="inline-flex items-center gap-2 text-[#5B7C8D] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -453,7 +453,7 @@ export default function NewsArticleDetailClient({
|
||||
|
||||
{/* Related Articles Sidebar */}
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Articles
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -472,10 +472,10 @@ export default function NewsArticleDetailClient({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#5B7C8D] 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-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#5B7C8D] transition-colors line-clamp-2 leading-snug">
|
||||
{related.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -485,7 +485,7 @@ export default function NewsArticleDetailClient({
|
||||
</div>
|
||||
|
||||
{/* Newsletter Signup */}
|
||||
<div className="bg-[#0d1520] border border-[#1e3a5f] p-5">
|
||||
<div className="bg-[#0d1520] border border-[#2F4F5F] p-5">
|
||||
<h3 className="font-heading text-[#e0e0e0] font-bold mb-2">Stay Updated</h3>
|
||||
<p className="font-body text-[#777] text-xs mb-4">
|
||||
Get the latest broadcast engineering news delivered to your inbox.
|
||||
@@ -522,7 +522,7 @@ export default function NewsArticleDetailClient({
|
||||
onClick={prevCarousel}
|
||||
disabled={carouselIndex === 0}
|
||||
aria-label="Previous articles"
|
||||
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#5B7C8D] hover:text-[#5B7C8D] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -538,7 +538,7 @@ export default function NewsArticleDetailClient({
|
||||
onClick={nextCarousel}
|
||||
disabled={carouselIndex >= maxIndex}
|
||||
aria-label="Next articles"
|
||||
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="w-8 h-8 flex items-center justify-center border border-[#2a2a2a] text-[#666] hover:border-[#5B7C8D] hover:text-[#5B7C8D] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -577,11 +577,11 @@ export default function NewsArticleDetailClient({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
<span className="text-[#5B7C8D] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
{related.category}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug group-hover:text-[#3b82f6] transition-colors line-clamp-2 mb-1.5">
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug group-hover:text-[#5B7C8D] transition-colors line-clamp-2 mb-1.5">
|
||||
{related.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs line-clamp-2 leading-relaxed">
|
||||
@@ -606,7 +606,7 @@ export default function NewsArticleDetailClient({
|
||||
onClick={() => setCarouselIndex(i)}
|
||||
aria-label={`Go to slide ${i + 1}`}
|
||||
className={`w-1.5 h-1.5 rounded-full transition-colors focus:outline-none ${
|
||||
i === carouselIndex ? "bg-[#3b82f6]" : "bg-[#333]"
|
||||
i === carouselIndex ? "bg-[#5B7C8D]" : "bg-[#333]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
@@ -639,7 +639,7 @@ export default function NewsArticleDetailClient({
|
||||
setEmailStatus("idle");
|
||||
}}
|
||||
aria-label="Close share dialog"
|
||||
className="absolute top-4 right-4 text-[#555] hover:text-[#e0e0e0] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
className="absolute top-4 right-4 text-[#555] hover:text-[#e0e0e0] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -654,13 +654,13 @@ export default function NewsArticleDetailClient({
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className="w-9 h-9 rounded-sm bg-[#1a2a3a] border border-[#1e3a5f] flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-9 h-9 rounded-sm bg-[#1a2a3a] border border-[#2F4F5F] flex items-center justify-center flex-shrink-0">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3b82f6"
|
||||
stroke="#5B7C8D"
|
||||
strokeWidth="2"
|
||||
aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
@@ -706,7 +706,7 @@ export default function NewsArticleDetailClient({
|
||||
<label
|
||||
htmlFor="email-to"
|
||||
className="block font-body text-xs text-[#888] uppercase tracking-wider mb-1.5">
|
||||
Recipient Email <span className="text-[#3b82f6]">*</span>
|
||||
Recipient Email <span className="text-[#5B7C8D]">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email-to"
|
||||
@@ -714,7 +714,7 @@ export default function NewsArticleDetailClient({
|
||||
value={emailTo}
|
||||
onChange={(e) => setEmailTo(e.target.value)}
|
||||
placeholder="colleague@example.com"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#5B7C8D] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -729,7 +729,7 @@ export default function NewsArticleDetailClient({
|
||||
onChange={(e) => setEmailNote(e.target.value)}
|
||||
placeholder="Thought you might find this interesting..."
|
||||
rows={3}
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors resize-none"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] focus:border-[#5B7C8D] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -773,7 +773,7 @@ export default function NewsArticleDetailClient({
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#5B7C8D] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
{saveToast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,7 +62,7 @@ function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
||||
const blocks = Array.isArray(newsletter.article_blocks)
|
||||
? newsletter.article_blocks.filter((b) => b.title)
|
||||
: [];
|
||||
const accent = newsletter.accent_color || "#3b82f6";
|
||||
const accent = newsletter.accent_color || "#5B7C8D";
|
||||
|
||||
const layoutLabel: Record<string, string> = {
|
||||
featured: "Weekly Digest",
|
||||
@@ -112,7 +112,7 @@ function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
||||
<span className="mt-1.5 w-1 h-1 rounded-full bg-[#444] flex-shrink-0" />
|
||||
<span className="font-body text-[#888] text-xs leading-relaxed line-clamp-1">
|
||||
{block.category && (
|
||||
<span className="text-[#3b82f6] font-bold mr-1">{block.category}:</span>
|
||||
<span className="text-[#5B7C8D] font-bold mr-1">{block.category}:</span>
|
||||
)}
|
||||
{block.title}
|
||||
</span>
|
||||
@@ -131,7 +131,7 @@ function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
||||
<span className="font-body text-[#555] text-xs">
|
||||
{blocks.length} {blocks.length === 1 ? "story" : "stories"}
|
||||
</span>
|
||||
<span className="font-body text-[#3b82f6] text-xs font-medium">
|
||||
<span className="font-body text-[#5B7C8D] text-xs font-medium">
|
||||
Issue #{newsletter.id.slice(0, 6).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -180,12 +180,12 @@ function SignupCTA() {
|
||||
|
||||
return (
|
||||
<section
|
||||
className="bg-[#0d1520] border border-[#1e3a5f] rounded-sm p-6 md:p-8"
|
||||
className="bg-[#0d1520] border border-[#2F4F5F] rounded-sm p-6 md:p-8"
|
||||
aria-labelledby={`${formId}-heading`}>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="flex-1 h-px bg-[#1e3a5f]" />
|
||||
<div className="flex-1 h-px bg-[#2F4F5F]" />
|
||||
<span className="section-label" id={`${formId}-heading`}>Subscribe</span>
|
||||
<div className="flex-1 h-px bg-[#1e3a5f]" />
|
||||
<div className="flex-1 h-px bg-[#2F4F5F]" />
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold text-center mb-1">
|
||||
Never Miss an Issue
|
||||
@@ -196,9 +196,9 @@ function SignupCTA() {
|
||||
|
||||
{status === "success" ? (
|
||||
<div className="text-center py-4" role="status" aria-live="polite">
|
||||
<div className="w-10 h-10 rounded-full bg-[#3b82f6]/20 flex items-center justify-center mx-auto mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#5B7C8D]/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="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 10l4 4 8-8" stroke="#5B7C8D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] font-bold mb-1">You're subscribed!</p>
|
||||
@@ -227,7 +227,7 @@ function SignupCTA() {
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#3b82f6] border-[#3b82f6]" : "border-[#444]"}`}>
|
||||
className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#5B7C8D] border-[#5B7C8D]" : "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" />
|
||||
@@ -261,7 +261,7 @@ function SignupCTA() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "loading"}
|
||||
className={`btn-subscribe py-2.5 px-6 text-sm whitespace-nowrap focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${status === "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-[#5B7C8D] ${status === "loading" ? "opacity-70 cursor-not-allowed" : ""}`}>
|
||||
{status === "loading" ? "Subscribing..." : "Subscribe Free"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -274,7 +274,7 @@ function SignupCTA() {
|
||||
|
||||
<p className="font-body text-[#555] text-xs mt-3 text-center">
|
||||
No spam. Unsubscribe anytime.{" "}
|
||||
<Link href="/privacy" className="text-[#3b82f6] hover:underline">
|
||||
<Link href="/privacy" className="text-[#5B7C8D] hover:underline">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
@@ -418,9 +418,9 @@ export default function NewsletterArchivePage() {
|
||||
<button
|
||||
key={topic}
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
className={`font-body text-xs px-3 py-1.5 rounded-sm border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
className={`font-body text-xs px-3 py-1.5 rounded-sm border transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${
|
||||
selectedTopic === topic
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
? "bg-[#5B7C8D] border-[#5B7C8D] text-white"
|
||||
: "bg-transparent border-[#333] text-[#888] hover:border-[#555] hover:text-[#ccc]"
|
||||
}`}
|
||||
aria-pressed={selectedTopic === topic}>
|
||||
@@ -461,7 +461,7 @@ export default function NewsletterArchivePage() {
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="font-body text-xs text-[#3b82f6] hover:text-[#60a5fa] transition-colors underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="font-body text-xs text-[#5B7C8D] hover:text-[#8FB0C3] transition-colors underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
@@ -497,7 +497,7 @@ export default function NewsletterArchivePage() {
|
||||
<p className="font-body text-red-400 text-sm mb-3">{error}</p>
|
||||
<button
|
||||
onClick={fetchArchive}
|
||||
className="font-body text-xs text-[#3b82f6] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="font-body text-xs text-[#5B7C8D] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
@@ -520,7 +520,7 @@ export default function NewsletterArchivePage() {
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="font-body text-xs text-[#3b82f6] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="font-body text-xs text-[#5B7C8D] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -551,9 +551,9 @@ export default function NewsletterArchivePage() {
|
||||
<li key={topic}>
|
||||
<button
|
||||
onClick={() => setSelectedTopic(topic)}
|
||||
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${
|
||||
selectedTopic === topic
|
||||
? "text-[#3b82f6] bg-[#3b82f6]/10"
|
||||
? "text-[#5B7C8D] bg-[#5B7C8D]/10"
|
||||
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
||||
}`}>
|
||||
{topic}
|
||||
@@ -574,9 +574,9 @@ export default function NewsletterArchivePage() {
|
||||
<li key={year}>
|
||||
<button
|
||||
onClick={() => { setSelectedYear(String(year)); setSelectedMonth(""); }}
|
||||
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
className={`w-full text-left font-body text-sm py-1.5 px-2 rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] ${
|
||||
selectedYear === String(year)
|
||||
? "text-[#3b82f6] bg-[#3b82f6]/10"
|
||||
? "text-[#5B7C8D] bg-[#5B7C8D]/10"
|
||||
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
||||
}`}>
|
||||
{year}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function PrivacyPage() {
|
||||
<li><strong className="text-[#ccc]">Opt-out (CCPA):</strong> California residents may opt out of the sale of personal information</li>
|
||||
<li><strong className="text-[#ccc]">Unsubscribe:</strong> Opt out of marketing emails at any time via the unsubscribe link</li>
|
||||
</ul>
|
||||
<p className="mt-3">To exercise any of these rights, contact us at <a href="mailto:privacy@broadcastbeat.com" className="text-[#3b82f6] underline hover:text-blue-400 transition-colors">privacy@broadcastbeat.com</a>. We will respond within 30 days.</p>
|
||||
<p className="mt-3">To exercise any of these rights, contact us at <a href="mailto:privacy@broadcastbeat.com" className="text-[#5B7C8D] underline hover:text-blue-400 transition-colors">privacy@broadcastbeat.com</a>. We will respond within 30 days.</p>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="section-6">
|
||||
@@ -127,7 +127,7 @@ export default function PrivacyPage() {
|
||||
<address className="not-italic mt-3 space-y-1 text-[#999]">
|
||||
<p><strong className="text-[#ccc]">Relevant Media Properties, LLC</strong></p>
|
||||
<p>Privacy Officer</p>
|
||||
<p>Email: <a href="mailto:privacy@broadcastbeat.com" className="text-[#3b82f6] underline hover:text-blue-400 transition-colors">privacy@broadcastbeat.com</a></p>
|
||||
<p>Email: <a href="mailto:privacy@broadcastbeat.com" className="text-[#5B7C8D] underline hover:text-blue-400 transition-colors">privacy@broadcastbeat.com</a></p>
|
||||
</address>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ function Avatar({ name, size = 'lg' }: { name: string; size?: 'sm' | 'lg' }) {
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
.slice(0, 2);
|
||||
const colors = ['#1e3a5f', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
|
||||
const colors = ['#2F4F5F', '#1a3a2a', '#3a1a1a', '#2a1a3a', '#1a2a3a'];
|
||||
const colorIdx = name.charCodeAt(0) % colors.length;
|
||||
const sizeClass = size === 'lg' ? 'w-20 h-20 text-2xl' : 'w-9 h-9 text-xs';
|
||||
return (
|
||||
@@ -113,7 +113,7 @@ function ReputationBadge({ rep }: { rep: number }) {
|
||||
let color = 'text-[#888] border-[#333]';
|
||||
if (rep >= 1000) { label = 'Expert'; color = 'text-yellow-400 border-yellow-700'; }
|
||||
else if (rep >= 500) { label = 'Veteran'; color = 'text-purple-400 border-purple-700'; }
|
||||
else if (rep >= 200) { label = 'Contributor'; color = 'text-[#3b82f6] border-[#3b82f6]/50'; }
|
||||
else if (rep >= 200) { label = 'Contributor'; color = 'text-[#5B7C8D] border-[#5B7C8D]/50'; }
|
||||
else if (rep >= 50) { label = 'Member'; color = 'text-green-400 border-green-700'; }
|
||||
return (
|
||||
<span className={`font-body text-xs font-bold uppercase tracking-wider px-2 py-0.5 border rounded-sm ${color}`}>
|
||||
@@ -130,9 +130,9 @@ const BADGE_CONFIG: Record<string, { label: string; icon: React.ReactNode; color
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" />
|
||||
</svg>
|
||||
),
|
||||
color: 'text-[#3b82f6]',
|
||||
bg: 'bg-[#1e3a5f]/40',
|
||||
border: 'border-[#3b82f6]/40',
|
||||
color: 'text-[#5B7C8D]',
|
||||
bg: 'bg-[#2F4F5F]/40',
|
||||
border: 'border-[#5B7C8D]/40',
|
||||
description: 'Active community contributor',
|
||||
},
|
||||
mentor: {
|
||||
@@ -362,7 +362,7 @@ export default function UserProfilePage() {
|
||||
<Header />
|
||||
<div className="max-w-container mx-auto px-4 py-16 text-center">
|
||||
<p className="font-body text-[#888] text-sm">{error || 'User not found.'}</p>
|
||||
<Link href="/forum" className="mt-4 inline-block font-body text-xs font-bold uppercase tracking-wider text-[#3b82f6] hover:underline">
|
||||
<Link href="/forum" className="mt-4 inline-block font-body text-xs font-bold uppercase tracking-wider text-[#5B7C8D] hover:underline">
|
||||
← Back to Forum
|
||||
</Link>
|
||||
</div>
|
||||
@@ -387,9 +387,9 @@ export default function UserProfilePage() {
|
||||
<div className="bg-[#0d0d0d] 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-[#3b82f6] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">Home</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#5B7C8D] transition-colors">Forum</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span className="text-[#888]">{profile.full_name || 'User Profile'}</span>
|
||||
</nav>
|
||||
@@ -415,8 +415,8 @@ export default function UserProfilePage() {
|
||||
disabled={followLoading}
|
||||
className={`font-body text-xs font-bold uppercase tracking-wider px-4 py-1.5 rounded-sm border transition-colors disabled:opacity-60 ${
|
||||
followData.isFollowing
|
||||
? 'bg-[#1e3a5f] text-[#3b82f6] border-[#3b82f6]/50 hover:bg-red-900/30 hover:text-red-400 hover:border-red-700/50'
|
||||
: 'bg-[#3b82f6] text-white border-[#3b82f6] hover:bg-[#2563eb]'
|
||||
? 'bg-[#2F4F5F] text-[#5B7C8D] border-[#5B7C8D]/50 hover:bg-red-900/30 hover:text-red-400 hover:border-red-700/50'
|
||||
: 'bg-[#5B7C8D] text-white border-[#5B7C8D] hover:bg-[#4A6473]'
|
||||
}`}
|
||||
>
|
||||
{followLoading ? '...' : followData.isFollowing ? 'Following' : '+ Follow'}
|
||||
@@ -425,7 +425,7 @@ export default function UserProfilePage() {
|
||||
{currentUserId === userId && (
|
||||
<Link
|
||||
href="/account"
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#3b82f6] border border-[#2a2a2a] hover:border-[#3b82f6] px-3 py-1 rounded-sm transition-colors"
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#5B7C8D] border border-[#2a2a2a] hover:border-[#5B7C8D] px-3 py-1 rounded-sm transition-colors"
|
||||
>
|
||||
Edit Profile
|
||||
</Link>
|
||||
@@ -449,7 +449,7 @@ export default function UserProfilePage() {
|
||||
href={profile.website.startsWith('http') ? profile.website : `https://${profile.website}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 hover:text-[#3b82f6] transition-colors"
|
||||
className="flex items-center gap-1 hover:text-[#5B7C8D] transition-colors"
|
||||
>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
@@ -477,7 +477,7 @@ export default function UserProfilePage() {
|
||||
{/* Reputation & Stats Row */}
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 lg:grid-cols-6 gap-3 mt-6 pt-5 border-t border-[#2a2a2a]">
|
||||
<div className="text-center">
|
||||
<div className="font-heading text-2xl font-bold text-[#3b82f6]">{(profile.reputation || 0).toLocaleString()}</div>
|
||||
<div className="font-heading text-2xl font-bold text-[#5B7C8D]">{(profile.reputation || 0).toLocaleString()}</div>
|
||||
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Reputation</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
@@ -493,11 +493,11 @@ export default function UserProfilePage() {
|
||||
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Upvotes</div>
|
||||
</div>
|
||||
<div className="text-center cursor-pointer" onClick={() => setActiveTab('followers')}>
|
||||
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#3b82f6] transition-colors">{followData.followerCount}</div>
|
||||
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#5B7C8D] transition-colors">{followData.followerCount}</div>
|
||||
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Followers</div>
|
||||
</div>
|
||||
<div className="text-center cursor-pointer" onClick={() => setActiveTab('following')}>
|
||||
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#3b82f6] transition-colors">{followData.followingCount}</div>
|
||||
<div className="font-heading text-2xl font-bold text-[#e0e0e0] hover:text-[#5B7C8D] transition-colors">{followData.followingCount}</div>
|
||||
<div className="font-body text-xs text-[#666] uppercase tracking-wider mt-0.5">Following</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -511,7 +511,7 @@ export default function UserProfilePage() {
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`font-body text-sm font-bold uppercase tracking-wider px-5 py-3 border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${
|
||||
activeTab === tab.id
|
||||
? 'border-[#3b82f6] text-[#3b82f6]'
|
||||
? 'border-[#5B7C8D] text-[#5B7C8D]'
|
||||
: 'border-transparent text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -549,17 +549,17 @@ export default function UserProfilePage() {
|
||||
<div key={thread.id} className="group">
|
||||
<Link
|
||||
href={`/forum/thread/${thread.id}`}
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug"
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#5B7C8D] transition-colors line-clamp-2 leading-snug"
|
||||
>
|
||||
{thread.title}
|
||||
</Link>
|
||||
<div className="flex items-center gap-3 mt-1 text-xs font-body text-[#555]">
|
||||
{thread.forum_categories && (
|
||||
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#3b82f6]/70 hover:text-[#3b82f6]">
|
||||
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#5B7C8D]/70 hover:text-[#5B7C8D]">
|
||||
{thread.forum_categories.name}
|
||||
</Link>
|
||||
)}
|
||||
<span className={`font-semibold ${(thread.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
|
||||
<span className={`font-semibold ${(thread.vote_score || 0) >= 0 ? 'text-[#5B7C8D]' : 'text-red-400'}`}>
|
||||
{(thread.vote_score || 0) > 0 ? '+' : ''}{thread.vote_score || 0} pts
|
||||
</span>
|
||||
<span>{thread.reply_count || 0} replies</span>
|
||||
@@ -585,7 +585,7 @@ export default function UserProfilePage() {
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<Link
|
||||
href={`/forum/${cat.slug}`}
|
||||
className="font-body text-xs font-semibold text-[#aaa] hover:text-[#3b82f6] transition-colors"
|
||||
className="font-body text-xs font-semibold text-[#aaa] hover:text-[#5B7C8D] transition-colors"
|
||||
>
|
||||
{cat.name}
|
||||
</Link>
|
||||
@@ -593,7 +593,7 @@ export default function UserProfilePage() {
|
||||
</div>
|
||||
<div className="h-1.5 bg-[#2a2a2a] rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-[#3b82f6] rounded-full transition-all duration-500"
|
||||
className="h-full bg-[#5B7C8D] rounded-full transition-all duration-500"
|
||||
style={{ width: `${(cat.count / maxCategoryCount) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -621,7 +621,7 @@ export default function UserProfilePage() {
|
||||
.map((entry, idx) => (
|
||||
<div key={idx} className="flex items-start gap-3 py-2 border-b border-[#1e1e1e] last:border-0">
|
||||
<span className={`font-body text-xs font-bold uppercase tracking-wider px-2 py-0.5 rounded-sm flex-shrink-0 mt-0.5 ${
|
||||
entry.type === 'thread' ? 'bg-[#1e3a5f] text-[#3b82f6]' : 'bg-[#1a2a1a] text-green-400'
|
||||
entry.type === 'thread' ? 'bg-[#2F4F5F] text-[#5B7C8D]' : 'bg-[#1a2a1a] text-green-400'
|
||||
}`}>
|
||||
{entry.type === 'thread' ? 'Thread' : 'Reply'}
|
||||
</span>
|
||||
@@ -629,7 +629,7 @@ export default function UserProfilePage() {
|
||||
{entry.type === 'thread' ? (
|
||||
<Link
|
||||
href={`/forum/thread/${(entry.item as ForumThread).id}`}
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-1"
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#5B7C8D] transition-colors line-clamp-1"
|
||||
>
|
||||
{(entry.item as ForumThread).title}
|
||||
</Link>
|
||||
@@ -637,7 +637,7 @@ export default function UserProfilePage() {
|
||||
<div>
|
||||
<Link
|
||||
href={`/forum/thread/${(entry.item as ForumReply).thread_id}`}
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-1"
|
||||
className="font-body text-sm text-[#ccc] hover:text-[#5B7C8D] transition-colors line-clamp-1"
|
||||
>
|
||||
Re: {(entry.item as ForumReply).forum_threads?.title || 'Thread'}
|
||||
</Link>
|
||||
@@ -671,14 +671,14 @@ export default function UserProfilePage() {
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link
|
||||
href={`/forum/thread/${thread.id}`}
|
||||
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#3b82f6] transition-colors line-clamp-2"
|
||||
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#5B7C8D] transition-colors line-clamp-2"
|
||||
>
|
||||
{thread.title}
|
||||
</Link>
|
||||
<p className="font-body text-xs text-[#555] line-clamp-1 mt-1">{thread.body}</p>
|
||||
<div className="flex flex-wrap items-center gap-3 mt-2 text-xs font-body text-[#555]">
|
||||
{thread.forum_categories && (
|
||||
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#3b82f6]/70 hover:text-[#3b82f6]">
|
||||
<Link href={`/forum/${thread.forum_categories.slug}`} className="text-[#5B7C8D]/70 hover:text-[#5B7C8D]">
|
||||
{thread.forum_categories.name}
|
||||
</Link>
|
||||
)}
|
||||
@@ -688,7 +688,7 @@ export default function UserProfilePage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0">
|
||||
<div className={`font-heading text-lg font-bold ${(thread.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
|
||||
<div className={`font-heading text-lg font-bold ${(thread.vote_score || 0) >= 0 ? 'text-[#5B7C8D]' : 'text-red-400'}`}>
|
||||
{(thread.vote_score || 0) > 0 ? '+' : ''}{thread.vote_score || 0}
|
||||
</div>
|
||||
<div className="font-body text-xs text-[#555]">score</div>
|
||||
@@ -714,13 +714,13 @@ export default function UserProfilePage() {
|
||||
<div key={reply.id} className="p-4 hover:bg-[#1e1e1e] transition-colors">
|
||||
<Link
|
||||
href={`/forum/thread/${reply.thread_id}`}
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#3b82f6]/70 hover:text-[#3b82f6] transition-colors mb-1 block"
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#5B7C8D]/70 hover:text-[#5B7C8D] transition-colors mb-1 block"
|
||||
>
|
||||
Re: {reply.forum_threads?.title || 'Thread'}
|
||||
</Link>
|
||||
<p className="font-body text-sm text-[#aaa] line-clamp-3 leading-relaxed">{reply.body}</p>
|
||||
<div className="flex items-center gap-3 mt-2 text-xs font-body text-[#555]">
|
||||
<span className={`font-semibold ${(reply.vote_score || 0) >= 0 ? 'text-[#3b82f6]' : 'text-red-400'}`}>
|
||||
<span className={`font-semibold ${(reply.vote_score || 0) >= 0 ? 'text-[#5B7C8D]' : 'text-red-400'}`}>
|
||||
{(reply.vote_score || 0) > 0 ? '+' : ''}{reply.vote_score || 0} pts
|
||||
</span>
|
||||
<span>{timeAgo(reply.created_at)}</span>
|
||||
@@ -782,7 +782,7 @@ function FollowUserList({
|
||||
<div className="flex-1 min-w-0">
|
||||
<Link
|
||||
href={`/profile/${u.id}`}
|
||||
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#3b82f6] transition-colors"
|
||||
className="font-body text-sm font-semibold text-[#ccc] hover:text-[#5B7C8D] transition-colors"
|
||||
>
|
||||
{u.full_name || 'Anonymous User'}
|
||||
</Link>
|
||||
@@ -796,7 +796,7 @@ function FollowUserList({
|
||||
{currentUserId && currentUserId !== u.id && (
|
||||
<Link
|
||||
href={`/profile/${u.id}`}
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#3b82f6] border border-[#2a2a2a] hover:border-[#3b82f6] px-3 py-1 rounded-sm transition-colors"
|
||||
className="font-body text-xs font-bold uppercase tracking-wider text-[#888] hover:text-[#5B7C8D] border border-[#2a2a2a] hover:border-[#5B7C8D] px-3 py-1 rounded-sm transition-colors"
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
|
||||
@@ -116,8 +116,8 @@ export default function ReadingListPage() {
|
||||
<main className="bg-[#111111] min-h-screen">
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-[#1a2535] border border-[#3b82f6] text-[#e0e0e0] font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2.5" aria-hidden="true">
|
||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 bg-[#1a2535] border border-[#5B7C8D] text-[#e0e0e0] font-body text-sm px-5 py-3 rounded-sm shadow-lg flex items-center gap-2">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5B7C8D" strokeWidth="2.5" aria-hidden="true">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{toast}
|
||||
@@ -128,7 +128,7 @@ export default function ReadingListPage() {
|
||||
<div className="bg-[#0d0d0d] 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-[#3b82f6] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">Home</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span className="text-[#888]">Reading List</span>
|
||||
</nav>
|
||||
@@ -139,7 +139,7 @@ export default function ReadingListPage() {
|
||||
{/* Title Row */}
|
||||
<div className="flex items-center justify-between mb-8 pb-4 border-b border-[#2a2a2a]">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" strokeWidth="2" aria-hidden="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5B7C8D" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
<h1 className="font-heading text-xl md:text-2xl font-bold text-[#e8e8e8]">Reading List</h1>
|
||||
@@ -152,7 +152,7 @@ export default function ReadingListPage() {
|
||||
{savedArticles.length > 0 && (
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className="font-body text-xs text-[#555] hover:text-red-400 transition-colors uppercase tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
className="font-body text-xs text-[#555] hover:text-red-400 transition-colors uppercase tracking-wide focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]">
|
||||
Clear All
|
||||
</button>
|
||||
)}
|
||||
@@ -242,12 +242,12 @@ export default function ReadingListPage() {
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
{item.articleCategory && (
|
||||
<span className="inline-block font-body text-[9px] font-bold uppercase tracking-widest text-[#3b82f6] mb-1">
|
||||
<span className="inline-block font-body text-[9px] font-bold uppercase tracking-widest text-[#5B7C8D] mb-1">
|
||||
{item.articleCategory}
|
||||
</span>
|
||||
)}
|
||||
<Link href={`/articles/${item.articleSlug}`}>
|
||||
<h2 className="font-heading text-sm md:text-base font-bold text-[#e0e0e0] group-hover:text-[#3b82f6] transition-colors leading-snug line-clamp-2 mb-1">
|
||||
<h2 className="font-heading text-sm md:text-base font-bold text-[#e0e0e0] group-hover:text-[#5B7C8D] transition-colors leading-snug line-clamp-2 mb-1">
|
||||
{item.articleTitle}
|
||||
</h2>
|
||||
</Link>
|
||||
@@ -274,7 +274,7 @@ export default function ReadingListPage() {
|
||||
onClick={() => handleRemove(item.id, item.articleSlug)}
|
||||
disabled={removingId === item.id}
|
||||
aria-label={`Remove "${item.articleTitle}" from reading list`}
|
||||
className="flex-shrink-0 w-7 h-7 flex items-center justify-center text-[#444] hover:text-red-400 hover:bg-[#2a1a1a] rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] disabled:opacity-50">
|
||||
className="flex-shrink-0 w-7 h-7 flex items-center justify-center text-[#444] hover:text-red-400 hover:bg-[#2a1a1a] rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] disabled:opacity-50">
|
||||
{removingId === item.id ? (
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="animate-spin" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="10" strokeOpacity="0.25" />
|
||||
@@ -300,7 +300,7 @@ export default function ReadingListPage() {
|
||||
</span>
|
||||
<Link
|
||||
href={`/articles/${item.articleSlug}`}
|
||||
className="font-body text-[10px] font-bold uppercase tracking-wide text-[#3b82f6] hover:text-[#2563eb] transition-colors">
|
||||
className="font-body text-[10px] font-bold uppercase tracking-wide text-[#5B7C8D] hover:text-[#4A6473] transition-colors">
|
||||
Read Article →
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function RegisterPage() {
|
||||
<div className="min-h-screen bg-[#111111]">
|
||||
<Header />
|
||||
<div className="flex items-center justify-center py-24">
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#5B7C8D] border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@ export default function RegisterPage() {
|
||||
<div className="bg-[#0d0d0d] 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-[#3b82f6] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#5B7C8D] transition-colors">Home</Link>
|
||||
<span aria-hidden="true">/</span>
|
||||
<span className="text-[#888]">Create Account</span>
|
||||
</nav>
|
||||
@@ -142,7 +142,7 @@ export default function RegisterPage() {
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
placeholder="Jane Smith"
|
||||
disabled={loading}
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#5B7C8D] focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@ export default function RegisterPage() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com"
|
||||
disabled={loading}
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#5B7C8D] focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -176,7 +176,7 @@ export default function RegisterPage() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Min. 6 characters"
|
||||
disabled={loading}
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#5B7C8D] focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -193,7 +193,7 @@ export default function RegisterPage() {
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
placeholder="Re-enter password"
|
||||
disabled={loading}
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors disabled:opacity-50"
|
||||
className="w-full h-10 px-3 bg-[#0d0d0d] border border-[#2a2a2a] text-[#e0e0e0] font-body text-sm rounded-sm placeholder-[#444] focus:outline-none focus:border-[#5B7C8D] focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors disabled:opacity-50"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +201,7 @@ export default function RegisterPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full h-10 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||
className="w-full h-10 bg-[#5B7C8D] hover:bg-[#4A6473] text-white font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||
@@ -223,7 +223,7 @@ export default function RegisterPage() {
|
||||
{/* Login Link */}
|
||||
<Link
|
||||
href="/login"
|
||||
className="block w-full h-10 border border-[#2a2a2a] hover:border-[#3b82f6] text-[#888] hover:text-[#3b82f6] font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors flex items-center justify-center">
|
||||
className="block w-full h-10 border border-[#2a2a2a] hover:border-[#5B7C8D] text-[#888] hover:text-[#5B7C8D] font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors flex items-center justify-center">
|
||||
Sign In
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
{query && results.length === 0 && (
|
||||
<div className="py-16 text-center">
|
||||
<p className="text-[#555] font-body text-sm mb-3">No articles match your search.</p>
|
||||
<Link href="/news" className="text-[#3b82f6] text-xs font-body hover:underline">
|
||||
<Link href="/news" className="text-[#5B7C8D] text-xs font-body hover:underline">
|
||||
Browse all news →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -73,11 +73,11 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
<span className="text-[#5B7C8D] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
{article.category || "NEWS"}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#5B7C8D] transition-colors line-clamp-2">
|
||||
{article.title || "Untitled"}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">
|
||||
|
||||
@@ -161,7 +161,7 @@ export default async function EventCoveragePage({
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(eventJsonLd) }}
|
||||
/>
|
||||
<Link href="/show-coverage" className="text-sm text-[var(--color-text-info,#60a5fa)] hover:underline">
|
||||
<Link href="/show-coverage" className="text-sm text-[var(--color-text-info,#8FB0C3)] hover:underline">
|
||||
← All events
|
||||
</Link>
|
||||
|
||||
@@ -173,7 +173,7 @@ export default async function EventCoveragePage({
|
||||
</span>
|
||||
)}
|
||||
{!isLive && !isPast && (
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||||
T-{daysUntil}d
|
||||
</span>
|
||||
)}
|
||||
@@ -194,7 +194,7 @@ export default async function EventCoveragePage({
|
||||
{[event.venue, event.city, event.country].filter(Boolean).join(" · ")}
|
||||
</div>
|
||||
{event.url && (
|
||||
<div className="text-xs font-mono text-[var(--color-text-info,#60a5fa)] mt-2">
|
||||
<div className="text-xs font-mono text-[var(--color-text-info,#8FB0C3)] mt-2">
|
||||
<a href={event.url} target="_blank" rel="noopener noreferrer">
|
||||
{new URL(event.url).hostname.replace(/^www\./, "")} →
|
||||
</a>
|
||||
|
||||
@@ -106,7 +106,7 @@ function EventCard({ e }: { e: Row }) {
|
||||
<li>
|
||||
<Link
|
||||
href={`/show-coverage/${e.slug}`}
|
||||
className="block p-5 rounded border border-[#252525] hover:border-[var(--color-text-info,#60a5fa)] bg-[#0b0f17] transition-colors"
|
||||
className="block p-5 rounded border border-[#252525] hover:border-[var(--color-text-info,#8FB0C3)] bg-[#0b0f17] transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-serif text-xl font-semibold">{e.name}</h3>
|
||||
@@ -120,7 +120,7 @@ function EventCard({ e }: { e: Row }) {
|
||||
{e.url && (
|
||||
<>
|
||||
{" · "}
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">{new URL(e.url).hostname.replace(/^www\./, "")}</span>
|
||||
<span className="text-[var(--color-text-info,#8FB0C3)]">{new URL(e.url).hostname.replace(/^www\./, "")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default async function TechnologyPage() {
|
||||
<div className="mb-10">
|
||||
<Link
|
||||
href={`/articles/${articles[0].slug}`}
|
||||
className="flex flex-col md:flex-row gap-6 bg-[#111] border border-[#222] overflow-hidden group hover:border-[#3b82f6] transition-colors p-5">
|
||||
className="flex flex-col md:flex-row gap-6 bg-[#111] border border-[#222] overflow-hidden group hover:border-[#5B7C8D] transition-colors p-5">
|
||||
<div className="flex-shrink-0 relative w-full md:w-[320px] h-[200px] md:h-[200px] overflow-hidden rounded-sm">
|
||||
<AppImage
|
||||
src={articles[0].image}
|
||||
@@ -58,13 +58,13 @@ export default async function TechnologyPage() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#3b82f6] font-body text-[10px] font-bold uppercase tracking-wider">{articles[0].category}</span>
|
||||
<span className="text-[#5B7C8D] font-body text-[10px] font-bold uppercase tracking-wider">{articles[0].category}</span>
|
||||
<span className="text-[#444] text-[10px]">·</span>
|
||||
<span className="text-[#555] font-body text-[11px]">{articles[0].date}</span>
|
||||
<span className="text-[#444] text-[10px]">·</span>
|
||||
<span className="text-[#555] font-body text-[11px]">{articles[0].readTime}</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-xl font-bold leading-tight mb-3 group-hover:text-[#3b82f6] transition-colors">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-xl font-bold leading-tight mb-3 group-hover:text-[#5B7C8D] transition-colors">
|
||||
{articles[0].title}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed mb-4">{articles[0].excerpt}</p>
|
||||
@@ -86,7 +86,7 @@ export default async function TechnologyPage() {
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={`/articles/${article.slug}`}
|
||||
className="flex gap-4 bg-[#111] border border-[#222] p-4 group hover:border-[#3b82f6] transition-colors">
|
||||
className="flex gap-4 bg-[#111] border border-[#222] p-4 group hover:border-[#5B7C8D] transition-colors">
|
||||
<div className="flex-shrink-0 w-[120px] h-[80px] relative overflow-hidden rounded-sm">
|
||||
<AppImage
|
||||
src={article.image}
|
||||
@@ -98,11 +98,11 @@ export default async function TechnologyPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider">{article.category}</span>
|
||||
<span className="text-[#5B7C8D] font-body text-[9px] font-bold uppercase tracking-wider">{article.category}</span>
|
||||
<span className="text-[#444] text-[9px]">·</span>
|
||||
<span className="text-[#555] font-body text-[10px]">{article.readTime}</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-1 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-1 group-hover:text-[#5B7C8D] transition-colors line-clamp-2">
|
||||
{article.title}
|
||||
</h2>
|
||||
<span className="text-[#555] font-body text-[11px]">{article.date}</span>
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function TermsPage() {
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-heading text-[#e0e0f0] text-xl font-bold mb-3">7. Contact</h2>
|
||||
<p>Questions about these Terms? Contact us at <a href="mailto:legal@broadcastbeat.com" className="text-[#3b82f6] hover:underline">legal@broadcastbeat.com</a>.</p>
|
||||
<p>Questions about these Terms? Contact us at <a href="mailto:legal@broadcastbeat.com" className="text-[#5B7C8D] hover:underline">legal@broadcastbeat.com</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -103,16 +103,16 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-[#3b82f6]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#5B7C8D]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.347.346A3.5 3.5 0 0114.5 20h-5a3.5 3.5 0 01-2.47-1.024l-.347-.346z" />
|
||||
</svg>
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest">
|
||||
<h3 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest">
|
||||
AI Picks For You
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowTopicPicker(!showTopicPicker)}
|
||||
className="text-[10px] font-body text-[#555] hover:text-[#3b82f6] transition-colors flex items-center gap-1"
|
||||
className="text-[10px] font-body text-[#555] hover:text-[#5B7C8D] transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
@@ -132,8 +132,8 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
onClick={() => toggleInterest(topic)}
|
||||
className={`px-2 py-0.5 text-[10px] font-body rounded-sm border transition-colors ${
|
||||
interests.includes(topic)
|
||||
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
? 'bg-[#5B7C8D] border-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D]'
|
||||
}`}
|
||||
>
|
||||
{topic}
|
||||
@@ -142,7 +142,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleApplyTopics}
|
||||
className="text-[10px] font-body bg-[#3b82f6] text-white px-3 py-1 rounded-sm hover:bg-[#2563eb] transition-colors"
|
||||
className="text-[10px] font-body bg-[#5B7C8D] text-white px-3 py-1 rounded-sm hover:bg-[#4A6473] transition-colors"
|
||||
>
|
||||
Update Suggestions
|
||||
</button>
|
||||
@@ -180,10 +180,10 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[#3b82f6] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#5B7C8D] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
{article.category}
|
||||
</p>
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-semibold leading-snug line-clamp-2 group-hover:text-[#3b82f6] transition-colors">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-semibold leading-snug line-clamp-2 group-hover:text-[#5B7C8D] transition-colors">
|
||||
{article.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -203,7 +203,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 text-[#3b82f6]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#5B7C8D]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.347.346A3.5 3.5 0 0114.5 20h-5a3.5 3.5 0 01-2.47-1.024l-.347-.346z" />
|
||||
</svg>
|
||||
<span className="section-label">AI Picks For You</span>
|
||||
@@ -212,7 +212,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowTopicPicker(!showTopicPicker)}
|
||||
className="text-[11px] font-body text-[#555] hover:text-[#3b82f6] transition-colors flex items-center gap-1 ml-4 shrink-0"
|
||||
className="text-[11px] font-body text-[#555] hover:text-[#5B7C8D] transition-colors flex items-center gap-1 ml-4 shrink-0"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
|
||||
@@ -232,8 +232,8 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
onClick={() => toggleInterest(topic)}
|
||||
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
||||
interests.includes(topic)
|
||||
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
? 'bg-[#5B7C8D] border-[#5B7C8D] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#5B7C8D] hover:text-[#5B7C8D]'
|
||||
}`}
|
||||
>
|
||||
{topic}
|
||||
@@ -242,7 +242,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleApplyTopics}
|
||||
className="text-xs font-body bg-[#3b82f6] text-white px-4 py-1.5 rounded-sm hover:bg-[#2563eb] transition-colors"
|
||||
className="text-xs font-body bg-[#5B7C8D] text-white px-4 py-1.5 rounded-sm hover:bg-[#4A6473] transition-colors"
|
||||
>
|
||||
Update Suggestions
|
||||
</button>
|
||||
@@ -280,11 +280,11 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
sizes="(max-width: 768px) 50vw, 25vw"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<span className="absolute bottom-2 left-2 text-[9px] font-body font-bold text-white uppercase tracking-wider bg-[#3b82f6] px-1.5 py-0.5 rounded-sm">
|
||||
<span className="absolute bottom-2 left-2 text-[9px] font-body font-bold text-white uppercase tracking-wider bg-[#5B7C8D] px-1.5 py-0.5 rounded-sm">
|
||||
{article.category}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-semibold leading-snug line-clamp-2 group-hover:text-[#3b82f6] transition-colors">
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-semibold leading-snug line-clamp-2 group-hover:text-[#5B7C8D] transition-colors">
|
||||
{article.title}
|
||||
</h3>
|
||||
<p className="text-[#555] font-body text-[11px] mt-1">{article.readTime}</p>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function AboutDropdown() {
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
className="nav-link-bb h-[60px] inline-flex items-center gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"
|
||||
className="nav-link-bb h-[60px] inline-flex items-center gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]"
|
||||
>
|
||||
<span>About</span>
|
||||
<span aria-hidden="true" className="text-[10px]">▾</span>
|
||||
@@ -54,7 +54,7 @@ export default function AboutDropdown() {
|
||||
href={it.href}
|
||||
role="menuitem"
|
||||
onClick={() => setOpen(false)}
|
||||
className="flex items-center justify-between px-4 py-2 text-sm hover:bg-[#1a1a1a] hover:text-[var(--color-text-info,#60a5fa)] focus:outline-none focus-visible:bg-[#1a1a1a] transition-colors"
|
||||
className="flex items-center justify-between px-4 py-2 text-sm hover:bg-[#1a1a1a] hover:text-[var(--color-text-info,#8FB0C3)] focus:outline-none focus-visible:bg-[#1a1a1a] transition-colors"
|
||||
>
|
||||
<span>{it.label}</span>
|
||||
<span aria-hidden="true" className="text-[10px] text-[#6b7280]">›</span>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function AskBBAI() {
|
||||
aria-label="Ask BB AI"
|
||||
>
|
||||
<span className="relative inline-block w-3 h-3">
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#60a5fa)]" />
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#8FB0C3)]" />
|
||||
<span className="bb-ring" />
|
||||
</span>
|
||||
<span className="font-serif">Ask BB AI</span>
|
||||
@@ -89,7 +89,7 @@ export default function AskBBAI() {
|
||||
<header className="flex items-center justify-between px-5 py-4 border-b border-[#252525]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative inline-block w-3 h-3">
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#60a5fa)]" />
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#8FB0C3)]" />
|
||||
<span className="bb-ring" />
|
||||
</span>
|
||||
<h2 className="font-serif text-lg font-semibold text-white">BB AI</h2>
|
||||
@@ -110,7 +110,7 @@ export default function AskBBAI() {
|
||||
className={
|
||||
"inline-block max-w-[85%] rounded px-3 py-2 text-sm whitespace-pre-wrap " +
|
||||
(m.role === "user"
|
||||
? "bg-[var(--color-text-info,#60a5fa)] text-black"
|
||||
? "bg-[var(--color-text-info,#8FB0C3)] text-black"
|
||||
: "bg-[#111] border border-[#252525] text-[#e5e7eb]")
|
||||
}
|
||||
dangerouslySetInnerHTML={{ __html: m.role === "assistant" ? linkifyMarkdown(m.content) : escapeHtml(m.content) }}
|
||||
@@ -134,12 +134,12 @@ export default function AskBBAI() {
|
||||
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||
placeholder="Ask about an event, a vendor, a workflow…"
|
||||
disabled={busy}
|
||||
className="flex-1 bg-[#111] border border-[#252525] rounded px-3 py-2 text-sm text-[#e5e7eb] focus:border-[var(--color-text-info,#60a5fa)] focus:outline-none"
|
||||
className="flex-1 bg-[#111] border border-[#252525] rounded px-3 py-2 text-sm text-[#e5e7eb] focus:border-[var(--color-text-info,#8FB0C3)] focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={send}
|
||||
disabled={busy || !input.trim()}
|
||||
className="bg-[var(--color-text-info,#60a5fa)] text-black font-semibold px-4 py-2 rounded text-sm disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="bg-[var(--color-text-info,#8FB0C3)] text-black font-semibold px-4 py-2 rounded text-sm disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
@@ -162,7 +162,7 @@ function linkifyMarkdown(s: string): string {
|
||||
let out = escapeHtml(s);
|
||||
out = out.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" class="text-[#60a5fa] hover:underline">$1</a>'
|
||||
'<a href="$2" class="text-[#8FB0C3] hover:underline">$1</a>'
|
||||
);
|
||||
out = out.replace(/\n/g, "<br/>");
|
||||
return out;
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function CookieConsent() {
|
||||
<strong className="text-white">Accept All</strong>, you consent to our use of cookies.{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="text-[#3b82f6] underline hover:text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors"
|
||||
className="text-[#5B7C8D] underline hover:text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
@@ -70,7 +70,7 @@ export default function CookieConsent() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDecline}
|
||||
className="px-4 py-2 text-sm font-body font-bold text-[#888] border border-[#333] rounded hover:border-[#555] hover:text-[#aaa] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0d1117]"
|
||||
className="px-4 py-2 text-sm font-body font-bold text-[#888] border border-[#333] rounded hover:border-[#555] hover:text-[#aaa] transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0d1117]"
|
||||
aria-label="Decline non-essential cookies"
|
||||
>
|
||||
Decline
|
||||
@@ -79,7 +79,7 @@ export default function CookieConsent() {
|
||||
ref={acceptRef}
|
||||
type="button"
|
||||
onClick={handleAccept}
|
||||
className="px-5 py-2 text-sm font-body font-bold text-white bg-[#3b82f6] rounded hover:bg-blue-500 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1117]"
|
||||
className="px-5 py-2 text-sm font-body font-bold text-white bg-[#5B7C8D] rounded hover:bg-blue-500 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1117]"
|
||||
aria-label="Accept all cookies"
|
||||
>
|
||||
Accept All
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function EventsDropdown() {
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
className="nav-link-bb h-[60px] inline-flex items-center gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"
|
||||
className="nav-link-bb h-[60px] inline-flex items-center gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]"
|
||||
>
|
||||
<span>Show coverage</span>
|
||||
<span aria-hidden="true" className="text-[10px]">▾</span>
|
||||
@@ -113,12 +113,12 @@ export default function EventsDropdown() {
|
||||
style={{ borderRadius: "var(--border-radius-md,6px)" }}
|
||||
>
|
||||
<div className="relative p-4">
|
||||
<span aria-hidden className="absolute top-2 left-2 w-3 h-3 border-t border-l border-[var(--color-text-info,#60a5fa)]"></span>
|
||||
<span aria-hidden className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[var(--color-text-info,#60a5fa)]"></span>
|
||||
<span aria-hidden className="absolute top-2 left-2 w-3 h-3 border-t border-l border-[var(--color-text-info,#8FB0C3)]"></span>
|
||||
<span aria-hidden className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[var(--color-text-info,#8FB0C3)]"></span>
|
||||
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<h3 className="font-serif text-base font-semibold">Show coverage</h3>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||||
<span className="bb-pulse-dot" />
|
||||
BB AI
|
||||
</span>
|
||||
@@ -158,7 +158,7 @@ export default function EventsDropdown() {
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">T-{e.days_until}d</span>
|
||||
<span className="text-[var(--color-text-info,#8FB0C3)]">T-{e.days_until}d</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@ export default function EventsDropdown() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[10px] font-mono text-[#6b7280] mt-1">
|
||||
<span>{fmtDateRange(e.start_date, e.end_date)}</span>
|
||||
{host && <span className="text-[var(--color-text-info,#60a5fa)]">· {host}</span>}
|
||||
{host && <span className="text-[var(--color-text-info,#8FB0C3)]">· {host}</span>}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -177,7 +177,7 @@ export default function EventsDropdown() {
|
||||
</ul>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-[#252525] flex items-center justify-between text-[10px] font-mono text-[#6b7280]">
|
||||
<Link href="/show-coverage" className="text-[var(--color-text-info,#60a5fa)] hover:underline" onClick={() => setOpen(false)}>
|
||||
<Link href="/show-coverage" className="text-[var(--color-text-info,#8FB0C3)] hover:underline" onClick={() => setOpen(false)}>
|
||||
See all 13 events →
|
||||
</Link>
|
||||
{loadedAt && <span>auto-updated · {ago(loadedAt)}</span>}
|
||||
|
||||
@@ -64,7 +64,7 @@ export default async function FeaturedBento() {
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280]">
|
||||
Featured · staff editorial
|
||||
</h2>
|
||||
<Link href="/news?category=Featured" className="text-[11px] font-mono text-[var(--color-text-info,#60a5fa)] hover:underline">
|
||||
<Link href="/news?category=Featured" className="text-[11px] font-mono text-[var(--color-text-info,#8FB0C3)] hover:underline">
|
||||
All featured →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -72,7 +72,7 @@ export default async function FeaturedBento() {
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||
<Link
|
||||
href={`/news/${hero.wp_slug}`}
|
||||
className="lg:col-span-2 group block rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#60a5fa)] transition-colors"
|
||||
className="lg:col-span-2 group block rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#8FB0C3)] transition-colors"
|
||||
>
|
||||
<div className="relative aspect-[16/9] bg-[#111]">
|
||||
<img
|
||||
@@ -81,7 +81,7 @@ export default async function FeaturedBento() {
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-2 left-2 flex gap-1 text-[10px] font-mono">
|
||||
<span className="bg-[var(--color-text-info,#60a5fa)] text-black px-2 py-0.5 rounded uppercase tracking-wider font-semibold">
|
||||
<span className="bg-[var(--color-text-info,#8FB0C3)] text-black px-2 py-0.5 rounded uppercase tracking-wider font-semibold">
|
||||
Featured
|
||||
</span>
|
||||
<span className="bg-black/60 text-white px-2 py-0.5 rounded uppercase tracking-wider">
|
||||
@@ -90,7 +90,7 @@ export default async function FeaturedBento() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<h3 className="font-serif text-2xl md:text-3xl leading-snug text-[#e5e7eb] group-hover:text-[var(--color-text-info,#60a5fa)]">
|
||||
<h3 className="font-serif text-2xl md:text-3xl leading-snug text-[#e5e7eb] group-hover:text-[var(--color-text-info,#8FB0C3)]">
|
||||
{hero.title}
|
||||
</h3>
|
||||
{hero.excerpt && (
|
||||
@@ -111,16 +111,16 @@ export default async function FeaturedBento() {
|
||||
<Link
|
||||
key={r.wp_slug}
|
||||
href={`/news/${r.wp_slug}`}
|
||||
className="group flex gap-3 rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#60a5fa)] transition-colors"
|
||||
className="group flex gap-3 rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#8FB0C3)] transition-colors"
|
||||
>
|
||||
<div className="shrink-0 w-[120px] aspect-square bg-[#111] relative">
|
||||
<img src={img(r)} alt={r.featured_image_alt || r.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 p-2 pr-3">
|
||||
<div className="text-[9px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)] mb-1">
|
||||
<div className="text-[9px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)] mb-1">
|
||||
{r.category || "News"}
|
||||
</div>
|
||||
<h4 className="font-serif text-sm leading-tight text-[#e5e7eb] group-hover:text-[var(--color-text-info,#60a5fa)] line-clamp-3">
|
||||
<h4 className="font-serif text-sm leading-tight text-[#e5e7eb] group-hover:text-[var(--color-text-info,#8FB0C3)] line-clamp-3">
|
||||
{r.title}
|
||||
</h4>
|
||||
<div className="text-[10px] font-mono text-[#6b7280] mt-2">
|
||||
|
||||
@@ -49,7 +49,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280]">
|
||||
Featured · staff editorial
|
||||
</h2>
|
||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||||
<span className="bb-pulse-dot" /> auto · 4s
|
||||
</span>
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 flex gap-1 text-[10px] font-mono">
|
||||
<span className="bg-black/60 text-[var(--color-text-info,#60a5fa)] px-1.5 py-0.5 rounded">
|
||||
<span className="bg-black/60 text-[var(--color-text-info,#8FB0C3)] px-1.5 py-0.5 rounded">
|
||||
[ID {s.id.slice(0, 4).toUpperCase()}]
|
||||
</span>
|
||||
<span className="bg-black/60 text-white px-1.5 py-0.5 rounded uppercase">
|
||||
@@ -83,7 +83,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
</div>
|
||||
|
||||
<Link href={`/news/${s.slug}`} className="block group">
|
||||
<h3 className="font-serif text-2xl leading-snug text-[#e5e7eb] group-hover:text-[var(--color-text-info,#60a5fa)]">
|
||||
<h3 className="font-serif text-2xl leading-snug text-[#e5e7eb] group-hover:text-[var(--color-text-info,#8FB0C3)]">
|
||||
{s.title}
|
||||
</h3>
|
||||
</Link>
|
||||
@@ -93,10 +93,10 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
|
||||
{s.neural_summary && Array.isArray(s.neural_summary.bullets) && s.neural_summary.bullets.length > 0 && (
|
||||
<div className="relative mt-4 p-3 border border-[#252525] rounded">
|
||||
<span aria-hidden className="absolute top-1 left-1 w-2.5 h-2.5 border-t border-l border-[var(--color-text-info,#60a5fa)]" />
|
||||
<span aria-hidden className="absolute bottom-1 right-1 w-2.5 h-2.5 border-b border-r border-[var(--color-text-info,#60a5fa)]" />
|
||||
<span aria-hidden className="absolute top-1 left-1 w-2.5 h-2.5 border-t border-l border-[var(--color-text-info,#8FB0C3)]" />
|
||||
<span aria-hidden className="absolute bottom-1 right-1 w-2.5 h-2.5 border-b border-r border-[var(--color-text-info,#8FB0C3)]" />
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
<span className="text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#8FB0C3)]">
|
||||
Neural summary
|
||||
</span>
|
||||
{typeof s.neural_summary.confidence === "number" && (
|
||||
@@ -108,7 +108,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
<ul className="space-y-1 text-xs text-[#d1d5db]">
|
||||
{s.neural_summary.bullets.slice(0, 3).map((b, idx) => (
|
||||
<li key={idx} className="flex gap-2">
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">·</span>
|
||||
<span className="text-[var(--color-text-info,#8FB0C3)]">·</span>
|
||||
<span>{b}</span>
|
||||
</li>
|
||||
))}
|
||||
@@ -116,7 +116,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
{s.neural_summary.entities && s.neural_summary.entities.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-1">
|
||||
{s.neural_summary.entities.slice(0, 5).map((e) => (
|
||||
<span key={e} className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-[#1a1f2e] text-[var(--color-text-info,#60a5fa)]">
|
||||
<span key={e} className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-[#1a1f2e] text-[var(--color-text-info,#8FB0C3)]">
|
||||
{e}
|
||||
</span>
|
||||
))}
|
||||
@@ -126,7 +126,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
)}
|
||||
|
||||
<div className="mt-4 pt-3 border-t border-[#252525] flex items-center gap-2">
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#1a1f2e] text-[var(--color-text-info,#60a5fa)] text-xs font-semibold">
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#1a1f2e] text-[var(--color-text-info,#8FB0C3)] text-xs font-semibold">
|
||||
{s.author_initials}
|
||||
</span>
|
||||
<div>
|
||||
@@ -145,7 +145,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
onClick={() => setI(n)}
|
||||
aria-label={`Slide ${n + 1}`}
|
||||
className={`w-2 h-2 rounded-full transition-colors ${
|
||||
n === i ? "bg-[var(--color-text-info,#60a5fa)]" : "bg-[#374151]"
|
||||
n === i ? "bg-[var(--color-text-info,#8FB0C3)]" : "bg-[#374151]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
@@ -154,7 +154,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
<button
|
||||
onClick={() => setI((v) => (v - 1 + slides.length) % slides.length)}
|
||||
aria-label="Previous"
|
||||
className="hover:text-[var(--color-text-info,#60a5fa)]"
|
||||
className="hover:text-[var(--color-text-info,#8FB0C3)]"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
@@ -162,7 +162,7 @@ export default function FeaturedCarousel({ slides }: { slides: Slide[] }) {
|
||||
<button
|
||||
onClick={() => setI((v) => (v + 1) % slides.length)}
|
||||
aria-label="Next"
|
||||
className="hover:text-[var(--color-text-info,#60a5fa)]"
|
||||
className="hover:text-[var(--color-text-info,#8FB0C3)]"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function Footer() {
|
||||
<div className="border-t border-[#222] pt-8 pb-6 grid grid-cols-2 md:grid-cols-4 gap-8">
|
||||
{footerColumns?.map((col) =>
|
||||
<div key={col?.heading}>
|
||||
<h4 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#3b82f6]/20">
|
||||
<h4 className="font-body font-bold text-xs text-[#5B7C8D] uppercase tracking-widest mb-4 pb-2 border-b border-[#5B7C8D]/20">
|
||||
{col?.heading}
|
||||
</h4>
|
||||
<ul className="space-y-2">
|
||||
@@ -86,13 +86,13 @@ export default function Footer() {
|
||||
href={link?.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="footer-link font-body text-sm text-[#777777] hover:text-[#3b82f6] transition-colors leading-relaxed inline-block">
|
||||
className="footer-link font-body text-sm text-[#777777] hover:text-[#5B7C8D] transition-colors leading-relaxed inline-block">
|
||||
{link?.label}
|
||||
</a> :
|
||||
|
||||
<Link
|
||||
href={link?.href}
|
||||
className="footer-link font-body text-sm text-[#777777] hover:text-[#3b82f6] transition-colors leading-relaxed inline-block">
|
||||
className="footer-link font-body text-sm text-[#777777] hover:text-[#5B7C8D] transition-colors leading-relaxed inline-block">
|
||||
{link?.label}
|
||||
</Link>
|
||||
}
|
||||
@@ -118,26 +118,26 @@ export default function Footer() {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<a href="https://linkedin.com/company/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on LinkedIn"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#555] hover:text-[#5B7C8D] transition-colors social-icon">
|
||||
<LinkedInIcon size={17} />
|
||||
</a>
|
||||
<a href="https://instagram.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Instagram"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#555] hover:text-[#5B7C8D] transition-colors social-icon">
|
||||
<InstagramIcon size={17} />
|
||||
</a>
|
||||
<a href="https://twitter.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Twitter/X"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#555] hover:text-[#5B7C8D] transition-colors social-icon">
|
||||
<TwitterXIcon size={17} />
|
||||
</a>
|
||||
<a href="https://facebook.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Facebook"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#555] hover:text-[#5B7C8D] transition-colors social-icon">
|
||||
<FacebookIcon size={17} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs font-body text-[#555]">
|
||||
<Link href="/privacy" className="hover:text-[#3b82f6] transition-colors">Privacy Policy</Link>
|
||||
<Link href="/terms" className="hover:text-[#3b82f6] transition-colors">Terms of Service</Link>
|
||||
<Link href="/privacy" className="hover:text-[#5B7C8D] transition-colors">Privacy Policy</Link>
|
||||
<Link href="/terms" className="hover:text-[#5B7C8D] transition-colors">Terms of Service</Link>
|
||||
<span>© 2026 Relevant Media Properties. All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,18 +215,18 @@ export default function Header() {
|
||||
</p>
|
||||
<div className="flex items-center gap-3 ml-auto">
|
||||
<a href="https://linkedin.com/company/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on LinkedIn"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors social-icon">
|
||||
<LinkedInIcon size={14} />
|
||||
</a>
|
||||
<a href="https://instagram.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Instagram"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors social-icon">
|
||||
<InstagramIcon size={14} />
|
||||
</a>
|
||||
<a href="https://facebook.com/broadcastbeat" target="_blank" rel="noopener noreferrer" aria-label="BroadcastBeat on Facebook"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors social-icon">
|
||||
<FacebookIcon size={14} />
|
||||
</a>
|
||||
<a href="/rss" aria-label="BroadcastBeat RSS Feed" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors social-icon">
|
||||
<a href="/rss" aria-label="BroadcastBeat RSS Feed" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors social-icon">
|
||||
<RssIcon size={14} />
|
||||
</a>
|
||||
<div className="w-px h-3.5 bg-[#333] mx-1" />
|
||||
@@ -237,22 +237,22 @@ export default function Header() {
|
||||
distribution.relevantmediaproperties.com; /wp-login,
|
||||
/wp-admin, /login redirect there from middleware. */}
|
||||
{currentUserId && (
|
||||
<Link href="/account" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/account" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
My Profile
|
||||
</Link>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<>
|
||||
<Link href="/admin/import" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/admin/import" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
WP Import
|
||||
</Link>
|
||||
<Link href="/admin/articles" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/admin/articles" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Articles
|
||||
</Link>
|
||||
<Link href="/dashboard/editorial" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/dashboard/editorial" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Editorial
|
||||
</Link>
|
||||
<Link href="/admin/newsletter" className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/admin/newsletter" className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Newsletter
|
||||
</Link>
|
||||
|
||||
@@ -261,7 +261,7 @@ export default function Header() {
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="true"
|
||||
className="text-[#999] hover:text-[#3b82f6] focus:text-[#3b82f6] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider inline-flex items-center gap-1">
|
||||
className="text-[#999] hover:text-[#5B7C8D] focus:text-[#5B7C8D] focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider inline-flex items-center gap-1">
|
||||
Tools
|
||||
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" aria-hidden="true">
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
@@ -271,25 +271,25 @@ export default function Header() {
|
||||
role="menu"
|
||||
aria-label="Admin tools"
|
||||
className="absolute right-0 top-full min-w-[200px] bg-[#0d0d0d] border border-[#252525] shadow-xl rounded-sm py-1 opacity-0 invisible translate-y-1 group-hover/tools:opacity-100 group-hover/tools:visible group-hover/tools:translate-y-0 group-focus-within/tools:opacity-100 group-focus-within/tools:visible group-focus-within/tools:translate-y-0 transition-all duration-150 z-50">
|
||||
<Link href="/admin/users" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Users</Link>
|
||||
<Link href="/admin/analytics" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Analytics</Link>
|
||||
<Link href="/admin/audit-log" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Audit Log</Link>
|
||||
<Link href="/admin/notifications" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Notifications</Link>
|
||||
<Link href="/admin/forum-seed" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Forum Seed</Link>
|
||||
<Link href="/admin/company-tracker" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Company Tracker</Link>
|
||||
<Link href="/dashboard/show-calendar" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Show Calendar</Link>
|
||||
<Link href="/dashboard/ai-console" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#3b82f6] hover:bg-[#1a1a1a] hover:text-blue-300 focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-blue-300 uppercase tracking-wider transition-colors">AI Console</Link>
|
||||
<Link href="/admin/banned-terms" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">Banned Terms</Link>
|
||||
<Link href="/admin/users" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Users</Link>
|
||||
<Link href="/admin/analytics" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Analytics</Link>
|
||||
<Link href="/admin/audit-log" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Audit Log</Link>
|
||||
<Link href="/admin/notifications" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Notifications</Link>
|
||||
<Link href="/admin/forum-seed" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Forum Seed</Link>
|
||||
<Link href="/admin/company-tracker" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Company Tracker</Link>
|
||||
<Link href="/dashboard/show-calendar" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Show Calendar</Link>
|
||||
<Link href="/dashboard/ai-console" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#5B7C8D] hover:bg-[#1a1a1a] hover:text-blue-300 focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-blue-300 uppercase tracking-wider transition-colors">AI Console</Link>
|
||||
<Link href="/admin/banned-terms" role="menuitem" className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">Banned Terms</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/admin" className="text-[#3b82f6] hover:text-blue-300 focus:text-blue-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
<Link href="/admin" className="text-[#5B7C8D] hover:text-blue-300 focus:text-blue-300 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] transition-colors text-xs font-body font-bold uppercase tracking-wider">
|
||||
Dashboard
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{/* Inline Email Subscription Form */}
|
||||
{subStatus === "success" ? (
|
||||
<span className="text-[#3b82f6] text-xs font-body font-bold whitespace-nowrap">{subMessage}</span>
|
||||
<span className="text-[#5B7C8D] text-xs font-body font-bold whitespace-nowrap">{subMessage}</span>
|
||||
) : (
|
||||
<form onSubmit={handleSubscribe} className="flex items-center gap-1" aria-label="Newsletter subscription">
|
||||
<input
|
||||
@@ -299,12 +299,12 @@ export default function Header() {
|
||||
placeholder="your@email.com"
|
||||
aria-label="Email address for newsletter"
|
||||
disabled={subStatus === "loading"}
|
||||
className="h-5 px-2 text-[11px] bg-[#0d1520] border border-[#2a3a50] text-[#ccc] placeholder-[#555] rounded focus:outline-none focus:border-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] w-32 disabled:opacity-50 transition-colors"
|
||||
className="h-5 px-2 text-[11px] bg-[#0d1520] border border-[#2a3a50] text-[#ccc] placeholder-[#555] rounded focus:outline-none focus:border-[#5B7C8D] focus-visible:ring-1 focus-visible:ring-[#5B7C8D] w-32 disabled:opacity-50 transition-colors"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={subStatus === "loading"}
|
||||
className="h-5 px-2.5 text-[11px] font-body font-bold uppercase tracking-wider bg-[#3b82f6] hover:bg-[#2563eb] text-white rounded disabled:opacity-50 transition-colors whitespace-nowrap focus:outline-none focus-visible:ring-1 focus-visible:ring-white focus-visible:ring-offset-1 focus-visible:ring-offset-[#3b82f6]"
|
||||
className="h-5 px-2.5 text-[11px] font-body font-bold uppercase tracking-wider bg-[#5B7C8D] hover:bg-[#4A6473] text-white rounded disabled:opacity-50 transition-colors whitespace-nowrap focus:outline-none focus-visible:ring-1 focus-visible:ring-white focus-visible:ring-offset-1 focus-visible:ring-offset-[#5B7C8D]"
|
||||
>
|
||||
{subStatus === "loading" ? "..." : "Subscribe"}
|
||||
</button>
|
||||
@@ -323,7 +323,7 @@ export default function Header() {
|
||||
<div className="max-w-container mx-auto px-4">
|
||||
<div className="flex items-center justify-between h-[60px] gap-4">
|
||||
{/* Logo */}
|
||||
<Link href="/home-page" className="flex-shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
<Link href="/home-page" className="flex-shrink-0 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
<AppImage
|
||||
src="/assets/images/logo.png"
|
||||
alt="BroadcastBeat logo"
|
||||
@@ -348,7 +348,7 @@ export default function Header() {
|
||||
<Link
|
||||
href={link?.href}
|
||||
ref={(el) => { navLinksRef.current[i] = el; }}
|
||||
className="nav-link-bb h-[60px] flex items-center focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]"
|
||||
className="nav-link-bb h-[60px] flex items-center focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]"
|
||||
onKeyDown={(e) => handleNavKeyDown(e, i, navLinksRef)}
|
||||
tabIndex={i === 0 ? 0 : -1}
|
||||
aria-haspopup={link?.dropdown ? "true" : undefined}>
|
||||
@@ -364,7 +364,7 @@ export default function Header() {
|
||||
key={sub.href + sub.label}
|
||||
href={sub.href}
|
||||
role="menuitem"
|
||||
className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#3b82f6] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#3b82f6] uppercase tracking-wider transition-colors">
|
||||
className="block px-4 py-2 text-xs font-body text-[#bbb] hover:bg-[#1a1a1a] hover:text-[#5B7C8D] focus:outline-none focus-visible:bg-[#1a1a1a] focus-visible:text-[#5B7C8D] uppercase tracking-wider transition-colors">
|
||||
{sub.label}
|
||||
</Link>
|
||||
))}
|
||||
@@ -379,7 +379,7 @@ export default function Header() {
|
||||
<Link
|
||||
href="/reading-list"
|
||||
aria-label="Reading List"
|
||||
className="flex items-center text-[#888] hover:text-[#3b82f6] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
className="flex items-center text-[#888] hover:text-[#5B7C8D] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D] focus-visible:ring-offset-1 focus-visible:ring-offset-[#111]">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
@@ -425,7 +425,7 @@ export default function Header() {
|
||||
type="button"
|
||||
onClick={submitSearch}
|
||||
aria-label="Submit search"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#3b82f6] hover:text-[#60a5fa] transition-colors cursor-pointer"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#5B7C8D] hover:text-[#8FB0C3] transition-colors cursor-pointer"
|
||||
>
|
||||
<SearchIcon size={13} strokeWidth={1.75} />
|
||||
</button>
|
||||
@@ -435,7 +435,7 @@ export default function Header() {
|
||||
type="button"
|
||||
onClick={() => searchRef.current?.focus()}
|
||||
aria-label="Focus search"
|
||||
className="absolute right-8 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#3b82f6] transition-colors cursor-pointer"
|
||||
className="absolute right-8 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#5B7C8D] transition-colors cursor-pointer"
|
||||
>
|
||||
<SearchIcon size={13} strokeWidth={1.75} />
|
||||
</button>
|
||||
@@ -446,7 +446,7 @@ export default function Header() {
|
||||
{/* Mobile Hamburger */}
|
||||
<button
|
||||
ref={hamburgerRef}
|
||||
className="lg:hidden p-2 text-[#cccccc] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||
className="lg:hidden p-2 text-[#cccccc] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#5B7C8D]"
|
||||
onClick={() => setMobileOpen(!mobileOpen)}
|
||||
aria-label={mobileOpen ? "Close menu" : "Open menu"}
|
||||
aria-expanded={mobileOpen}
|
||||
@@ -469,7 +469,7 @@ export default function Header() {
|
||||
key={link?.label}
|
||||
href={link?.href}
|
||||
ref={(el) => { navLinksRef.current[i] = el; }}
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-accent hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6]"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-accent hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#5B7C8D]"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
{link?.label}
|
||||
</Link>
|
||||
@@ -477,7 +477,7 @@ export default function Header() {
|
||||
{/* Reading List mobile link */}
|
||||
<Link
|
||||
href="/reading-list"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-accent hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6] flex items-center gap-2"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-accent hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#5B7C8D] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||
@@ -488,7 +488,7 @@ export default function Header() {
|
||||
{currentUserId && (
|
||||
<Link
|
||||
href="/account"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#3b82f6] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6] flex items-center gap-2"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#5B7C8D] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#5B7C8D] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
|
||||
@@ -500,7 +500,7 @@ export default function Header() {
|
||||
{isAdmin && (
|
||||
<Link
|
||||
href="/admin/import"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#3b82f6] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6] flex items-center gap-2"
|
||||
className="px-2 py-2.5 text-sm font-body font-bold uppercase tracking-wide text-[#cccccc] hover:text-[#5B7C8D] hover:bg-[#1a1a1a] border-b border-[#222] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#5B7C8D] flex items-center gap-2"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<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" />
|
||||
@@ -514,14 +514,14 @@ export default function Header() {
|
||||
.filter((n) => n.dropdown && n.dropdown.length > 0)
|
||||
.map((section) => (
|
||||
<div key={section.label} className="border-b border-[#222]">
|
||||
<div className="px-2 py-2 text-[10px] font-body font-bold text-[#3b82f6] uppercase tracking-widest bg-[#0d0d0d]">
|
||||
<div className="px-2 py-2 text-[10px] font-body font-bold text-[#5B7C8D] uppercase tracking-widest bg-[#0d0d0d]">
|
||||
{section.label}
|
||||
</div>
|
||||
{section.dropdown!.map((link) => (
|
||||
<Link
|
||||
key={link.href + link.label}
|
||||
href={link.href}
|
||||
className="px-4 py-2 text-xs font-body text-[#888] hover:text-accent hover:bg-[#1a1a1a] block transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#3b82f6]"
|
||||
className="px-4 py-2 text-xs font-body text-[#888] hover:text-accent hover:bg-[#1a1a1a] block transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-[#5B7C8D]"
|
||||
onClick={() => setMobileOpen(false)}>
|
||||
{link.label}
|
||||
</Link>
|
||||
@@ -549,7 +549,7 @@ export default function Header() {
|
||||
type="button"
|
||||
onClick={submitSearch}
|
||||
aria-label="Submit search"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#3b82f6] transition-colors cursor-pointer"
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-[#666] hover:text-[#5B7C8D] transition-colors cursor-pointer"
|
||||
>
|
||||
<SearchIcon size={13} strokeWidth={1.75} />
|
||||
</button>
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function LanguageSwitcher({ currentLang, onLanguageChange, compac
|
||||
<div ref={dropdownRef} className="relative">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#5B7C8D]"
|
||||
aria-label={`Language: ${currentLangData.name}. Click to change.`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="listbox"
|
||||
@@ -129,14 +129,14 @@ export default function LanguageSwitcher({ currentLang, onLanguageChange, compac
|
||||
onClick={() => handleSelect(lang.code)}
|
||||
className={`w-full flex items-center gap-2.5 px-3 py-2 text-xs transition-colors text-left ${
|
||||
activeLang === lang.code
|
||||
? 'bg-[#3b82f6]/10 text-[#3b82f6]'
|
||||
? 'bg-[#5B7C8D]/10 text-[#5B7C8D]'
|
||||
: 'text-[#888] hover:text-white hover:bg-[#1a1a1a]'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm">{lang.flag}</span>
|
||||
<span>{lang.nativeName}</span>
|
||||
{activeLang === lang.code && (
|
||||
<svg className="w-3 h-3 ml-auto text-[#3b82f6]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<svg className="w-3 h-3 ml-auto text-[#5B7C8D]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user