Compare commits
1 Commits
pre-bb-clo
...
archive/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43f78b161 |
@@ -14,11 +14,7 @@ ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
||||
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID
|
||||
|
||||
COPY package*.json ./
|
||||
# Force dev-deps even if NODE_ENV=production was supplied as a Coolify
|
||||
# build arg — autoprefixer/postcss/tailwindcss live in devDependencies and
|
||||
# Next.js needs them at build time. Same fix that landed for RMP cold-build
|
||||
# on 2026-05-30.
|
||||
RUN npm install --include=dev
|
||||
RUN npm install
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
ENV NODE_ENV=production
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Pending — broadcastbeat.com tonight's deferred work
|
||||
# Pending — avbeat.com tonight's deferred work
|
||||
|
||||
## Featured-image backfill (P0-1, deferred)
|
||||
|
||||
@@ -12,7 +12,7 @@ in Supabase storage) is **NOT done**. The smoke-test attempt
|
||||
`/tmp/bb-image-backfill.py` ran on coolify01:
|
||||
|
||||
- Matrox+Amagi smoke article (`matrox-video-and-amagi-collaborate-...`):
|
||||
Wayback returned no archived snapshot of `https://www.broadcastbeat.com/<slug>/`,
|
||||
Wayback returned no archived snapshot of `https://www.avbeat.com/<slug>/`,
|
||||
so the og:image extraction path didn't apply. Many of the broken-image
|
||||
articles are recent NAB 2026 posts that have never been crawled by
|
||||
Wayback.
|
||||
@@ -22,7 +22,7 @@ in Supabase storage) is **NOT done**. The smoke-test attempt
|
||||
**What needs to happen next session:**
|
||||
|
||||
1. For each broken article, try multiple sources in order:
|
||||
- Wayback Machine snapshot of `broadcastbeat.com/<slug>/` (skip if 404).
|
||||
- Wayback Machine snapshot of `avbeat.com/<slug>/` (skip if 404).
|
||||
- Manufacturer press-kit page based on title keywords (Matrox, Amagi,
|
||||
Telestream, Calrec, AJA, etc. — keep a per-vendor mapping of press
|
||||
kit URLs).
|
||||
|
||||
@@ -29,6 +29,6 @@ export const imageHosts = [
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'www.broadcastbeat.com',
|
||||
hostname: 'www.avbeat.com',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -70,7 +70,7 @@ const nextConfig = {
|
||||
destination: 'https://relevantmediaproperties.com/broadcast-beat---media-kit',
|
||||
permanent: true,
|
||||
},
|
||||
// WordPress admin redirects for broadcastbeat.com (301 permanent)
|
||||
// WordPress admin redirects for avbeat.com (301 permanent)
|
||||
{
|
||||
source: '/wp-admin',
|
||||
destination: '/admin',
|
||||
|
||||
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 21 KiB |
BIN
public/assets/images/personas/ryan-salazar.jpg
Normal file
|
After Width: | Height: | Size: 64 KiB |
13
public/assets/logos/av.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 96" width="360" height="96">
|
||||
<defs>
|
||||
<linearGradient id="av-grad" x1="0" x2="1">
|
||||
<stop offset="0%" stop-color="#FFB300"/>
|
||||
<stop offset="55%" stop-color="#FF6B00"/>
|
||||
<stop offset="100%" stop-color="#D60701"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g font-family="Plus Jakarta Sans, sans-serif" font-weight="800">
|
||||
<text x="0" y="44" font-size="40" fill="url(#av-grad)" letter-spacing="2">AVB</text>
|
||||
<text x="0" y="82" font-size="20" fill="#f4f4f6" letter-spacing="2.5" opacity="0.85">AV Beat</text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 594 B |
|
Before Width: | Height: | Size: 286 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 362 KiB |
@@ -52,8 +52,8 @@ export default function ContactPage() {
|
||||
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
||||
<main>
|
||||
<header className="mb-8 border-b border-[#252525] pb-6">
|
||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#E67E22)] mb-2">
|
||||
AV Beat · Where AV Meets IT
|
||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
|
||||
AV Beat
|
||||
</div>
|
||||
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">Contact</h1>
|
||||
<p className="mt-3 text-[#9ca3af] text-base">
|
||||
|
||||
@@ -4,7 +4,7 @@ import SidebarAdStack from "@/components/SidebarAdStack";
|
||||
|
||||
export const metadata = {
|
||||
title: "About AV Beat",
|
||||
description: "AV Beat is an independent industry publication covering the convergence of pro AV and IT — broadcast, live production, streaming, and post-production technology.",
|
||||
description: "AV Beat is an independent industry publication covering broadcast, production, and post-production technology.",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
@@ -15,12 +15,12 @@ export default function AboutPage() {
|
||||
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
||||
<main>
|
||||
<header className="mb-8 border-b border-[#252525] pb-6">
|
||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#E67E22)] mb-2">
|
||||
AV Beat · Where AV Meets IT
|
||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
|
||||
AV Beat
|
||||
</div>
|
||||
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">About</h1>
|
||||
<p className="mt-3 text-[#9ca3af] text-base max-w-2xl">
|
||||
An independent trade publication covering the convergence of pro AV and IT — broadcast, live production, streaming, and post-production technology.
|
||||
An independent trade publication covering broadcast, live production, streaming, and post-production technology.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
@@ -32,11 +32,11 @@ export default function AboutPage() {
|
||||
|
||||
<section className="space-y-6 text-[#d1d5db] text-[15px] leading-relaxed">
|
||||
<Block title="What we cover">
|
||||
AV Beat reports on the equipment, services, and people that
|
||||
move the professional AV and IT-converged video industry
|
||||
forward. We follow product launches, executive moves,
|
||||
acquisitions, and the live production / IP / cloud workflows
|
||||
reshaping how content gets made and delivered.
|
||||
AV Beat reports on the equipment, services, and people
|
||||
that move the professional video industry forward. We follow
|
||||
product launches, executive moves, acquisitions, and the live
|
||||
production / IP / cloud workflows reshaping how content gets
|
||||
made and delivered.
|
||||
</Block>
|
||||
|
||||
<Block title="Where we go">
|
||||
@@ -57,9 +57,9 @@ export default function AboutPage() {
|
||||
</Block>
|
||||
|
||||
<Block title="Get involved">
|
||||
For editorial pitches: <a className="text-[var(--color-text-info,#E67E22)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>.
|
||||
{" "}For advertising: <a className="text-[var(--color-text-info,#E67E22)] hover:underline" href="/about/advertise">see the media kit</a>.
|
||||
{" "}For our team: <a className="text-[var(--color-text-info,#E67E22)] hover:underline" href="/about/team">meet the editorial team</a>.
|
||||
For editorial pitches: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:editor@avbeat.com">editor@avbeat.com</a>.
|
||||
{" "}For advertising: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/advertise">see the media kit</a>.
|
||||
{" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>.
|
||||
</Block>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -13,23 +13,23 @@ 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,#E67E22)] hover:underline" href="/assets/images/logo.png" download>AV Beat logo (PNG)</a></li>
|
||||
<li><a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/assets/images/logo.png" download>AV Beat logo (PNG)</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
|
||||
<p className="text-[#d1d5db] leading-relaxed">
|
||||
AV Beat is an independent trade publication covering the convergence
|
||||
of pro AV and IT — broadcast, live production, streaming, and
|
||||
post-production technology. Founded to bring engineering-rigor
|
||||
reporting to industry coverage, AV Beat publishes daily on the
|
||||
equipment, services, and people moving professional video forward.
|
||||
AV Beat is an independent trade publication covering broadcast,
|
||||
live production, streaming, and post-production technology. Founded
|
||||
to bring engineering-rigor reporting to industry coverage, AV Beat
|
||||
publishes daily on the equipment, services, and people that move
|
||||
professional video forward.
|
||||
</p>
|
||||
</section>
|
||||
<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,#E67E22)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
<a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="mailto:ryan.salazar@relevantmediaproperties.com">ryan.salazar@relevantmediaproperties.com</a>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ export const revalidate = 600;
|
||||
|
||||
export const metadata = {
|
||||
title: "Our team — AV Beat",
|
||||
description: "Meet the AV Beat team — editorial staff plus accounts and support, covering the convergence of pro AV and IT.",
|
||||
description: "Meet the AV Beat team — editorial staff plus accounts and support, covering broadcast, streaming, and post-production.",
|
||||
};
|
||||
|
||||
interface Persona {
|
||||
@@ -63,7 +63,7 @@ export default async function TeamPage() {
|
||||
.from("personas")
|
||||
.select("id, display_name, role, email, phone, extension, avatar_url")
|
||||
.eq("active", true)
|
||||
.in("id", ["chloe", "jeff", "riley"]);
|
||||
.in("id", ["chloe", "jeff", "riley", "brian", "avery"]);
|
||||
const ops = (opsData || []) as OpsPersona[];
|
||||
|
||||
const displayBeat = (p: Persona) =>
|
||||
@@ -98,10 +98,10 @@ export default async function TeamPage() {
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<header className="relative overflow-hidden rounded-xl mb-10 border border-[#1a1f2a] bg-gradient-to-br from-[#0d1118] via-[#0b0f17] to-[#0d1118]">
|
||||
<div className="absolute inset-0 opacity-[0.04] bg-[radial-gradient(ellipse_at_top_right,#E67E22,transparent_60%)]" aria-hidden />
|
||||
<div className="absolute inset-0 opacity-[0.04] bg-[radial-gradient(ellipse_at_top_right,#d60701,transparent_60%)]" aria-hidden />
|
||||
<div className="relative px-6 py-10 sm:px-10 sm:py-14">
|
||||
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-[#F0B07A] mb-3">
|
||||
<span className="w-6 h-px bg-[#E67E22]" />
|
||||
<div className="inline-flex items-center gap-2 text-[10px] font-mono uppercase tracking-[0.22em] text-[#ff5a55] mb-3">
|
||||
<span className="w-6 h-px bg-[#d60701]" />
|
||||
AV Beat · editorial
|
||||
</div>
|
||||
<h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
|
||||
@@ -129,10 +129,10 @@ export default async function TeamPage() {
|
||||
return (
|
||||
<li
|
||||
key={p.slug}
|
||||
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#E67E22]/50 hover:shadow-[0_0_0_1px_rgba(230,126,34,0.15)] transition-all"
|
||||
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#d60701]/50 hover:shadow-[0_0_0_1px_rgba(214,7,1,0.15)] transition-all"
|
||||
>
|
||||
{/* Top accent */}
|
||||
<div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#E67E22]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden />
|
||||
<div className="absolute top-0 left-5 right-5 h-px bg-gradient-to-r from-transparent via-[#d60701]/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" aria-hidden />
|
||||
|
||||
<div className="flex items-start gap-4">
|
||||
{p.avatar_url ? (
|
||||
@@ -142,7 +142,7 @@ export default async function TeamPage() {
|
||||
alt={p.name}
|
||||
width={96}
|
||||
height={96}
|
||||
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#E67E22]/40 transition-colors object-cover"
|
||||
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#d60701]/40 transition-colors object-cover"
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
@@ -155,7 +155,7 @@ export default async function TeamPage() {
|
||||
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3>
|
||||
</a>
|
||||
{(p.beat || p.slug === "ryan-salazar") && (
|
||||
<div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#E67E22]/10 border border-[#E67E22]/25 text-[10px] font-mono uppercase tracking-[0.12em] text-[#F0B07A]">
|
||||
<div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#d60701]/10 border border-[#d60701]/25 text-[10px] font-mono uppercase tracking-[0.12em] text-[#ff6b66]">
|
||||
{displayBeat(p)}
|
||||
</div>
|
||||
)}
|
||||
@@ -173,11 +173,11 @@ export default async function TeamPage() {
|
||||
<ObfuscatedEmail
|
||||
user={e.user}
|
||||
domain={e.domain}
|
||||
className="text-[#aab1bd] hover:text-[#F0B07A] font-mono"
|
||||
className="text-[#aab1bd] hover:text-[#ff6b66] font-mono"
|
||||
/>
|
||||
<a
|
||||
href={`/authors/${p.slug}`}
|
||||
className="text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#F0B07A]"
|
||||
className="text-[10px] font-mono uppercase tracking-[0.12em] text-[#6c7686] hover:text-[#ff6b66]"
|
||||
>
|
||||
View →
|
||||
</a>
|
||||
@@ -209,7 +209,7 @@ export default async function TeamPage() {
|
||||
{ops.map((p) => (
|
||||
<li
|
||||
key={p.id}
|
||||
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#E67E22]/40 transition-colors"
|
||||
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#d60701]/40 transition-colors"
|
||||
>
|
||||
{p.avatar_url ? (
|
||||
<img
|
||||
@@ -235,13 +235,13 @@ export default async function TeamPage() {
|
||||
if (!e) return null;
|
||||
return (
|
||||
<div className="font-mono">
|
||||
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#F0B07A]" />
|
||||
<ObfuscatedEmail user={e.user} domain={e.domain} className="hover:text-[#ff6b66]" />
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{p.phone && (
|
||||
<div>
|
||||
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#F0B07A] font-mono">
|
||||
<a href={`tel:${p.phone.replace(/[^\d+]/g, '')}`} className="hover:text-[#ff6b66] font-mono">
|
||||
{p.phone}{p.extension ? ` ext. ${p.extension}` : ''}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const STATUSES = ['active','completed','cancelled'];
|
||||
const SCHEDULES = ['single','monthly','quarterly'];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); }
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
export default function ReportsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function AdOpsIOsPage() {
|
||||
</select>
|
||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function AdOpsDashboardPage() {
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Statuses</option>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const PRODUCT_TYPES = [
|
||||
'display_banner_300x250','display_banner_728x90','display_banner_300x600',
|
||||
'display_banner_970x250','newsletter_banner_600x100','dedicated_email_blast',
|
||||
|
||||
@@ -9,7 +9,7 @@ interface BannedTerm {
|
||||
id: string;
|
||||
term: string;
|
||||
ban_level: 'soft' | 'hard';
|
||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
||||
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||
is_active: boolean;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
@@ -19,7 +19,7 @@ interface BannedTerm {
|
||||
interface FormState {
|
||||
term: string;
|
||||
ban_level: 'soft' | 'hard';
|
||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
||||
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function BannedTermsPage() {
|
||||
<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]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ export default function BannedTermsPage() {
|
||||
<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]">
|
||||
<option value="">All Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat</option>
|
||||
<option value="avbeat">AV Beat</option>
|
||||
<option value="avbeat">AV Beat</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
@@ -336,7 +336,7 @@ export default function BannedTermsPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'broadcastbeat' ? 'Broadcast Beat' : 'AV Beat'}</span>
|
||||
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
|
||||
@@ -379,7 +379,7 @@ export default function BannedTermsPage() {
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400 font-bold">Hard Ban</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@broadcastbeat.com for assistance." Admin receives email notification.</p>
|
||||
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@avbeat.com for assistance." Admin receives email notification.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ type TabType = 'queue' | 'companies' | 'crawl-log' | 'settings';
|
||||
type QueueStatus = 'generated' | 'pending' | 'approved' | 'published' | 'rejected';
|
||||
|
||||
const SITE_CONFIG = {
|
||||
1: { name: 'Broadcast Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
|
||||
1: { name: 'AV Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
|
||||
2: { name: 'AV Beat', badge: 'AV', bgClass: 'bg-[#f97316]/10', textClass: 'text-[#f97316]' },
|
||||
};
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function CompanyTrackerPage() {
|
||||
<span className="text-[#888] text-sm font-body">Company Tracker</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-display font-bold text-white">Company Story Engine</h1>
|
||||
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across Broadcast Beat & AV Beat</p>
|
||||
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across AV Beat & AV Beat</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Auto-Pilot toggle */}
|
||||
@@ -344,7 +344,7 @@ export default function CompanyTrackerPage() {
|
||||
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
<option value="1">Broadcast Beat</option>
|
||||
<option value="1">AV Beat</option>
|
||||
<option value="2">AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -412,7 +412,7 @@ export default function CompanyTrackerPage() {
|
||||
<select onChange={e => handleQueueAction(item.id, 'reassign_site', { site_id: parseInt(e.target.value) })}
|
||||
defaultValue={item.site_id}
|
||||
className="bg-[#111] border border-[#333] rounded px-2 py-1 text-xs text-white font-body focus:outline-none">
|
||||
<option value={1}>Broadcast Beat</option>
|
||||
<option value={1}>AV Beat</option>
|
||||
<option value={2}>AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ export default function CompanyTrackerPage() {
|
||||
<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]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -494,7 +494,7 @@ export default function CompanyTrackerPage() {
|
||||
<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-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 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
|
||||
@@ -47,7 +47,7 @@ interface LaunchChecklist {
|
||||
// ─── Site labels ──────────────────────────────────────────────────────────────
|
||||
|
||||
const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = {
|
||||
'broadcast-beat': { label: 'Broadcast Beat', color: 'text-blue-400', target: '15–25/day' },
|
||||
'broadcast-beat': { label: 'AV Beat', color: 'text-blue-400', target: '15–25/day' },
|
||||
'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '10–15/day' },
|
||||
'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '10–15/day' },
|
||||
'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '10–15/day' },
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
function DnsRow({ label, valid, checkedAt }: { label: string; valid: boolean; checkedAt: string | null }) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
export default function SendLogPage() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
@@ -4,13 +4,13 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SUPP_TYPES = ['unsubscribe','bounce_hard','bounce_soft','spam_complaint','hostile_reply','manual'];
|
||||
|
||||
export default function SuppressionsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [site, setSite] = useState('broadcastbeat');
|
||||
const [site, setSite] = useState('avbeat');
|
||||
const [suppressions, setSuppressions] = useState<any[]>([]);
|
||||
const [keywords, setKeywords] = useState<any[]>([]);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
|
||||
@@ -158,8 +158,8 @@ 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">
|
||||
broadcastbeat.com
|
||||
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
||||
avbeat.com
|
||||
</a>{' '}
|
||||
via WordPress REST API
|
||||
</p>
|
||||
@@ -228,7 +228,7 @@ export default function WPImportPage() {
|
||||
<div className="w-5 h-5 border-2 border-[#3b82f6] 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-[#666] text-xs mt-0.5">Fetching posts from broadcastbeat.com and saving to database...</p>
|
||||
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
|
||||
@@ -365,11 +365,11 @@ export default function WPImportPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<a
|
||||
href={`https://www.broadcastbeat.com/${post.wp_slug}`}
|
||||
href={`https://www.avbeat.com/${post.wp_slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1"
|
||||
title="View on broadcastbeat.com">
|
||||
title="View on avbeat.com">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
|
||||
@@ -632,7 +632,7 @@ export default function CampaignEditorPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-white">Broadcast Beat</span>
|
||||
<span className="text-xs font-medium text-white">AV Beat</span>
|
||||
<span className="text-[10px] text-[#444]">now</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#888] truncate">{campaign.subject || 'Your subject line…'}</p>
|
||||
@@ -645,7 +645,7 @@ export default function CampaignEditorPage() {
|
||||
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
|
||||
<p className="text-xs font-bold text-white tracking-widest uppercase">Broadcast Beat</p>
|
||||
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
|
||||
{campaign.previewText && (
|
||||
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
|
||||
)}
|
||||
@@ -680,7 +680,7 @@ export default function CampaignEditorPage() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center pt-2 border-t border-[#1a1a1a]">
|
||||
<p className="text-[9px] text-[#333]">Broadcast Beat · Unsubscribe · View in browser</p>
|
||||
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -689,7 +689,7 @@ export default function AdminNewsletterPage() {
|
||||
type="text"
|
||||
value={digestSubject}
|
||||
onChange={(e) => setDigestSubject(e.target.value)}
|
||||
placeholder="e.g. Broadcast Beat Weekly — NAB Show Highlights"
|
||||
placeholder="e.g. AV Beat Weekly — NAB Show Highlights"
|
||||
className="search-input w-full py-2.5 px-3 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -435,7 +435,7 @@ export default function NewsletterTemplatesPage() {
|
||||
type="text"
|
||||
value={form.subject_prefix}
|
||||
onChange={(e) => setForm((f) => ({ ...f, subject_prefix: e.target.value }))}
|
||||
placeholder="e.g. Broadcast Beat Weekly —"
|
||||
placeholder="e.g. AV Beat Weekly —"
|
||||
className="search-input w-full py-2.5 px-3 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-[#555] font-body mt-1">Prepended to the digest subject line when using this template</p>
|
||||
|
||||
@@ -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-[#E67E22] text-2xl font-bold mb-1">{stat.value}</div>
|
||||
<div className="font-heading text-[#3b82f6] 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-[#E67E22] font-bold text-xs uppercase tracking-wider">Placement</th>
|
||||
<th className="text-left py-3 px-4 text-[#E67E22] font-bold text-xs uppercase tracking-wider">Dimensions</th>
|
||||
<th className="text-left py-3 px-4 text-[#E67E22] font-bold text-xs uppercase tracking-wider">Location</th>
|
||||
<th className="text-left py-3 px-4 text-[#E67E22] font-bold text-xs uppercase tracking-wider">Rate</th>
|
||||
<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>
|
||||
</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-[#E67E22] font-medium">{zone.cpm}</td>
|
||||
<td className="py-3 px-4 text-[#3b82f6] 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-[#E67E22] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p>
|
||||
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Advertising Sales</p>
|
||||
<p className="font-body text-[#e0e0e0] font-medium">Jeff Victor</p>
|
||||
<a href="mailto:jeff@avbeat.com" className="font-body text-[#E67E22] text-sm hover:underline">
|
||||
<a href="mailto:jeff@avbeat.com" className="font-body text-[#3b82f6] text-sm hover:underline">
|
||||
jeff@avbeat.com
|
||||
</a>
|
||||
</div>
|
||||
<div className="border-t border-[#222] pt-4">
|
||||
<p className="font-body text-[#E67E22] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
|
||||
<p className="font-body text-[#3b82f6] text-xs font-bold uppercase tracking-wider mb-1">Publication</p>
|
||||
<p className="font-body text-[#777] text-sm">AV Beat</p>
|
||||
<p className="font-body text-[#777] text-sm">Relevant Media Properties</p>
|
||||
</div>
|
||||
<div className="border-t border-[#222] pt-4">
|
||||
<p className="font-body text-[#E67E22] text-xs font-bold uppercase tracking-wider mb-2">What We Offer</p>
|
||||
<p className="font-body text-[#3b82f6] 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-[#E67E22] mt-0.5">✓</span>
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] p-8 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-[#E67E22]/20 flex items-center justify-center mx-auto mb-4">
|
||||
<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">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M5 12l4 4 10-10" stroke="#E67E22" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M5 12l4 4 10-10" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
|
||||
<p className="font-body text-[#777] text-sm">
|
||||
Thanks for reaching out. Jeff will be in touch within one business day at{" "}
|
||||
<a href="mailto:jeff@avbeat.com" className="text-[#E67E22] hover:underline">jeff@avbeat.com</a>.
|
||||
<a href="mailto:jeff@avbeat.com" className="text-[#3b82f6] hover:underline">jeff@avbeat.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@avbeat.com" className="text-[#E67E22] hover:underline">jeff@avbeat.com</a>
|
||||
Or email directly: <a href="mailto:jeff@avbeat.com" className="text-[#3b82f6] hover:underline">jeff@avbeat.com</a>
|
||||
</p>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -56,7 +56,7 @@ export async function GET(request: NextRequest) {
|
||||
.select('site, amount_cents, status, paid_date')
|
||||
.gte('created_at', yearStart);
|
||||
|
||||
const sites = ['broadcastbeat', 'avbeat', 'backlotbeat', 'broadcastengineering'];
|
||||
const sites = ['avbeat', 'avbeat', 'backlotbeat', 'broadcastengineering'];
|
||||
const siteRevenue = sites.map(site => {
|
||||
const siteData = (siteInvoices ?? []).filter((i: any) => i.site === site);
|
||||
const revenue_ytd = siteData.filter((i: any) => i.status === 'paid').reduce((s: number, r: any) => s + (r.amount_cents ?? 0), 0);
|
||||
|
||||
@@ -81,7 +81,7 @@ export async function POST(req: NextRequest) {
|
||||
// Trigger article publish notification if published immediately
|
||||
if (status === 'published' && data) {
|
||||
try {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
fetch(`${siteUrl}/api/newsletter/notify-article`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -152,7 +152,7 @@ export async function PATCH(req: NextRequest) {
|
||||
.single();
|
||||
|
||||
if (articleData) {
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
// Fire-and-forget: call notify-article route internally
|
||||
fetch(`${siteUrl}/api/newsletter/notify-article`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -13,9 +13,9 @@ export async function POST(request: NextRequest) {
|
||||
});
|
||||
|
||||
await transport.sendMail({
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: 'editor@broadcastbeat.com',
|
||||
subject: `[Broadcast Beat] Blocked post attempt — banned term triggered`,
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: 'editor@avbeat.com',
|
||||
subject: `[AV Beat] Blocked post attempt — banned term triggered`,
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
|
||||
<h2 style="color:#ef4444;margin:0 0 16px;">Blocked Post Attempt</h2>
|
||||
|
||||
@@ -30,7 +30,7 @@ export async function POST(request: NextRequest) {
|
||||
const titleLower = (post.title || '').toLowerCase();
|
||||
for (const bt of bannedTerms) {
|
||||
const termLower = bt.term.toLowerCase();
|
||||
const siteMatch = bt.site_scope === 'both' || (bt.site_scope === 'broadcastbeat' && post.site_id === 1) || (bt.site_scope === 'avbeat' && post.site_id === 2);
|
||||
const siteMatch = bt.site_scope === 'both' || (bt.site_scope === 'avbeat' && post.site_id === 1) || (bt.site_scope === 'avbeat' && post.site_id === 2);
|
||||
if (siteMatch && titleLower.includes(termLower)) {
|
||||
matches.push({ title: post.title, author: post.author_name, published_at: post.published_at, site_id: post.site_id, matched_term: bt.term, ban_level: bt.ban_level });
|
||||
break; // one match per post
|
||||
|
||||
@@ -13,14 +13,14 @@ export async function POST(request: NextRequest) {
|
||||
const companyList = (companies || []).slice(0, 10).map((c: string) => `<li>${c}</li>`).join('');
|
||||
const moreText = companies?.length > 10 ? `<p style="color:#888;font-size:13px;">...and ${companies.length - 10} more</p>` : '';
|
||||
await transport.sendMail({
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: email || 'ryan.salazar@relevantmediaproperties.com',
|
||||
subject: `[Relevant Media Properties] ${count} new company story suggestion${count !== 1 ? 's' : ''} queued`,
|
||||
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>
|
||||
<p style="color:#555;font-size:12px;margin:0;">Broadcast Beat · AV Beat — Company Story Engine</p>
|
||||
<p style="color:#555;font-size:12px;margin:0;">AV 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>
|
||||
<p style="color:#aaa;font-size:14px;line-height:1.6;margin:0 0 20px;">
|
||||
@@ -31,12 +31,12 @@ export async function POST(request: NextRequest) {
|
||||
<ul style="margin:0;padding-left:20px;color:#e5e5e5;font-size:14px;line-height:1.8;">${companyList}</ul>
|
||||
${moreText}
|
||||
</div>
|
||||
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com'}/admin/company-tracker"
|
||||
<a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/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;">
|
||||
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://broadcastbeat.com'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a>
|
||||
Manage settings at <a href="${process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com'}/admin/company-tracker" style="color:#3b82f6;">Company Tracker</a>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
|
||||
@@ -23,7 +23,7 @@ function pickPenName(siteId: number, recentNames: string[]): string {
|
||||
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
|
||||
return available[Math.floor(Math.random() * available.length)] || pool[0];
|
||||
}
|
||||
// Broadcast Beat: fixed byline (Ryan) unless overridden to "rotate".
|
||||
// AV Beat: fixed byline (Ryan) unless overridden to "rotate".
|
||||
if (BB_AUTOSTORY_BYLINE.toLowerCase() === 'rotate') {
|
||||
const pool = BB_PEN_NAMES_LEGACY;
|
||||
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
|
||||
@@ -224,7 +224,7 @@ export async function POST(request: NextRequest) {
|
||||
if ((count || 0) >= 3) continue; // max 3 per week
|
||||
|
||||
const siteId = article.site_id || 1;
|
||||
const siteName = siteId === 2 ? 'AV Beat' : 'Broadcast Beat';
|
||||
const siteName = siteId === 2 ? 'AV Beat' : 'AV Beat';
|
||||
const penName = pickPenName(siteId, recentPenNames);
|
||||
recentPenNames.push(penName);
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ export async function POST(request: NextRequest) {
|
||||
const penName = pickPenNameForShow(site_id, story_type, recentNames, preferred_pen_name);
|
||||
const penNameData = (site_id === 2 ? AV_PEN_NAMES : BB_PEN_NAMES).find(p => p.name === penName);
|
||||
const phase = getShowPhase(event);
|
||||
const siteName = site_id === 2 ? 'AV Beat' : 'Broadcast Beat';
|
||||
const siteName = site_id === 2 ? 'AV Beat' : 'AV Beat';
|
||||
|
||||
const story = await generateShowStory({
|
||||
penName,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
|
||||
async function sendSuspensionEmail(
|
||||
email: string,
|
||||
@@ -35,8 +35,8 @@ async function sendSuspensionEmail(
|
||||
: '';
|
||||
|
||||
const subject = isBanned
|
||||
? `Your Broadcast Beat account has been banned`
|
||||
: `Your Broadcast Beat account has been suspended`;
|
||||
? `Your AV Beat account has been banned`
|
||||
: `Your AV Beat account has been suspended`;
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
@@ -45,7 +45,7 @@ async function sendSuspensionEmail(
|
||||
<body style="background:#0d1117;color:#e5e7eb;font-family:system-ui,sans-serif;margin:0;padding:0;">
|
||||
<div style="max-width:560px;margin:40px auto;padding:32px;background:#111827;border:1px solid #1f2937;border-radius:12px;">
|
||||
<div style="margin-bottom:24px;">
|
||||
<span style="font-size:24px;font-weight:800;color:#fff;">Broadcast Beat</span>
|
||||
<span style="font-size:24px;font-weight:800;color:#fff;">AV Beat</span>
|
||||
</div>
|
||||
<div style="background:${isBanned ? '#7f1d1d' : '#78350f'};border:1px solid ${isBanned ? '#991b1b' : '#92400e'};border-radius:8px;padding:16px;margin-bottom:24px;">
|
||||
<p style="margin:0;font-size:14px;font-weight:700;color:${isBanned ? '#fca5a5' : '#fcd34d'};">
|
||||
@@ -54,7 +54,7 @@ async function sendSuspensionEmail(
|
||||
</div>
|
||||
<p style="color:#d1d5db;font-size:15px;line-height:1.6;">Hi ${fullName || 'there'},</p>
|
||||
<p style="color:#d1d5db;font-size:15px;line-height:1.6;">
|
||||
Your Broadcast Beat account has been <strong style="color:#fff;">${isBanned ? 'permanently banned' : `suspended ${durationText}`}</strong>.
|
||||
Your AV Beat account has been <strong style="color:#fff;">${isBanned ? 'permanently banned' : `suspended ${durationText}`}</strong>.
|
||||
</p>
|
||||
<div style="background:#0d1117;border:1px solid #1f2937;border-radius:8px;padding:16px;margin:20px 0;">
|
||||
<p style="margin:0 0 8px;color:#888;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;">Reason</p>
|
||||
@@ -65,7 +65,7 @@ async function sendSuspensionEmail(
|
||||
${isBanned ? 'This decision is final. If you believe this is an error, please contact our support team.' : 'During this period, you will not be able to post threads or replies. If you believe this is an error, please contact our support team.'}
|
||||
</p>
|
||||
<div style="margin-top:24px;padding-top:24px;border-top:1px solid #1f2937;">
|
||||
<p style="margin:0;color:#555;font-size:12px;">Broadcast Beat — ${siteUrl}</p>
|
||||
<p style="margin:0;color:#555;font-size:12px;">AV Beat — ${siteUrl}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -73,7 +73,7 @@ async function sendSuspensionEmail(
|
||||
`;
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to: email,
|
||||
subject,
|
||||
html,
|
||||
|
||||
@@ -107,7 +107,7 @@ export async function POST(req: NextRequest) {
|
||||
// Send email via Supabase Edge Function
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
|
||||
try {
|
||||
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-contributor-invite`, {
|
||||
@@ -118,7 +118,7 @@ export async function POST(req: NextRequest) {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'Broadcast Beat Admin',
|
||||
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'AV Beat Admin',
|
||||
role: role || 'contributor',
|
||||
token,
|
||||
message,
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function GET(request: NextRequest) {
|
||||
</head>
|
||||
<body>
|
||||
<div class="site-header">
|
||||
<div class="site-logo">Broadcast Beat</div>
|
||||
<div class="site-logo">AV Beat</div>
|
||||
<nav class="site-nav">
|
||||
<a href="#">News</a>
|
||||
<a href="#">Reviews</a>
|
||||
|
||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
||||
date: a.date,
|
||||
}));
|
||||
|
||||
const systemPrompt = `You are a personalized content recommendation engine for Broadcast Beat, a broadcast engineering news platform.
|
||||
const systemPrompt = `You are a personalized content recommendation engine for AV Beat, a broadcast engineering news platform.
|
||||
Given a reader's topic interests and reading history, select the 4 most relevant articles from the provided article list.
|
||||
Return ONLY a valid JSON array of slugs in order of relevance. Example: ["slug-1","slug-2","slug-3","slug-4"]
|
||||
Do not include any explanation or markdown — only the raw JSON array.`;
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; Broadcast Beat/1.0; +https://broadcastbeat.com)',
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; AV Beat/1.0; +https://avbeat.com)',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
signal: controller.signal,
|
||||
|
||||
@@ -24,11 +24,11 @@ function rateLimit(key: string): { ok: boolean; remaining: number } {
|
||||
return { ok: true, remaining: RATE_LIMIT_MAX - b.count };
|
||||
}
|
||||
|
||||
const SYSTEM_PROMPT = `You are BB AI, Broadcast Beat's intelligent research assistant.
|
||||
const SYSTEM_PROMPT = `You are BB AI, AV Beat's intelligent research assistant.
|
||||
|
||||
You help readers — broadcast engineers, post-production supervisors,
|
||||
streaming architects, and broadcast executives — find information from the
|
||||
Broadcast Beat archive.
|
||||
AV Beat archive.
|
||||
|
||||
Tone: trade-press, factual, lightly conversational. Plain English.
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function POST(request: NextRequest) {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: profile.full_name || user.email, term: bannedMatch.term }),
|
||||
}).catch(() => {});
|
||||
return NextResponse.json({ blocked: true, error: 'This post could not be submitted. Please contact the editorial team at editor@broadcastbeat.com for assistance.' }, { status: 422 });
|
||||
return NextResponse.json({ blocked: true, error: 'This post could not be submitted. Please contact the editorial team at editor@avbeat.com for assistance.' }, { status: 422 });
|
||||
}
|
||||
// Soft ban: flag but allow through
|
||||
// Will be tagged below
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
||||
.eq('domain', sendingDomain)
|
||||
.single();
|
||||
|
||||
const site = domainRecord?.site ?? 'broadcastbeat';
|
||||
const site = domainRecord?.site ?? 'avbeat';
|
||||
|
||||
// Load active keywords
|
||||
const { data: keywords } = await supabase
|
||||
|
||||
@@ -22,7 +22,7 @@ interface Row {
|
||||
export async function GET() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
||||
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
||||
const schema = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||||
const schema = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av";
|
||||
if (!url || !key) return NextResponse.json({ events: [] }, { status: 200 });
|
||||
|
||||
const sb = createClient(url, key, {
|
||||
|
||||
@@ -19,7 +19,7 @@ export async function POST(req: NextRequest) {
|
||||
[
|
||||
{
|
||||
role: 'system',
|
||||
content: `You are the Broadcast Beat AI Assistant — a knowledgeable, helpful assistant for broadcast engineering professionals. You have deep expertise in broadcast technology including live production, IP workflows, SMPTE ST 2110, audio engineering, cameras, streaming, post-production, MAM systems, and broadcast industry trends.
|
||||
content: `You are the AV Beat AI Assistant — a knowledgeable, helpful assistant for broadcast engineering professionals. You have deep expertise in broadcast technology including live production, IP workflows, SMPTE ST 2110, audio engineering, cameras, streaming, post-production, MAM systems, and broadcast industry trends.
|
||||
|
||||
You are responding in a community forum. Be helpful, technically accurate, and conversational. Acknowledge when a question is complex or when professional consultation is advisable. Keep responses focused and practical — broadcast engineers value concise, actionable information.
|
||||
|
||||
@@ -51,7 +51,7 @@ Please provide a helpful, technically accurate response for this broadcast engin
|
||||
.insert({
|
||||
thread_id,
|
||||
body: aiResponseText,
|
||||
author_name: 'Broadcast Beat AI',
|
||||
author_name: 'AV Beat AI',
|
||||
is_ai_response: true,
|
||||
})
|
||||
.select()
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function POST(req: NextRequest) {
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'Broadcast Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME || 'AV Beat'}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function POST(req: NextRequest) {
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Article shared with you — Broadcast Beat</title>
|
||||
<title>Article shared with you — AV Beat</title>
|
||||
</head>
|
||||
<body style="margin:0;padding:0;background:#0d0d0d;font-family:Arial,sans-serif;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0d0d0d;padding:32px 16px;">
|
||||
@@ -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;">Broadcast Beat</p>
|
||||
<p style="margin:0;color:#3b82f6;font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;">AV Beat</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
@@ -62,8 +62,8 @@ export async function POST(req: NextRequest) {
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background:#0a0a0a;border-top:1px solid #1a1a1a;padding:16px 28px;">
|
||||
<p style="margin:0;color:#444;font-size:11px;">You received this because someone shared a Broadcast Beat article with you.</p>
|
||||
<p style="margin:6px 0 0;color:#333;font-size:11px;">© ${new Date().getFullYear()} Broadcast Beat. All rights reserved.</p>
|
||||
<p style="margin:0;color:#444;font-size:11px;">You received this because someone shared a AV Beat article with you.</p>
|
||||
<p style="margin:6px 0 0;color:#333;font-size:11px;">© ${new Date().getFullYear()} AV Beat. All rights reserved.</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -74,7 +74,7 @@ export async function POST(req: NextRequest) {
|
||||
</html>`;
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"${process.env.SMTP_FROM_NAME || "Broadcast Beat"}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
from: `"${process.env.SMTP_FROM_NAME || "AV Beat"}" <${process.env.SMTP_FROM_EMAIL}>`,
|
||||
to,
|
||||
subject: `Shared article: ${articleTitle}`,
|
||||
html,
|
||||
|
||||
@@ -68,8 +68,8 @@ async function sendWelcome(email: string, topics?: string[]) {
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'AV Beat';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass || !fromEmail) {
|
||||
// SMTP not configured — signal that to logs but don't blow up.
|
||||
@@ -92,18 +92,18 @@ async function sendWelcome(email: string, topics?: string[]) {
|
||||
await transporter.sendMail({
|
||||
from: `"${fromName}" <${fromEmail}>`,
|
||||
to: email,
|
||||
subject: 'Welcome to Broadcast Beat',
|
||||
subject: 'Welcome to AV Beat',
|
||||
html: `
|
||||
<!DOCTYPE html>
|
||||
<html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>Welcome to Broadcast Beat</title></head>
|
||||
<html><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width,initial-scale=1" /><title>Welcome to AV Beat</title></head>
|
||||
<body style="margin:0;padding:0;background:#0a0e15;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#0a0e15;padding:40px 16px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" width="600" style="max-width:600px;background:#11192a;border:1px solid #1e3a5f;border-radius:10px;overflow:hidden;">
|
||||
<tr><td style="padding:32px 36px 8px;">
|
||||
<div style="font-family:'Courier New',monospace;font-size:10px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:#3b82f6;">Broadcast Beat Newsletter</div>
|
||||
<div style="font-family:'Courier New',monospace;font-size:10px;font-weight:700;letter-spacing:0.22em;text-transform:uppercase;color:#3b82f6;">AV Beat Newsletter</div>
|
||||
<h1 style="margin:14px 0 8px;font-size:24px;line-height:1.2;color:#ffffff;font-weight:800;">You're in</h1>
|
||||
<p style="margin:0;color:#cfd6e1;font-size:15px;line-height:1.55;">Thanks for subscribing to <strong style="color:#ffffff;">Broadcast Beat</strong> — the digital platform for broadcast engineering professionals.</p>
|
||||
<p style="margin:0;color:#cfd6e1;font-size:15px;line-height:1.55;">Thanks for subscribing to <strong style="color:#ffffff;">AV Beat</strong> — the digital platform for broadcast engineering professionals.</p>
|
||||
${topicsBlock}
|
||||
</td></tr>
|
||||
<tr><td style="padding:18px 36px 8px;">
|
||||
@@ -113,7 +113,7 @@ async function sendWelcome(email: string, topics?: string[]) {
|
||||
<a href="${siteUrl}/news" style="display:inline-block;background:#3b82f6;color:#ffffff;text-decoration:none;padding:11px 22px;border-radius:6px;font-weight:700;font-size:13px;letter-spacing:0.05em;text-transform:uppercase;">Browse Industry News</a>
|
||||
</td></tr>
|
||||
<tr><td style="padding:18px 36px 28px;border-top:1px solid #1e3a5f;">
|
||||
<p style="margin:0;color:#5a6470;font-size:11px;line-height:1.5;">You're receiving this because you signed up at <a href="${siteUrl}" style="color:#3b82f6;text-decoration:none;">broadcastbeat.com</a>. <a href="${siteUrl}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#98a3b3;">Unsubscribe anytime</a>.</p>
|
||||
<p style="margin:0;color:#5a6470;font-size:11px;line-height:1.5;">You're receiving this because you signed up at <a href="${siteUrl}" style="color:#3b82f6;text-decoration:none;">avbeat.com</a>. <a href="${siteUrl}/unsubscribe?email=${encodeURIComponent(email)}" style="color:#98a3b3;">Unsubscribe anytime</a>.</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</td></tr>
|
||||
|
||||
@@ -95,8 +95,8 @@ export async function POST(req: NextRequest) {
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
||||
const fromName = process.env.SMTP_FROM_NAME || 'AV Beat';
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
return NextResponse.json({ error: 'SMTP credentials not configured' }, { status: 500 });
|
||||
@@ -140,7 +140,7 @@ export async function POST(req: NextRequest) {
|
||||
<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;">Broadcast Beat</h1>
|
||||
<h1 style="color:#3b82f6;font-size:22px;margin:0;font-weight:bold;">AV Beat</h1>
|
||||
<p style="color:#555;font-size:12px;margin:4px 0 0;">News & Intelligence for Broadcast, Post-Production & Streaming Technology</p>
|
||||
</div>
|
||||
<!-- Body -->
|
||||
@@ -151,9 +151,9 @@ export async function POST(req: NextRequest) {
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;">
|
||||
<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:0;">You're receiving this because you subscribed at avbeat.com.</p>
|
||||
<p style="color:#555;font-size:12px;margin:8px 0 0;">
|
||||
<a href="${siteUrl}" style="color:#3b82f6;">Visit Broadcast Beat</a> ·
|
||||
<a href="${siteUrl}" style="color:#3b82f6;">Visit AV Beat</a> ·
|
||||
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -42,7 +42,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ slug: s
|
||||
const { count } = await advSvc
|
||||
.from("active_advertisers_by_property")
|
||||
.select("client_id", { count: "exact", head: true })
|
||||
.eq("property", "broadcastbeat")
|
||||
.eq("property", "avbeat")
|
||||
.eq("company_name_lower", needle.toLowerCase());
|
||||
isActiveAdvertiser = (count || 0) > 0;
|
||||
} catch { /* tolerate adv lookup failures */ }
|
||||
|
||||
@@ -35,7 +35,7 @@ export async function GET(request: Request) {
|
||||
|
||||
// Decorate + reorder so premium and current-advertiser stories surface
|
||||
// at the top of the feed.
|
||||
const decorated = await decorateHomepageFeed("broadcastbeat", base);
|
||||
const decorated = await decorateHomepageFeed("avbeat", base);
|
||||
const payload = decorated.map((d) => ({
|
||||
...d.article,
|
||||
promoted_kind: d.promoted_kind,
|
||||
|
||||
@@ -6,7 +6,7 @@ export const dynamic = "force-dynamic";
|
||||
|
||||
const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
||||
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb";
|
||||
const SCHEMA = process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av";
|
||||
|
||||
function client(schema = SCHEMA) {
|
||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||
@@ -75,7 +75,7 @@ export async function GET(req: Request) {
|
||||
const { data: ads } = await adv
|
||||
.from("active_advertisers_by_property")
|
||||
.select("company_name_lower")
|
||||
.eq("property", "broadcastbeat");
|
||||
.eq("property", "avbeat");
|
||||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||||
} catch { /* tolerate */ }
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
|
||||
const featuredImage = body?.featured_image_url || null;
|
||||
const attachments = Array.isArray(body?.attachments) ? body.attachments : [];
|
||||
const submissionId = body?.submission_id || null;
|
||||
const property = body?.property || 'broadcastbeat';
|
||||
const property = body?.property || 'avbeat';
|
||||
|
||||
// Defense in depth: re-check the contributor's approval status against
|
||||
// distribute.users. distribute-rmp's submit route already gates, but a
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '../../../lib/supabase/server';
|
||||
|
||||
const WP_BASE_URL = 'https://www.broadcastbeat.com/wp-json/wp/v2';
|
||||
const WP_BASE_URL = 'https://www.avbeat.com/wp-json/wp/v2';
|
||||
const PER_PAGE = 20;
|
||||
const IMAGE_BUCKET = 'post-images';
|
||||
|
||||
@@ -29,7 +29,7 @@ function stripHtml(html: string): string {
|
||||
async function fetchWPPosts(page: number): Promise<{ posts: WPPost[]; totalPages: number }> {
|
||||
const url = `${WP_BASE_URL}/posts?page=${page}&per_page=${PER_PAGE}&_embed=1&status=publish`;
|
||||
const res = await fetch(url, {
|
||||
headers: { 'User-Agent': 'Broadcast Beat-Importer/1.0' },
|
||||
headers: { 'User-Agent': 'AV Beat-Importer/1.0' },
|
||||
next: { revalidate: 0 },
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ async function downloadAndStoreImage(
|
||||
try {
|
||||
// Fetch the image
|
||||
const response = await fetch(imageUrl, {
|
||||
headers: { 'User-Agent': 'Broadcast Beat-Importer/1.0' },
|
||||
headers: { 'User-Agent': 'AV Beat-Importer/1.0' },
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
|
||||
@@ -229,9 +229,9 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{/* 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-[#E67E22] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link>
|
||||
<span>/</span>
|
||||
<Link href={sectionHref} className="hover:text-[#E67E22] transition-colors">{sectionLabel}</Link>
|
||||
<Link href={sectionHref} className="hover:text-[#3b82f6] transition-colors">{sectionLabel}</Link>
|
||||
<span>/</span>
|
||||
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
|
||||
</div>
|
||||
@@ -245,7 +245,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{/* Article Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#E67E22] hover:underline">
|
||||
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline">
|
||||
{article.category}
|
||||
</Link>
|
||||
<span className="text-xs text-[#777]">{article.date}</span>
|
||||
@@ -271,7 +271,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div>
|
||||
<Link
|
||||
href={`/authors/${article.authorSlug}`}
|
||||
className="font-heading font-bold text-sm hover:text-[#E67E22] transition-colors text-[#e0e0e0]">
|
||||
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
|
||||
{article.author}
|
||||
</Link>
|
||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||
@@ -286,7 +286,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<>
|
||||
<a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`}
|
||||
aria-label="Share via email" title="Email"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#E67E22] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
@@ -297,7 +297,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
|
||||
</a>
|
||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=AVBeat`}
|
||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`}
|
||||
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
|
||||
@@ -320,7 +320,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<button type="button"
|
||||
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
|
||||
aria-label="Copy article link" title="Copy link"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#E67E22] hover:bg-[#1a1a1a] transition-colors">
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
@@ -334,7 +334,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
onClick={handleSaveToReadingList}
|
||||
disabled={savingState !== "idle"}
|
||||
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#E67E22] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
{savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -368,7 +368,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{article.tags.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22] transition-colors rounded-sm cursor-default">
|
||||
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">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -380,7 +380,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div className="pt-6 border-t border-[#222]">
|
||||
<Link
|
||||
href={sectionHref}
|
||||
className="inline-flex items-center gap-2 text-[#E67E22] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
@@ -398,7 +398,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
{/* Related Articles */}
|
||||
{/* DO NOT OVERRIDE — related articles sidebar */}
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Articles
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -417,10 +417,10 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[#E67E22] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
{related.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -435,19 +435,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
recently-active threads so the box is never empty. */}
|
||||
{relatedForumThreads.length > 0 && (
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Forum Posts
|
||||
</h3>
|
||||
<ul className="space-y-3.5">
|
||||
{relatedForumThreads.map((t) => (
|
||||
<li key={t.id}>
|
||||
<Link href={`/forum/thread/${t.id}`} className="group block">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#E67E22] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
{t.title}
|
||||
</p>
|
||||
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
||||
{t.category_name && (
|
||||
<span className="text-[#E67E22] font-body font-bold uppercase tracking-wider">
|
||||
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider">
|
||||
{t.category_name}
|
||||
</span>
|
||||
)}
|
||||
@@ -461,7 +461,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
</ul>
|
||||
<Link
|
||||
href="/forum"
|
||||
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#E67E22] hover:underline font-body font-semibold uppercase tracking-wider"
|
||||
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#3b82f6] hover:underline font-body font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Browse all forum threads →
|
||||
</Link>
|
||||
@@ -479,7 +479,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#E67E22] 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-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
{saveToast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -47,7 +47,7 @@ interface Stats {
|
||||
}
|
||||
|
||||
const PUBLICATIONS = [
|
||||
{ id: 'broadcast-beat', name: 'Broadcast Beat', short: 'BB' },
|
||||
{ id: 'broadcast-beat', name: 'AV Beat', short: 'BB' },
|
||||
{ id: 'av-beat', name: 'AV Beat', short: 'AV' },
|
||||
];
|
||||
|
||||
@@ -218,7 +218,7 @@ export default function AdOpsDashboard() {
|
||||
</button>
|
||||
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium bg-orange-900/50 text-orange-300 border border-orange-700">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-orange-400" />
|
||||
adops@broadcastbeat.com
|
||||
adops@avbeat.com
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,9 +191,9 @@ const QUICK_ACTIONS = [
|
||||
];
|
||||
|
||||
const QUICK_ACTION_TEMPLATES: Record<string, string> = {
|
||||
url_story: `Write a news story for [Broadcast Beat / AV Beat] based on this URL: [URL]. Use the byline [pen name]. Target length: [400-600] words. Focus angle: [product launch / company news / industry trend].`,
|
||||
url_story: `Write a news story for [AV Beat / AV Beat] based on this URL: [URL]. Use the byline [pen name]. Target length: [400-600] words. Focus angle: [product launch / company news / industry trend].`,
|
||||
exhibitor_preview: `Write a pre-show exhibitor preview for [company name] at [NAB Show / InfoComm] [year]. They will be showing [product/technology]. Booth: [number if known]. Use [pen name] byline. 400-500 words.`,
|
||||
press_release: `Rewrite the following press release as a real news story for [Broadcast Beat / AV Beat]. Remove marketing language, lead with the news hook, add industry context. Byline: [pen name].
|
||||
press_release: `Rewrite the following press release as a real news story for [AV Beat / AV Beat]. Remove marketing language, lead with the news hook, add industry context. Byline: [pen name].
|
||||
|
||||
[PASTE PRESS RELEASE HERE]`,
|
||||
nab_story: `Write a [story type: preview / announcement / recap] story about [topic/company] for NAB Show [year]. Include relevant SEO keywords. Byline: James Mercer. [400-800] words.`,
|
||||
@@ -203,7 +203,7 @@ Story headline: [HEADLINE]
|
||||
Story summary: [SUMMARY]`,
|
||||
headlines: `Generate 8 headline options for this story. Include 2 SEO-optimized versions targeting [keyword], 2 social media-friendly versions, 2 trade publication style versions, and 2 alternatives. Story summary: [SUMMARY]`,
|
||||
translate: `Translate the following story into [language]. Maintain the professional trade publication tone. Keep technical terms in their internationally recognized form. [PASTE STORY HERE]`,
|
||||
company_bio: `Write a 150-word company bio for [company name] suitable for use as a boilerplate paragraph at the end of news stories on [Broadcast Beat / AV Beat]. The company makes [products/services]. Website: [URL].`,
|
||||
company_bio: `Write a 150-word company bio for [company name] suitable for use as a boilerplate paragraph at the end of news stories on [AV Beat / AV Beat]. The company makes [products/services]. Website: [URL].`,
|
||||
product_desc: `Write a 200-word product description for [product name] by [company]. Key features: [features]. Target audience: [integrators / broadcast engineers / etc]. Suitable for use in a product announcement story.`,
|
||||
};
|
||||
|
||||
@@ -243,7 +243,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
|
||||
<div className="flex items-center justify-between px-5 py-4 border-b border-[#1e2a3a]">
|
||||
<div>
|
||||
<h2 className="text-white font-semibold text-sm">Draft Preview</h2>
|
||||
<p className="text-[#555] text-xs mt-0.5">Review before saving to {siteId === 1 ? 'Broadcast Beat' : 'AV Beat'}</p>
|
||||
<p className="text-[#555] text-xs mt-0.5">Review before saving to {siteId === 1 ? 'AV Beat' : 'AV Beat'}</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-[#555] hover:text-white transition-colors text-lg leading-none">✕</button>
|
||||
</div>
|
||||
@@ -256,7 +256,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[#555] text-xs">Site:</span>
|
||||
<span className={`text-xs font-bold px-1.5 py-0.5 rounded ${siteId === 2 ? 'bg-orange-900/40 text-orange-400' : 'bg-blue-900/40 text-blue-400'}`}>
|
||||
{siteId === 2 ? 'AV Beat' : 'Broadcast Beat'}
|
||||
{siteId === 2 ? 'AV Beat' : 'AV Beat'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -487,7 +487,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
||||
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'}`}
|
||||
>
|
||||
{f === 'ALL' ? 'All Writers' : f === 'BB' ? 'Broadcast Beat' : 'AV Beat'}
|
||||
{f === 'ALL' ? 'All Writers' : f === 'BB' ? 'AV Beat' : 'AV Beat'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -670,7 +670,7 @@ export default function AIConsolePage() {
|
||||
const penNames = selectedSite === 1 ? BB_PEN_NAMES : AV_PEN_NAMES;
|
||||
|
||||
const buildSystemPrompt = () => {
|
||||
const siteName = selectedSite === 1 ? 'Broadcast Beat' : 'AV Beat';
|
||||
const siteName = selectedSite === 1 ? 'AV Beat' : 'AV Beat';
|
||||
const siteDesc = selectedSite === 1
|
||||
? 'a professional B2B trade publication for broadcast engineers, production professionals, and industry executives'
|
||||
: 'a professional B2B trade publication for AV integrators, technology managers, and AV industry professionals';
|
||||
@@ -1068,7 +1068,7 @@ Respond conversationally and helpfully. If asked to write a story, write the ful
|
||||
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]"
|
||||
>
|
||||
<option value={1}>Broadcast Beat</option>
|
||||
<option value={1}>AV Beat</option>
|
||||
<option value={2}>AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -303,12 +303,12 @@ export default function AIConsoleSettingsPage() {
|
||||
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]"
|
||||
>
|
||||
<option value={1}>Broadcast Beat</option>
|
||||
<option value={1}>AV Beat</option>
|
||||
<option value={2}>AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Pen Name (Broadcast Beat)</label>
|
||||
<label className="block text-[#aaa] text-xs font-medium mb-1.5">Default Pen Name (AV Beat)</label>
|
||||
<select
|
||||
value={settings.default_pen_name_bb}
|
||||
onChange={e => update('default_pen_name_bb', e.target.value)}
|
||||
|
||||
@@ -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-[#E67E22] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#3b82f6] 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-[#E67E22] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors"
|
||||
className="px-3 py-1.5 bg-[#3b82f6] 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-[#E67E22] text-white'
|
||||
? 'bg-[#3b82f6] 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-[#E67E22] text-white'
|
||||
? 'border-[#3b82f6] 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-[#E67E22] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-8 h-8 border-2 border-[#3b82f6] 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-[#E67E22] hover:underline">{event.official_site}</a>
|
||||
className="text-[#3b82f6] 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-[#E67E22] text-white'
|
||||
? 'bg-[#3b82f6] 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-[#E67E22] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
className="flex-1 py-2 bg-[#3b82f6] 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-[#E67E22] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
className="flex-1 py-2 bg-[#3b82f6] hover:bg-blue-500 text-white text-sm rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === 'generate' ? 'Generating...' : 'Generate Story'}
|
||||
</button>
|
||||
|
||||
@@ -54,7 +54,7 @@ function ForumLoginInner() {
|
||||
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]">
|
||||
<div className="max-w-md mx-auto px-4 py-16">
|
||||
<div className="text-center mb-6">
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#E67E22] font-mono mb-2">Welcome back</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-2">Welcome back</div>
|
||||
<h1 className="text-3xl font-heading font-bold text-white">Sign in to AV Beat</h1>
|
||||
<p className="text-[#888] font-body text-sm mt-2">
|
||||
Your account covers the forum, newsletter preferences, and saved articles.
|
||||
@@ -79,7 +79,7 @@ function ForumLoginInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -92,20 +92,20 @@ function ForumLoginInner() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
|
||||
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2.5 rounded-lg transition-colors"
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign In'}
|
||||
</button>
|
||||
<div className="text-right text-xs font-body">
|
||||
<Link
|
||||
href={`/forum/forgot-password?email=${encodeURIComponent(email)}`}
|
||||
className="text-[#888] hover:text-[#E67E22] hover:underline"
|
||||
className="text-[#888] hover:text-[#3b82f6] hover:underline"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
@@ -115,7 +115,7 @@ function ForumLoginInner() {
|
||||
No account yet?{' '}
|
||||
<Link
|
||||
href={`/forum/register?next=${encodeURIComponent(next)}`}
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
Create one
|
||||
</Link>
|
||||
|
||||
@@ -103,7 +103,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
|
||||
const getMetricValue = (thread: ForumThread) => {
|
||||
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#E67E22]' };
|
||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
|
||||
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
|
||||
};
|
||||
|
||||
@@ -123,7 +123,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-[#E67E22] text-white'
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
: 'text-[#666] hover:text-[#aaa]'
|
||||
}`}
|
||||
>
|
||||
@@ -171,13 +171,13 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
||||
</span>
|
||||
{/* Thread info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white font-body text-sm font-semibold group-hover:text-[#E67E22] transition-colors truncate leading-snug">
|
||||
<p className="text-white font-body text-sm font-semibold group-hover:text-[#3b82f6] transition-colors truncate leading-snug">
|
||||
{thread.title}
|
||||
</p>
|
||||
<p className="text-[#555] font-body text-xs mt-0.5">
|
||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#E67E22] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||
{thread.forum_categories && (
|
||||
<> · <span className="text-[#E67E22]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -301,7 +301,7 @@ export default function ForumIndexPage() {
|
||||
{currentUserId ? (
|
||||
<Link
|
||||
href="/forum/new"
|
||||
className="inline-flex items-center gap-2 bg-[#E67E22] hover:bg-[#C8651B] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
|
||||
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
@@ -311,7 +311,7 @@ export default function ForumIndexPage() {
|
||||
) : (
|
||||
<Link
|
||||
href="/forum/login?next=/forum"
|
||||
className="inline-flex items-center gap-2 bg-[#E67E22] hover:bg-[#C8651B] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
|
||||
className="inline-flex items-center gap-2 bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm rounded-lg px-5 py-2.5 transition-colors"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
|
||||
@@ -333,7 +333,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-[#E67E22] text-white' : 'text-[#888] hover:text-white'
|
||||
feedMode === 'discover' ? 'bg-[#3b82f6] text-white' : 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
@@ -344,7 +344,7 @@ export default function ForumIndexPage() {
|
||||
<button
|
||||
onClick={() => setFeedMode('following')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
|
||||
feedMode === 'following' ? 'bg-[#E67E22] text-white' : 'text-[#888] hover:text-white'
|
||||
feedMode === 'following' ? 'bg-[#3b82f6] 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">
|
||||
@@ -363,7 +363,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-[#E67E22] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#C8651B] transition-colors"
|
||||
className="inline-block bg-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
@@ -384,7 +384,7 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
href="/forum"
|
||||
onClick={() => setFeedMode('discover')}
|
||||
className="inline-block bg-[#E67E22] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#C8651B] transition-colors"
|
||||
className="inline-block bg-[#3b82f6] text-white font-body text-sm font-bold px-5 py-2 rounded-lg hover:bg-[#2563eb] transition-colors"
|
||||
>
|
||||
Discover Members
|
||||
</Link>
|
||||
@@ -398,10 +398,10 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={thread.id}
|
||||
href={`/forum/thread/${thread.id}`}
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#E67E22] 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-[#3b82f6] 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-[#E67E22] transition-colors truncate">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
@@ -409,7 +409,7 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
href={`/profile/${thread.author_id}`}
|
||||
onClick={e => e.stopPropagation()}
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
{thread.author_name}
|
||||
</Link>
|
||||
@@ -450,7 +450,7 @@ export default function ForumIndexPage() {
|
||||
placeholder="Search threads..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#E67E22] 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-[#3b82f6] rounded-lg pl-9 pr-4 py-2.5 text-white font-body text-sm placeholder-[#555] focus:outline-none transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
@@ -470,7 +470,7 @@ export default function ForumIndexPage() {
|
||||
<select
|
||||
value={selectedCategory}
|
||||
onChange={e => setSelectedCategory(e.target.value)}
|
||||
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#E67E22] 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-[#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]"
|
||||
>
|
||||
<option value="">All Categories</option>
|
||||
{categories.map(cat => (
|
||||
@@ -490,7 +490,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-[#E67E22] text-white'
|
||||
? 'bg-[#3b82f6] text-white'
|
||||
: 'text-[#888] hover:text-white'
|
||||
}`}
|
||||
>
|
||||
@@ -523,16 +523,16 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={thread.id}
|
||||
href={`/forum/thread/${thread.id}`}
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#E67E22] 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-[#3b82f6] 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-[#E67E22] transition-colors truncate">
|
||||
<h3 className="text-white font-body font-semibold text-sm group-hover:text-[#3b82f6] transition-colors truncate">
|
||||
{thread.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#E67E22] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#3b82f6] hover:underline" onClick={(e) => e.stopPropagation()}>{thread.author_name}</Link>
|
||||
{thread.forum_categories && (
|
||||
<> · <span className="text-[#E67E22]">{thread.forum_categories.name}</span></>
|
||||
<> · <span className="text-[#3b82f6]">{thread.forum_categories.name}</span></>
|
||||
)}
|
||||
{' · '}{timeAgo(thread.created_at)}
|
||||
</p>
|
||||
@@ -584,11 +584,11 @@ export default function ForumIndexPage() {
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={`/forum/${cat.slug}`}
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#E67E22] rounded-lg p-4 transition-all group"
|
||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#3b82f6] 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-[#E67E22] transition-colors">
|
||||
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#3b82f6] transition-colors">
|
||||
{cat.name}
|
||||
</h2>
|
||||
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
|
||||
@@ -610,7 +610,7 @@ export default function ForumIndexPage() {
|
||||
<div className="text-white font-body font-semibold text-sm">{cat.post_count}</div>
|
||||
<div className="text-[#666] font-body text-xs">posts</div>
|
||||
</div>
|
||||
<svg className="w-4 h-4 text-[#555] group-hover:text-[#E67E22] 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-[#3b82f6] 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>
|
||||
@@ -629,7 +629,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-[#E67E22]">AV Beat AI</span> assistant is available in threads to help answer questions — it is clearly labeled</li>
|
||||
<li>• The <span className="text-[#3b82f6]">AV Beat AI</span> assistant is available in threads to help answer questions — it is clearly labeled</li>
|
||||
<li>• No spam, self-promotion, or vendor pitches without disclosure</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -105,7 +105,7 @@ function ForumRegisterInner() {
|
||||
<div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start">
|
||||
{/* Left rail — what membership gets you */}
|
||||
<aside className="hidden lg:block">
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#E67E22] font-mono mb-3">Membership</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-3">Membership</div>
|
||||
<h1 className="font-heading text-white text-4xl xl:text-5xl font-bold leading-tight mb-4">
|
||||
Join AV Beat
|
||||
</h1>
|
||||
@@ -131,7 +131,7 @@ function ForumRegisterInner() {
|
||||
<div className="mt-6 text-[11px] text-[#666] leading-relaxed max-w-md border-t border-[#252525] pt-4">
|
||||
<strong className="text-[#888]">PR firms & contributors:</strong> a separate contributor application is required to submit press releases.
|
||||
{" "}
|
||||
<Link href="/about/advertise" className="text-[#E67E22] hover:underline">Contact us</Link> if you represent a manufacturer or agency.
|
||||
<Link href="/about/advertise" className="text-[#3b82f6] hover:underline">Contact us</Link> if you represent a manufacturer or agency.
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -148,7 +148,7 @@ function ForumRegisterInner() {
|
||||
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl"
|
||||
>
|
||||
<div className="hidden lg:block mb-2">
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#E67E22] font-mono mb-1">Create your account</div>
|
||||
<div className="text-[10px] uppercase tracking-widest text-[#3b82f6] font-mono mb-1">Create your account</div>
|
||||
<div className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div>
|
||||
</div>
|
||||
{error && (
|
||||
@@ -171,7 +171,7 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setFullName(e.target.value)}
|
||||
autoComplete="name"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -184,7 +184,7 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -198,7 +198,7 @@ function ForumRegisterInner() {
|
||||
autoComplete="new-password"
|
||||
required
|
||||
minLength={6}
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -211,21 +211,21 @@ function ForumRegisterInner() {
|
||||
onChange={(e) => setConfirm(e.target.value)}
|
||||
autoComplete="new-password"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
|
||||
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-3 rounded-lg transition-colors"
|
||||
>
|
||||
{loading ? 'Creating account…' : 'Create Free Account'}
|
||||
</button>
|
||||
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
|
||||
By creating an account you agree to our{' '}
|
||||
<Link href="/terms" className="text-[#888] hover:text-[#E67E22] underline">terms</Link>
|
||||
<Link href="/terms" className="text-[#888] hover:text-[#3b82f6] underline">terms</Link>
|
||||
{' '}and{' '}
|
||||
<Link href="/privacy" className="text-[#888] hover:text-[#E67E22] underline">privacy policy</Link>.
|
||||
<Link href="/privacy" className="text-[#888] hover:text-[#3b82f6] underline">privacy policy</Link>.
|
||||
We’ll never sell your email.
|
||||
</p>
|
||||
<SocialAuthButtons next={next} />
|
||||
@@ -233,7 +233,7 @@ function ForumRegisterInner() {
|
||||
Already have an account?{' '}
|
||||
<Link
|
||||
href={`/forum/login?next=${encodeURIComponent(next)}`}
|
||||
className="text-[#E67E22] hover:underline font-semibold"
|
||||
className="text-[#3b82f6] hover:underline font-semibold"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
|
||||
@@ -67,8 +67,8 @@ 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-[#E67E22] flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-[#E67E22] text-xs font-bold">AI</span>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -133,7 +133,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
}
|
||||
};
|
||||
|
||||
const scoreColor = counts.vote_score > 0 ? 'text-[#E67E22]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]';
|
||||
const scoreColor = counts.vote_score > 0 ? 'text-[#3b82f6]' : counts.vote_score < 0 ? 'text-red-400' : 'text-[#666]';
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-2">
|
||||
@@ -144,8 +144,8 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Upvote'}
|
||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
|
||||
currentVote === 1
|
||||
? 'bg-[#E67E22]/20 border-[#E67E22] text-[#E67E22]'
|
||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#E67E22] hover:text-[#E67E22]'
|
||||
? 'bg-[#3b82f6]/20 border-[#3b82f6] text-[#3b82f6]'
|
||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
aria-label="Upvote"
|
||||
>
|
||||
@@ -178,7 +178,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
||||
</button>
|
||||
|
||||
{!isAuthenticated && (
|
||||
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#E67E22] ml-1 transition-colors">
|
||||
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#3b82f6] ml-1 transition-colors">
|
||||
Sign in to vote
|
||||
</Link>
|
||||
)}
|
||||
@@ -322,7 +322,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-[#E67E22] hover:underline font-body text-sm">
|
||||
<Link href="/forum" className="mt-4 inline-block text-[#3b82f6] hover:underline font-body text-sm">
|
||||
← Back to Forum
|
||||
</Link>
|
||||
</div>
|
||||
@@ -342,11 +342,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-[#E67E22] transition-colors">Forum</Link>
|
||||
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
|
||||
<span className="mx-2">›</span>
|
||||
{category && (
|
||||
<>
|
||||
<Link href={`/forum/${category.slug}`} className="hover:text-[#E67E22] transition-colors">{category.name}</Link>
|
||||
<Link href={`/forum/${category.slug}`} className="hover:text-[#3b82f6] transition-colors">{category.name}</Link>
|
||||
<span className="mx-2">›</span>
|
||||
</>
|
||||
)}
|
||||
@@ -366,7 +366,7 @@ export default function ForumThreadPage() {
|
||||
<Avatar name={thread.author_name} isAI={false} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#E67E22] hover:underline">{thread.author_name}</Link>
|
||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#3b82f6] hover:underline">{thread.author_name}</Link>
|
||||
<span className="text-[#555] font-body text-xs">·</span>
|
||||
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
|
||||
<span className="text-[#555] font-body text-xs">·</span>
|
||||
@@ -403,9 +403,9 @@ export default function ForumThreadPage() {
|
||||
<Avatar name={reply.author_name} isAI={reply.is_ai_response} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||
<Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#E67E22] hover:underline">{reply.author_name}</Link>
|
||||
<Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#3b82f6] hover:underline">{reply.author_name}</Link>
|
||||
{reply.is_ai_response && (
|
||||
<span className="text-xs bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/30 px-2 py-0.5 rounded font-body font-semibold">
|
||||
<span className="text-xs bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/30 px-2 py-0.5 rounded font-body font-semibold">
|
||||
AI Assistant
|
||||
</span>
|
||||
)}
|
||||
@@ -443,12 +443,12 @@ export default function ForumThreadPage() {
|
||||
{/* AI Assistant Panel */}
|
||||
<div className="bg-[#0d1f35] border border-[#1e3a5f] 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-[#E67E22] flex items-center justify-center">
|
||||
<span className="text-[#E67E22] text-xs font-bold">AI</span>
|
||||
<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>
|
||||
<div>
|
||||
<span className="text-white font-body font-semibold text-sm">AV Beat AI Assistant</span>
|
||||
<span className="ml-2 text-xs bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/30 px-2 py-0.5 rounded font-body">AI — Not a human</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>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[#7a9ab8] font-body text-xs mb-3">
|
||||
@@ -461,12 +461,12 @@ export default function ForumThreadPage() {
|
||||
value={aiQuestion}
|
||||
onChange={e => setAiQuestion(e.target.value)}
|
||||
disabled={aiLoading}
|
||||
className="flex-1 bg-[#111] border border-[#2a3a50] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#3a5a7a] focus:outline-none focus:border-[#E67E22] 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-[#3b82f6] disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={aiLoading || !aiQuestion.trim()}
|
||||
className="bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body font-semibold text-sm px-4 py-2 rounded-lg transition-colors flex-shrink-0"
|
||||
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"
|
||||
>
|
||||
{aiLoading ? (
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -495,7 +495,7 @@ 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-[#E67E22] 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-[#3b82f6] resize-none"
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
|
||||
@@ -509,7 +509,7 @@ export default function ForumThreadPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submitting || !replyBody.trim()}
|
||||
className="bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
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"
|
||||
>
|
||||
{submitting ? 'Posting...' : 'Post Reply'}
|
||||
</button>
|
||||
@@ -526,13 +526,13 @@ export default function ForumThreadPage() {
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Link
|
||||
href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
||||
className="bg-[#E67E22] hover:bg-[#C8651B] text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
<Link
|
||||
href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
||||
className="border border-[#E67E22] text-[#E67E22] hover:bg-[#E67E22]/10 font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
className="border border-[#3b82f6] text-[#3b82f6] hover:bg-[#3b82f6]/10 font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
|
||||
@@ -49,7 +49,7 @@ export default async function GearPage() {
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={`/articles/${article.slug}`}
|
||||
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#E67E22] transition-colors">
|
||||
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#3b82f6] transition-colors">
|
||||
<div className="relative h-[200px] overflow-hidden">
|
||||
<AppImage
|
||||
src={article.image}
|
||||
@@ -59,7 +59,7 @@ export default async function GearPage() {
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||
/>
|
||||
<div className="absolute top-2 left-2">
|
||||
<span className="bg-[#E67E22] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase">
|
||||
<span className="bg-[#3b82f6] text-white font-body text-[9px] font-bold px-2 py-0.5 tracking-wider uppercase">
|
||||
{article.category}
|
||||
</span>
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@ export default async function GearPage() {
|
||||
)}
|
||||
<span className="text-[#555] font-body text-[11px]">{article.readTime}</span>
|
||||
</div>
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#E67E22] transition-colors line-clamp-3">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#3b82f6] 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: "#E67E22", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }}
|
||||
style={{ padding: "8px 16px", background: "#3b82f6", color: "white", border: 0, cursor: "pointer", borderRadius: 2 }}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
|
||||
@@ -314,7 +314,7 @@ export default function ArticleFeed() {
|
||||
value={sortOrder}
|
||||
onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }}
|
||||
aria-label="Sort articles"
|
||||
className="bg-[#111] border border-[#2a2a2a] text-[#aaa] font-body text-[11px] font-semibold uppercase tracking-wide rounded-sm pl-2.5 pr-7 py-1 appearance-none focus:outline-none focus:border-[#E67E22] focus:ring-1 focus:ring-[#E67E22]/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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark] cursor-pointer hover:border-[#3a3a3a]"
|
||||
>
|
||||
<option value="recent">Recent</option>
|
||||
<option value="trending">Trending</option>
|
||||
@@ -333,8 +333,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-[#E67E22] ${
|
||||
feedMode === "pagination" ?"bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/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-[#3b82f6] ${
|
||||
feedMode === "pagination" ?"bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
}`}>
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="2.5" rx="0.5" fill="currentColor" opacity="0.9"/>
|
||||
@@ -347,8 +347,8 @@ export default function ArticleFeed() {
|
||||
onClick={() => handleFeedModeChange("infinite")}
|
||||
aria-pressed={feedMode === "infinite"}
|
||||
title="Infinite scroll mode"
|
||||
className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
||||
feedMode === "infinite" ?"bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/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-[#3b82f6] ${
|
||||
feedMode === "infinite" ?"bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
}`}>
|
||||
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<rect x="1" y="1" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.9"/>
|
||||
@@ -372,7 +372,7 @@ export default function ArticleFeed() {
|
||||
value={dateFrom}
|
||||
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||
max={dateTo || undefined}
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#E67E22] transition-colors [color-scheme:dark]"
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
title="From date"
|
||||
aria-label="Filter articles from date"
|
||||
/>
|
||||
@@ -382,7 +382,7 @@ export default function ArticleFeed() {
|
||||
value={dateTo}
|
||||
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||
min={dateFrom || undefined}
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#E67E22] transition-colors [color-scheme:dark]"
|
||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
title="To date"
|
||||
aria-label="Filter articles to date"
|
||||
/>
|
||||
@@ -401,9 +401,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-[#E67E22] 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-[#3b82f6] whitespace-nowrap ${
|
||||
showAdvanced || hasAdvancedActive
|
||||
? "bg-[#E67E22]/15 text-[#E67E22] border-[#E67E22]/40"
|
||||
? "bg-[#3b82f6]/15 text-[#3b82f6] border-[#3b82f6]/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">
|
||||
@@ -411,7 +411,7 @@ export default function ArticleFeed() {
|
||||
</svg>
|
||||
More
|
||||
{hasAdvancedActive && (
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#E67E22] inline-block ml-0.5" aria-label="Active filters" />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6] inline-block ml-0.5" aria-label="Active filters" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -428,9 +428,9 @@ export default function ArticleFeed() {
|
||||
type="button"
|
||||
onClick={() => { setActiveCategory(cat); setPage(1); }}
|
||||
aria-pressed={isActive}
|
||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${
|
||||
isActive
|
||||
? "bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/50"
|
||||
? "bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/50"
|
||||
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc] hover:border-[#3a3a3a]"
|
||||
}`}>
|
||||
{cat}
|
||||
@@ -458,7 +458,7 @@ export default function ArticleFeed() {
|
||||
id={`${feedId}-author`}
|
||||
value={authorFilter}
|
||||
onChange={(e) => { setAuthorFilter(e.target.value); setPage(1); }}
|
||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 pr-8 appearance-none focus:outline-none focus:border-[#E67E22] focus:ring-1 focus:ring-[#E67E22]/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-[#3b82f6] focus:ring-1 focus:ring-[#3b82f6]/30 transition-colors [color-scheme:dark]"
|
||||
aria-label="Filter by author">
|
||||
<option value="All">All Authors</option>
|
||||
{uniqueAuthors.map((author) => (
|
||||
@@ -483,10 +483,10 @@ export default function ArticleFeed() {
|
||||
key={sf}
|
||||
onClick={() => { setSourceFilter(sf); setPage(1); }}
|
||||
aria-pressed={isActive}
|
||||
className={`px-3 py-1 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
||||
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] ${
|
||||
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-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
||||
: 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}
|
||||
{sf === "Imported" && importedCount > 0 && (
|
||||
@@ -507,7 +507,7 @@ export default function ArticleFeed() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }}
|
||||
className="font-body text-[11px] text-[#E67E22] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="font-body text-[11px] text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear advanced filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -537,7 +537,7 @@ export default function ArticleFeed() {
|
||||
{sortedFiltered.length === 0 && (
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="ml-1 text-[#E67E22] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="ml-1 text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
@@ -557,7 +557,7 @@ export default function ArticleFeed() {
|
||||
</p>
|
||||
<button
|
||||
onClick={clearAllFilters}
|
||||
className="mt-4 font-body text-sm text-[#E67E22] hover:underline focus:outline-none focus-visible:underline">
|
||||
className="mt-4 font-body text-sm text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||
Clear filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -585,7 +585,7 @@ export default function ArticleFeed() {
|
||||
const promotedClass = promoted === 'premium'
|
||||
? 'border-l-4 border-amber-400 bg-gradient-to-r from-amber-500/10 to-transparent pl-3 -ml-2 mr-0'
|
||||
: promoted === 'client'
|
||||
? 'border-l-4 border-[#E67E22] bg-gradient-to-r from-[#E67E22]/8 to-transparent pl-3 -ml-2 mr-0'
|
||||
? 'border-l-4 border-[#3b82f6] bg-gradient-to-r from-[#3b82f6]/8 to-transparent pl-3 -ml-2 mr-0'
|
||||
: '';
|
||||
return (
|
||||
<Fragment key={`feed-${i}`}>
|
||||
@@ -593,7 +593,7 @@ export default function ArticleFeed() {
|
||||
<Link
|
||||
href={articleHref}
|
||||
{...linkProps}
|
||||
className="flex-shrink-0 img-zoom overflow-hidden w-[100px] h-[70px] sm:w-[140px] sm:h-[95px] md:w-[190px] md:h-[128px] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]"
|
||||
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]"
|
||||
tabIndex={-1}
|
||||
aria-hidden="true"
|
||||
>
|
||||
@@ -614,21 +614,21 @@ export default function ArticleFeed() {
|
||||
</span>
|
||||
)}
|
||||
{promoted === 'client' && (
|
||||
<span className="font-body text-[9px] font-bold text-[#7aa7d4] bg-[#E67E22]/15 border border-[#E67E22]/40 px-1.5 py-0.5 rounded uppercase tracking-widest" title={article?.promoted_advertiser ? `Advertiser coverage: ${article.promoted_advertiser}` : 'Advertiser coverage'}>
|
||||
<span className="font-body text-[9px] font-bold text-[#7aa7d4] bg-[#3b82f6]/15 border border-[#3b82f6]/40 px-1.5 py-0.5 rounded uppercase tracking-widest" title={article?.promoted_advertiser ? `Advertiser coverage: ${article.promoted_advertiser}` : 'Advertiser coverage'}>
|
||||
◆ Advertiser
|
||||
</span>
|
||||
)}
|
||||
{article?.category && article.category.toLowerCase() !== "legacy" && (
|
||||
<span className="font-body text-[10px] font-bold text-[#E67E22] uppercase tracking-wide">
|
||||
<span className="font-body text-[10px] font-bold text-[#3b82f6] uppercase tracking-wide">
|
||||
{article.category}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-heading text-[#e0e0e0] font-bold text-[0.875rem] md:text-[1rem] leading-snug mb-1 md:mb-1.5 group-hover:text-[#E67E22] 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-[#3b82f6] transition-colors duration-200 line-clamp-2">
|
||||
<Link
|
||||
href={articleHref}
|
||||
{...linkProps}
|
||||
className="focus:outline-none focus-visible:text-[#E67E22] focus-visible:underline">
|
||||
className="focus:outline-none focus-visible:text-[#3b82f6] focus-visible:underline">
|
||||
{article?.title}
|
||||
</Link>
|
||||
</h3>
|
||||
@@ -675,7 +675,7 @@ export default function ArticleFeed() {
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#0a66c2] hover:bg-[#1a1a1a] transition-colors">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
|
||||
</a>
|
||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}&via=AVBeat`}
|
||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}&via=BroadcastBeat`}
|
||||
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-white hover:bg-[#1a1a1a] transition-colors">
|
||||
@@ -702,7 +702,7 @@ export default function ArticleFeed() {
|
||||
<a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
||||
aria-label="Share via email" title="Email"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#E67E22] hover:bg-[#1a1a1a] transition-colors">
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
</svg>
|
||||
@@ -735,9 +735,9 @@ export default function ArticleFeed() {
|
||||
{isLoadingMore && (
|
||||
<div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles">
|
||||
<div className="flex gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#E67E22] animate-bounce" style={{ animationDelay: "0ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#E67E22] animate-bounce" style={{ animationDelay: "150ms" }} />
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#E67E22] animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||
<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" }} />
|
||||
</div>
|
||||
<span className="font-body text-xs text-[#555]">Loading more articles…</span>
|
||||
</div>
|
||||
@@ -762,7 +762,7 @@ export default function ArticleFeed() {
|
||||
</p>
|
||||
<div className="flex items-center gap-1" role="navigation" aria-label="Pagination">
|
||||
<button
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]"
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||
disabled={page === 1}
|
||||
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Previous page">
|
||||
@@ -786,7 +786,7 @@ export default function ArticleFeed() {
|
||||
) : (
|
||||
<button
|
||||
key={p}
|
||||
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${page === p ? "active" : ""}`}
|
||||
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6] ${page === p ? "active" : ""}`}
|
||||
onClick={() => { setPage(p as number); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label={`Page ${p}`}
|
||||
aria-current={page === p ? "page" : undefined}>
|
||||
@@ -796,7 +796,7 @@ export default function ArticleFeed() {
|
||||
);
|
||||
})()}
|
||||
<button
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]"
|
||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||
disabled={page === totalPages}
|
||||
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||
aria-label="Next page">
|
||||
|
||||
@@ -9,7 +9,7 @@ const hero = {
|
||||
title: "Sony BRC-AM7 + HXC-FZ90: Better Together in the Field",
|
||||
category: "BROADCAST",
|
||||
excerpt: "Sony's latest camera pairing delivers unprecedented flexibility for field production teams, combining 4K HDR capture with compact form factors designed for the realities of live broadcast.",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/BRC-AM7_012-Mid.png",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/BRC-AM7_012-Mid.png",
|
||||
alt: "Sony BRC-AM7 broadcast camera in field setting",
|
||||
slug: "sony-brc-am7-hxc-fz90",
|
||||
};
|
||||
@@ -19,28 +19,28 @@ const rotatingStories = [
|
||||
{
|
||||
title: "Sony\'s BVM-HX1710: The Compact Powerhouse Changing How Broadcast & Post See Their Work",
|
||||
category: "POST PRODUCTION",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/bvmhx1710_3q_250131_02-copy-scaled.png",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/bvmhx1710_3q_250131_02-copy-scaled.png",
|
||||
alt: "Sony BVM-HX1710 broadcast monitor three-quarter view",
|
||||
slug: "sony-bvm-hx1710",
|
||||
},
|
||||
{
|
||||
title: "The Top 5 Challenges in MCRs & NOCs—And How to Stay Ahead",
|
||||
category: "BROADCAST",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/data-insight-ip-monitoring-1024x597.pngw3_.webp",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/data-insight-ip-monitoring-1024x597.pngw3_.webp",
|
||||
alt: "IP monitoring dashboard showing data insights for MCR operations",
|
||||
slug: "top-5-challenges-mcrs-nocs",
|
||||
},
|
||||
{
|
||||
title: "TESSERA SQ200: THE NEW PACE-SETTER IN LED PROCESSING",
|
||||
category: "FEATURED",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/Hero-Image_SQ200-Gen-3_Sep2025_CMYK.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/Hero-Image_SQ200-Gen-3_Sep2025_CMYK.jpg",
|
||||
alt: "Tessera SQ200 Gen 3 LED processor hero image",
|
||||
slug: "tessera-sq200-led-processing",
|
||||
},
|
||||
{
|
||||
title: "Backlight adds real-time collaboration to Iconik, AI-powered livestreams to Wildmoka at IBC2025",
|
||||
category: "BROADCAST",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/Iconik-scaled.jpeg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/Iconik-scaled.jpeg",
|
||||
alt: "Iconik media asset management platform interface",
|
||||
slug: "backlight-iconik-wildmoka-ibc2025",
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Vizrt",
|
||||
title: "From Start-Up to Global Powerhouse in Video in Just Two Decades",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/michael-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/michael-320x220.jpg",
|
||||
alt: "Vizrt company spotlight profile image",
|
||||
tag: "Company",
|
||||
href: "/news"
|
||||
@@ -16,7 +16,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Scott Freeman",
|
||||
title: "The Sherlock Holmes of Video Post-Production",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/scott-freeman.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/scott-freeman.jpg",
|
||||
alt: "Scott Freeman video post-production professional headshot",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -24,7 +24,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Al Roker",
|
||||
title: "A Weatherman Who Wears Many Hats",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/al-roker2-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/al-roker2-320x220.jpg",
|
||||
alt: "Al Roker television weatherman and broadcaster portrait",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -32,7 +32,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Michael Kammes",
|
||||
title: "Shaping the Best Workflow for Media Organizations",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/kammes-2-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/kammes-2-320x220.jpg",
|
||||
alt: "Michael Kammes media workflow specialist headshot",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -40,7 +40,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Gary Trenda",
|
||||
title: "Top Frequency \'Traffic Cop\' for Wireless Microphones",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/gary-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/gary-320x220.jpg",
|
||||
alt: "Gary Trenda wireless microphone frequency coordinator portrait",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -48,7 +48,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Jim Anderson & Ulrike Schwarz",
|
||||
title: "Partners in Immersive Sound",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/urike-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/urike-320x220.jpg",
|
||||
alt: "Jim Anderson and Ulrike Schwarz immersive sound designers",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -56,7 +56,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Sound Effects",
|
||||
title: "Creating Cutting-Edge New Sound Effects — Film by Film",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/mark-spot-320x215.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/mark-spot-320x215.jpg",
|
||||
alt: "Sound effects creation for film and broadcast production",
|
||||
tag: "Feature",
|
||||
href: "/gear"
|
||||
@@ -64,7 +64,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Libby Casey",
|
||||
title: "Pioneering Television Broadcasting at the Washington Post",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/spotlight-320x215.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/spotlight-320x215.jpg",
|
||||
alt: "Libby Casey television broadcaster Washington Post studio",
|
||||
tag: "Profile",
|
||||
href: "/news"
|
||||
@@ -72,7 +72,7 @@ const spotlights = [
|
||||
{
|
||||
name: "NBCU Academy",
|
||||
title: "Preparing Students for Careers in News, Technology and Broadcast",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/nbcu-spotlight-320x215.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/nbcu-spotlight-320x215.jpg",
|
||||
alt: "NBCU Academy students learning broadcast journalism and technology",
|
||||
tag: "Education",
|
||||
href: "/about"
|
||||
@@ -80,7 +80,7 @@ const spotlights = [
|
||||
{
|
||||
name: "BirdDog",
|
||||
title: "Simplifying Global Television Production",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/bird-sptfeat.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/bird-sptfeat.jpg",
|
||||
alt: "BirdDog broadcast production technology company spotlight",
|
||||
tag: "Company",
|
||||
href: "/news"
|
||||
@@ -88,7 +88,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Michael Cioni",
|
||||
title: "Speeding Digital Cinema with Camera to Cloud",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/michael-copy-320x220.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/michael-copy-320x220.jpg",
|
||||
alt: "Michael Cioni digital cinema and camera to cloud technology expert",
|
||||
tag: "Profile",
|
||||
href: "/technology"
|
||||
@@ -96,7 +96,7 @@ const spotlights = [
|
||||
{
|
||||
name: "Jeff Greenberg",
|
||||
title: "Finding Workarounds for Gaps in the Video Production Workflow",
|
||||
image: "https://www.broadcastbeat.com/wp-content/uploads/greenberg-320x215.jpg",
|
||||
image: "https://www.avbeat.com/wp-content/uploads/greenberg-320x215.jpg",
|
||||
alt: "Jeff Greenberg video production workflow consultant headshot",
|
||||
tag: "Profile",
|
||||
href: "/technology"
|
||||
|
||||
@@ -13,14 +13,14 @@ import SponsorLogoStrip from "@/components/SponsorLogoStrip";
|
||||
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'AV Beat covers the convergence of pro AV and IT — broadcast, live production, streaming, and post-production technology. Breaking news, deep-dive features, and industry spotlights from NAB to InfoComm and beyond.',
|
||||
title: 'AV Beat — Broadcast Engineering News & Insights',
|
||||
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||
alternates: {
|
||||
canonical: '/',
|
||||
},
|
||||
openGraph: {
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
||||
title: 'AV Beat — Broadcast News',
|
||||
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||
url: '/',
|
||||
type: 'website',
|
||||
images: [
|
||||
@@ -28,14 +28,14 @@ export const metadata: Metadata = {
|
||||
url: '/assets/images/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'AV Beat — Where AV Meets IT',
|
||||
alt: 'AV Beat — Broadcast Engineering News & Insights',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
||||
title: 'AV Beat — Broadcast News',
|
||||
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||
images: ['/assets/images/og-image.png'],
|
||||
},
|
||||
};
|
||||
@@ -138,7 +138,7 @@ export default function HomePage() {
|
||||
|
||||
{/* H1 Hero Heading */}
|
||||
<div className="sr-only">
|
||||
<h1>AV Beat — Where AV Meets IT</h1>
|
||||
<h1>AV Beat — Broadcast Engineering News & Insights</h1>
|
||||
</div>
|
||||
|
||||
{/* Featured: cinematic hero + 4-up rail below (FeaturedBentoFromDb) */}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
/**
|
||||
* Image proxy: serves files from Supabase Storage under a clean broadcastbeat.com
|
||||
* Image proxy: serves files from Supabase Storage under a clean avbeat.com
|
||||
* URL so article HTML doesn't ship the long `https://supabase.onsethost.com/
|
||||
* storage/v1/object/public/...` path. Better for SEO + makes images look
|
||||
* first-party.
|
||||
|
||||
@@ -34,8 +34,8 @@ export const viewport: Viewport = {
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'AV Beat covers the convergence of pro AV and IT — broadcast, live production, streaming, and post-production technology. Breaking news, deep-dive features, and industry spotlights from NAB to InfoComm and beyond.',
|
||||
title: 'AV Beat — Broadcast Engineering News & Insights',
|
||||
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
|
||||
@@ -59,14 +59,14 @@ export const metadata: Metadata = {
|
||||
locale: 'en_US',
|
||||
url: '/',
|
||||
siteName: 'AV Beat',
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
||||
title: 'AV Beat — Broadcast News',
|
||||
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||
images: [
|
||||
{
|
||||
url: '/assets/images/og-image.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'AV Beat — Where AV Meets IT',
|
||||
alt: 'AV Beat — Broadcast Engineering News & Insights',
|
||||
type: 'image/png'
|
||||
}]
|
||||
|
||||
@@ -75,8 +75,8 @@ export const metadata: Metadata = {
|
||||
card: 'summary_large_image',
|
||||
site: '@AV Beat',
|
||||
creator: '@AV Beat',
|
||||
title: 'AV Beat — Where AV Meets IT',
|
||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
||||
title: 'AV Beat — Broadcast News',
|
||||
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||
images: ['/assets/images/og-image.png']
|
||||
},
|
||||
robots: {
|
||||
@@ -125,7 +125,7 @@ export default function RootLayout({
|
||||
name: 'AV Beat',
|
||||
url: 'https://avbeat.com',
|
||||
logo: "https://avbeat.com/assets/images/logo.png",
|
||||
description: 'AV Beat covers the convergence of pro AV and IT — broadcast, live production, streaming, and post-production technology.',
|
||||
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||
sameAs: [
|
||||
'https://linkedin.com',
|
||||
'https://twitter.com',
|
||||
@@ -144,7 +144,7 @@ export default function RootLayout({
|
||||
{/* Skip to main content — WCAG 2.4.1 */}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[10000] focus:px-4 focus:py-2 focus:bg-[#E67E22] 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-[#3b82f6] focus:text-white focus:rounded focus:font-bold focus:text-sm focus:outline-none"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
@@ -77,7 +77,7 @@ function LoginInner() {
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -90,19 +90,19 @@ function LoginInner() {
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body font-semibold py-2 rounded-lg transition-colors"
|
||||
className="w-full bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body font-semibold py-2 rounded-lg transition-colors"
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign in'}
|
||||
</button>
|
||||
<p className="text-xs text-[#666] font-body text-center pt-2">
|
||||
PR firm / contributor?{' '}
|
||||
<Link href="/client-login" className="text-[#E67E22] hover:underline">
|
||||
<Link href="/client-login" className="text-[#3b82f6] hover:underline">
|
||||
Use the contributor portal
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -206,7 +206,7 @@ export default async function ManufacturerProfile({
|
||||
</span>
|
||||
)}
|
||||
{company.phone && (
|
||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#E67E22]">
|
||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#3b82f6]">
|
||||
{company.phone}
|
||||
</a>
|
||||
)}
|
||||
@@ -217,7 +217,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.company_website}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
Website ↗
|
||||
</a>
|
||||
@@ -227,7 +227,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.press_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
Newsroom ↗
|
||||
</a>
|
||||
@@ -237,7 +237,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.contact_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
Contact ↗
|
||||
</a>
|
||||
@@ -247,7 +247,7 @@ export default async function ManufacturerProfile({
|
||||
href={company.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#888] hover:text-[#E67E22]"
|
||||
className="text-[#888] hover:text-[#3b82f6]"
|
||||
>
|
||||
LinkedIn
|
||||
</a>
|
||||
@@ -257,7 +257,7 @@ export default async function ManufacturerProfile({
|
||||
href={`https://twitter.com/${company.twitter_handle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#888] hover:text-[#E67E22]"
|
||||
className="text-[#888] hover:text-[#3b82f6]"
|
||||
>
|
||||
X / Twitter
|
||||
</a>
|
||||
@@ -306,7 +306,7 @@ export default async function ManufacturerProfile({
|
||||
href={e.linkedin_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[11px] text-[#E67E22] hover:underline"
|
||||
className="text-[11px] text-[#3b82f6] hover:underline"
|
||||
>
|
||||
LinkedIn ↗
|
||||
</a>
|
||||
@@ -328,14 +328,14 @@ export default async function ManufacturerProfile({
|
||||
)}
|
||||
{company.phone && (
|
||||
<div className="mt-1">
|
||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#E67E22] hover:underline">
|
||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#3b82f6] hover:underline">
|
||||
{company.phone}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{company.contact_email && (
|
||||
<div>
|
||||
<a href={`mailto:${company.contact_email}`} className="text-[#E67E22] hover:underline">
|
||||
<a href={`mailto:${company.contact_email}`} className="text-[#3b82f6] hover:underline">
|
||||
{company.contact_email}
|
||||
</a>
|
||||
</div>
|
||||
@@ -401,7 +401,7 @@ export default async function ManufacturerProfile({
|
||||
<section className="mb-10">
|
||||
<header className="flex items-baseline justify-between mb-3">
|
||||
<h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2>
|
||||
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#E67E22] hover:underline">
|
||||
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#3b82f6] hover:underline">
|
||||
All stories →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -410,7 +410,7 @@ export default async function ManufacturerProfile({
|
||||
<Link
|
||||
key={r.wp_slug}
|
||||
href={`/news/${r.wp_slug}`}
|
||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-md p-3 hover:border-[#E67E22] hover:shadow-sm transition-all flex gap-3"
|
||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-md p-3 hover:border-[#3b82f6] hover:shadow-sm transition-all flex gap-3"
|
||||
>
|
||||
{/* Image fallback cascade — Ryan rule: never show an empty
|
||||
gray box on a coverage card. Falls through:
|
||||
@@ -423,7 +423,7 @@ export default async function ManufacturerProfile({
|
||||
loading="lazy"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#E67E22] line-clamp-3">
|
||||
<h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#3b82f6] line-clamp-3">
|
||||
{r.title}
|
||||
</h3>
|
||||
{r.wp_published_at && (
|
||||
@@ -447,7 +447,7 @@ export default async function ManufacturerProfile({
|
||||
{p.slug ? (
|
||||
<Link
|
||||
href={`/news/${p.slug}`}
|
||||
className="text-[#E67E22] hover:underline"
|
||||
className="text-[#3b82f6] hover:underline"
|
||||
>
|
||||
{p.title}
|
||||
</Link>
|
||||
|
||||
@@ -102,7 +102,7 @@ export default async function ManufacturersIndex() {
|
||||
</p>
|
||||
<p className="mt-2">
|
||||
Are you a manufacturer and want to update your listing?{" "}
|
||||
<a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#E67E22] hover:underline">
|
||||
<a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#3b82f6] hover:underline">
|
||||
Contact our editors
|
||||
</a>
|
||||
.
|
||||
|
||||
@@ -62,7 +62,7 @@ export default async function NabHub() {
|
||||
const { data: ads } = await adv
|
||||
.from("active_advertisers_by_property")
|
||||
.select("company_name_lower")
|
||||
.eq("property", "broadcastbeat");
|
||||
.eq("property", "avbeat");
|
||||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||||
} catch { /* tolerate */ }
|
||||
|
||||
@@ -153,7 +153,7 @@ export default async function NabHub() {
|
||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||
Latest from the show floor
|
||||
</h2>
|
||||
<Link href="/news?search=NAB+2026" className="text-xs text-[#E67E22] hover:underline">
|
||||
<Link href="/news?search=NAB+2026" className="text-xs text-[#3b82f6] hover:underline">
|
||||
See all NAB coverage →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -162,7 +162,7 @@ export default async function NabHub() {
|
||||
<Link
|
||||
key={a.wp_slug}
|
||||
href={`/news/${a.wp_slug}`}
|
||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#E67E22] hover:shadow-sm transition-all"
|
||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#3b82f6] hover:shadow-sm transition-all"
|
||||
>
|
||||
{a.featured_image && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
@@ -174,7 +174,7 @@ export default async function NabHub() {
|
||||
/>
|
||||
)}
|
||||
<div className="p-3">
|
||||
<h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#E67E22] line-clamp-3 mb-1">
|
||||
<h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#3b82f6] line-clamp-3 mb-1">
|
||||
{a.title}
|
||||
</h3>
|
||||
{a.wp_published_at && (
|
||||
@@ -195,7 +195,7 @@ export default async function NabHub() {
|
||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||
All 2026 NAB Show exhibitors
|
||||
</h2>
|
||||
<Link href="/manufacturers" className="text-xs text-[#E67E22] hover:underline">
|
||||
<Link href="/manufacturers" className="text-xs text-[#3b82f6] hover:underline">
|
||||
Full directory →
|
||||
</Link>
|
||||
</header>
|
||||
@@ -205,7 +205,7 @@ export default async function NabHub() {
|
||||
key={e.slug}
|
||||
href={`/manufacturers/${e.slug}`}
|
||||
data-company-slug={e.slug}
|
||||
className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#E67E22] transition-all flex items-center gap-2.5"
|
||||
className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#3b82f6] transition-all flex items-center gap-2.5"
|
||||
>
|
||||
{e.logo_url ? (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
@@ -222,7 +222,7 @@ export default async function NabHub() {
|
||||
{otherRows.length > 200 && (
|
||||
<p className="text-center text-xs text-[#888] mt-4">
|
||||
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
|
||||
<Link href="/manufacturers" className="text-[#E67E22] hover:underline">Browse the full directory →</Link>
|
||||
<Link href="/manufacturers" className="text-[#3b82f6] hover:underline">Browse the full directory →</Link>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function NewsPage({
|
||||
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-[#E67E22] 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-[#3b82f6] transition-colors"
|
||||
/>
|
||||
{search && (
|
||||
<button
|
||||
@@ -196,7 +196,7 @@ export default function NewsPage({
|
||||
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-[#E67E22] 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-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
title="From date"
|
||||
/>
|
||||
<span className="text-[#444] text-xs">–</span>
|
||||
@@ -204,7 +204,7 @@ export default function NewsPage({
|
||||
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-[#E67E22] 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-[#3b82f6] transition-colors [color-scheme:dark]"
|
||||
title="To date"
|
||||
/>
|
||||
</div>
|
||||
@@ -214,7 +214,7 @@ export default function NewsPage({
|
||||
<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-[#E67E22] 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-[#3b82f6] transition-colors cursor-pointer"
|
||||
>
|
||||
{SORT_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||
@@ -236,8 +236,8 @@ export default function NewsPage({
|
||||
onClick={() => setActiveCategory(cat)}
|
||||
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
||||
activeCategory === cat
|
||||
? "bg-[#E67E22] border-[#E67E22] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]"
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||
}`}
|
||||
>
|
||||
{cat}
|
||||
@@ -275,7 +275,7 @@ export default function NewsPage({
|
||||
<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-[#E67E22] text-xs font-body hover:underline">Clear filters</button>
|
||||
<button onClick={clearFilters} className="mt-3 text-[#3b82f6] text-xs font-body hover:underline">Clear filters</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0">
|
||||
@@ -297,9 +297,9 @@ export default function NewsPage({
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#E67E22] font-body text-[10px] font-bold uppercase tracking-wider">{article.category || "NEWS"}</span>
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] 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>
|
||||
@@ -326,7 +326,7 @@ export default function NewsPage({
|
||||
{!hideAiSuggestions && <AISuggestedArticles variant="full" />}
|
||||
|
||||
<div className="bg-[#111] border border-[#222] p-4">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-3 pb-2 border-b border-[#222]">
|
||||
Popular Tags
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -336,8 +336,8 @@ export default function NewsPage({
|
||||
onClick={() => setActiveCategory(tag)}
|
||||
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
|
||||
activeCategory === tag
|
||||
? "bg-[#E67E22] border-[#E67E22] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]"
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||
}`}
|
||||
>
|
||||
{tag}
|
||||
@@ -346,7 +346,7 @@ export default function NewsPage({
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-2">Newsletter</h3>
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] 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>
|
||||
|
||||
@@ -278,11 +278,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-[#E67E22] transition-colors">
|
||||
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">
|
||||
Home
|
||||
</Link>
|
||||
<span>/</span>
|
||||
<Link href="/news" className="hover:text-[#E67E22] transition-colors">
|
||||
<Link href="/news" className="hover:text-[#3b82f6] transition-colors">
|
||||
News
|
||||
</Link>
|
||||
<span>/</span>
|
||||
@@ -300,7 +300,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-[#E67E22] hover:underline">
|
||||
className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline">
|
||||
{article.category}
|
||||
</Link>
|
||||
<span className="text-xs text-[#777]">{article.readTime}</span>
|
||||
@@ -329,7 +329,7 @@ export default function NewsArticleDetailClient({
|
||||
<div>
|
||||
<Link
|
||||
href={`/authors/${article.authorSlug}`}
|
||||
className="font-heading font-bold text-sm hover:text-[#E67E22] transition-colors text-[#e0e0e0]">
|
||||
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
|
||||
{article.author}
|
||||
</Link>
|
||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||
@@ -343,7 +343,7 @@ export default function NewsArticleDetailClient({
|
||||
onClick={() => setEmailShareOpen(true)}
|
||||
aria-label="Share via email"
|
||||
title="Email"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#E67E22] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||
@@ -364,7 +364,7 @@ export default function NewsArticleDetailClient({
|
||||
|
||||
{/* X / Twitter */}
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=AVBeat`}
|
||||
href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=BroadcastBeat`}
|
||||
target="_blank" rel="noopener noreferrer"
|
||||
aria-label="Share on X"
|
||||
title="X (Twitter)"
|
||||
@@ -418,7 +418,7 @@ export default function NewsArticleDetailClient({
|
||||
}}
|
||||
aria-label="Copy article link"
|
||||
title="Copy link"
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#E67E22] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#3b82f6] hover:bg-[#1a1a1a] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
@@ -432,7 +432,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-[#E67E22] hover:text-blue-300 disabled:opacity-50 transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
{savingState === "saving" ?"Saving..."
|
||||
: savingState === "removing" ?"Removing..."
|
||||
: isSaved
|
||||
@@ -469,7 +469,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-[#E67E22] hover:text-[#E67E22] transition-colors rounded-sm cursor-default">
|
||||
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">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
@@ -483,7 +483,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-[#E67E22] text-[#888] hover:text-[#E67E22] text-xs font-bold uppercase tracking-widest transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
<svg
|
||||
width="13"
|
||||
height="13"
|
||||
@@ -504,7 +504,7 @@ export default function NewsArticleDetailClient({
|
||||
<div className="pt-6 border-t border-[#222]">
|
||||
<Link
|
||||
href="/news"
|
||||
className="inline-flex items-center gap-2 text-[#E67E22] font-body text-sm hover:underline focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -534,7 +534,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-[#E67E22] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Articles
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -553,10 +553,10 @@ export default function NewsArticleDetailClient({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-[#E67E22] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
{related.title}
|
||||
</p>
|
||||
</div>
|
||||
@@ -568,19 +568,19 @@ export default function NewsArticleDetailClient({
|
||||
{/* Related Forum Posts — same widget as /articles/[slug] */}
|
||||
{relatedForumThreads.length > 0 && (
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Related Forum Posts
|
||||
</h3>
|
||||
<ul className="space-y-3.5">
|
||||
{relatedForumThreads.map((t) => (
|
||||
<li key={t.id}>
|
||||
<Link href={`/forum/thread/${t.id}`} className="group block">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#E67E22] transition-colors line-clamp-2 leading-snug">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#3b82f6] transition-colors line-clamp-2 leading-snug">
|
||||
{t.title}
|
||||
</p>
|
||||
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
||||
{t.category_name && (
|
||||
<span className="text-[#E67E22] font-body font-bold uppercase tracking-wider">
|
||||
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider">
|
||||
{t.category_name}
|
||||
</span>
|
||||
)}
|
||||
@@ -594,7 +594,7 @@ export default function NewsArticleDetailClient({
|
||||
</ul>
|
||||
<Link
|
||||
href="/forum"
|
||||
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#E67E22] hover:underline font-body font-semibold uppercase tracking-wider"
|
||||
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#3b82f6] hover:underline font-body font-semibold uppercase tracking-wider"
|
||||
>
|
||||
Browse all forum threads →
|
||||
</Link>
|
||||
@@ -639,7 +639,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-[#E67E22] hover:text-[#E67E22] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -655,7 +655,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-[#E67E22] hover:text-[#E67E22] disabled:opacity-30 disabled:cursor-not-allowed transition-colors rounded-sm focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]">
|
||||
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]">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
@@ -694,11 +694,11 @@ export default function NewsArticleDetailClient({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<span className="text-[#E67E22] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2 mb-1.5">
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug group-hover:text-[#3b82f6] transition-colors line-clamp-2 mb-1.5">
|
||||
{related.title}
|
||||
</h3>
|
||||
<p className="text-[#666] font-body text-xs line-clamp-2 leading-relaxed">
|
||||
@@ -723,7 +723,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-[#E67E22]" : "bg-[#333]"
|
||||
i === carouselIndex ? "bg-[#3b82f6]" : "bg-[#333]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
@@ -756,7 +756,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-[#E67E22]">
|
||||
className="absolute top-4 right-4 text-[#555] hover:text-[#e0e0e0] transition-colors focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
@@ -777,7 +777,7 @@ export default function NewsArticleDetailClient({
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#E67E22"
|
||||
stroke="#3b82f6"
|
||||
strokeWidth="2"
|
||||
aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
@@ -823,7 +823,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-[#E67E22]">*</span>
|
||||
Recipient Email <span className="text-[#3b82f6]">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="email-to"
|
||||
@@ -831,7 +831,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-[#E67E22] 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-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -846,7 +846,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-[#E67E22] 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-[#3b82f6] text-[#e0e0e0] placeholder-[#444] text-sm font-body px-3 py-2.5 rounded-sm outline-none transition-colors resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -890,7 +890,7 @@ export default function NewsArticleDetailClient({
|
||||
<div
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#E67E22] 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-[#3b82f6] text-white px-4 py-3 rounded-sm text-sm font-body shadow-lg z-50">
|
||||
{saveToast}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -63,7 +63,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 || "#E67E22";
|
||||
const accent = newsletter.accent_color || "#3b82f6";
|
||||
|
||||
const layoutLabel: Record<string, string> = {
|
||||
featured: "Weekly Digest",
|
||||
@@ -113,7 +113,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-[#E67E22] font-bold mr-1">{block.category}:</span>
|
||||
<span className="text-[#3b82f6] font-bold mr-1">{block.category}:</span>
|
||||
)}
|
||||
{block.title}
|
||||
</span>
|
||||
@@ -132,7 +132,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-[#E67E22] text-xs font-medium">
|
||||
<span className="font-body text-[#3b82f6] text-xs font-medium">
|
||||
Issue #{newsletter.id.slice(0, 6).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
@@ -197,9 +197,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-[#E67E22]/20 flex items-center justify-center mx-auto mb-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[#3b82f6]/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="#E67E22" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M4 10l4 4 8-8" stroke="#3b82f6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-heading text-[#e0e0e0] font-bold mb-1">You're subscribed!</p>
|
||||
@@ -228,7 +228,7 @@ function SignupCTA() {
|
||||
/>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#E67E22] border-[#E67E22]" : "border-[#444]"}`}>
|
||||
className={`mr-1.5 inline-block w-3 h-3 rounded-sm border transition-colors ${checked ? "bg-[#3b82f6] border-[#3b82f6]" : "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" />
|
||||
@@ -262,7 +262,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-[#E67E22] ${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-[#3b82f6] ${status === "loading" ? "opacity-70 cursor-not-allowed" : ""}`}>
|
||||
{status === "loading" ? "Subscribing..." : "Subscribe Free"}
|
||||
</button>
|
||||
</div>
|
||||
@@ -275,7 +275,7 @@ function SignupCTA() {
|
||||
|
||||
<p className="font-body text-[#555] text-xs mt-3 text-center">
|
||||
No spam. Unsubscribe anytime.{" "}
|
||||
<Link href="/privacy" className="text-[#E67E22] hover:underline">
|
||||
<Link href="/privacy" className="text-[#3b82f6] hover:underline">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
@@ -419,9 +419,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-[#E67E22] ${
|
||||
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] ${
|
||||
selectedTopic === topic
|
||||
? "bg-[#E67E22] border-[#E67E22] text-white"
|
||||
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||
: "bg-transparent border-[#333] text-[#888] hover:border-[#555] hover:text-[#ccc]"
|
||||
}`}
|
||||
aria-pressed={selectedTopic === topic}>
|
||||
@@ -462,7 +462,7 @@ export default function NewsletterArchivePage() {
|
||||
{hasActiveFilters && (
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="font-body text-xs text-[#E67E22] hover:text-[#E67E22] transition-colors underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
||||
className="font-body text-xs text-[#3b82f6] hover:text-[#60a5fa] transition-colors underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
@@ -498,7 +498,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-[#E67E22] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
||||
className="font-body text-xs text-[#3b82f6] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
@@ -521,7 +521,7 @@ export default function NewsletterArchivePage() {
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClearFilters}
|
||||
className="font-body text-xs text-[#E67E22] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]">
|
||||
className="font-body text-xs text-[#3b82f6] hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]">
|
||||
Clear all filters
|
||||
</button>
|
||||
</div>
|
||||
@@ -555,9 +555,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-[#E67E22] ${
|
||||
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] ${
|
||||
selectedTopic === topic
|
||||
? "text-[#E67E22] bg-[#E67E22]/10"
|
||||
? "text-[#3b82f6] bg-[#3b82f6]/10"
|
||||
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
||||
}`}>
|
||||
{topic}
|
||||
@@ -578,9 +578,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-[#E67E22] ${
|
||||
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] ${
|
||||
selectedYear === String(year)
|
||||
? "text-[#E67E22] bg-[#E67E22]/10"
|
||||
? "text-[#3b82f6] bg-[#3b82f6]/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@avbeat.com" className="text-[#E67E22] underline hover:text-blue-400 transition-colors">privacy@avbeat.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@avbeat.com" className="text-[#3b82f6] underline hover:text-blue-400 transition-colors">privacy@avbeat.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@avbeat.com" className="text-[#E67E22] underline hover:text-blue-400 transition-colors">privacy@avbeat.com</a></p>
|
||||
<p>Email: <a href="mailto:privacy@avbeat.com" className="text-[#3b82f6] underline hover:text-blue-400 transition-colors">privacy@avbeat.com</a></p>
|
||||
</address>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -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-[#E67E22] border-t-transparent rounded-full animate-spin" />
|
||||
<div className="w-6 h-6 border-2 border-[#3b82f6] 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-[#E67E22] transition-colors">Home</Link>
|
||||
<Link href="/home-page" className="hover:text-[#3b82f6] 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-[#E67E22] focus-visible:ring-1 focus-visible:ring-[#E67E22] 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-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] 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-[#E67E22] focus-visible:ring-1 focus-visible:ring-[#E67E22] 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-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] 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-[#E67E22] focus-visible:ring-1 focus-visible:ring-[#E67E22] 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-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] 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-[#E67E22] focus-visible:ring-1 focus-visible:ring-[#E67E22] 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-[#3b82f6] focus-visible:ring-1 focus-visible:ring-[#3b82f6] 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-[#E67E22] hover:bg-[#C8651B] 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-[#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">
|
||||
{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-[#E67E22] text-[#888] hover:text-[#E67E22] 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-[#3b82f6] text-[#888] hover:text-[#3b82f6] font-body text-sm font-bold uppercase tracking-wider rounded-sm transition-colors flex items-center justify-center">
|
||||
Sign In
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,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-[#E67E22] text-xs font-body hover:underline">
|
||||
<Link href="/news" className="text-[#3b82f6] text-xs font-body hover:underline">
|
||||
Browse all news →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -77,11 +77,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-[#E67E22] font-body text-[10px] font-bold uppercase tracking-wider">
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-base font-bold leading-snug mb-2 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
{article.title || "Untitled"}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">
|
||||
|
||||
@@ -170,7 +170,7 @@ export default async function EventCoveragePage({
|
||||
/>
|
||||
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-8">
|
||||
<main>
|
||||
<Link href="/show-coverage" className="text-sm text-[var(--color-text-info,#E67E22)] hover:underline">
|
||||
<Link href="/show-coverage" className="text-sm text-[var(--color-text-info,#60a5fa)] hover:underline">
|
||||
← All events
|
||||
</Link>
|
||||
|
||||
@@ -182,7 +182,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,#E67E22)]">
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
T-{daysUntil}d
|
||||
</span>
|
||||
)}
|
||||
@@ -203,7 +203,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,#E67E22)] mt-2">
|
||||
<div className="text-xs font-mono text-[var(--color-text-info,#60a5fa)] mt-2">
|
||||
<a href={event.url} target="_blank" rel="noopener noreferrer">
|
||||
{new URL(event.url).hostname.replace(/^www\./, "")} →
|
||||
</a>
|
||||
|
||||
@@ -123,7 +123,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,#E67E22)] bg-[#0b0f17] transition-colors"
|
||||
className="block p-5 rounded border border-[#252525] hover:border-[var(--color-text-info,#60a5fa)] bg-[#0b0f17] transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h3 className="font-serif text-xl font-semibold">{e.name}</h3>
|
||||
@@ -137,7 +137,7 @@ function EventCard({ e }: { e: Row }) {
|
||||
{e.url && (
|
||||
<>
|
||||
{" · "}
|
||||
<span className="text-[var(--color-text-info,#E67E22)]">{new URL(e.url).hostname.replace(/^www\./, "")}</span>
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">{new URL(e.url).hostname.replace(/^www\./, "")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
<span className="section-label">Technology</span>
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
{filter && (
|
||||
<Link href="/technology" className="text-[#666] hover:text-[#E67E22] font-body text-[11px] underline">
|
||||
<Link href="/technology" className="text-[#666] hover:text-[#3b82f6] font-body text-[11px] underline">
|
||||
clear filter
|
||||
</Link>
|
||||
)}
|
||||
@@ -101,7 +101,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
href="/technology"
|
||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
||||
!filterSlug
|
||||
? "bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/50"
|
||||
? "bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/50"
|
||||
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
||||
}`}>
|
||||
All
|
||||
@@ -114,7 +114,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
href={`/technology?category=${slug}`}
|
||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
||||
isActive
|
||||
? "bg-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/50"
|
||||
? "bg-[#3b82f6]/20 text-[#3b82f6] border border-[#3b82f6]/50"
|
||||
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
||||
}`}>
|
||||
{def.title}
|
||||
@@ -133,7 +133,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
No articles found{filter ? ` for "${filter.title}"` : ""}.
|
||||
</p>
|
||||
{filter && (
|
||||
<Link href="/technology" className="inline-block mt-3 text-[#E67E22] hover:underline font-body text-sm">
|
||||
<Link href="/technology" className="inline-block mt-3 text-[#3b82f6] hover:underline font-body text-sm">
|
||||
← Back to all technology coverage
|
||||
</Link>
|
||||
)}
|
||||
@@ -145,7 +145,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
<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-[#E67E22] transition-colors p-5">
|
||||
className="flex flex-col md:flex-row gap-6 bg-[#111] border border-[#222] overflow-hidden group hover:border-[#3b82f6] 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}
|
||||
@@ -157,13 +157,13 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-[#E67E22] font-body text-[10px] font-bold uppercase tracking-wider">{articles[0].category}</span>
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] transition-colors">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-xl font-bold leading-tight mb-3 group-hover:text-[#3b82f6] transition-colors">
|
||||
{articles[0].title}
|
||||
</h2>
|
||||
<p className="text-[#777] font-body text-sm leading-relaxed mb-4">{articles[0].excerpt}</p>
|
||||
@@ -185,7 +185,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
<Link
|
||||
key={article.slug}
|
||||
href={`/articles/${article.slug}`}
|
||||
className="flex gap-4 bg-[#111] border border-[#222] p-4 group hover:border-[#E67E22] transition-colors">
|
||||
className="flex gap-4 bg-[#111] border border-[#222] p-4 group hover:border-[#3b82f6] transition-colors">
|
||||
<div className="flex-shrink-0 w-[120px] h-[80px] relative overflow-hidden rounded-sm">
|
||||
<AppImage
|
||||
src={article.image}
|
||||
@@ -197,11 +197,11 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[#E67E22] font-body text-[9px] font-bold uppercase tracking-wider">{article.category}</span>
|
||||
<span className="text-[#3b82f6] 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-[#E67E22] transition-colors line-clamp-2">
|
||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-1 group-hover:text-[#3b82f6] transition-colors line-clamp-2">
|
||||
{article.title}
|
||||
</h2>
|
||||
{article.category !== "Featured" && (
|
||||
|
||||
@@ -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@avbeat.com" className="text-[#E67E22] hover:underline">legal@avbeat.com</a>.</p>
|
||||
<p>Questions about these Terms? Contact us at <a href="mailto:legal@avbeat.com" className="text-[#3b82f6] hover:underline">legal@avbeat.com</a>.</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
180
src/app/video-archive/page.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import Header from "@/components/Header";
|
||||
import Footer from "@/components/Footer";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "From the Vault — On-Camera Archive · AV Beat",
|
||||
description:
|
||||
"Highlights from a decade of AV Beat on-camera coverage — booth walkthroughs, product demos, and exec interviews from NAB Show, IBC, SVG, and the trade circuit (2014–2021). Today our coverage runs through written reporting; this page is a permanent archive of the video era.",
|
||||
alternates: { canonical: "/video-archive" },
|
||||
openGraph: {
|
||||
title: "From the Vault — On-Camera Archive · AV Beat",
|
||||
description:
|
||||
"A decade of on-camera coverage from the NAB Show floor, IBC, SVG, and beyond — archived, not deprecated.",
|
||||
type: "website",
|
||||
url: "https://avbeat.com/video-archive",
|
||||
},
|
||||
};
|
||||
|
||||
// Server-side fetch with revalidation so we don't hammer YouTube and the page
|
||||
// stays static-fast. RSS only returns ~15 latest videos but that's fine for
|
||||
// an archive landing — "See full library" link takes them to YouTube.
|
||||
export const revalidate = 21600; // 6 hours
|
||||
|
||||
type Video = {
|
||||
videoId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
publishedYear: number;
|
||||
thumbnail: string;
|
||||
};
|
||||
|
||||
async function fetchArchive(): Promise<Video[]> {
|
||||
try {
|
||||
const r = await fetch(
|
||||
"https://www.youtube.com/feeds/videos.xml?channel_id=UC_eyQjdiEtNjNE67KDWzQug",
|
||||
{ next: { revalidate: 21600 }, headers: { "User-Agent": "Mozilla/5.0 avbeat.com archive" } },
|
||||
);
|
||||
if (!r.ok) return [];
|
||||
const xml = await r.text();
|
||||
// Lightweight XML parsing — every <entry>…</entry> block is one video.
|
||||
const entries = xml.split("<entry>").slice(1);
|
||||
return entries.map((block) => {
|
||||
const get = (re: RegExp) => (block.match(re)?.[1] ?? "").trim();
|
||||
const videoId = get(/<yt:videoId>([^<]+)<\/yt:videoId>/);
|
||||
const title = get(/<title>([^<]+)<\/title>/);
|
||||
const description = get(/<media:description>([\s\S]*?)<\/media:description>/);
|
||||
const published = get(/<published>([^<]+)<\/published>/);
|
||||
const publishedYear = published ? new Date(published).getFullYear() : 2018;
|
||||
return {
|
||||
videoId,
|
||||
title,
|
||||
description: description.slice(0, 200),
|
||||
publishedYear,
|
||||
thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
|
||||
};
|
||||
}).filter(v => v.videoId);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function VideoArchive() {
|
||||
const videos = await fetchArchive();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<main className="bg-[var(--color-bg,#0a0a0c)] text-[var(--color-text,#e7e7ea)] min-h-screen">
|
||||
{/* Archive intro — frames the section explicitly as historical so users
|
||||
don't read "old videos" as "stale publication". */}
|
||||
<section className="border-b border-white/8 bg-gradient-to-b from-[#11111a] to-transparent">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<div className="inline-flex items-center gap-2 bg-amber-500/10 border border-amber-500/30 text-amber-300 text-[10px] font-bold uppercase tracking-[0.2em] px-3 py-1 rounded mb-5">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" /> From the Vault · 2014 – 2021
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-black text-white tracking-tight mb-4" style={{ fontFamily: "'Barlow Condensed', sans-serif", letterSpacing: '-0.01em' }}>
|
||||
ON-CAMERA ARCHIVE
|
||||
</h1>
|
||||
<p className="text-white/65 text-base md:text-lg leading-relaxed max-w-3xl">
|
||||
For seven years our team carried cameras through NAB Show, IBC, SVG, and dozens of regional production gatherings,
|
||||
capturing booth walkthroughs, product unveils, and exec interviews on the show floor. Today our coverage is
|
||||
all written reporting — but the video archive lives on, preserved as a record of an industry that moves
|
||||
faster than memory.
|
||||
</p>
|
||||
<p className="text-white/40 text-sm mt-4">
|
||||
These clips are kept for reference. We're not producing new video at this time.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Video grid */}
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
||||
{videos.length === 0 ? (
|
||||
<div className="bg-[#111118] border border-white/8 rounded-xl p-12 text-center">
|
||||
<p className="text-white/40 text-sm">Archive feed is temporarily unavailable. Browse the full library directly on{" "}
|
||||
<a href="https://youtube.com/@avbeat" target="_blank" rel="noopener noreferrer" className="text-[var(--color-text-info,#60a5fa)] hover:underline">YouTube</a>.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-[11px] uppercase tracking-[0.2em] text-white/40 font-bold">Selected clips</h2>
|
||||
<a
|
||||
href="https://youtube.com/@avbeat"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-xs text-white/50 hover:text-white transition uppercase tracking-wider"
|
||||
>
|
||||
See full library on YouTube →
|
||||
</a>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||
{videos.map((v) => (
|
||||
<a
|
||||
key={v.videoId}
|
||||
href={`https://www.youtube.com/watch?v=${v.videoId}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group bg-[#111118] border border-white/8 rounded-lg overflow-hidden hover:border-white/25 transition-all"
|
||||
>
|
||||
<div className="aspect-video bg-black relative overflow-hidden">
|
||||
<img
|
||||
src={v.thumbnail}
|
||||
alt={v.title}
|
||||
loading="lazy"
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 opacity-90 group-hover:opacity-100"
|
||||
/>
|
||||
{/* Subtle aged-archive tint so the thumbnail signals "vault" */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent pointer-events-none" />
|
||||
{/* Play badge */}
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="w-14 h-14 rounded-full bg-black/60 border border-white/30 flex items-center justify-center group-hover:bg-[var(--color-brand,#cc0000)]/80 transition">
|
||||
<svg className="w-5 h-5 text-white ml-0.5" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h3 className="text-white font-semibold text-sm leading-snug line-clamp-2 group-hover:text-[var(--color-text-info,#60a5fa)] transition">
|
||||
{v.title}
|
||||
</h3>
|
||||
<p className="text-white/35 text-xs mt-2 uppercase tracking-wider">From the BB vault</p>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Why no recent videos — meta-explanation that turns "old" into "deliberate choice" */}
|
||||
<section className="border-t border-white/8 bg-[#0d0d14]">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
||||
<h2 className="text-white font-bold text-xl mb-3" style={{ fontFamily: "'Barlow Condensed', sans-serif" }}>
|
||||
Why we stopped shooting video
|
||||
</h2>
|
||||
<p className="text-white/55 text-sm leading-relaxed mb-3">
|
||||
The video era — bullpens of camera crews on the NAB floor, B-roll runs through manufacturer booths,
|
||||
same-day edits uploaded from press rooms — produced thousands of hours of footage. It also produced a
|
||||
very specific kind of coverage: surface-level, sponsor-friendly, optimized for trade-show buzz.
|
||||
</p>
|
||||
<p className="text-white/55 text-sm leading-relaxed mb-3">
|
||||
We made the call to step away from on-camera coverage and rebuild AV Beat as a written publication —
|
||||
deeper reporting, more original analysis, fewer canned booth tours. Most of our peers in the trade press
|
||||
are doing the opposite. We think the readers (and the industry) are better served this way.
|
||||
</p>
|
||||
<p className="text-white/55 text-sm leading-relaxed">
|
||||
The vault stays online so the work isn't lost. If you're looking for what's next from BB,
|
||||
head to the{" "}
|
||||
<Link href="/" className="text-[var(--color-text-info,#60a5fa)] hover:underline">homepage</Link>{" "}
|
||||
or subscribe to the{" "}
|
||||
<Link href="/newsletter" className="text-[var(--color-text-info,#60a5fa)] hover:underline">daily newsletter</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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-[#E67E22]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#3b82f6]" 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-[#E67E22] uppercase tracking-widest">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest">
|
||||
AI Picks For You
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowTopicPicker(!showTopicPicker)}
|
||||
className="text-[10px] font-body text-[#555] hover:text-[#E67E22] transition-colors flex items-center gap-1"
|
||||
className="text-[10px] font-body text-[#555] hover:text-[#3b82f6] 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-[#E67E22] border-[#E67E22] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]'
|
||||
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
}`}
|
||||
>
|
||||
{topic}
|
||||
@@ -142,7 +142,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleApplyTopics}
|
||||
className="text-[10px] font-body bg-[#E67E22] text-white px-3 py-1 rounded-sm hover:bg-[#C8651B] transition-colors"
|
||||
className="text-[10px] font-body bg-[#3b82f6] text-white px-3 py-1 rounded-sm hover:bg-[#2563eb] 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-[#E67E22] font-body text-[9px] font-bold uppercase tracking-wider mb-0.5">
|
||||
<p className="text-[#3b82f6] 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-[#E67E22] transition-colors">
|
||||
<p className="font-heading text-[#e0e0e0] text-xs font-semibold leading-snug line-clamp-2 group-hover:text-[#3b82f6] 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-[#E67E22]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg className="w-4 h-4 text-[#3b82f6]" 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-[#E67E22] transition-colors flex items-center gap-1 ml-4 shrink-0"
|
||||
className="text-[11px] font-body text-[#555] hover:text-[#3b82f6] 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-[#E67E22] border-[#E67E22] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]'
|
||||
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||
}`}
|
||||
>
|
||||
{topic}
|
||||
@@ -242,7 +242,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
||||
</div>
|
||||
<button
|
||||
onClick={handleApplyTopics}
|
||||
className="text-xs font-body bg-[#E67E22] text-white px-4 py-1.5 rounded-sm hover:bg-[#C8651B] transition-colors"
|
||||
className="text-xs font-body bg-[#3b82f6] text-white px-4 py-1.5 rounded-sm hover:bg-[#2563eb] 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-[#E67E22] 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-[#3b82f6] 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-[#E67E22] transition-colors">
|
||||
<h3 className="font-heading text-[#e0e0e0] text-sm font-semibold leading-snug line-clamp-2 group-hover:text-[#3b82f6] 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="bb-browse-item gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]"
|
||||
className="bb-browse-item gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"
|
||||
>
|
||||
<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,#E67E22)] 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,#60a5fa)] focus:outline-none focus-visible:bg-[#1a1a1a] transition-colors"
|
||||
>
|
||||
<span>{it.label}</span>
|
||||
<span aria-hidden="true" className="text-[10px] text-[#6b7280]">›</span>
|
||||
|
||||
@@ -163,7 +163,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
||||
{replyTo && (
|
||||
<div className="flex items-center justify-between mb-2 text-xs text-[#888]">
|
||||
<span>Replying to comment…</span>
|
||||
<button onClick={() => setReplyTo(null)} className="text-[#E67E22] hover:underline">Cancel reply</button>
|
||||
<button onClick={() => setReplyTo(null)} className="text-[#3b82f6] hover:underline">Cancel reply</button>
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
@@ -171,7 +171,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
placeholder={replyTo ? "Write your reply…" : "Share your take on this story…"}
|
||||
rows={3}
|
||||
className="w-full bg-[#111] border border-[#252525] rounded text-white text-sm font-body p-2 resize-none focus:outline-none focus:border-[#E67E22]"
|
||||
className="w-full bg-[#111] border border-[#252525] rounded text-white text-sm font-body p-2 resize-none focus:outline-none focus:border-[#3b82f6]"
|
||||
/>
|
||||
{error && <p className="text-[11px] text-red-400 mt-1">{error}</p>}
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
@@ -179,7 +179,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
||||
<button
|
||||
onClick={submit}
|
||||
disabled={submitting || !body.trim()}
|
||||
className="bg-[#E67E22] hover:bg-[#C8651B] disabled:opacity-50 text-white font-body text-xs font-semibold px-4 py-1.5 rounded transition-colors"
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] disabled:opacity-50 text-white font-body text-xs font-semibold px-4 py-1.5 rounded transition-colors"
|
||||
>
|
||||
{submitting ? "Posting…" : replyTo ? "Post reply" : "Post comment"}
|
||||
</button>
|
||||
@@ -193,13 +193,13 @@ export default function ArticleComments({ articleSlug }: Props) {
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link
|
||||
href={`/forum/login?next=${encodeURIComponent(typeof window !== "undefined" ? window.location.pathname : "")}`}
|
||||
className="bg-[#E67E22] hover:bg-[#C8651B] text-white font-body text-xs font-semibold px-5 py-2 rounded"
|
||||
className="bg-[#3b82f6] hover:bg-[#2563eb] text-white font-body text-xs font-semibold px-5 py-2 rounded"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href={`/forum/register?next=${encodeURIComponent(typeof window !== "undefined" ? window.location.pathname : "")}`}
|
||||
className="border border-[#E67E22] text-[#E67E22] hover:bg-[#E67E22]/10 font-body text-xs font-semibold px-5 py-2 rounded"
|
||||
className="border border-[#3b82f6] text-[#3b82f6] hover:bg-[#3b82f6]/10 font-body text-xs font-semibold px-5 py-2 rounded"
|
||||
>
|
||||
Create account
|
||||
</Link>
|
||||
@@ -250,14 +250,14 @@ function CommentItem({
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={comment.author_avatar_url} alt="" className="w-7 h-7 rounded-full bg-[#1a1a1a] object-cover flex-shrink-0" />
|
||||
) : (
|
||||
<div className="w-7 h-7 rounded-full bg-[#1a2535] text-[#E67E22] text-xs font-bold flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-7 h-7 rounded-full bg-[#1a2535] text-[#3b82f6] text-xs font-bold flex items-center justify-center flex-shrink-0">
|
||||
{(comment.author_display_name || "?").trim().charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm font-semibold text-white">
|
||||
{comment.author_display_name || "Member"}
|
||||
{comment.is_ai_seeded && (
|
||||
<span className="ml-1.5 text-[9px] uppercase tracking-widest bg-[#1e3a5f] text-[#E67E22] px-1.5 py-0.5 rounded">AI</span>
|
||||
<span className="ml-1.5 text-[9px] uppercase tracking-widest bg-[#1e3a5f] text-[#60a5fa] px-1.5 py-0.5 rounded">AI</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[11px] text-[#666]">· {timeAgo(comment.created_at)}</span>
|
||||
@@ -288,7 +288,7 @@ function CommentItem({
|
||||
</button>
|
||||
</div>
|
||||
{canInteract && (
|
||||
<button onClick={() => onReply(comment.id)} className="text-[#E67E22] hover:underline text-[11px]">
|
||||
<button onClick={() => onReply(comment.id)} className="text-[#3b82f6] hover:underline text-[11px]">
|
||||
Reply
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -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,#E67E22)]" />
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#60a5fa)]" />
|
||||
<span className="bb-ring" />
|
||||
</span>
|
||||
<span className="font-serif">Ask BB AI</span>
|
||||
@@ -88,7 +88,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,#E67E22)]" />
|
||||
<span className="absolute inset-0 rounded-full bg-[var(--color-text-info,#60a5fa)]" />
|
||||
<span className="bb-ring" />
|
||||
</span>
|
||||
<h2 className="font-serif text-lg font-semibold text-white">BB AI</h2>
|
||||
@@ -109,7 +109,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,#E67E22)] text-black"
|
||||
? "bg-[var(--color-text-info,#60a5fa)] text-black"
|
||||
: "bg-[#111] border border-[#252525] text-[#e5e7eb]")
|
||||
}
|
||||
dangerouslySetInnerHTML={{ __html: m.role === "assistant" ? linkifyMarkdown(m.content) : escapeHtml(m.content) }}
|
||||
@@ -133,12 +133,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,#E67E22)] 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,#60a5fa)] focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
onClick={send}
|
||||
disabled={busy || !input.trim()}
|
||||
className="bg-[var(--color-text-info,#E67E22)] text-black font-semibold px-4 py-2 rounded text-sm disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
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"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
@@ -161,7 +161,7 @@ function linkifyMarkdown(s: string): string {
|
||||
let out = escapeHtml(s);
|
||||
out = out.replace(
|
||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
||||
'<a href="$2" class="text-[#E67E22] hover:underline">$1</a>'
|
||||
'<a href="$2" class="text-[#60a5fa] hover:underline">$1</a>'
|
||||
);
|
||||
out = out.replace(/\n/g, "<br/>");
|
||||
return out;
|
||||
|
||||
@@ -27,7 +27,7 @@ async function loadTiles(slugs: string[]): Promise<CompanyTile[]> {
|
||||
const { data: ads } = await adv
|
||||
.from("active_advertisers_by_property")
|
||||
.select("company_name_lower")
|
||||
.eq("property", "broadcastbeat");
|
||||
.eq("property", "avbeat");
|
||||
advNames = new Set((ads || []).map((r: any) => r.company_name_lower));
|
||||
} catch { /* tolerate */ }
|
||||
|
||||
@@ -57,7 +57,7 @@ export default async function CompaniesInThisStory({ slugs }: { slugs: string[]
|
||||
|
||||
return (
|
||||
<div className="bg-[#111] border border-[#222] p-5">
|
||||
<h3 className="font-body font-bold text-xs text-[#E67E22] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
<h3 className="font-body font-bold text-xs text-[#3b82f6] uppercase tracking-widest mb-4 pb-2 border-b border-[#222]">
|
||||
Companies in this story
|
||||
</h3>
|
||||
<ul className="space-y-3">
|
||||
@@ -83,7 +83,7 @@ export default async function CompaniesInThisStory({ slugs }: { slugs: string[]
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
<span className="font-heading text-[#e0e0e0] text-sm font-bold group-hover:text-[#E67E22] transition-colors truncate">
|
||||
<span className="font-heading text-[#e0e0e0] text-sm font-bold group-hover:text-[#3b82f6] transition-colors truncate">
|
||||
{t.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -112,12 +112,12 @@ export default function CompanyMentionsHover() {
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
<img src={preview.logoUrl} alt="" className="w-12 h-12 rounded bg-white p-1 object-contain flex-shrink-0" loading="lazy" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded bg-[#1a2535] flex items-center justify-center text-[#E67E22] font-bold flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded bg-[#1a2535] flex items-center justify-center text-[#3b82f6] font-bold flex-shrink-0">
|
||||
{(preview.name || "?").trim().charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link href={`/manufacturers/${preview.slug}`} className="font-semibold text-white hover:text-[#E67E22] block truncate">
|
||||
<Link href={`/manufacturers/${preview.slug}`} className="font-semibold text-white hover:text-[#3b82f6] block truncate">
|
||||
{preview.name}
|
||||
</Link>
|
||||
<div className="flex items-center gap-1.5 mt-1 flex-wrap text-[10px]">
|
||||
@@ -145,7 +145,7 @@ export default function CompanyMentionsHover() {
|
||||
<ul className="space-y-1">
|
||||
{preview.stories.slice(0, 3).map((s) => (
|
||||
<li key={s.slug}>
|
||||
<Link href={`/news/${s.slug}`} className="text-xs text-[#E67E22] hover:text-[#93c5fd] hover:underline line-clamp-2">
|
||||
<Link href={`/news/${s.slug}`} className="text-xs text-[#60a5fa] hover:text-[#93c5fd] hover:underline line-clamp-2">
|
||||
{s.title}
|
||||
</Link>
|
||||
{s.date && <span className="text-[10px] text-[#666] ml-1">· {new Date(s.date).toISOString().slice(0, 10)}</span>}
|
||||
@@ -156,11 +156,11 @@ export default function CompanyMentionsHover() {
|
||||
)}
|
||||
|
||||
<div className="border-t border-[#2a3a50] mt-3 pt-2 flex items-center justify-between">
|
||||
<Link href={`/manufacturers/${preview.slug}`} className="text-xs font-semibold text-[#E67E22] hover:text-[#93c5fd] hover:underline">
|
||||
<Link href={`/manufacturers/${preview.slug}`} className="text-xs font-semibold text-[#60a5fa] hover:text-[#93c5fd] hover:underline">
|
||||
Full profile →
|
||||
</Link>
|
||||
{preview.website && (
|
||||
<a href={preview.website} target="_blank" rel="noopener noreferrer" className="text-[10px] text-[#888] hover:text-[#E67E22]">
|
||||
<a href={preview.website} target="_blank" rel="noopener noreferrer" className="text-[10px] text-[#888] hover:text-[#3b82f6]">
|
||||
{preview.website.replace(/^https?:\/\//, "").replace(/\/$/, "")} ↗
|
||||
</a>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function CookieConsent() {
|
||||
<strong className="text-white">Accept All</strong>, you consent to our use of cookies.{" "}
|
||||
<Link
|
||||
href="/privacy"
|
||||
className="text-[#E67E22] underline hover:text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22] transition-colors"
|
||||
className="text-[#3b82f6] underline hover:text-blue-400 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6] transition-colors"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
@@ -86,7 +86,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-[#E67E22] 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-[#3b82f6] focus-visible:ring-offset-1 focus-visible:ring-offset-[#0d1117]"
|
||||
aria-label="Decline non-essential cookies"
|
||||
>
|
||||
Decline
|
||||
@@ -95,7 +95,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-[#E67E22] rounded hover:bg-blue-500 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] focus-visible:ring-offset-2 focus-visible:ring-offset-[#0d1117]"
|
||||
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]"
|
||||
aria-label="Accept all cookies"
|
||||
>
|
||||
Accept All
|
||||
|
||||
@@ -135,7 +135,7 @@ export default async function DoubleTicker() {
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" },
|
||||
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av") as "public" },
|
||||
auth: { persistSession: false },
|
||||
}
|
||||
);
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function EventsDropdown() {
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open}
|
||||
className="bb-browse-item gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#E67E22]"
|
||||
className="bb-browse-item gap-1 focus:outline-none focus-visible:ring-1 focus-visible:ring-[#3b82f6]"
|
||||
>
|
||||
<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,#E67E22)]"></span>
|
||||
<span aria-hidden className="absolute bottom-2 right-2 w-3 h-3 border-b border-r border-[var(--color-text-info,#E67E22)]"></span>
|
||||
<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>
|
||||
|
||||
<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,#E67E22)]">
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
<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,#E67E22)]">T-{e.days_until}d</span>
|
||||
<span className="text-[var(--color-text-info,#60a5fa)]">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,#E67E22)]">· {host}</span>}
|
||||
{host && <span className="text-[var(--color-text-info,#60a5fa)]">· {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,#E67E22)] hover:underline" onClick={() => setOpen(false)}>
|
||||
<Link href="/show-coverage" className="text-[var(--color-text-info,#60a5fa)] hover:underline" onClick={() => setOpen(false)}>
|
||||
See all 13 events →
|
||||
</Link>
|
||||
{loadedAt && <span>auto-updated · {ago(loadedAt)}</span>}
|
||||
|
||||
@@ -37,7 +37,7 @@ export default async function FeaturedBento() {
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||
{
|
||||
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "bb") as "public" },
|
||||
db: { schema: (process.env.NEXT_PUBLIC_SUPABASE_SCHEMA || "av") as "public" },
|
||||
auth: { persistSession: false },
|
||||
}
|
||||
);
|
||||
@@ -73,7 +73,7 @@ export default async function FeaturedBento() {
|
||||
<div className="flex items-center gap-3 mb-4 md:mb-5 flex-wrap">
|
||||
<span className="section-label">Staff Editorial</span>
|
||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||
<Link href="/news/featured" className="text-[11px] font-mono text-[var(--color-text-info,#E67E22)] hover:underline whitespace-nowrap">
|
||||
<Link href="/news/featured" className="text-[11px] font-mono text-[var(--color-text-info,#60a5fa)] hover:underline whitespace-nowrap">
|
||||
All featured →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -89,7 +89,7 @@ export default async function FeaturedBento() {
|
||||
{/* Cinematic 21:9 hero — fills left column, aspect ratio preserved */}
|
||||
<Link
|
||||
href={`/news/${hero.wp_slug}`}
|
||||
className="group block relative overflow-hidden rounded border border-[#252525] bg-[#0b0f17] hover:border-[var(--color-text-info,#E67E22)] transition-colors"
|
||||
className="group block relative overflow-hidden rounded border border-[#252525] bg-[#0b0f17] hover:border-[var(--color-text-info,#60a5fa)] transition-colors"
|
||||
>
|
||||
<div className="relative aspect-[21/9] md:aspect-[21/8] bg-[#111]">
|
||||
<img
|
||||
@@ -99,7 +99,7 @@ export default async function FeaturedBento() {
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/65 to-transparent" />
|
||||
|
||||
<span className="absolute top-4 left-4 inline-flex items-center gap-1.5 bg-[var(--color-text-info,#E67E22)] text-black px-3 py-1 rounded font-mono text-[11px] uppercase tracking-widest font-bold shadow-md">
|
||||
<span className="absolute top-4 left-4 inline-flex items-center gap-1.5 bg-[var(--color-text-info,#60a5fa)] text-black px-3 py-1 rounded font-mono text-[11px] uppercase tracking-widest font-bold shadow-md">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 2l2.6 7.6H22l-6.2 4.5L18.4 22 12 17.3 5.6 22l2.6-7.9L2 9.6h7.4z" />
|
||||
</svg>
|
||||
@@ -108,11 +108,11 @@ export default async function FeaturedBento() {
|
||||
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10">
|
||||
{hero.category && (
|
||||
<div className="text-[10px] font-mono uppercase tracking-widest text-[var(--color-text-info,#E67E22)] mb-2">
|
||||
<div className="text-[10px] font-mono uppercase tracking-widest text-[var(--color-text-info,#60a5fa)] mb-2">
|
||||
{hero.category}
|
||||
</div>
|
||||
)}
|
||||
<h3 className="font-serif text-2xl md:text-4xl lg:text-5xl text-white leading-tight max-w-3xl group-hover:text-[var(--color-text-info,#E67E22)] transition-colors">
|
||||
<h3 className="font-serif text-2xl md:text-4xl lg:text-5xl text-white leading-tight max-w-3xl group-hover:text-[var(--color-text-info,#60a5fa)] transition-colors">
|
||||
{hero.title}
|
||||
</h3>
|
||||
{hero.excerpt && (
|
||||
@@ -123,7 +123,7 @@ export default async function FeaturedBento() {
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3 text-[11px] font-mono text-[#9ca3af]">
|
||||
<span>{hero.author_name || "AV Beat"}</span>
|
||||
<StarRating seed={hero.wp_slug} publishedAt={hero.wp_published_at} size="sm" />
|
||||
<span aria-hidden="true" className="text-[var(--color-text-info,#E67E22)] ml-2">→ Read full story</span>
|
||||
<span aria-hidden="true" className="text-[var(--color-text-info,#60a5fa)] ml-2">→ Read full story</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -140,7 +140,7 @@ export default async function FeaturedBento() {
|
||||
<Link
|
||||
key={r.wp_slug}
|
||||
href={`/news/${r.wp_slug}`}
|
||||
className="group flex flex-col h-full rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#E67E22)] transition-colors"
|
||||
className="group flex flex-col h-full rounded border border-[#252525] bg-[#0b0f17] overflow-hidden hover:border-[var(--color-text-info,#60a5fa)] transition-colors"
|
||||
>
|
||||
<div className="aspect-[16/10] bg-[#111] relative flex-shrink-0">
|
||||
<img
|
||||
@@ -150,10 +150,10 @@ export default async function FeaturedBento() {
|
||||
/>
|
||||
</div>
|
||||
<div className="p-3 flex flex-col gap-1 flex-1 min-h-0 overflow-hidden">
|
||||
<div className="text-[9px] font-mono uppercase tracking-wider text-[var(--color-text-info,#E67E22)]">
|
||||
<div className="text-[9px] font-mono uppercase tracking-wider text-[var(--color-text-info,#60a5fa)]">
|
||||
{r.category || "News"}
|
||||
</div>
|
||||
<h4 className="font-serif text-sm leading-tight text-[#e5e7eb] group-hover:text-[var(--color-text-info,#E67E22)] line-clamp-2">
|
||||
<h4 className="font-serif text-sm leading-tight text-[#e5e7eb] group-hover:text-[var(--color-text-info,#60a5fa)] line-clamp-2">
|
||||
{r.title}
|
||||
</h4>
|
||||
<div className="mt-auto">
|
||||
|
||||