av: clone broadcastbeat sources + AV Beat rebrand (Phase 1)
- Replaces the prior Rocket scaffold (saved on pre-bb-clone-rollback branch) - Domain: broadcastbeat.com -> avbeat.com - Brand: Broadcast Beat -> AV Beat - Slug: broadcastbeat -> avbeat (package, identifiers) - Schema fallback: bb -> av (env var name unchanged) - Placeholder AV BEAT logo (gold->red gradient) at /assets/logos/av.svg - All BB/RMP logo references repointed Outstanding before public DNS swap: - Supabase av schema bootstrap (mirror of bb tables) - WordPress archive import (avbeat-com -> av.articles) - Coolify env vars - dev-avbeat.onsethost.com staging deploy + visual review
@@ -14,11 +14,7 @@ ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
|
|||||||
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID
|
ENV NEXT_PUBLIC_GA_MEASUREMENT_ID=$NEXT_PUBLIC_GA_MEASUREMENT_ID
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
# Force dev-deps even if NODE_ENV=production was supplied as a Coolify
|
RUN npm install
|
||||||
# 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
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
ENV NODE_ENV=production
|
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)
|
## 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:
|
`/tmp/bb-image-backfill.py` ran on coolify01:
|
||||||
|
|
||||||
- Matrox+Amagi smoke article (`matrox-video-and-amagi-collaborate-...`):
|
- 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
|
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
|
articles are recent NAB 2026 posts that have never been crawled by
|
||||||
Wayback.
|
Wayback.
|
||||||
@@ -22,7 +22,7 @@ in Supabase storage) is **NOT done**. The smoke-test attempt
|
|||||||
**What needs to happen next session:**
|
**What needs to happen next session:**
|
||||||
|
|
||||||
1. For each broken article, try multiple sources in order:
|
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,
|
- Manufacturer press-kit page based on title keywords (Matrox, Amagi,
|
||||||
Telestream, Calrec, AJA, etc. — keep a per-vendor mapping of press
|
Telestream, Calrec, AJA, etc. — keep a per-vendor mapping of press
|
||||||
kit URLs).
|
kit URLs).
|
||||||
|
|||||||
@@ -29,6 +29,6 @@ export const imageHosts = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: 'https',
|
protocol: 'https',
|
||||||
hostname: 'www.broadcastbeat.com',
|
hostname: 'www.avbeat.com',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ const nextConfig = {
|
|||||||
destination: 'https://relevantmediaproperties.com/broadcast-beat---media-kit',
|
destination: 'https://relevantmediaproperties.com/broadcast-beat---media-kit',
|
||||||
permanent: true,
|
permanent: true,
|
||||||
},
|
},
|
||||||
// WordPress admin redirects for broadcastbeat.com (301 permanent)
|
// WordPress admin redirects for avbeat.com (301 permanent)
|
||||||
{
|
{
|
||||||
source: '/wp-admin',
|
source: '/wp-admin',
|
||||||
destination: '/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">
|
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
||||||
<main>
|
<main>
|
||||||
<header className="mb-8 border-b border-[#252525] pb-6">
|
<header className="mb-8 border-b border-[#252525] pb-6">
|
||||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#E67E22)] mb-2">
|
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
|
||||||
AV Beat · Where AV Meets IT
|
AV Beat
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">Contact</h1>
|
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">Contact</h1>
|
||||||
<p className="mt-3 text-[#9ca3af] text-base">
|
<p className="mt-3 text-[#9ca3af] text-base">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import SidebarAdStack from "@/components/SidebarAdStack";
|
|||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "About AV Beat",
|
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() {
|
export default function AboutPage() {
|
||||||
@@ -15,12 +15,12 @@ export default function AboutPage() {
|
|||||||
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-10">
|
||||||
<main>
|
<main>
|
||||||
<header className="mb-8 border-b border-[#252525] pb-6">
|
<header className="mb-8 border-b border-[#252525] pb-6">
|
||||||
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#E67E22)] mb-2">
|
<div className="text-[10px] font-mono uppercase tracking-[0.22em] text-[var(--color-text-info,#60a5fa)] mb-2">
|
||||||
AV Beat · Where AV Meets IT
|
AV Beat
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-serif text-4xl md:text-5xl font-bold tracking-tight">About</h1>
|
<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">
|
<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>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -32,11 +32,11 @@ export default function AboutPage() {
|
|||||||
|
|
||||||
<section className="space-y-6 text-[#d1d5db] text-[15px] leading-relaxed">
|
<section className="space-y-6 text-[#d1d5db] text-[15px] leading-relaxed">
|
||||||
<Block title="What we cover">
|
<Block title="What we cover">
|
||||||
AV Beat reports on the equipment, services, and people that
|
AV Beat reports on the equipment, services, and people
|
||||||
move the professional AV and IT-converged video industry
|
that move the professional video industry forward. We follow
|
||||||
forward. We follow product launches, executive moves,
|
product launches, executive moves, acquisitions, and the live
|
||||||
acquisitions, and the live production / IP / cloud workflows
|
production / IP / cloud workflows reshaping how content gets
|
||||||
reshaping how content gets made and delivered.
|
made and delivered.
|
||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block title="Where we go">
|
<Block title="Where we go">
|
||||||
@@ -57,9 +57,9 @@ export default function AboutPage() {
|
|||||||
</Block>
|
</Block>
|
||||||
|
|
||||||
<Block title="Get involved">
|
<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 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,#E67E22)] hover:underline" href="/about/advertise">see the media kit</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,#E67E22)] hover:underline" href="/about/team">meet the editorial team</a>.
|
{" "}For our team: <a className="text-[var(--color-text-info,#60a5fa)] hover:underline" href="/about/team">meet the editorial team</a>.
|
||||||
</Block>
|
</Block>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -13,23 +13,23 @@ export default function PressKitPage() {
|
|||||||
<section>
|
<section>
|
||||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
|
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Logos</h2>
|
||||||
<ul className="space-y-1">
|
<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>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
|
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Boilerplate</h2>
|
||||||
<p className="text-[#d1d5db] leading-relaxed">
|
<p className="text-[#d1d5db] leading-relaxed">
|
||||||
AV Beat is an independent trade publication covering the convergence
|
AV Beat is an independent trade publication covering broadcast,
|
||||||
of pro AV and IT — broadcast, live production, streaming, and
|
live production, streaming, and post-production technology. Founded
|
||||||
post-production technology. Founded to bring engineering-rigor
|
to bring engineering-rigor reporting to industry coverage, AV Beat
|
||||||
reporting to industry coverage, AV Beat publishes daily on the
|
publishes daily on the equipment, services, and people that move
|
||||||
equipment, services, and people moving professional video forward.
|
professional video forward.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
|
<h2 className="font-mono text-xs uppercase tracking-wider text-[#6b7280] mb-2">Media contact</h2>
|
||||||
<p>
|
<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>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const revalidate = 600;
|
|||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Our team — AV Beat",
|
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 {
|
interface Persona {
|
||||||
@@ -63,7 +63,7 @@ export default async function TeamPage() {
|
|||||||
.from("personas")
|
.from("personas")
|
||||||
.select("id, display_name, role, email, phone, extension, avatar_url")
|
.select("id, display_name, role, email, phone, extension, avatar_url")
|
||||||
.eq("active", true)
|
.eq("active", true)
|
||||||
.in("id", ["chloe", "jeff", "riley"]);
|
.in("id", ["chloe", "jeff", "riley", "brian", "avery"]);
|
||||||
const ops = (opsData || []) as OpsPersona[];
|
const ops = (opsData || []) as OpsPersona[];
|
||||||
|
|
||||||
const displayBeat = (p: Persona) =>
|
const displayBeat = (p: Persona) =>
|
||||||
@@ -98,10 +98,10 @@ export default async function TeamPage() {
|
|||||||
<main>
|
<main>
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<header className="relative overflow-hidden rounded-xl mb-10 border border-[#1a1f2a] bg-gradient-to-br from-[#0d1118] via-[#0b0f17] to-[#0d1118]">
|
<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="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">
|
<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-[#E67E22]" />
|
<span className="w-6 h-px bg-[#d60701]" />
|
||||||
AV Beat · editorial
|
AV Beat · editorial
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
|
<h1 className="font-serif text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight leading-tight">
|
||||||
@@ -129,10 +129,10 @@ export default async function TeamPage() {
|
|||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={p.slug}
|
key={p.slug}
|
||||||
className="group relative rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 hover:border-[#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 */}
|
{/* 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">
|
<div className="flex items-start gap-4">
|
||||||
{p.avatar_url ? (
|
{p.avatar_url ? (
|
||||||
@@ -142,7 +142,7 @@ export default async function TeamPage() {
|
|||||||
alt={p.name}
|
alt={p.name}
|
||||||
width={96}
|
width={96}
|
||||||
height={96}
|
height={96}
|
||||||
className="w-24 h-24 rounded-full border-2 border-[#1a1f2a] group-hover:border-[#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>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
@@ -155,7 +155,7 @@ export default async function TeamPage() {
|
|||||||
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3>
|
<h3 className="font-serif text-xl font-bold leading-tight">{p.name}</h3>
|
||||||
</a>
|
</a>
|
||||||
{(p.beat || p.slug === "ryan-salazar") && (
|
{(p.beat || p.slug === "ryan-salazar") && (
|
||||||
<div className="inline-block mt-1.5 px-2 py-0.5 rounded-sm bg-[#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)}
|
{displayBeat(p)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -173,11 +173,11 @@ export default async function TeamPage() {
|
|||||||
<ObfuscatedEmail
|
<ObfuscatedEmail
|
||||||
user={e.user}
|
user={e.user}
|
||||||
domain={e.domain}
|
domain={e.domain}
|
||||||
className="text-[#aab1bd] hover:text-[#F0B07A] font-mono"
|
className="text-[#aab1bd] hover:text-[#ff6b66] font-mono"
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
href={`/authors/${p.slug}`}
|
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 →
|
View →
|
||||||
</a>
|
</a>
|
||||||
@@ -209,7 +209,7 @@ export default async function TeamPage() {
|
|||||||
{ops.map((p) => (
|
{ops.map((p) => (
|
||||||
<li
|
<li
|
||||||
key={p.id}
|
key={p.id}
|
||||||
className="rounded-xl border border-[#1a1f2a] bg-[#0b0f17] p-5 flex gap-4 hover:border-[#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 ? (
|
{p.avatar_url ? (
|
||||||
<img
|
<img
|
||||||
@@ -235,13 +235,13 @@ export default async function TeamPage() {
|
|||||||
if (!e) return null;
|
if (!e) return null;
|
||||||
return (
|
return (
|
||||||
<div className="font-mono">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{p.phone && (
|
{p.phone && (
|
||||||
<div>
|
<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}` : ''}
|
{p.phone}{p.extension ? ` ext. ${p.extension}` : ''}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||||
const STATUSES = ['active','completed','cancelled'];
|
const STATUSES = ['active','completed','cancelled'];
|
||||||
const SCHEDULES = ['single','monthly','quarterly'];
|
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 }); }
|
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() {
|
export default function ReportsPage() {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export default function AdOpsIOsPage() {
|
|||||||
</select>
|
</select>
|
||||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||||
<option value="">All Sites</option>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export default function AdOpsDashboardPage() {
|
|||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||||
<option value="">All Sites</option>
|
<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>
|
||||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||||
<option value="">All Statuses</option>
|
<option value="">All Statuses</option>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||||
const PRODUCT_TYPES = [
|
const PRODUCT_TYPES = [
|
||||||
'display_banner_300x250','display_banner_728x90','display_banner_300x600',
|
'display_banner_300x250','display_banner_728x90','display_banner_300x600',
|
||||||
'display_banner_970x250','newsletter_banner_600x100','dedicated_email_blast',
|
'display_banner_970x250','newsletter_banner_600x100','dedicated_email_blast',
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface BannedTerm {
|
|||||||
id: string;
|
id: string;
|
||||||
term: string;
|
term: string;
|
||||||
ban_level: 'soft' | 'hard';
|
ban_level: 'soft' | 'hard';
|
||||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||||
is_active: boolean;
|
is_active: boolean;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
@@ -19,7 +19,7 @@ interface BannedTerm {
|
|||||||
interface FormState {
|
interface FormState {
|
||||||
term: string;
|
term: string;
|
||||||
ban_level: 'soft' | 'hard';
|
ban_level: 'soft' | 'hard';
|
||||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||||
notes: string;
|
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 }))}
|
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
|
||||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
||||||
<option value="both">Both Sites</option>
|
<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>
|
<option value="avbeat">AV Beat only</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +291,7 @@ export default function BannedTermsPage() {
|
|||||||
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
|
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
|
||||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
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="">All Sites</option>
|
||||||
<option value="broadcastbeat">Broadcast Beat</option>
|
<option value="avbeat">AV Beat</option>
|
||||||
<option value="avbeat">AV Beat</option>
|
<option value="avbeat">AV Beat</option>
|
||||||
<option value="both">Both</option>
|
<option value="both">Both</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -336,7 +336,7 @@ export default function BannedTermsPage() {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3">
|
<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>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
|
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
|
||||||
@@ -379,7 +379,7 @@ export default function BannedTermsPage() {
|
|||||||
<div className="flex items-center gap-2 mb-1">
|
<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>
|
<span className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400 font-bold">Hard Ban</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ type TabType = 'queue' | 'companies' | 'crawl-log' | 'settings';
|
|||||||
type QueueStatus = 'generated' | 'pending' | 'approved' | 'published' | 'rejected';
|
type QueueStatus = 'generated' | 'pending' | 'approved' | 'published' | 'rejected';
|
||||||
|
|
||||||
const SITE_CONFIG = {
|
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]' },
|
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>
|
<span className="text-[#888] text-sm font-body">Company Tracker</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-display font-bold text-white">Company Story Engine</h1>
|
<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>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* Auto-Pilot toggle */}
|
{/* Auto-Pilot toggle */}
|
||||||
@@ -344,7 +344,7 @@ export default function CompanyTrackerPage() {
|
|||||||
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
|
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
|
||||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
|
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
|
||||||
<option value="">All Sites</option>
|
<option value="">All Sites</option>
|
||||||
<option value="1">Broadcast Beat</option>
|
<option value="1">AV Beat</option>
|
||||||
<option value="2">AV Beat</option>
|
<option value="2">AV Beat</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -412,7 +412,7 @@ export default function CompanyTrackerPage() {
|
|||||||
<select onChange={e => handleQueueAction(item.id, 'reassign_site', { site_id: parseInt(e.target.value) })}
|
<select onChange={e => handleQueueAction(item.id, 'reassign_site', { site_id: parseInt(e.target.value) })}
|
||||||
defaultValue={item.site_id}
|
defaultValue={item.site_id}
|
||||||
className="bg-[#111] border border-[#333] rounded px-2 py-1 text-xs text-white font-body focus:outline-none">
|
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>
|
<option value={2}>AV Beat</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -455,7 +455,7 @@ export default function CompanyTrackerPage() {
|
|||||||
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
|
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
|
||||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
||||||
<option value="both">Both Sites</option>
|
<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>
|
<option value="avbeat">AV Beat only</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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-sm font-body font-semibold text-white">{company.company_name}</td>
|
||||||
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">{company.company_website}</a> : '—'}</td>
|
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#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-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">
|
<td className="px-4 py-3">
|
||||||
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
|
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
|
||||||
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}>
|
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 ──────────────────────────────────────────────────────────────
|
// ─── Site labels ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = {
|
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' },
|
'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' },
|
'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' },
|
'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 { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
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 }) {
|
function DnsRow({ label, valid, checkedAt }: { label: string; valid: boolean; checkedAt: string | null }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter } from 'next/navigation';
|
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 }) {
|
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||||
|
|
||||||
export default function SendLogPage() {
|
export default function SendLogPage() {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import Link from 'next/link';
|
|||||||
import { useAuth } from '@/contexts/AuthContext';
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { useRouter } from 'next/navigation';
|
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'];
|
const SUPP_TYPES = ['unsubscribe','bounce_hard','bounce_soft','spam_complaint','hostile_reply','manual'];
|
||||||
|
|
||||||
export default function SuppressionsPage() {
|
export default function SuppressionsPage() {
|
||||||
const { user, loading } = useAuth();
|
const { user, loading } = useAuth();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [site, setSite] = useState('broadcastbeat');
|
const [site, setSite] = useState('avbeat');
|
||||||
const [suppressions, setSuppressions] = useState<any[]>([]);
|
const [suppressions, setSuppressions] = useState<any[]>([]);
|
||||||
const [keywords, setKeywords] = useState<any[]>([]);
|
const [keywords, setKeywords] = useState<any[]>([]);
|
||||||
const [stats, setStats] = useState<any>(null);
|
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>
|
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
|
||||||
<p className="text-[#666] text-sm mt-1">
|
<p className="text-[#666] text-sm mt-1">
|
||||||
Import posts from{' '}
|
Import posts from{' '}
|
||||||
<a href="https://www.broadcastbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
||||||
broadcastbeat.com
|
avbeat.com
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
via WordPress REST API
|
via WordPress REST API
|
||||||
</p>
|
</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 className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p>
|
<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>
|
</div>
|
||||||
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
|
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
|
||||||
@@ -365,11 +365,11 @@ export default function WPImportPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<a
|
<a
|
||||||
href={`https://www.broadcastbeat.com/${post.wp_slug}`}
|
href={`https://www.avbeat.com/${post.wp_slug}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1"
|
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">
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
|
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ export default function CampaignEditorPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2">
|
<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>
|
<span className="text-[10px] text-[#444]">now</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-[#888] truncate">{campaign.subject || 'Your subject line…'}</p>
|
<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">
|
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
|
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#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 && (
|
{campaign.previewText && (
|
||||||
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
|
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
|
||||||
)}
|
)}
|
||||||
@@ -680,7 +680,7 @@ export default function CampaignEditorPage() {
|
|||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="text-center pt-2 border-t border-[#1a1a1a]">
|
<div className="text-center pt-2 border-t border-[#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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -689,7 +689,7 @@ export default function AdminNewsletterPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={digestSubject}
|
value={digestSubject}
|
||||||
onChange={(e) => setDigestSubject(e.target.value)}
|
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"
|
className="search-input w-full py-2.5 px-3 text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ export default function NewsletterTemplatesPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={form.subject_prefix}
|
value={form.subject_prefix}
|
||||||
onChange={(e) => setForm((f) => ({ ...f, subject_prefix: e.target.value }))}
|
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"
|
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>
|
<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">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{stats.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center">
|
<div key={stat.label} className="bg-[#111] border border-[#222] p-5 text-center">
|
||||||
<div className="font-heading text-[#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 className="font-body text-[#777] text-xs uppercase tracking-wide">{stat.label}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -86,10 +86,10 @@ export default function AdvertisePage() {
|
|||||||
<table className="w-full text-sm font-body">
|
<table className="w-full text-sm font-body">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-[#222]">
|
<tr className="border-b border-[#222]">
|
||||||
<th className="text-left py-3 px-4 text-[#E67E22] 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">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-[#3b82f6] 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-[#3b82f6] 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">Rate</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<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-[#e0e0e0] font-medium">{zone.name}</td>
|
||||||
<td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td>
|
<td className="py-3 px-4 text-[#888] font-mono text-xs">{zone.dimensions}</td>
|
||||||
<td className="py-3 px-4 text-[#777]">{zone.placement}</td>
|
<td className="py-3 px-4 text-[#777]">{zone.placement}</td>
|
||||||
<td className="py-3 px-4 text-[#E67E22] font-medium">{zone.cpm}</td>
|
<td className="py-3 px-4 text-[#3b82f6] font-medium">{zone.cpm}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -123,23 +123,23 @@ export default function AdvertisePage() {
|
|||||||
<h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3>
|
<h3 className="font-heading text-[#e0e0e0] text-lg font-bold mb-4">Sales Contact</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-body text-[#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>
|
<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
|
jeff@avbeat.com
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-[#222] pt-4">
|
<div className="border-t border-[#222] pt-4">
|
||||||
<p className="font-body text-[#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">AV Beat</p>
|
||||||
<p className="font-body text-[#777] text-sm">Relevant Media Properties</p>
|
<p className="font-body text-[#777] text-sm">Relevant Media Properties</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-[#222] pt-4">
|
<div className="border-t border-[#222] pt-4">
|
||||||
<p className="font-body text-[#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">
|
<ul className="space-y-1.5">
|
||||||
{["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => (
|
{["Display advertising (web)", "Newsletter sponsorships", "Sponsored content", "Event coverage partnerships", "Podcast sponsorships"].map((item) => (
|
||||||
<li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body">
|
<li key={item} className="flex items-start gap-2 text-[#777] text-sm font-body">
|
||||||
<span className="text-[#E67E22] mt-0.5">✓</span>
|
<span className="text-[#3b82f6] mt-0.5">✓</span>
|
||||||
{item}
|
{item}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
@@ -152,16 +152,16 @@ export default function AdvertisePage() {
|
|||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className="lg:col-span-8">
|
<div className="lg:col-span-8">
|
||||||
{submitted ? (
|
{submitted ? (
|
||||||
<div className="bg-[#111] border border-[#E67E22] p-8 text-center">
|
<div className="bg-[#111] border border-[#3b82f6] 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="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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
|
<h3 className="font-heading text-[#e0e0e0] text-xl font-bold mb-2">Message Sent!</h3>
|
||||||
<p className="font-body text-[#777] text-sm">
|
<p className="font-body text-[#777] text-sm">
|
||||||
Thanks for reaching out. Jeff will be in touch within one business day at{" "}
|
Thanks for reaching out. Jeff will be in touch within one business day at{" "}
|
||||||
<a href="mailto:jeff@avbeat.com" className="text-[#E67E22] hover:underline">jeff@avbeat.com</a>.
|
<a href="mailto:jeff@avbeat.com" className="text-[#3b82f6] hover:underline">jeff@avbeat.com</a>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -245,7 +245,7 @@ export default function AdvertisePage() {
|
|||||||
{loading ? "Sending..." : "Send Inquiry"}
|
{loading ? "Sending..." : "Send Inquiry"}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-[#555] text-xs font-body">
|
<p className="text-[#555] text-xs font-body">
|
||||||
Or email directly: <a href="mailto:jeff@avbeat.com" className="text-[#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>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export async function GET(request: NextRequest) {
|
|||||||
.select('site, amount_cents, status, paid_date')
|
.select('site, amount_cents, status, paid_date')
|
||||||
.gte('created_at', yearStart);
|
.gte('created_at', yearStart);
|
||||||
|
|
||||||
const sites = ['broadcastbeat', 'avbeat', 'backlotbeat', 'broadcastengineering'];
|
const sites = ['avbeat', 'avbeat', 'backlotbeat', 'broadcastengineering'];
|
||||||
const siteRevenue = sites.map(site => {
|
const siteRevenue = sites.map(site => {
|
||||||
const siteData = (siteInvoices ?? []).filter((i: any) => i.site === 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);
|
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
|
// Trigger article publish notification if published immediately
|
||||||
if (status === 'published' && data) {
|
if (status === 'published' && data) {
|
||||||
try {
|
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`, {
|
fetch(`${siteUrl}/api/newsletter/notify-article`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -152,7 +152,7 @@ export async function PATCH(req: NextRequest) {
|
|||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (articleData) {
|
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
|
// Fire-and-forget: call notify-article route internally
|
||||||
fetch(`${siteUrl}/api/newsletter/notify-article`, {
|
fetch(`${siteUrl}/api/newsletter/notify-article`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export async function POST(request: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await transport.sendMail({
|
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: 'editor@broadcastbeat.com',
|
to: 'editor@avbeat.com',
|
||||||
subject: `[Broadcast Beat] Blocked post attempt — banned term triggered`,
|
subject: `[AV Beat] Blocked post attempt — banned term triggered`,
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
|
<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>
|
<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();
|
const titleLower = (post.title || '').toLowerCase();
|
||||||
for (const bt of bannedTerms) {
|
for (const bt of bannedTerms) {
|
||||||
const termLower = bt.term.toLowerCase();
|
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)) {
|
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 });
|
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
|
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 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>` : '';
|
const moreText = companies?.length > 10 ? `<p style="color:#888;font-size:13px;">...and ${companies.length - 10} more</p>` : '';
|
||||||
await transport.sendMail({
|
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',
|
to: email || 'ryan.salazar@relevantmediaproperties.com',
|
||||||
subject: `[Relevant Media Properties] ${count} new company story suggestion${count !== 1 ? 's' : ''} queued`,
|
subject: `[Relevant Media Properties] ${count} new company story suggestion${count !== 1 ? 's' : ''} queued`,
|
||||||
html: `
|
html: `
|
||||||
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
|
<div style="font-family:Arial,sans-serif;max-width:600px;background:#0a0a0a;color:#e5e5e5;padding:32px;border-radius:8px;">
|
||||||
<div style="margin-bottom:24px;">
|
<div style="margin-bottom:24px;">
|
||||||
<h1 style="color:#3b82f6;font-size:20px;margin:0 0 4px;">Relevant Media Properties</h1>
|
<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>
|
</div>
|
||||||
<h2 style="font-size:18px;color:#fff;margin:0 0 12px;">New Company Stories Queued for Review</h2>
|
<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;">
|
<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>
|
<ul style="margin:0;padding-left:20px;color:#e5e5e5;font-size:14px;line-height:1.8;">${companyList}</ul>
|
||||||
${moreText}
|
${moreText}
|
||||||
</div>
|
</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;">
|
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 →
|
Review Story Queue →
|
||||||
</a>
|
</a>
|
||||||
<p style="color:#444;font-size:12px;margin-top:24px;">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function pickPenName(siteId: number, recentNames: string[]): string {
|
|||||||
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
|
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
|
||||||
return available[Math.floor(Math.random() * available.length)] || pool[0];
|
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') {
|
if (BB_AUTOSTORY_BYLINE.toLowerCase() === 'rotate') {
|
||||||
const pool = BB_PEN_NAMES_LEGACY;
|
const pool = BB_PEN_NAMES_LEGACY;
|
||||||
const available = pool.filter(n => !recentNames.slice(-3).includes(n));
|
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
|
if ((count || 0) >= 3) continue; // max 3 per week
|
||||||
|
|
||||||
const siteId = article.site_id || 1;
|
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);
|
const penName = pickPenName(siteId, recentPenNames);
|
||||||
recentPenNames.push(penName);
|
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 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 penNameData = (site_id === 2 ? AV_PEN_NAMES : BB_PEN_NAMES).find(p => p.name === penName);
|
||||||
const phase = getShowPhase(event);
|
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({
|
const story = await generateShowStory({
|
||||||
penName,
|
penName,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import nodemailer from 'nodemailer';
|
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(
|
async function sendSuspensionEmail(
|
||||||
email: string,
|
email: string,
|
||||||
@@ -35,8 +35,8 @@ async function sendSuspensionEmail(
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
const subject = isBanned
|
const subject = isBanned
|
||||||
? `Your Broadcast Beat account has been banned`
|
? `Your AV Beat account has been banned`
|
||||||
: `Your Broadcast Beat account has been suspended`;
|
: `Your AV Beat account has been suspended`;
|
||||||
|
|
||||||
const html = `
|
const html = `
|
||||||
<!DOCTYPE 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;">
|
<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="max-width:560px;margin:40px auto;padding:32px;background:#111827;border:1px solid #1f2937;border-radius:12px;">
|
||||||
<div style="margin-bottom:24px;">
|
<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>
|
||||||
<div style="background:${isBanned ? '#7f1d1d' : '#78350f'};border:1px solid ${isBanned ? '#991b1b' : '#92400e'};border-radius:8px;padding:16px;margin-bottom:24px;">
|
<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'};">
|
<p style="margin:0;font-size:14px;font-weight:700;color:${isBanned ? '#fca5a5' : '#fcd34d'};">
|
||||||
@@ -54,7 +54,7 @@ async function sendSuspensionEmail(
|
|||||||
</div>
|
</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;">Hi ${fullName || 'there'},</p>
|
||||||
<p style="color:#d1d5db;font-size:15px;line-height:1.6;">
|
<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>
|
</p>
|
||||||
<div style="background:#0d1117;border:1px solid #1f2937;border-radius:8px;padding:16px;margin:20px 0;">
|
<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>
|
<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.'}
|
${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>
|
</p>
|
||||||
<div style="margin-top:24px;padding-top:24px;border-top:1px solid #1f2937;">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
@@ -73,7 +73,7 @@ async function sendSuspensionEmail(
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
await transporter.sendMail({
|
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,
|
to: email,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export async function POST(req: NextRequest) {
|
|||||||
// Send email via Supabase Edge Function
|
// Send email via Supabase Edge Function
|
||||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
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 {
|
try {
|
||||||
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-contributor-invite`, {
|
const emailRes = await fetch(`${supabaseUrl}/functions/v1/send-contributor-invite`, {
|
||||||
@@ -118,7 +118,7 @@ export async function POST(req: NextRequest) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
email,
|
email,
|
||||||
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'Broadcast Beat Admin',
|
inviterName: inviterProfile?.full_name || inviterProfile?.email || 'AV Beat Admin',
|
||||||
role: role || 'contributor',
|
role: role || 'contributor',
|
||||||
token,
|
token,
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export async function GET(request: NextRequest) {
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="site-header">
|
<div class="site-header">
|
||||||
<div class="site-logo">Broadcast Beat</div>
|
<div class="site-logo">AV Beat</div>
|
||||||
<nav class="site-nav">
|
<nav class="site-nav">
|
||||||
<a href="#">News</a>
|
<a href="#">News</a>
|
||||||
<a href="#">Reviews</a>
|
<a href="#">Reviews</a>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
date: a.date,
|
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.
|
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"]
|
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.`;
|
Do not include any explanation or markdown — only the raw JSON array.`;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export async function POST(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
response = await fetch(url, {
|
response = await fetch(url, {
|
||||||
headers: {
|
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',
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
},
|
},
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
|
|||||||
@@ -24,11 +24,11 @@ function rateLimit(key: string): { ok: boolean; remaining: number } {
|
|||||||
return { ok: true, remaining: RATE_LIMIT_MAX - b.count };
|
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,
|
You help readers — broadcast engineers, post-production supervisors,
|
||||||
streaming architects, and broadcast executives — find information from the
|
streaming architects, and broadcast executives — find information from the
|
||||||
Broadcast Beat archive.
|
AV Beat archive.
|
||||||
|
|
||||||
Tone: trade-press, factual, lightly conversational. Plain English.
|
Tone: trade-press, factual, lightly conversational. Plain English.
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export async function POST(request: NextRequest) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username: profile.full_name || user.email, term: bannedMatch.term }),
|
body: JSON.stringify({ username: profile.full_name || user.email, term: bannedMatch.term }),
|
||||||
}).catch(() => {});
|
}).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
|
// Soft ban: flag but allow through
|
||||||
// Will be tagged below
|
// Will be tagged below
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
.eq('domain', sendingDomain)
|
.eq('domain', sendingDomain)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
const site = domainRecord?.site ?? 'broadcastbeat';
|
const site = domainRecord?.site ?? 'avbeat';
|
||||||
|
|
||||||
// Load active keywords
|
// Load active keywords
|
||||||
const { data: keywords } = await supabase
|
const { data: keywords } = await supabase
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ interface Row {
|
|||||||
export async function GET() {
|
export async function GET() {
|
||||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
||||||
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
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 });
|
if (!url || !key) return NextResponse.json({ events: [] }, { status: 200 });
|
||||||
|
|
||||||
const sb = createClient(url, key, {
|
const sb = createClient(url, key, {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export async function POST(req: NextRequest) {
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
role: 'system',
|
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.
|
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({
|
.insert({
|
||||||
thread_id,
|
thread_id,
|
||||||
body: aiResponseText,
|
body: aiResponseText,
|
||||||
author_name: 'Broadcast Beat AI',
|
author_name: 'AV Beat AI',
|
||||||
is_ai_response: true,
|
is_ai_response: true,
|
||||||
})
|
})
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export async function POST(req: NextRequest) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await transporter.sendMail({
|
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,
|
to,
|
||||||
subject,
|
subject,
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function POST(req: NextRequest) {
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
</head>
|
||||||
<body style="margin:0;padding:0;background:#0d0d0d;font-family:Arial,sans-serif;">
|
<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;">
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0d0d0d;padding:32px 16px;">
|
||||||
@@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background:#0a0a0a;border-bottom:1px solid #1a1a1a;padding:20px 28px;">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
@@ -62,8 +62,8 @@ export async function POST(req: NextRequest) {
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<tr>
|
<tr>
|
||||||
<td style="background:#0a0a0a;border-top:1px solid #1a1a1a;padding:16px 28px;">
|
<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: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()} Broadcast Beat. All rights reserved.</p>
|
<p style="margin:6px 0 0;color:#333;font-size:11px;">© ${new Date().getFullYear()} AV Beat. All rights reserved.</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -74,7 +74,7 @@ export async function POST(req: NextRequest) {
|
|||||||
</html>`;
|
</html>`;
|
||||||
|
|
||||||
await transporter.sendMail({
|
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,
|
to,
|
||||||
subject: `Shared article: ${articleTitle}`,
|
subject: `Shared article: ${articleTitle}`,
|
||||||
html,
|
html,
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ async function sendWelcome(email: string, topics?: string[]) {
|
|||||||
const smtpUser = process.env.SMTP_USER;
|
const smtpUser = process.env.SMTP_USER;
|
||||||
const smtpPass = process.env.SMTP_PASS;
|
const smtpPass = process.env.SMTP_PASS;
|
||||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||||
const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat';
|
const fromName = process.env.SMTP_FROM_NAME || 'AV Beat';
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||||
|
|
||||||
if (!smtpHost || !smtpUser || !smtpPass || !fromEmail) {
|
if (!smtpHost || !smtpUser || !smtpPass || !fromEmail) {
|
||||||
// SMTP not configured — signal that to logs but don't blow up.
|
// 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({
|
await transporter.sendMail({
|
||||||
from: `"${fromName}" <${fromEmail}>`,
|
from: `"${fromName}" <${fromEmail}>`,
|
||||||
to: email,
|
to: email,
|
||||||
subject: 'Welcome to Broadcast Beat',
|
subject: 'Welcome to AV Beat',
|
||||||
html: `
|
html: `
|
||||||
<!DOCTYPE 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;">
|
<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;">
|
<table role="presentation" cellpadding="0" cellspacing="0" width="100%" style="background:#0a0e15;padding:40px 16px;">
|
||||||
<tr><td align="center">
|
<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;">
|
<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;">
|
<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>
|
<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}
|
${topicsBlock}
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td style="padding:18px 36px 8px;">
|
<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>
|
<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>
|
</td></tr>
|
||||||
<tr><td style="padding:18px 36px 28px;border-top:1px solid #1e3a5f;">
|
<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>
|
</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
|
|||||||
@@ -95,8 +95,8 @@ export async function POST(req: NextRequest) {
|
|||||||
const smtpUser = process.env.SMTP_USER;
|
const smtpUser = process.env.SMTP_USER;
|
||||||
const smtpPass = process.env.SMTP_PASS;
|
const smtpPass = process.env.SMTP_PASS;
|
||||||
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
const fromEmail = process.env.SMTP_FROM_EMAIL || smtpUser;
|
||||||
const fromName = process.env.SMTP_FROM_NAME || 'Broadcast Beat';
|
const fromName = process.env.SMTP_FROM_NAME || 'AV Beat';
|
||||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://broadcastbeat.com';
|
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://avbeat.com';
|
||||||
|
|
||||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||||
return NextResponse.json({ error: 'SMTP credentials not configured' }, { status: 500 });
|
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;">
|
<div style="background:#0d1520;padding:0;font-family:sans-serif;max-width:600px;margin:0 auto;">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div style="background:#111;border-bottom:2px solid #3b82f6;padding:24px 32px;">
|
<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>
|
<p style="color:#555;font-size:12px;margin:4px 0 0;">News & Intelligence for Broadcast, Post-Production & Streaming Technology</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
@@ -151,9 +151,9 @@ export async function POST(req: NextRequest) {
|
|||||||
</div>
|
</div>
|
||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<div style="background:#0a0f1a;padding:24px 32px;border-top:1px solid #1e3a5f;">
|
<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;">
|
<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>
|
<a href="${siteUrl}/unsubscribe?email={{email}}" style="color:#3b82f6;">Unsubscribe</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ slug: s
|
|||||||
const { count } = await advSvc
|
const { count } = await advSvc
|
||||||
.from("active_advertisers_by_property")
|
.from("active_advertisers_by_property")
|
||||||
.select("client_id", { count: "exact", head: true })
|
.select("client_id", { count: "exact", head: true })
|
||||||
.eq("property", "broadcastbeat")
|
.eq("property", "avbeat")
|
||||||
.eq("company_name_lower", needle.toLowerCase());
|
.eq("company_name_lower", needle.toLowerCase());
|
||||||
isActiveAdvertiser = (count || 0) > 0;
|
isActiveAdvertiser = (count || 0) > 0;
|
||||||
} catch { /* tolerate adv lookup failures */ }
|
} catch { /* tolerate adv lookup failures */ }
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
// Decorate + reorder so premium and current-advertiser stories surface
|
// Decorate + reorder so premium and current-advertiser stories surface
|
||||||
// at the top of the feed.
|
// at the top of the feed.
|
||||||
const decorated = await decorateHomepageFeed("broadcastbeat", base);
|
const decorated = await decorateHomepageFeed("avbeat", base);
|
||||||
const payload = decorated.map((d) => ({
|
const payload = decorated.map((d) => ({
|
||||||
...d.article,
|
...d.article,
|
||||||
promoted_kind: d.promoted_kind,
|
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_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
|
||||||
const SUPABASE_ANON = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
|
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) {
|
function client(schema = SCHEMA) {
|
||||||
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
return createClient(SUPABASE_URL, SUPABASE_ANON, {
|
||||||
@@ -75,7 +75,7 @@ export async function GET(req: Request) {
|
|||||||
const { data: ads } = await adv
|
const { data: ads } = await adv
|
||||||
.from("active_advertisers_by_property")
|
.from("active_advertisers_by_property")
|
||||||
.select("company_name_lower")
|
.select("company_name_lower")
|
||||||
.eq("property", "broadcastbeat");
|
.eq("property", "avbeat");
|
||||||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||||||
} catch { /* tolerate */ }
|
} catch { /* tolerate */ }
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const featuredImage = body?.featured_image_url || null;
|
const featuredImage = body?.featured_image_url || null;
|
||||||
const attachments = Array.isArray(body?.attachments) ? body.attachments : [];
|
const attachments = Array.isArray(body?.attachments) ? body.attachments : [];
|
||||||
const submissionId = body?.submission_id || null;
|
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
|
// Defense in depth: re-check the contributor's approval status against
|
||||||
// distribute.users. distribute-rmp's submit route already gates, but a
|
// distribute.users. distribute-rmp's submit route already gates, but a
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { createClient } from '../../../lib/supabase/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 PER_PAGE = 20;
|
||||||
const IMAGE_BUCKET = 'post-images';
|
const IMAGE_BUCKET = 'post-images';
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ function stripHtml(html: string): string {
|
|||||||
async function fetchWPPosts(page: number): Promise<{ posts: WPPost[]; totalPages: number }> {
|
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 url = `${WP_BASE_URL}/posts?page=${page}&per_page=${PER_PAGE}&_embed=1&status=publish`;
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
headers: { 'User-Agent': 'Broadcast Beat-Importer/1.0' },
|
headers: { 'User-Agent': 'AV Beat-Importer/1.0' },
|
||||||
next: { revalidate: 0 },
|
next: { revalidate: 0 },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ async function downloadAndStoreImage(
|
|||||||
try {
|
try {
|
||||||
// Fetch the image
|
// Fetch the image
|
||||||
const response = await fetch(imageUrl, {
|
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),
|
signal: AbortSignal.timeout(15000),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -229,9 +229,9 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
{/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
|
{/* DO NOT OVERRIDE — article breadcrumb and back navigation */}
|
||||||
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]">
|
<div className="bg-[#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]">
|
<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>
|
<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>/</span>
|
||||||
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
|
<span className="text-[#888] truncate max-w-[300px]">{article.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,7 +245,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
{/* Article Header */}
|
{/* Article Header */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#E67E22] hover:underline">
|
<Link href={sectionHref} className="text-xs font-bold uppercase tracking-widest text-[#3b82f6] hover:underline">
|
||||||
{article.category}
|
{article.category}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-xs text-[#777]">{article.date}</span>
|
<span className="text-xs text-[#777]">{article.date}</span>
|
||||||
@@ -271,7 +271,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href={`/authors/${article.authorSlug}`}
|
href={`/authors/${article.authorSlug}`}
|
||||||
className="font-heading font-bold text-sm hover:text-[#E67E22] transition-colors text-[#e0e0e0]">
|
className="font-heading font-bold text-sm hover:text-[#3b82f6] transition-colors text-[#e0e0e0]">
|
||||||
{article.author}
|
{article.author}
|
||||||
</Link>
|
</Link>
|
||||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||||
@@ -286,7 +286,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
<>
|
<>
|
||||||
<a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`}
|
<a href={`mailto:?subject=${encodeURIComponent(article.title)}&body=${encodeURIComponent(articleUrl)}`}
|
||||||
aria-label="Share via email" title="Email"
|
aria-label="Share via email" title="Email"
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#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">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||||
@@ -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">
|
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>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(articleUrl)}&text=${encodeURIComponent(article.title)}&via=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)"
|
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#1a1a1a] transition-colors">
|
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-white hover:bg-[#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>
|
<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"
|
<button type="button"
|
||||||
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
|
onClick={async () => { try { await navigator.clipboard.writeText(articleUrl); } catch {} }}
|
||||||
aria-label="Copy article link" title="Copy link"
|
aria-label="Copy article link" title="Copy link"
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#888] hover:text-[#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">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||||
@@ -334,7 +334,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
onClick={handleSaveToReadingList}
|
onClick={handleSaveToReadingList}
|
||||||
disabled={savingState !== "idle"}
|
disabled={savingState !== "idle"}
|
||||||
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
|
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-[#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"}
|
{savingState === "saving" ? "Saving..." : savingState === "removing" ? "Removing..." : isSaved ? "✓ Saved" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,7 +368,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
{article.tags.map((tag) => (
|
{article.tags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#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}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -380,7 +380,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
<div className="pt-6 border-t border-[#222]">
|
<div className="pt-6 border-t border-[#222]">
|
||||||
<Link
|
<Link
|
||||||
href={sectionHref}
|
href={sectionHref}
|
||||||
className="inline-flex items-center gap-2 text-[#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">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<polyline points="15 18 9 12 15 6" />
|
<polyline points="15 18 9 12 15 6" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -398,7 +398,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
{/* Related Articles */}
|
{/* Related Articles */}
|
||||||
{/* DO NOT OVERRIDE — related articles sidebar */}
|
{/* DO NOT OVERRIDE — related articles sidebar */}
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#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
|
Related Articles
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -417,10 +417,10 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[#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}
|
{related.category}
|
||||||
</p>
|
</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}
|
{related.title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -435,19 +435,19 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
recently-active threads so the box is never empty. */}
|
recently-active threads so the box is never empty. */}
|
||||||
{relatedForumThreads.length > 0 && (
|
{relatedForumThreads.length > 0 && (
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#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
|
Related Forum Posts
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3.5">
|
<ul className="space-y-3.5">
|
||||||
{relatedForumThreads.map((t) => (
|
{relatedForumThreads.map((t) => (
|
||||||
<li key={t.id}>
|
<li key={t.id}>
|
||||||
<Link href={`/forum/thread/${t.id}`} className="group block">
|
<Link href={`/forum/thread/${t.id}`} className="group block">
|
||||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#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}
|
{t.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
||||||
{t.category_name && (
|
{t.category_name && (
|
||||||
<span className="text-[#E67E22] font-body font-bold uppercase tracking-wider">
|
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider">
|
||||||
{t.category_name}
|
{t.category_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -461,7 +461,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
</ul>
|
</ul>
|
||||||
<Link
|
<Link
|
||||||
href="/forum"
|
href="/forum"
|
||||||
className="block mt-4 pt-3 border-t border-[#222] text-[11px] text-[#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 →
|
Browse all forum threads →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -479,7 +479,7 @@ export default function ArticleDetailClient({ article, relatedArticles, relatedF
|
|||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
className="fixed bottom-4 right-4 bg-[#1a1a1a] border border-[#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}
|
{saveToast}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ interface Stats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PUBLICATIONS = [
|
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' },
|
{ id: 'av-beat', name: 'AV Beat', short: 'AV' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ export default function AdOpsDashboard() {
|
|||||||
</button>
|
</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="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" />
|
<span className="w-1.5 h-1.5 rounded-full bg-orange-400" />
|
||||||
adops@broadcastbeat.com
|
adops@avbeat.com
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -191,9 +191,9 @@ const QUICK_ACTIONS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const QUICK_ACTION_TEMPLATES: Record<string, string> = {
|
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.`,
|
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]`,
|
[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.`,
|
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]`,
|
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]`,
|
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]`,
|
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.`,
|
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 className="flex items-center justify-between px-5 py-4 border-b border-[#1e2a3a]">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-white font-semibold text-sm">Draft Preview</h2>
|
<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>
|
</div>
|
||||||
<button onClick={onClose} className="text-[#555] hover:text-white transition-colors text-lg leading-none">✕</button>
|
<button onClick={onClose} className="text-[#555] hover:text-white transition-colors text-lg leading-none">✕</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -256,7 +256,7 @@ function DraftPreviewModal({ content, title, penName, siteId, onClose, onSave }:
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-[#555] text-xs">Site:</span>
|
<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'}`}>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -487,7 +487,7 @@ function StyleGuidesPanel({ onClose, onSelectPenName }: StyleGuidesPanelProps) {
|
|||||||
onClick={() => setFilterSite(f)}
|
onClick={() => setFilterSite(f)}
|
||||||
className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#3b82f6] text-white' : 'bg-[#1e2a3a] text-[#aaa] hover:text-white'}`}
|
className={`text-xs px-2.5 py-1 rounded transition-colors ${filterSite === f ? 'bg-[#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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -670,7 +670,7 @@ export default function AIConsolePage() {
|
|||||||
const penNames = selectedSite === 1 ? BB_PEN_NAMES : AV_PEN_NAMES;
|
const penNames = selectedSite === 1 ? BB_PEN_NAMES : AV_PEN_NAMES;
|
||||||
|
|
||||||
const buildSystemPrompt = () => {
|
const buildSystemPrompt = () => {
|
||||||
const siteName = selectedSite === 1 ? 'Broadcast Beat' : 'AV Beat';
|
const siteName = selectedSite === 1 ? 'AV Beat' : 'AV Beat';
|
||||||
const siteDesc = selectedSite === 1
|
const siteDesc = selectedSite === 1
|
||||||
? 'a professional B2B trade publication for broadcast engineers, production professionals, and industry executives'
|
? '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';
|
: '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)}
|
onChange={e => setSelectedSite(Number(e.target.value) as 1 | 2)}
|
||||||
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#3b82f6]"
|
className="bg-[#1e2a3a] border border-[#2a3a50] text-white text-xs rounded px-2 py-1 focus:outline-none focus:border-[#3b82f6]"
|
||||||
>
|
>
|
||||||
<option value={1}>Broadcast Beat</option>
|
<option value={1}>AV Beat</option>
|
||||||
<option value={2}>AV Beat</option>
|
<option value={2}>AV Beat</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -303,12 +303,12 @@ export default function AIConsoleSettingsPage() {
|
|||||||
onChange={e => update('default_site_id', Number(e.target.value))}
|
onChange={e => update('default_site_id', Number(e.target.value))}
|
||||||
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
className="w-full bg-[#0d1117] border border-[#2a3a50] text-white text-sm rounded-lg px-3 py-2.5 focus:outline-none focus:border-[#3b82f6]"
|
||||||
>
|
>
|
||||||
<option value={1}>Broadcast Beat</option>
|
<option value={1}>AV Beat</option>
|
||||||
<option value={2}>AV Beat</option>
|
<option value={2}>AV Beat</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<select
|
||||||
value={settings.default_pen_name_bb}
|
value={settings.default_pen_name_bb}
|
||||||
onChange={e => update('default_pen_name_bb', e.target.value)}
|
onChange={e => update('default_pen_name_bb', e.target.value)}
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ export default function ShowCalendarPage() {
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
|
||||||
<div className="w-8 h-8 border-2 border-[#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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -294,7 +294,7 @@ export default function ShowCalendarPage() {
|
|||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }}
|
onClick={() => { setEditingEvent({ site_id: 1, year: new Date().getFullYear(), seo_tier: 3, event_type: 'trade_show' }); setShowEventModal(true); }}
|
||||||
className="px-3 py-1.5 bg-[#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
|
+ Add Event
|
||||||
</button>
|
</button>
|
||||||
@@ -316,7 +316,7 @@ export default function ShowCalendarPage() {
|
|||||||
onClick={() => setSiteFilter(s)}
|
onClick={() => setSiteFilter(s)}
|
||||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||||
siteFilter === s
|
siteFilter === s
|
||||||
? 'bg-[#E67E22] text-white'
|
? 'bg-[#3b82f6] text-white'
|
||||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -333,7 +333,7 @@ export default function ShowCalendarPage() {
|
|||||||
onClick={() => setActiveTab(tab.id)}
|
onClick={() => setActiveTab(tab.id)}
|
||||||
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||||
activeTab === tab.id
|
activeTab === tab.id
|
||||||
? 'border-[#E67E22] text-white'
|
? 'border-[#3b82f6] text-white'
|
||||||
: 'border-transparent text-[#666] hover:text-white'
|
: 'border-transparent text-[#666] hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -347,7 +347,7 @@ export default function ShowCalendarPage() {
|
|||||||
<div className="max-w-7xl mx-auto px-4 py-6">
|
<div className="max-w-7xl mx-auto px-4 py-6">
|
||||||
{loadingData ? (
|
{loadingData ? (
|
||||||
<div className="flex items-center justify-center py-20">
|
<div className="flex items-center justify-center py-20">
|
||||||
<div className="w-8 h-8 border-2 border-[#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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -383,7 +383,7 @@ export default function ShowCalendarPage() {
|
|||||||
<span>Engine: {formatDate(event.story_engine_start_date)} – {formatDate(event.story_engine_end_date)}</span>
|
<span>Engine: {formatDate(event.story_engine_start_date)} – {formatDate(event.story_engine_end_date)}</span>
|
||||||
{event.official_site && (
|
{event.official_site && (
|
||||||
<a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer"
|
<a href={`https://${event.official_site}`} target="_blank" rel="noopener noreferrer"
|
||||||
className="text-[#E67E22] hover:underline">{event.official_site}</a>
|
className="text-[#3b82f6] hover:underline">{event.official_site}</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
|
<div className="flex items-center gap-3 mt-2 text-xs text-[#555]">
|
||||||
@@ -440,7 +440,7 @@ export default function ShowCalendarPage() {
|
|||||||
onClick={() => setQueueStatus(s)}
|
onClick={() => setQueueStatus(s)}
|
||||||
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
className={`px-3 py-1 rounded text-xs font-medium transition-colors ${
|
||||||
queueStatus === s
|
queueStatus === s
|
||||||
? 'bg-[#E67E22] text-white'
|
? 'bg-[#3b82f6] text-white'
|
||||||
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
: 'bg-[#1a1a1a] text-[#888] hover:text-white border border-[#333]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -779,7 +779,7 @@ export default function ShowCalendarPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={actionLoading === 'save-event'}
|
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'}
|
{actionLoading === 'save-event' ? 'Saving...' : 'Save Event'}
|
||||||
</button>
|
</button>
|
||||||
@@ -857,7 +857,7 @@ export default function ShowCalendarPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={actionLoading === 'generate'}
|
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'}
|
{actionLoading === 'generate' ? 'Generating...' : 'Generate Story'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ function ForumLoginInner() {
|
|||||||
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]">
|
<main className="min-h-screen bg-gradient-to-b from-[#0d0d0d] via-[#111111] to-[#0d0d0d]">
|
||||||
<div className="max-w-md mx-auto px-4 py-16">
|
<div className="max-w-md mx-auto px-4 py-16">
|
||||||
<div className="text-center mb-6">
|
<div className="text-center mb-6">
|
||||||
<div className="text-[10px] uppercase tracking-widest text-[#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>
|
<h1 className="text-3xl font-heading font-bold text-white">Sign in to AV Beat</h1>
|
||||||
<p className="text-[#888] font-body text-sm mt-2">
|
<p className="text-[#888] font-body text-sm mt-2">
|
||||||
Your account covers the forum, newsletter preferences, and saved articles.
|
Your account covers the forum, newsletter preferences, and saved articles.
|
||||||
@@ -79,7 +79,7 @@ function ForumLoginInner() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<div>
|
<div>
|
||||||
@@ -92,20 +92,20 @@ function ForumLoginInner() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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'}
|
{loading ? 'Signing in…' : 'Sign In'}
|
||||||
</button>
|
</button>
|
||||||
<div className="text-right text-xs font-body">
|
<div className="text-right text-xs font-body">
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/forgot-password?email=${encodeURIComponent(email)}`}
|
href={`/forum/forgot-password?email=${encodeURIComponent(email)}`}
|
||||||
className="text-[#888] hover:text-[#E67E22] hover:underline"
|
className="text-[#888] hover:text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
Forgot password?
|
Forgot password?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -115,7 +115,7 @@ function ForumLoginInner() {
|
|||||||
No account yet?{' '}
|
No account yet?{' '}
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/register?next=${encodeURIComponent(next)}`}
|
href={`/forum/register?next=${encodeURIComponent(next)}`}
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
Create one
|
Create one
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
|||||||
|
|
||||||
const getMetricValue = (thread: ForumThread) => {
|
const getMetricValue = (thread: ForumThread) => {
|
||||||
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
|
if (activeMetric === 'upvotes') return { value: thread.upvotes ?? 0, label: 'votes', color: 'text-[#22c55e]' };
|
||||||
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#E67E22]' };
|
if (activeMetric === 'replies') return { value: thread.reply_count, label: 'replies', color: 'text-[#3b82f6]' };
|
||||||
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
|
return { value: thread.view_count, label: 'views', color: 'text-[#f59e0b]' };
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
|||||||
onClick={() => setActiveMetric(m.value)}
|
onClick={() => setActiveMetric(m.value)}
|
||||||
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
className={`px-3 py-1 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||||
activeMetric === m.value
|
activeMetric === m.value
|
||||||
? 'bg-[#E67E22] text-white'
|
? 'bg-[#3b82f6] text-white'
|
||||||
: 'text-[#666] hover:text-[#aaa]'
|
: 'text-[#666] hover:text-[#aaa]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -171,13 +171,13 @@ function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
|
|||||||
</span>
|
</span>
|
||||||
{/* Thread info */}
|
{/* Thread info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-white font-body text-sm font-semibold group-hover:text-[#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}
|
{thread.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[#555] font-body text-xs mt-0.5">
|
<p className="text-[#555] font-body text-xs mt-0.5">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#666] hover:text-[#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 && (
|
{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)}
|
{' · '}{timeAgo(thread.created_at)}
|
||||||
</p>
|
</p>
|
||||||
@@ -301,7 +301,7 @@ export default function ForumIndexPage() {
|
|||||||
{currentUserId ? (
|
{currentUserId ? (
|
||||||
<Link
|
<Link
|
||||||
href="/forum/new"
|
href="/forum/new"
|
||||||
className="inline-flex items-center gap-2 bg-[#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">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<path d="M12 5v14M5 12h14" />
|
<path d="M12 5v14M5 12h14" />
|
||||||
@@ -311,7 +311,7 @@ export default function ForumIndexPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href="/forum/login?next=/forum"
|
href="/forum/login?next=/forum"
|
||||||
className="inline-flex items-center gap-2 bg-[#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">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
|
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4M10 17l5-5-5-5M15 12H3" />
|
||||||
@@ -333,7 +333,7 @@ export default function ForumIndexPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setFeedMode('discover')}
|
onClick={() => setFeedMode('discover')}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
|
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
|
||||||
feedMode === 'discover' ? 'bg-[#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">
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
@@ -344,7 +344,7 @@ export default function ForumIndexPage() {
|
|||||||
<button
|
<button
|
||||||
onClick={() => setFeedMode('following')}
|
onClick={() => setFeedMode('following')}
|
||||||
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
|
className={`px-4 py-2 rounded-md text-sm font-body font-semibold transition-colors flex items-center gap-2 ${
|
||||||
feedMode === 'following' ? 'bg-[#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">
|
<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>
|
<p className="text-[#aab4c4] font-body text-sm mb-3">Sign in to see threads from people you follow.</p>
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
href="/login"
|
||||||
className="inline-block bg-[#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
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
@@ -384,7 +384,7 @@ export default function ForumIndexPage() {
|
|||||||
<Link
|
<Link
|
||||||
href="/forum"
|
href="/forum"
|
||||||
onClick={() => setFeedMode('discover')}
|
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
|
Discover Members
|
||||||
</Link>
|
</Link>
|
||||||
@@ -398,10 +398,10 @@ export default function ForumIndexPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={thread.id}
|
key={thread.id}
|
||||||
href={`/forum/thread/${thread.id}`}
|
href={`/forum/thread/${thread.id}`}
|
||||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#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">
|
<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}
|
{thread.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||||
@@ -409,7 +409,7 @@ export default function ForumIndexPage() {
|
|||||||
<Link
|
<Link
|
||||||
href={`/profile/${thread.author_id}`}
|
href={`/profile/${thread.author_id}`}
|
||||||
onClick={e => e.stopPropagation()}
|
onClick={e => e.stopPropagation()}
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
{thread.author_name}
|
{thread.author_name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -450,7 +450,7 @@ export default function ForumIndexPage() {
|
|||||||
placeholder="Search threads..."
|
placeholder="Search threads..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
className="w-full bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#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 && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
@@ -470,7 +470,7 @@ export default function ForumIndexPage() {
|
|||||||
<select
|
<select
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={e => setSelectedCategory(e.target.value)}
|
onChange={e => setSelectedCategory(e.target.value)}
|
||||||
className="appearance-none bg-[#1a1a1a] border border-[#2a2a2a] focus:border-[#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>
|
<option value="">All Categories</option>
|
||||||
{categories.map(cat => (
|
{categories.map(cat => (
|
||||||
@@ -490,7 +490,7 @@ export default function ForumIndexPage() {
|
|||||||
onClick={() => setSort(opt.value)}
|
onClick={() => setSort(opt.value)}
|
||||||
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
className={`px-3 py-1.5 rounded-md text-xs font-body font-semibold transition-colors whitespace-nowrap ${
|
||||||
sort === opt.value
|
sort === opt.value
|
||||||
? 'bg-[#E67E22] text-white'
|
? 'bg-[#3b82f6] text-white'
|
||||||
: 'text-[#888] hover:text-white'
|
: 'text-[#888] hover:text-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -523,16 +523,16 @@ export default function ForumIndexPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={thread.id}
|
key={thread.id}
|
||||||
href={`/forum/thread/${thread.id}`}
|
href={`/forum/thread/${thread.id}`}
|
||||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#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">
|
<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}
|
{thread.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#666] font-body text-xs mt-0.5">
|
<p className="text-[#666] font-body text-xs mt-0.5">
|
||||||
by <Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-[#888] hover:text-[#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 && (
|
{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)}
|
{' · '}{timeAgo(thread.created_at)}
|
||||||
</p>
|
</p>
|
||||||
@@ -584,11 +584,11 @@ export default function ForumIndexPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={cat.id}
|
key={cat.id}
|
||||||
href={`/forum/${cat.slug}`}
|
href={`/forum/${cat.slug}`}
|
||||||
className="flex items-center gap-4 bg-[#1a1a1a] hover:bg-[#1e2a3a] border border-[#252525] hover:border-[#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>
|
<span className="text-3xl flex-shrink-0 w-10 text-center">{cat.icon}</span>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#E67E22] transition-colors">
|
<h2 className="text-white font-heading font-semibold text-lg group-hover:text-[#3b82f6] transition-colors">
|
||||||
{cat.name}
|
{cat.name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
|
<p className="text-[#888] font-body text-sm mt-0.5 truncate">{cat.description}</p>
|
||||||
@@ -610,7 +610,7 @@ export default function ForumIndexPage() {
|
|||||||
<div className="text-white font-body font-semibold text-sm">{cat.post_count}</div>
|
<div className="text-white font-body font-semibold text-sm">{cat.post_count}</div>
|
||||||
<div className="text-[#666] font-body text-xs">posts</div>
|
<div className="text-[#666] font-body text-xs">posts</div>
|
||||||
</div>
|
</div>
|
||||||
<svg className="w-4 h-4 text-[#555] group-hover:text-[#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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -629,7 +629,7 @@ export default function ForumIndexPage() {
|
|||||||
<ul className="text-[#aab4c4] font-body text-sm space-y-1">
|
<ul className="text-[#aab4c4] font-body text-sm space-y-1">
|
||||||
<li>• Be respectful and professional — this is a community of peers</li>
|
<li>• Be respectful and professional — this is a community of peers</li>
|
||||||
<li>• Share real-world experience and cite sources when possible</li>
|
<li>• Share real-world experience and cite sources when possible</li>
|
||||||
<li>• The <span className="text-[#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>
|
<li>• No spam, self-promotion, or vendor pitches without disclosure</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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">
|
<div className="max-w-5xl mx-auto px-4 py-12 grid grid-cols-1 lg:grid-cols-[1fr_440px] gap-10 items-start">
|
||||||
{/* Left rail — what membership gets you */}
|
{/* Left rail — what membership gets you */}
|
||||||
<aside className="hidden lg:block">
|
<aside className="hidden lg:block">
|
||||||
<div className="text-[10px] uppercase tracking-widest text-[#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">
|
<h1 className="font-heading text-white text-4xl xl:text-5xl font-bold leading-tight mb-4">
|
||||||
Join AV Beat
|
Join AV Beat
|
||||||
</h1>
|
</h1>
|
||||||
@@ -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">
|
<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.
|
<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>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ function ForumRegisterInner() {
|
|||||||
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl"
|
className="bg-[#1a1a1a] border border-[#252525] rounded-lg p-6 space-y-4 shadow-2xl"
|
||||||
>
|
>
|
||||||
<div className="hidden lg:block mb-2">
|
<div className="hidden lg:block mb-2">
|
||||||
<div className="text-[10px] uppercase tracking-widest text-[#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 className="text-[#888] text-xs">Takes 30 seconds. Free, no card needed.</div>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -171,7 +171,7 @@ function ForumRegisterInner() {
|
|||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
autoComplete="name"
|
autoComplete="name"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<div>
|
<div>
|
||||||
@@ -184,7 +184,7 @@ function ForumRegisterInner() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<div>
|
<div>
|
||||||
@@ -198,7 +198,7 @@ function ForumRegisterInner() {
|
|||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
minLength={6}
|
minLength={6}
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<div>
|
<div>
|
||||||
@@ -211,21 +211,21 @@ function ForumRegisterInner() {
|
|||||||
onChange={(e) => setConfirm(e.target.value)}
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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'}
|
{loading ? 'Creating account…' : 'Create Free Account'}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
|
<p className="text-[10px] text-[#666] leading-relaxed text-center px-2">
|
||||||
By creating an account you agree to our{' '}
|
By creating an account you agree to our{' '}
|
||||||
<Link href="/terms" className="text-[#888] hover:text-[#E67E22] underline">terms</Link>
|
<Link href="/terms" className="text-[#888] hover:text-[#3b82f6] underline">terms</Link>
|
||||||
{' '}and{' '}
|
{' '}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.
|
We’ll never sell your email.
|
||||||
</p>
|
</p>
|
||||||
<SocialAuthButtons next={next} />
|
<SocialAuthButtons next={next} />
|
||||||
@@ -233,7 +233,7 @@ function ForumRegisterInner() {
|
|||||||
Already have an account?{' '}
|
Already have an account?{' '}
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/login?next=${encodeURIComponent(next)}`}
|
href={`/forum/login?next=${encodeURIComponent(next)}`}
|
||||||
className="text-[#E67E22] hover:underline font-semibold"
|
className="text-[#3b82f6] hover:underline font-semibold"
|
||||||
>
|
>
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ function timeAgo(dateStr: string): string {
|
|||||||
function Avatar({ name, isAI }: { name: string; isAI: boolean }) {
|
function Avatar({ name, isAI }: { name: string; isAI: boolean }) {
|
||||||
if (isAI) {
|
if (isAI) {
|
||||||
return (
|
return (
|
||||||
<div className="w-9 h-9 rounded-full bg-[#1e3a5f] border border-[#E67E22] flex items-center justify-center flex-shrink-0">
|
<div className="w-9 h-9 rounded-full bg-[#1e3a5f] border border-[#3b82f6] flex items-center justify-center flex-shrink-0">
|
||||||
<span className="text-[#E67E22] text-xs font-bold">AI</span>
|
<span className="text-[#3b82f6] text-xs font-bold">AI</span>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="flex items-center gap-1 mt-2">
|
<div className="flex items-center gap-1 mt-2">
|
||||||
@@ -144,8 +144,8 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
|||||||
title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Upvote'}
|
title={!isAuthenticated ? 'Sign in to vote' : isAI ? 'Cannot vote on AI responses' : 'Upvote'}
|
||||||
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
|
className={`flex items-center gap-1 px-2 py-1 rounded text-xs font-body font-semibold transition-colors border ${
|
||||||
currentVote === 1
|
currentVote === 1
|
||||||
? 'bg-[#E67E22]/20 border-[#E67E22] text-[#E67E22]'
|
? 'bg-[#3b82f6]/20 border-[#3b82f6] text-[#3b82f6]'
|
||||||
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#E67E22] hover:text-[#E67E22]'
|
: 'bg-transparent border-[#2a2a2a] text-[#666] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||||
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||||
aria-label="Upvote"
|
aria-label="Upvote"
|
||||||
>
|
>
|
||||||
@@ -178,7 +178,7 @@ function VoteButtons({ targetType, targetId, upvotes, downvotes, voteScore, isAI
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{!isAuthenticated && (
|
{!isAuthenticated && (
|
||||||
<Link href="/login" className="text-xs font-body text-[#555] hover:text-[#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
|
Sign in to vote
|
||||||
</Link>
|
</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">
|
<div className="bg-red-900/20 border border-red-800 rounded-lg p-4 text-red-400 font-body">
|
||||||
{error || 'Thread not found.'}
|
{error || 'Thread not found.'}
|
||||||
</div>
|
</div>
|
||||||
<Link href="/forum" className="mt-4 inline-block text-[#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
|
← Back to Forum
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -342,11 +342,11 @@ export default function ForumThreadPage() {
|
|||||||
<div className="bg-[#1a2535] border-b border-[#2a3a50]">
|
<div className="bg-[#1a2535] border-b border-[#2a3a50]">
|
||||||
<div className="max-w-container mx-auto px-4 py-4">
|
<div className="max-w-container mx-auto px-4 py-4">
|
||||||
<nav className="text-sm font-body text-[#666]">
|
<nav className="text-sm font-body text-[#666]">
|
||||||
<Link href="/forum" className="hover:text-[#E67E22] transition-colors">Forum</Link>
|
<Link href="/forum" className="hover:text-[#3b82f6] transition-colors">Forum</Link>
|
||||||
<span className="mx-2">›</span>
|
<span className="mx-2">›</span>
|
||||||
{category && (
|
{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>
|
<span className="mx-2">›</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -366,7 +366,7 @@ export default function ForumThreadPage() {
|
|||||||
<Avatar name={thread.author_name} isAI={false} />
|
<Avatar name={thread.author_name} isAI={false} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(thread.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#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">·</span>
|
||||||
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
|
<span className="text-[#555] font-body text-xs">{timeAgo(thread.created_at)}</span>
|
||||||
<span className="text-[#555] font-body text-xs">·</span>
|
<span className="text-[#555] font-body text-xs">·</span>
|
||||||
@@ -403,9 +403,9 @@ export default function ForumThreadPage() {
|
|||||||
<Avatar name={reply.author_name} isAI={reply.is_ai_response} />
|
<Avatar name={reply.author_name} isAI={reply.is_ai_response} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
<div className="flex items-center gap-2 mb-2 flex-wrap">
|
||||||
<Link href={`/forum/user/${encodeURIComponent(reply.author_name)}`} className="text-white font-body font-semibold text-sm hover:text-[#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 && (
|
{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
|
AI Assistant
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -443,12 +443,12 @@ export default function ForumThreadPage() {
|
|||||||
{/* AI Assistant Panel */}
|
{/* AI Assistant Panel */}
|
||||||
<div className="bg-[#0d1f35] border border-[#1e3a5f] rounded-lg p-5 mb-6">
|
<div className="bg-[#0d1f35] border border-[#1e3a5f] rounded-lg p-5 mb-6">
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<div className="w-7 h-7 rounded-full bg-[#1e3a5f] border border-[#E67E22] flex items-center justify-center">
|
<div className="w-7 h-7 rounded-full bg-[#1e3a5f] border border-[#3b82f6] flex items-center justify-center">
|
||||||
<span className="text-[#E67E22] text-xs font-bold">AI</span>
|
<span className="text-[#3b82f6] text-xs font-bold">AI</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="text-white font-body font-semibold text-sm">AV Beat AI Assistant</span>
|
<span className="text-white font-body font-semibold text-sm">AV Beat AI Assistant</span>
|
||||||
<span className="ml-2 text-xs bg-[#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>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[#7a9ab8] font-body text-xs mb-3">
|
<p className="text-[#7a9ab8] font-body text-xs mb-3">
|
||||||
@@ -461,12 +461,12 @@ export default function ForumThreadPage() {
|
|||||||
value={aiQuestion}
|
value={aiQuestion}
|
||||||
onChange={e => setAiQuestion(e.target.value)}
|
onChange={e => setAiQuestion(e.target.value)}
|
||||||
disabled={aiLoading}
|
disabled={aiLoading}
|
||||||
className="flex-1 bg-[#111] border border-[#2a3a50] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#3a5a7a] focus:outline-none focus:border-[#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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={aiLoading || !aiQuestion.trim()}
|
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 ? (
|
{aiLoading ? (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
@@ -495,7 +495,7 @@ export default function ForumThreadPage() {
|
|||||||
onChange={e => setReplyBody(e.target.value)}
|
onChange={e => setReplyBody(e.target.value)}
|
||||||
required
|
required
|
||||||
rows={5}
|
rows={5}
|
||||||
className="w-full bg-[#111] border border-[#333] rounded-lg px-3 py-2 text-white font-body text-sm placeholder-[#555] focus:outline-none focus:border-[#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">
|
<div className="flex items-center gap-3">
|
||||||
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
|
<ImageAttachButton onInsert={(md) => setReplyBody((b) => (b || "") + md)} />
|
||||||
@@ -509,7 +509,7 @@ export default function ForumThreadPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting || !replyBody.trim()}
|
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'}
|
{submitting ? 'Posting...' : 'Post Reply'}
|
||||||
</button>
|
</button>
|
||||||
@@ -526,13 +526,13 @@ export default function ForumThreadPage() {
|
|||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
href={`/forum/login?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
||||||
className="bg-[#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
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
href={`/forum/register?next=${encodeURIComponent(`/forum/thread/${threadId}`)}`}
|
||||||
className="border border-[#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
|
Register
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export default async function GearPage() {
|
|||||||
<Link
|
<Link
|
||||||
key={article.slug}
|
key={article.slug}
|
||||||
href={`/articles/${article.slug}`}
|
href={`/articles/${article.slug}`}
|
||||||
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#E67E22] transition-colors">
|
className="bg-[#111] border border-[#222] overflow-hidden group hover:border-[#3b82f6] transition-colors">
|
||||||
<div className="relative h-[200px] overflow-hidden">
|
<div className="relative h-[200px] overflow-hidden">
|
||||||
<AppImage
|
<AppImage
|
||||||
src={article.image}
|
src={article.image}
|
||||||
@@ -59,7 +59,7 @@ export default async function GearPage() {
|
|||||||
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
sizes="(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw"
|
||||||
/>
|
/>
|
||||||
<div className="absolute top-2 left-2">
|
<div className="absolute top-2 left-2">
|
||||||
<span className="bg-[#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}
|
{article.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -74,7 +74,7 @@ export default async function GearPage() {
|
|||||||
)}
|
)}
|
||||||
<span className="text-[#555] font-body text-[11px]">{article.readTime}</span>
|
<span className="text-[#555] font-body text-[11px]">{article.readTime}</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="font-heading text-[#e0e0e0] text-sm font-bold leading-snug mb-2 group-hover:text-[#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}
|
{article.title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#666] font-body text-xs leading-relaxed line-clamp-2">{article.excerpt}</p>
|
<p className="text-[#666] font-body text-xs leading-relaxed line-clamp-2">{article.excerpt}</p>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default function GlobalError({
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={() => reset()}
|
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
|
Try again
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ export default function ArticleFeed() {
|
|||||||
value={sortOrder}
|
value={sortOrder}
|
||||||
onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }}
|
onChange={(e) => { setSortOrder(e.target.value as SortOrder); setPage(1); setInfiniteCount(ARTICLES_PER_PAGE); }}
|
||||||
aria-label="Sort articles"
|
aria-label="Sort articles"
|
||||||
className="bg-[#111] border border-[#2a2a2a] text-[#aaa] font-body text-[11px] font-semibold uppercase tracking-wide rounded-sm pl-2.5 pr-7 py-1 appearance-none focus:outline-none focus:border-[#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="recent">Recent</option>
|
||||||
<option value="trending">Trending</option>
|
<option value="trending">Trending</option>
|
||||||
@@ -333,8 +333,8 @@ export default function ArticleFeed() {
|
|||||||
onClick={() => handleFeedModeChange("pagination")}
|
onClick={() => handleFeedModeChange("pagination")}
|
||||||
aria-pressed={feedMode === "pagination"}
|
aria-pressed={feedMode === "pagination"}
|
||||||
title="Pagination mode"
|
title="Pagination mode"
|
||||||
className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
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-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
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">
|
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||||
<rect x="1" y="1" width="10" height="2.5" rx="0.5" fill="currentColor" opacity="0.9"/>
|
<rect x="1" y="1" width="10" height="2.5" rx="0.5" fill="currentColor" opacity="0.9"/>
|
||||||
@@ -347,8 +347,8 @@ export default function ArticleFeed() {
|
|||||||
onClick={() => handleFeedModeChange("infinite")}
|
onClick={() => handleFeedModeChange("infinite")}
|
||||||
aria-pressed={feedMode === "infinite"}
|
aria-pressed={feedMode === "infinite"}
|
||||||
title="Infinite scroll mode"
|
title="Infinite scroll mode"
|
||||||
className={`flex items-center gap-1 px-2.5 py-1 rounded-sm font-body text-[10px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22] ${
|
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-[#E67E22]/20 text-[#E67E22] border border-[#E67E22]/40" :"text-[#666] hover:text-[#aaa] border border-transparent"
|
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">
|
<svg width="11" height="11" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||||
<rect x="1" y="1" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.9"/>
|
<rect x="1" y="1" width="10" height="2" rx="0.5" fill="currentColor" opacity="0.9"/>
|
||||||
@@ -372,7 +372,7 @@ export default function ArticleFeed() {
|
|||||||
value={dateFrom}
|
value={dateFrom}
|
||||||
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
onChange={(e) => { setDateFrom(e.target.value); setPage(1); }}
|
||||||
max={dateTo || undefined}
|
max={dateTo || undefined}
|
||||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#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"
|
title="From date"
|
||||||
aria-label="Filter articles from date"
|
aria-label="Filter articles from date"
|
||||||
/>
|
/>
|
||||||
@@ -382,7 +382,7 @@ export default function ArticleFeed() {
|
|||||||
value={dateTo}
|
value={dateTo}
|
||||||
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
onChange={(e) => { setDateTo(e.target.value); setPage(1); }}
|
||||||
min={dateFrom || undefined}
|
min={dateFrom || undefined}
|
||||||
className="bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] text-xs font-body px-3 py-2 rounded-sm focus:outline-none focus:border-[#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"
|
title="To date"
|
||||||
aria-label="Filter articles to date"
|
aria-label="Filter articles to date"
|
||||||
/>
|
/>
|
||||||
@@ -401,9 +401,9 @@ export default function ArticleFeed() {
|
|||||||
onClick={() => setShowAdvanced((v) => !v)}
|
onClick={() => setShowAdvanced((v) => !v)}
|
||||||
aria-expanded={showAdvanced}
|
aria-expanded={showAdvanced}
|
||||||
aria-controls={`${feedId}-advanced`}
|
aria-controls={`${feedId}-advanced`}
|
||||||
className={`flex items-center gap-1.5 px-3 py-2 rounded-sm border font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#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
|
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]"
|
: "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">
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||||
@@ -411,7 +411,7 @@ export default function ArticleFeed() {
|
|||||||
</svg>
|
</svg>
|
||||||
More
|
More
|
||||||
{hasAdvancedActive && (
|
{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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,9 +428,9 @@ export default function ArticleFeed() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setActiveCategory(cat); setPage(1); }}
|
onClick={() => { setActiveCategory(cat); setPage(1); }}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#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
|
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]"
|
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc] hover:border-[#3a3a3a]"
|
||||||
}`}>
|
}`}>
|
||||||
{cat}
|
{cat}
|
||||||
@@ -458,7 +458,7 @@ export default function ArticleFeed() {
|
|||||||
id={`${feedId}-author`}
|
id={`${feedId}-author`}
|
||||||
value={authorFilter}
|
value={authorFilter}
|
||||||
onChange={(e) => { setAuthorFilter(e.target.value); setPage(1); }}
|
onChange={(e) => { setAuthorFilter(e.target.value); setPage(1); }}
|
||||||
className="w-full bg-[#111] border border-[#2a2a2a] text-[#ccc] font-body text-sm rounded-sm px-3 py-1.5 pr-8 appearance-none focus:outline-none focus:border-[#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">
|
aria-label="Filter by author">
|
||||||
<option value="All">All Authors</option>
|
<option value="All">All Authors</option>
|
||||||
{uniqueAuthors.map((author) => (
|
{uniqueAuthors.map((author) => (
|
||||||
@@ -483,10 +483,10 @@ export default function ArticleFeed() {
|
|||||||
key={sf}
|
key={sf}
|
||||||
onClick={() => { setSourceFilter(sf); setPage(1); }}
|
onClick={() => { setSourceFilter(sf); setPage(1); }}
|
||||||
aria-pressed={isActive}
|
aria-pressed={isActive}
|
||||||
className={`px-3 py-1 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all duration-150 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#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
|
isActive
|
||||||
? sf === "Imported" ?"bg-amber-500/20 text-amber-400 border border-amber-500/40"
|
? sf === "Imported" ?"bg-amber-500/20 text-amber-400 border border-amber-500/40"
|
||||||
: sf === "Live" ?"bg-emerald-500/20 text-emerald-400 border border-emerald-500/40" :"bg-[#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}
|
||||||
{sf === "Imported" && importedCount > 0 && (
|
{sf === "Imported" && importedCount > 0 && (
|
||||||
@@ -507,7 +507,7 @@ export default function ArticleFeed() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }}
|
onClick={() => { setAuthorFilter("All"); setDateFrom(""); setDateTo(""); setPage(1); }}
|
||||||
className="font-body text-[11px] text-[#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
|
Clear advanced filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -537,7 +537,7 @@ export default function ArticleFeed() {
|
|||||||
{sortedFiltered.length === 0 && (
|
{sortedFiltered.length === 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={clearAllFilters}
|
onClick={clearAllFilters}
|
||||||
className="ml-1 text-[#E67E22] hover:underline focus:outline-none focus-visible:underline">
|
className="ml-1 text-[#3b82f6] hover:underline focus:outline-none focus-visible:underline">
|
||||||
Clear filters
|
Clear filters
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -557,7 +557,7 @@ export default function ArticleFeed() {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={clearAllFilters}
|
onClick={clearAllFilters}
|
||||||
className="mt-4 font-body text-sm text-[#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
|
Clear filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -585,7 +585,7 @@ export default function ArticleFeed() {
|
|||||||
const promotedClass = promoted === 'premium'
|
const promotedClass = promoted === 'premium'
|
||||||
? 'border-l-4 border-amber-400 bg-gradient-to-r from-amber-500/10 to-transparent pl-3 -ml-2 mr-0'
|
? 'border-l-4 border-amber-400 bg-gradient-to-r from-amber-500/10 to-transparent pl-3 -ml-2 mr-0'
|
||||||
: promoted === 'client'
|
: promoted === 'client'
|
||||||
? 'border-l-4 border-[#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 (
|
return (
|
||||||
<Fragment key={`feed-${i}`}>
|
<Fragment key={`feed-${i}`}>
|
||||||
@@ -593,7 +593,7 @@ export default function ArticleFeed() {
|
|||||||
<Link
|
<Link
|
||||||
href={articleHref}
|
href={articleHref}
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
className="flex-shrink-0 img-zoom overflow-hidden w-[100px] h-[70px] sm:w-[140px] sm:h-[95px] md:w-[190px] md:h-[128px] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#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}
|
tabIndex={-1}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
@@ -614,21 +614,21 @@ export default function ArticleFeed() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{promoted === 'client' && (
|
{promoted === 'client' && (
|
||||||
<span className="font-body text-[9px] font-bold text-[#7aa7d4] bg-[#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
|
◆ Advertiser
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{article?.category && article.category.toLowerCase() !== "legacy" && (
|
{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}
|
{article.category}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-heading text-[#e0e0e0] font-bold text-[0.875rem] md:text-[1rem] leading-snug mb-1 md:mb-1.5 group-hover:text-[#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
|
<Link
|
||||||
href={articleHref}
|
href={articleHref}
|
||||||
{...linkProps}
|
{...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}
|
{article?.title}
|
||||||
</Link>
|
</Link>
|
||||||
</h3>
|
</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">
|
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>
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M19 0h-14C2.24 0 0 2.24 0 5v14c0 2.76 2.24 5 5 5h14c2.76 0 5-2.24 5-5V5c0-2.76-2.24-5-5-5zM8 19H5V8h3v11zM6.5 6.73a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5zM20 19h-3v-5.6c0-3.37-4-3.12-4 0V19h-3V8h3v1.77c1.4-2.59 7-2.78 7 2.48V19z"/></svg>
|
||||||
</a>
|
</a>
|
||||||
<a href={`https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}&via=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)"
|
target="_blank" rel="noopener noreferrer" aria-label="Share on X" title="X (Twitter)"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-white hover:bg-[#1a1a1a] transition-colors">
|
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-white hover:bg-[#1a1a1a] transition-colors">
|
||||||
@@ -702,7 +702,7 @@ export default function ArticleFeed() {
|
|||||||
<a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
<a href={`mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(shareUrl)}`}
|
||||||
aria-label="Share via email" title="Email"
|
aria-label="Share via email" title="Email"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="flex items-center justify-center w-8 h-8 rounded-sm text-[#666] hover:text-[#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">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
<rect x="2" y="4" width="20" height="16" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -735,9 +735,9 @@ export default function ArticleFeed() {
|
|||||||
{isLoadingMore && (
|
{isLoadingMore && (
|
||||||
<div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles">
|
<div className="flex items-center justify-center gap-2 py-6" aria-live="polite" aria-label="Loading more articles">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-[#E67E22] animate-bounce" style={{ animationDelay: "0ms" }} />
|
<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-[#E67E22] animate-bounce" style={{ animationDelay: "150ms" }} />
|
<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-[#E67E22] animate-bounce" style={{ animationDelay: "300ms" }} />
|
<span className="w-1.5 h-1.5 rounded-full bg-[#3b82f6] animate-bounce" style={{ animationDelay: "300ms" }} />
|
||||||
</div>
|
</div>
|
||||||
<span className="font-body text-xs text-[#555]">Loading more articles…</span>
|
<span className="font-body text-xs text-[#555]">Loading more articles…</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -762,7 +762,7 @@ export default function ArticleFeed() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-1" role="navigation" aria-label="Pagination">
|
<div className="flex items-center gap-1" role="navigation" aria-label="Pagination">
|
||||||
<button
|
<button
|
||||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]"
|
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
onClick={() => { setPage(Math.max(1, page - 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||||
aria-label="Previous page">
|
aria-label="Previous page">
|
||||||
@@ -786,7 +786,7 @@ export default function ArticleFeed() {
|
|||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
key={p}
|
key={p}
|
||||||
className={`page-btn focus:outline-none focus-visible:ring-2 focus-visible:ring-[#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" }); }}
|
onClick={() => { setPage(p as number); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||||
aria-label={`Page ${p}`}
|
aria-label={`Page ${p}`}
|
||||||
aria-current={page === p ? "page" : undefined}>
|
aria-current={page === p ? "page" : undefined}>
|
||||||
@@ -796,7 +796,7 @@ export default function ArticleFeed() {
|
|||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<button
|
<button
|
||||||
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#E67E22]"
|
className="page-btn inline-flex items-center gap-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#3b82f6]"
|
||||||
disabled={page === totalPages}
|
disabled={page === totalPages}
|
||||||
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
onClick={() => { setPage(Math.min(totalPages, page + 1)); window.scrollTo({ top: 0, behavior: "smooth" }); }}
|
||||||
aria-label="Next page">
|
aria-label="Next page">
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const hero = {
|
|||||||
title: "Sony BRC-AM7 + HXC-FZ90: Better Together in the Field",
|
title: "Sony BRC-AM7 + HXC-FZ90: Better Together in the Field",
|
||||||
category: "BROADCAST",
|
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.",
|
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",
|
alt: "Sony BRC-AM7 broadcast camera in field setting",
|
||||||
slug: "sony-brc-am7-hxc-fz90",
|
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",
|
title: "Sony\'s BVM-HX1710: The Compact Powerhouse Changing How Broadcast & Post See Their Work",
|
||||||
category: "POST PRODUCTION",
|
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",
|
alt: "Sony BVM-HX1710 broadcast monitor three-quarter view",
|
||||||
slug: "sony-bvm-hx1710",
|
slug: "sony-bvm-hx1710",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "The Top 5 Challenges in MCRs & NOCs—And How to Stay Ahead",
|
title: "The Top 5 Challenges in MCRs & NOCs—And How to Stay Ahead",
|
||||||
category: "BROADCAST",
|
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",
|
alt: "IP monitoring dashboard showing data insights for MCR operations",
|
||||||
slug: "top-5-challenges-mcrs-nocs",
|
slug: "top-5-challenges-mcrs-nocs",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "TESSERA SQ200: THE NEW PACE-SETTER IN LED PROCESSING",
|
title: "TESSERA SQ200: THE NEW PACE-SETTER IN LED PROCESSING",
|
||||||
category: "FEATURED",
|
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",
|
alt: "Tessera SQ200 Gen 3 LED processor hero image",
|
||||||
slug: "tessera-sq200-led-processing",
|
slug: "tessera-sq200-led-processing",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Backlight adds real-time collaboration to Iconik, AI-powered livestreams to Wildmoka at IBC2025",
|
title: "Backlight adds real-time collaboration to Iconik, AI-powered livestreams to Wildmoka at IBC2025",
|
||||||
category: "BROADCAST",
|
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",
|
alt: "Iconik media asset management platform interface",
|
||||||
slug: "backlight-iconik-wildmoka-ibc2025",
|
slug: "backlight-iconik-wildmoka-ibc2025",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Vizrt",
|
name: "Vizrt",
|
||||||
title: "From Start-Up to Global Powerhouse in Video in Just Two Decades",
|
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",
|
alt: "Vizrt company spotlight profile image",
|
||||||
tag: "Company",
|
tag: "Company",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -16,7 +16,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Scott Freeman",
|
name: "Scott Freeman",
|
||||||
title: "The Sherlock Holmes of Video Post-Production",
|
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",
|
alt: "Scott Freeman video post-production professional headshot",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -24,7 +24,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Al Roker",
|
name: "Al Roker",
|
||||||
title: "A Weatherman Who Wears Many Hats",
|
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",
|
alt: "Al Roker television weatherman and broadcaster portrait",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -32,7 +32,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Michael Kammes",
|
name: "Michael Kammes",
|
||||||
title: "Shaping the Best Workflow for Media Organizations",
|
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",
|
alt: "Michael Kammes media workflow specialist headshot",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -40,7 +40,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Gary Trenda",
|
name: "Gary Trenda",
|
||||||
title: "Top Frequency \'Traffic Cop\' for Wireless Microphones",
|
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",
|
alt: "Gary Trenda wireless microphone frequency coordinator portrait",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -48,7 +48,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Jim Anderson & Ulrike Schwarz",
|
name: "Jim Anderson & Ulrike Schwarz",
|
||||||
title: "Partners in Immersive Sound",
|
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",
|
alt: "Jim Anderson and Ulrike Schwarz immersive sound designers",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -56,7 +56,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Sound Effects",
|
name: "Sound Effects",
|
||||||
title: "Creating Cutting-Edge New Sound Effects — Film by Film",
|
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",
|
alt: "Sound effects creation for film and broadcast production",
|
||||||
tag: "Feature",
|
tag: "Feature",
|
||||||
href: "/gear"
|
href: "/gear"
|
||||||
@@ -64,7 +64,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Libby Casey",
|
name: "Libby Casey",
|
||||||
title: "Pioneering Television Broadcasting at the Washington Post",
|
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",
|
alt: "Libby Casey television broadcaster Washington Post studio",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -72,7 +72,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "NBCU Academy",
|
name: "NBCU Academy",
|
||||||
title: "Preparing Students for Careers in News, Technology and Broadcast",
|
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",
|
alt: "NBCU Academy students learning broadcast journalism and technology",
|
||||||
tag: "Education",
|
tag: "Education",
|
||||||
href: "/about"
|
href: "/about"
|
||||||
@@ -80,7 +80,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "BirdDog",
|
name: "BirdDog",
|
||||||
title: "Simplifying Global Television Production",
|
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",
|
alt: "BirdDog broadcast production technology company spotlight",
|
||||||
tag: "Company",
|
tag: "Company",
|
||||||
href: "/news"
|
href: "/news"
|
||||||
@@ -88,7 +88,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Michael Cioni",
|
name: "Michael Cioni",
|
||||||
title: "Speeding Digital Cinema with Camera to Cloud",
|
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",
|
alt: "Michael Cioni digital cinema and camera to cloud technology expert",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/technology"
|
href: "/technology"
|
||||||
@@ -96,7 +96,7 @@ const spotlights = [
|
|||||||
{
|
{
|
||||||
name: "Jeff Greenberg",
|
name: "Jeff Greenberg",
|
||||||
title: "Finding Workarounds for Gaps in the Video Production Workflow",
|
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",
|
alt: "Jeff Greenberg video production workflow consultant headshot",
|
||||||
tag: "Profile",
|
tag: "Profile",
|
||||||
href: "/technology"
|
href: "/technology"
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import SponsorLogoStrip from "@/components/SponsorLogoStrip";
|
|||||||
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
import CompanyMentionsHover from "@/components/CompanyMentionsHover";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast Engineering News & Insights',
|
||||||
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.',
|
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||||
alternates: {
|
alternates: {
|
||||||
canonical: '/',
|
canonical: '/',
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast News',
|
||||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||||
url: '/',
|
url: '/',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
images: [
|
images: [
|
||||||
@@ -28,14 +28,14 @@ export const metadata: Metadata = {
|
|||||||
url: '/assets/images/og-image.png',
|
url: '/assets/images/og-image.png',
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: 'AV Beat — Where AV Meets IT',
|
alt: 'AV Beat — Broadcast Engineering News & Insights',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast News',
|
||||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||||
images: ['/assets/images/og-image.png'],
|
images: ['/assets/images/og-image.png'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -138,7 +138,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
{/* H1 Hero Heading */}
|
{/* H1 Hero Heading */}
|
||||||
<div className="sr-only">
|
<div className="sr-only">
|
||||||
<h1>AV Beat — Where AV Meets IT</h1>
|
<h1>AV Beat — Broadcast Engineering News & Insights</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Featured: cinematic hero + 4-up rail below (FeaturedBentoFromDb) */}
|
{/* Featured: cinematic hero + 4-up rail below (FeaturedBentoFromDb) */}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export const dynamic = "force-dynamic";
|
|||||||
export const revalidate = 0;
|
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/
|
* URL so article HTML doesn't ship the long `https://supabase.onsethost.com/
|
||||||
* storage/v1/object/public/...` path. Better for SEO + makes images look
|
* storage/v1/object/public/...` path. Better for SEO + makes images look
|
||||||
* first-party.
|
* first-party.
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ export const viewport: Viewport = {
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
|
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast Engineering News & Insights',
|
||||||
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.',
|
description: 'The digital platform for broadcast engineering professionals. Breaking news, deep-dive features, and industry spotlights from NAB to IBC and beyond.',
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
|
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
|
||||||
@@ -59,24 +59,24 @@ export const metadata: Metadata = {
|
|||||||
locale: 'en_US',
|
locale: 'en_US',
|
||||||
url: '/',
|
url: '/',
|
||||||
siteName: 'AV Beat',
|
siteName: 'AV Beat',
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast News',
|
||||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: '/assets/images/og-image.png',
|
url: '/assets/images/og-image.png',
|
||||||
width: 1200,
|
width: 1200,
|
||||||
height: 630,
|
height: 630,
|
||||||
alt: 'AV Beat — Where AV Meets IT',
|
alt: 'AV Beat — Broadcast Engineering News & Insights',
|
||||||
type: 'image/png'
|
type: 'image/png'
|
||||||
}]
|
}]
|
||||||
|
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
site: '@AVBeat',
|
site: '@AV Beat',
|
||||||
creator: '@AVBeat',
|
creator: '@AV Beat',
|
||||||
title: 'AV Beat — Where AV Meets IT',
|
title: 'AV Beat — Broadcast News',
|
||||||
description: 'Breaking news and insights at the intersection of pro AV and IT.',
|
description: 'Breaking news and insights for broadcast engineering professionals.',
|
||||||
images: ['/assets/images/og-image.png']
|
images: ['/assets/images/og-image.png']
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
@@ -125,7 +125,7 @@ export default function RootLayout({
|
|||||||
name: 'AV Beat',
|
name: 'AV Beat',
|
||||||
url: 'https://avbeat.com',
|
url: 'https://avbeat.com',
|
||||||
logo: "https://avbeat.com/assets/images/logo.png",
|
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: [
|
sameAs: [
|
||||||
'https://linkedin.com',
|
'https://linkedin.com',
|
||||||
'https://twitter.com',
|
'https://twitter.com',
|
||||||
@@ -144,7 +144,7 @@ export default function RootLayout({
|
|||||||
{/* Skip to main content — WCAG 2.4.1 */}
|
{/* Skip to main content — WCAG 2.4.1 */}
|
||||||
<a
|
<a
|
||||||
href="#main-content"
|
href="#main-content"
|
||||||
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[10000] focus:px-4 focus:py-2 focus:bg-[#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
|
Skip to main content
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ function LoginInner() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<div>
|
<div>
|
||||||
@@ -90,19 +90,19 @@ function LoginInner() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full bg-[#0d0d0d] border border-[#2a2a2a] rounded-lg px-3 py-2 text-white font-body text-sm focus:outline-none focus:border-[#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>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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'}
|
{loading ? 'Signing in…' : 'Sign in'}
|
||||||
</button>
|
</button>
|
||||||
<p className="text-xs text-[#666] font-body text-center pt-2">
|
<p className="text-xs text-[#666] font-body text-center pt-2">
|
||||||
PR firm / contributor?{' '}
|
PR firm / contributor?{' '}
|
||||||
<Link href="/client-login" className="text-[#E67E22] hover:underline">
|
<Link href="/client-login" className="text-[#3b82f6] hover:underline">
|
||||||
Use the contributor portal
|
Use the contributor portal
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export default async function ManufacturerProfile({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{company.phone && (
|
{company.phone && (
|
||||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#E67E22]">
|
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#888] hover:text-[#3b82f6]">
|
||||||
{company.phone}
|
{company.phone}
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
@@ -217,7 +217,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={company.company_website}
|
href={company.company_website}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
Website ↗
|
Website ↗
|
||||||
</a>
|
</a>
|
||||||
@@ -227,7 +227,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={company.press_url}
|
href={company.press_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
Newsroom ↗
|
Newsroom ↗
|
||||||
</a>
|
</a>
|
||||||
@@ -237,7 +237,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={company.contact_url}
|
href={company.contact_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
Contact ↗
|
Contact ↗
|
||||||
</a>
|
</a>
|
||||||
@@ -247,7 +247,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={company.linkedin_url}
|
href={company.linkedin_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#888] hover:text-[#E67E22]"
|
className="text-[#888] hover:text-[#3b82f6]"
|
||||||
>
|
>
|
||||||
LinkedIn
|
LinkedIn
|
||||||
</a>
|
</a>
|
||||||
@@ -257,7 +257,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={`https://twitter.com/${company.twitter_handle}`}
|
href={`https://twitter.com/${company.twitter_handle}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[#888] hover:text-[#E67E22]"
|
className="text-[#888] hover:text-[#3b82f6]"
|
||||||
>
|
>
|
||||||
X / Twitter
|
X / Twitter
|
||||||
</a>
|
</a>
|
||||||
@@ -306,7 +306,7 @@ export default async function ManufacturerProfile({
|
|||||||
href={e.linkedin_url}
|
href={e.linkedin_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-[11px] text-[#E67E22] hover:underline"
|
className="text-[11px] text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
LinkedIn ↗
|
LinkedIn ↗
|
||||||
</a>
|
</a>
|
||||||
@@ -328,14 +328,14 @@ export default async function ManufacturerProfile({
|
|||||||
)}
|
)}
|
||||||
{company.phone && (
|
{company.phone && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#E67E22] hover:underline">
|
<a href={`tel:${company.phone.replace(/[^\d+]/g, '')}`} className="text-[#3b82f6] hover:underline">
|
||||||
{company.phone}
|
{company.phone}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{company.contact_email && (
|
{company.contact_email && (
|
||||||
<div>
|
<div>
|
||||||
<a href={`mailto:${company.contact_email}`} className="text-[#E67E22] hover:underline">
|
<a href={`mailto:${company.contact_email}`} className="text-[#3b82f6] hover:underline">
|
||||||
{company.contact_email}
|
{company.contact_email}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,7 +401,7 @@ export default async function ManufacturerProfile({
|
|||||||
<section className="mb-10">
|
<section className="mb-10">
|
||||||
<header className="flex items-baseline justify-between mb-3">
|
<header className="flex items-baseline justify-between mb-3">
|
||||||
<h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2>
|
<h2 className="text-lg font-display font-bold text-[#e0e0e0]">Recent coverage</h2>
|
||||||
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#E67E22] hover:underline">
|
<Link href={`/news?search=${encodeURIComponent(company.company_name)}`} className="text-xs text-[#3b82f6] hover:underline">
|
||||||
All stories →
|
All stories →
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
@@ -410,7 +410,7 @@ export default async function ManufacturerProfile({
|
|||||||
<Link
|
<Link
|
||||||
key={r.wp_slug}
|
key={r.wp_slug}
|
||||||
href={`/news/${r.wp_slug}`}
|
href={`/news/${r.wp_slug}`}
|
||||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-md p-3 hover:border-[#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
|
{/* Image fallback cascade — Ryan rule: never show an empty
|
||||||
gray box on a coverage card. Falls through:
|
gray box on a coverage card. Falls through:
|
||||||
@@ -423,7 +423,7 @@ export default async function ManufacturerProfile({
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#E67E22] line-clamp-3">
|
<h3 className="text-xs font-semibold text-[#e0e0e0] leading-snug group-hover:text-[#3b82f6] line-clamp-3">
|
||||||
{r.title}
|
{r.title}
|
||||||
</h3>
|
</h3>
|
||||||
{r.wp_published_at && (
|
{r.wp_published_at && (
|
||||||
@@ -447,7 +447,7 @@ export default async function ManufacturerProfile({
|
|||||||
{p.slug ? (
|
{p.slug ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/news/${p.slug}`}
|
href={`/news/${p.slug}`}
|
||||||
className="text-[#E67E22] hover:underline"
|
className="text-[#3b82f6] hover:underline"
|
||||||
>
|
>
|
||||||
{p.title}
|
{p.title}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default async function ManufacturersIndex() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="mt-2">
|
<p className="mt-2">
|
||||||
Are you a manufacturer and want to update your listing?{" "}
|
Are you a manufacturer and want to update your listing?{" "}
|
||||||
<a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#E67E22] hover:underline">
|
<a href="mailto:editors@avbeat.com?subject=Manufacturer%20Directory%20Update" className="text-[#3b82f6] hover:underline">
|
||||||
Contact our editors
|
Contact our editors
|
||||||
</a>
|
</a>
|
||||||
.
|
.
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default async function NabHub() {
|
|||||||
const { data: ads } = await adv
|
const { data: ads } = await adv
|
||||||
.from("active_advertisers_by_property")
|
.from("active_advertisers_by_property")
|
||||||
.select("company_name_lower")
|
.select("company_name_lower")
|
||||||
.eq("property", "broadcastbeat");
|
.eq("property", "avbeat");
|
||||||
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
sponsors = new Set((ads || []).map((r: any) => String(r.company_name_lower)));
|
||||||
} catch { /* tolerate */ }
|
} catch { /* tolerate */ }
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ export default async function NabHub() {
|
|||||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||||
Latest from the show floor
|
Latest from the show floor
|
||||||
</h2>
|
</h2>
|
||||||
<Link href="/news?search=NAB+2026" className="text-xs text-[#E67E22] hover:underline">
|
<Link href="/news?search=NAB+2026" className="text-xs text-[#3b82f6] hover:underline">
|
||||||
See all NAB coverage →
|
See all NAB coverage →
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
@@ -162,7 +162,7 @@ export default async function NabHub() {
|
|||||||
<Link
|
<Link
|
||||||
key={a.wp_slug}
|
key={a.wp_slug}
|
||||||
href={`/news/${a.wp_slug}`}
|
href={`/news/${a.wp_slug}`}
|
||||||
className="group bg-[#0d0d0d] border border-[#252525] rounded-lg overflow-hidden hover:border-[#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 && (
|
{a.featured_image && (
|
||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
@@ -174,7 +174,7 @@ export default async function NabHub() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<h3 className="font-semibold text-[#e0e0e0] text-sm leading-snug group-hover:text-[#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}
|
{a.title}
|
||||||
</h3>
|
</h3>
|
||||||
{a.wp_published_at && (
|
{a.wp_published_at && (
|
||||||
@@ -195,7 +195,7 @@ export default async function NabHub() {
|
|||||||
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
<h2 className="text-xl font-display font-bold text-[#e0e0e0]">
|
||||||
All 2026 NAB Show exhibitors
|
All 2026 NAB Show exhibitors
|
||||||
</h2>
|
</h2>
|
||||||
<Link href="/manufacturers" className="text-xs text-[#E67E22] hover:underline">
|
<Link href="/manufacturers" className="text-xs text-[#3b82f6] hover:underline">
|
||||||
Full directory →
|
Full directory →
|
||||||
</Link>
|
</Link>
|
||||||
</header>
|
</header>
|
||||||
@@ -205,7 +205,7 @@ export default async function NabHub() {
|
|||||||
key={e.slug}
|
key={e.slug}
|
||||||
href={`/manufacturers/${e.slug}`}
|
href={`/manufacturers/${e.slug}`}
|
||||||
data-company-slug={e.slug}
|
data-company-slug={e.slug}
|
||||||
className="bg-[#0d0d0d] border border-[#252525] rounded-md p-2.5 hover:border-[#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 ? (
|
{e.logo_url ? (
|
||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
/* eslint-disable-next-line @next/next/no-img-element */
|
||||||
@@ -222,7 +222,7 @@ export default async function NabHub() {
|
|||||||
{otherRows.length > 200 && (
|
{otherRows.length > 200 && (
|
||||||
<p className="text-center text-xs text-[#888] mt-4">
|
<p className="text-center text-xs text-[#888] mt-4">
|
||||||
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
|
Showing 200 of {otherRows.length.toLocaleString()} exhibitors.{" "}
|
||||||
<Link href="/manufacturers" className="text-[#E67E22] hover:underline">Browse the full directory →</Link>
|
<Link href="/manufacturers" className="text-[#3b82f6] hover:underline">Browse the full directory →</Link>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export default function NewsPage({
|
|||||||
placeholder="Search articles, topics, authors…"
|
placeholder="Search articles, topics, authors…"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
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 && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
@@ -196,7 +196,7 @@ export default function NewsPage({
|
|||||||
type="date"
|
type="date"
|
||||||
value={dateFrom}
|
value={dateFrom}
|
||||||
onChange={(e) => setDateFrom(e.target.value)}
|
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"
|
title="From date"
|
||||||
/>
|
/>
|
||||||
<span className="text-[#444] text-xs">–</span>
|
<span className="text-[#444] text-xs">–</span>
|
||||||
@@ -204,7 +204,7 @@ export default function NewsPage({
|
|||||||
type="date"
|
type="date"
|
||||||
value={dateTo}
|
value={dateTo}
|
||||||
onChange={(e) => setDateTo(e.target.value)}
|
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"
|
title="To date"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,7 +214,7 @@ export default function NewsPage({
|
|||||||
<select
|
<select
|
||||||
value={sortBy}
|
value={sortBy}
|
||||||
onChange={(e) => setSortBy(e.target.value)}
|
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) => (
|
{SORT_OPTIONS.map((opt) => (
|
||||||
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
@@ -236,8 +236,8 @@ export default function NewsPage({
|
|||||||
onClick={() => setActiveCategory(cat)}
|
onClick={() => setActiveCategory(cat)}
|
||||||
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
||||||
activeCategory === cat
|
activeCategory === cat
|
||||||
? "bg-[#E67E22] border-[#E67E22] text-white"
|
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]"
|
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{cat}
|
{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" />
|
<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>
|
</svg>
|
||||||
<p className="text-[#555] font-body text-sm">No articles match your filters.</p>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
@@ -297,9 +297,9 @@ export default function NewsPage({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
<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>
|
</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"}
|
{article.title || "Untitled"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">{article.excerpt || ""}</p>
|
<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" />}
|
{!hideAiSuggestions && <AISuggestedArticles variant="full" />}
|
||||||
|
|
||||||
<div className="bg-[#111] border border-[#222] p-4">
|
<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
|
Popular Tags
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -336,8 +336,8 @@ export default function NewsPage({
|
|||||||
onClick={() => setActiveCategory(tag)}
|
onClick={() => setActiveCategory(tag)}
|
||||||
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
|
className={`px-2 py-1 border text-xs font-body rounded-sm transition-colors ${
|
||||||
activeCategory === tag
|
activeCategory === tag
|
||||||
? "bg-[#E67E22] border-[#E67E22] text-white"
|
? "bg-[#3b82f6] border-[#3b82f6] text-white"
|
||||||
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]"
|
: "bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
@@ -346,7 +346,7 @@ export default function NewsPage({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-[#0d1520] border border-[#1e3a5f] p-4">
|
<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>
|
<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>
|
<Link href="/home-page#newsletter" className="btn-subscribe text-xs py-2 px-4 inline-block">Subscribe Free</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -278,11 +278,11 @@ export default function NewsArticleDetailClient({
|
|||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<div className="bg-[#0d0d0d] border-b border-[#1a1a1a]">
|
<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]">
|
<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
|
Home
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<Link href="/news" className="hover:text-[#E67E22] transition-colors">
|
<Link href="/news" className="hover:text-[#3b82f6] transition-colors">
|
||||||
News
|
News
|
||||||
</Link>
|
</Link>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
@@ -300,7 +300,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<Link
|
<Link
|
||||||
href="/news"
|
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}
|
{article.category}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-xs text-[#777]">{article.readTime}</span>
|
<span className="text-xs text-[#777]">{article.readTime}</span>
|
||||||
@@ -329,7 +329,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href={`/authors/${article.authorSlug}`}
|
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}
|
{article.author}
|
||||||
</Link>
|
</Link>
|
||||||
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
<p className="font-body text-xs text-[#777]">{article.authorTitle}</p>
|
||||||
@@ -343,7 +343,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onClick={() => setEmailShareOpen(true)}
|
onClick={() => setEmailShareOpen(true)}
|
||||||
aria-label="Share via email"
|
aria-label="Share via email"
|
||||||
title="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">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
||||||
@@ -364,7 +364,7 @@ export default function NewsArticleDetailClient({
|
|||||||
|
|
||||||
{/* X / Twitter */}
|
{/* X / Twitter */}
|
||||||
<a
|
<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"
|
target="_blank" rel="noopener noreferrer"
|
||||||
aria-label="Share on X"
|
aria-label="Share on X"
|
||||||
title="X (Twitter)"
|
title="X (Twitter)"
|
||||||
@@ -418,7 +418,7 @@ export default function NewsArticleDetailClient({
|
|||||||
}}
|
}}
|
||||||
aria-label="Copy article link"
|
aria-label="Copy article link"
|
||||||
title="Copy 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">
|
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
||||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||||
@@ -432,7 +432,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onClick={handleSaveToReadingList}
|
onClick={handleSaveToReadingList}
|
||||||
disabled={savingState !== "idle"}
|
disabled={savingState !== "idle"}
|
||||||
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
|
aria-label={isSaved ? "Remove from reading list" : "Save to reading list"}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-[#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 === "saving" ?"Saving..."
|
||||||
: savingState === "removing" ?"Removing..."
|
: savingState === "removing" ?"Removing..."
|
||||||
: isSaved
|
: isSaved
|
||||||
@@ -469,7 +469,7 @@ export default function NewsArticleDetailClient({
|
|||||||
{article.tags.map((tag) => (
|
{article.tags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
key={tag}
|
key={tag}
|
||||||
className="px-3 py-1 text-xs font-body bg-[#1a1a1a] border border-[#2a2a2a] text-[#888] hover:border-[#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}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
@@ -483,7 +483,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<p className="font-body text-sm text-[#777]">Found this article useful?</p>
|
<p className="font-body text-sm text-[#777]">Found this article useful?</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEmailShareOpen(true)}
|
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
|
<svg
|
||||||
width="13"
|
width="13"
|
||||||
height="13"
|
height="13"
|
||||||
@@ -504,7 +504,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<div className="pt-6 border-t border-[#222]">
|
<div className="pt-6 border-t border-[#222]">
|
||||||
<Link
|
<Link
|
||||||
href="/news"
|
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
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@@ -534,7 +534,7 @@ export default function NewsArticleDetailClient({
|
|||||||
|
|
||||||
{/* Related Articles Sidebar */}
|
{/* Related Articles Sidebar */}
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#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
|
Related Articles
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -553,10 +553,10 @@ export default function NewsArticleDetailClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<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}
|
{related.category}
|
||||||
</p>
|
</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}
|
{related.title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,19 +568,19 @@ export default function NewsArticleDetailClient({
|
|||||||
{/* Related Forum Posts — same widget as /articles/[slug] */}
|
{/* Related Forum Posts — same widget as /articles/[slug] */}
|
||||||
{relatedForumThreads.length > 0 && (
|
{relatedForumThreads.length > 0 && (
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#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
|
Related Forum Posts
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3.5">
|
<ul className="space-y-3.5">
|
||||||
{relatedForumThreads.map((t) => (
|
{relatedForumThreads.map((t) => (
|
||||||
<li key={t.id}>
|
<li key={t.id}>
|
||||||
<Link href={`/forum/thread/${t.id}`} className="group block">
|
<Link href={`/forum/thread/${t.id}`} className="group block">
|
||||||
<p className="font-heading text-[#e0e0e0] text-xs font-bold group-hover:text-[#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}
|
{t.title}
|
||||||
</p>
|
</p>
|
||||||
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
<p className="flex items-center gap-2 mt-1 text-[10px] text-[#666]">
|
||||||
{t.category_name && (
|
{t.category_name && (
|
||||||
<span className="text-[#E67E22] font-body font-bold uppercase tracking-wider">
|
<span className="text-[#3b82f6] font-body font-bold uppercase tracking-wider">
|
||||||
{t.category_name}
|
{t.category_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -594,7 +594,7 @@ export default function NewsArticleDetailClient({
|
|||||||
</ul>
|
</ul>
|
||||||
<Link
|
<Link
|
||||||
href="/forum"
|
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 →
|
Browse all forum threads →
|
||||||
</Link>
|
</Link>
|
||||||
@@ -639,7 +639,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onClick={prevCarousel}
|
onClick={prevCarousel}
|
||||||
disabled={carouselIndex === 0}
|
disabled={carouselIndex === 0}
|
||||||
aria-label="Previous articles"
|
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
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@@ -655,7 +655,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onClick={nextCarousel}
|
onClick={nextCarousel}
|
||||||
disabled={carouselIndex >= maxIndex}
|
disabled={carouselIndex >= maxIndex}
|
||||||
aria-label="Next articles"
|
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
|
<svg
|
||||||
width="14"
|
width="14"
|
||||||
height="14"
|
height="14"
|
||||||
@@ -694,11 +694,11 @@ export default function NewsArticleDetailClient({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
<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}
|
{related.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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}
|
{related.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#666] font-body text-xs line-clamp-2 leading-relaxed">
|
<p className="text-[#666] font-body text-xs line-clamp-2 leading-relaxed">
|
||||||
@@ -723,7 +723,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onClick={() => setCarouselIndex(i)}
|
onClick={() => setCarouselIndex(i)}
|
||||||
aria-label={`Go to slide ${i + 1}`}
|
aria-label={`Go to slide ${i + 1}`}
|
||||||
className={`w-1.5 h-1.5 rounded-full transition-colors focus:outline-none ${
|
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");
|
setEmailStatus("idle");
|
||||||
}}
|
}}
|
||||||
aria-label="Close share dialog"
|
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
|
<svg
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
@@ -777,7 +777,7 @@ export default function NewsArticleDetailClient({
|
|||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#E67E22"
|
stroke="#3b82f6"
|
||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||||
@@ -823,7 +823,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<label
|
<label
|
||||||
htmlFor="email-to"
|
htmlFor="email-to"
|
||||||
className="block font-body text-xs text-[#888] uppercase tracking-wider mb-1.5">
|
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>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="email-to"
|
id="email-to"
|
||||||
@@ -831,7 +831,7 @@ export default function NewsArticleDetailClient({
|
|||||||
value={emailTo}
|
value={emailTo}
|
||||||
onChange={(e) => setEmailTo(e.target.value)}
|
onChange={(e) => setEmailTo(e.target.value)}
|
||||||
placeholder="colleague@example.com"
|
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>
|
||||||
<div>
|
<div>
|
||||||
@@ -846,7 +846,7 @@ export default function NewsArticleDetailClient({
|
|||||||
onChange={(e) => setEmailNote(e.target.value)}
|
onChange={(e) => setEmailNote(e.target.value)}
|
||||||
placeholder="Thought you might find this interesting..."
|
placeholder="Thought you might find this interesting..."
|
||||||
rows={3}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -890,7 +890,7 @@ export default function NewsArticleDetailClient({
|
|||||||
<div
|
<div
|
||||||
role="status"
|
role="status"
|
||||||
aria-live="polite"
|
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}
|
{saveToast}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
|||||||
const blocks = Array.isArray(newsletter.article_blocks)
|
const blocks = Array.isArray(newsletter.article_blocks)
|
||||||
? newsletter.article_blocks.filter((b) => b.title)
|
? newsletter.article_blocks.filter((b) => b.title)
|
||||||
: [];
|
: [];
|
||||||
const accent = newsletter.accent_color || "#E67E22";
|
const accent = newsletter.accent_color || "#3b82f6";
|
||||||
|
|
||||||
const layoutLabel: Record<string, string> = {
|
const layoutLabel: Record<string, string> = {
|
||||||
featured: "Weekly Digest",
|
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="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">
|
<span className="font-body text-[#888] text-xs leading-relaxed line-clamp-1">
|
||||||
{block.category && (
|
{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}
|
{block.title}
|
||||||
</span>
|
</span>
|
||||||
@@ -132,7 +132,7 @@ function NewsletterCard({ newsletter }: { newsletter: Newsletter }) {
|
|||||||
<span className="font-body text-[#555] text-xs">
|
<span className="font-body text-[#555] text-xs">
|
||||||
{blocks.length} {blocks.length === 1 ? "story" : "stories"}
|
{blocks.length} {blocks.length === 1 ? "story" : "stories"}
|
||||||
</span>
|
</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()}
|
Issue #{newsletter.id.slice(0, 6).toUpperCase()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,9 +197,9 @@ function SignupCTA() {
|
|||||||
|
|
||||||
{status === "success" ? (
|
{status === "success" ? (
|
||||||
<div className="text-center py-4" role="status" aria-live="polite">
|
<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">
|
<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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<p className="font-heading text-[#e0e0e0] font-bold mb-1">You're subscribed!</p>
|
<p className="font-heading text-[#e0e0e0] font-bold mb-1">You're subscribed!</p>
|
||||||
@@ -228,7 +228,7 @@ function SignupCTA() {
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
aria-hidden="true"
|
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 && (
|
{checked && (
|
||||||
<svg viewBox="0 0 12 12" fill="none" className="w-full h-full" aria-hidden="true">
|
<svg viewBox="0 0 12 12" fill="none" className="w-full h-full" aria-hidden="true">
|
||||||
<path d="M2 6l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
<path d="M2 6l3 3 5-5" stroke="white" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
@@ -262,7 +262,7 @@ function SignupCTA() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={status === "loading"}
|
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"}
|
{status === "loading" ? "Subscribing..." : "Subscribe Free"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,7 +275,7 @@ function SignupCTA() {
|
|||||||
|
|
||||||
<p className="font-body text-[#555] text-xs mt-3 text-center">
|
<p className="font-body text-[#555] text-xs mt-3 text-center">
|
||||||
No spam. Unsubscribe anytime.{" "}
|
No spam. Unsubscribe anytime.{" "}
|
||||||
<Link href="/privacy" className="text-[#E67E22] hover:underline">
|
<Link href="/privacy" className="text-[#3b82f6] hover:underline">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
@@ -419,9 +419,9 @@ export default function NewsletterArchivePage() {
|
|||||||
<button
|
<button
|
||||||
key={topic}
|
key={topic}
|
||||||
onClick={() => setSelectedTopic(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
|
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]"
|
: "bg-transparent border-[#333] text-[#888] hover:border-[#555] hover:text-[#ccc]"
|
||||||
}`}
|
}`}
|
||||||
aria-pressed={selectedTopic === topic}>
|
aria-pressed={selectedTopic === topic}>
|
||||||
@@ -462,7 +462,7 @@ export default function NewsletterArchivePage() {
|
|||||||
{hasActiveFilters && (
|
{hasActiveFilters && (
|
||||||
<button
|
<button
|
||||||
onClick={handleClearFilters}
|
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
|
Clear filters
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -498,7 +498,7 @@ export default function NewsletterArchivePage() {
|
|||||||
<p className="font-body text-red-400 text-sm mb-3">{error}</p>
|
<p className="font-body text-red-400 text-sm mb-3">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={fetchArchive}
|
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
|
Try again
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -521,7 +521,7 @@ export default function NewsletterArchivePage() {
|
|||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
onClick={handleClearFilters}
|
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
|
Clear all filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -555,9 +555,9 @@ export default function NewsletterArchivePage() {
|
|||||||
<li key={topic}>
|
<li key={topic}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTopic(topic)}
|
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
|
selectedTopic === topic
|
||||||
? "text-[#E67E22] bg-[#E67E22]/10"
|
? "text-[#3b82f6] bg-[#3b82f6]/10"
|
||||||
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
||||||
}`}>
|
}`}>
|
||||||
{topic}
|
{topic}
|
||||||
@@ -578,9 +578,9 @@ export default function NewsletterArchivePage() {
|
|||||||
<li key={year}>
|
<li key={year}>
|
||||||
<button
|
<button
|
||||||
onClick={() => { setSelectedYear(String(year)); setSelectedMonth(""); }}
|
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)
|
selectedYear === String(year)
|
||||||
? "text-[#E67E22] bg-[#E67E22]/10"
|
? "text-[#3b82f6] bg-[#3b82f6]/10"
|
||||||
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
: "text-[#888] hover:text-[#ccc] hover:bg-[#1a1a1a]"
|
||||||
}`}>
|
}`}>
|
||||||
{year}
|
{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]">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>
|
<li><strong className="text-[#ccc]">Unsubscribe:</strong> Opt out of marketing emails at any time via the unsubscribe link</li>
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
<section aria-labelledby="section-6">
|
<section aria-labelledby="section-6">
|
||||||
@@ -127,7 +127,7 @@ export default function PrivacyPage() {
|
|||||||
<address className="not-italic mt-3 space-y-1 text-[#999]">
|
<address className="not-italic mt-3 space-y-1 text-[#999]">
|
||||||
<p><strong className="text-[#ccc]">Relevant Media Properties, LLC</strong></p>
|
<p><strong className="text-[#ccc]">Relevant Media Properties, LLC</strong></p>
|
||||||
<p>Privacy Officer</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>
|
</address>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default function RegisterPage() {
|
|||||||
<div className="min-h-screen bg-[#111111]">
|
<div className="min-h-screen bg-[#111111]">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="flex items-center justify-center py-24">
|
<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>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
@@ -97,7 +97,7 @@ export default function RegisterPage() {
|
|||||||
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]">
|
<div className="bg-[#0d0d0d] border-b border-[#1e1e1e]">
|
||||||
<div className="max-w-container mx-auto px-4 py-2.5">
|
<div className="max-w-container mx-auto px-4 py-2.5">
|
||||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
|
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-xs font-body text-[#555]">
|
||||||
<Link href="/home-page" className="hover:text-[#E67E22] transition-colors">Home</Link>
|
<Link href="/home-page" className="hover:text-[#3b82f6] transition-colors">Home</Link>
|
||||||
<span aria-hidden="true">/</span>
|
<span aria-hidden="true">/</span>
|
||||||
<span className="text-[#888]">Create Account</span>
|
<span className="text-[#888]">Create Account</span>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -142,7 +142,7 @@ export default function RegisterPage() {
|
|||||||
onChange={(e) => setFullName(e.target.value)}
|
onChange={(e) => setFullName(e.target.value)}
|
||||||
placeholder="Jane Smith"
|
placeholder="Jane Smith"
|
||||||
disabled={loading}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export default function RegisterPage() {
|
|||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
disabled={loading}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -176,7 +176,7 @@ export default function RegisterPage() {
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="Min. 6 characters"
|
placeholder="Min. 6 characters"
|
||||||
disabled={loading}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -193,7 +193,7 @@ export default function RegisterPage() {
|
|||||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
placeholder="Re-enter password"
|
placeholder="Re-enter password"
|
||||||
disabled={loading}
|
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>
|
</div>
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ export default function RegisterPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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 ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
@@ -223,7 +223,7 @@ export default function RegisterPage() {
|
|||||||
{/* Login Link */}
|
{/* Login Link */}
|
||||||
<Link
|
<Link
|
||||||
href="/login"
|
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
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
|||||||
{query && results.length === 0 && (
|
{query && results.length === 0 && (
|
||||||
<div className="py-16 text-center">
|
<div className="py-16 text-center">
|
||||||
<p className="text-[#555] font-body text-sm mb-3">No articles match your search.</p>
|
<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 →
|
Browse all news →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,11 +77,11 @@ export default async function SearchPage({ searchParams }: SearchPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1.5">
|
<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"}
|
{article.category || "NEWS"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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"}
|
{article.title || "Untitled"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#777] font-body text-sm leading-relaxed line-clamp-2">
|
<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">
|
<div className="lg:grid lg:grid-cols-[1fr,300px] lg:gap-8">
|
||||||
<main>
|
<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
|
← All events
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ export default async function EventCoveragePage({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isLive && !isPast && (
|
{!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
|
T-{daysUntil}d
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -203,7 +203,7 @@ export default async function EventCoveragePage({
|
|||||||
{[event.venue, event.city, event.country].filter(Boolean).join(" · ")}
|
{[event.venue, event.city, event.country].filter(Boolean).join(" · ")}
|
||||||
</div>
|
</div>
|
||||||
{event.url && (
|
{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">
|
<a href={event.url} target="_blank" rel="noopener noreferrer">
|
||||||
{new URL(event.url).hostname.replace(/^www\./, "")} →
|
{new URL(event.url).hostname.replace(/^www\./, "")} →
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ function EventCard({ e }: { e: Row }) {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href={`/show-coverage/${e.slug}`}
|
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">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<h3 className="font-serif text-xl font-semibold">{e.name}</h3>
|
<h3 className="font-serif text-xl font-semibold">{e.name}</h3>
|
||||||
@@ -137,7 +137,7 @@ function EventCard({ e }: { e: Row }) {
|
|||||||
{e.url && (
|
{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>
|
</div>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
<span className="section-label">Technology</span>
|
<span className="section-label">Technology</span>
|
||||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
||||||
{filter && (
|
{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
|
clear filter
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -101,7 +101,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
href="/technology"
|
href="/technology"
|
||||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
||||||
!filterSlug
|
!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]"
|
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
||||||
}`}>
|
}`}>
|
||||||
All
|
All
|
||||||
@@ -114,7 +114,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
href={`/technology?category=${slug}`}
|
href={`/technology?category=${slug}`}
|
||||||
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
className={`px-3 py-1.5 rounded-sm font-body text-[11px] font-semibold uppercase tracking-wide transition-all ${
|
||||||
isActive
|
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]"
|
: "bg-[#111] text-[#888] border border-[#2a2a2a] hover:text-[#ccc]"
|
||||||
}`}>
|
}`}>
|
||||||
{def.title}
|
{def.title}
|
||||||
@@ -133,7 +133,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
No articles found{filter ? ` for "${filter.title}"` : ""}.
|
No articles found{filter ? ` for "${filter.title}"` : ""}.
|
||||||
</p>
|
</p>
|
||||||
{filter && (
|
{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
|
← Back to all technology coverage
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
@@ -145,7 +145,7 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<Link
|
<Link
|
||||||
href={`/articles/${articles[0].slug}`}
|
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">
|
<div className="flex-shrink-0 relative w-full md:w-[320px] h-[200px] md:h-[200px] overflow-hidden rounded-sm">
|
||||||
<AppImage
|
<AppImage
|
||||||
src={articles[0].image}
|
src={articles[0].image}
|
||||||
@@ -157,13 +157,13 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<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-[#444] text-[10px]">·</span>
|
||||||
<span className="text-[#555] font-body text-[11px]">{articles[0].date}</span>
|
<span className="text-[#555] font-body text-[11px]">{articles[0].date}</span>
|
||||||
<span className="text-[#444] text-[10px]">·</span>
|
<span className="text-[#444] text-[10px]">·</span>
|
||||||
<span className="text-[#555] font-body text-[11px]">{articles[0].readTime}</span>
|
<span className="text-[#555] font-body text-[11px]">{articles[0].readTime}</span>
|
||||||
</div>
|
</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}
|
{articles[0].title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[#777] font-body text-sm leading-relaxed mb-4">{articles[0].excerpt}</p>
|
<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
|
<Link
|
||||||
key={article.slug}
|
key={article.slug}
|
||||||
href={`/articles/${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">
|
<div className="flex-shrink-0 w-[120px] h-[80px] relative overflow-hidden rounded-sm">
|
||||||
<AppImage
|
<AppImage
|
||||||
src={article.image}
|
src={article.image}
|
||||||
@@ -197,11 +197,11 @@ export default async function TechnologyPage({ searchParams }: PageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<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-[#444] text-[9px]">·</span>
|
||||||
<span className="text-[#555] font-body text-[10px]">{article.readTime}</span>
|
<span className="text-[#555] font-body text-[10px]">{article.readTime}</span>
|
||||||
</div>
|
</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}
|
{article.title}
|
||||||
</h2>
|
</h2>
|
||||||
{article.category !== "Featured" && (
|
{article.category !== "Featured" && (
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function TermsPage() {
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="font-heading text-[#e0e0f0] text-xl font-bold mb-3">7. Contact</h2>
|
<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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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 */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<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>
|
</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
|
AI Picks For You
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTopicPicker(!showTopicPicker)}
|
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">
|
<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" />
|
<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)}
|
onClick={() => toggleInterest(topic)}
|
||||||
className={`px-2 py-0.5 text-[10px] font-body rounded-sm border transition-colors ${
|
className={`px-2 py-0.5 text-[10px] font-body rounded-sm border transition-colors ${
|
||||||
interests.includes(topic)
|
interests.includes(topic)
|
||||||
? 'bg-[#E67E22] border-[#E67E22] text-white'
|
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]'
|
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{topic}
|
{topic}
|
||||||
@@ -142,7 +142,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleApplyTopics}
|
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
|
Update Suggestions
|
||||||
</button>
|
</button>
|
||||||
@@ -180,10 +180,10 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<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}
|
{article.category}
|
||||||
</p>
|
</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}
|
{article.title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 justify-between mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<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>
|
</svg>
|
||||||
<span className="section-label">AI Picks For You</span>
|
<span className="section-label">AI Picks For You</span>
|
||||||
@@ -212,7 +212,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowTopicPicker(!showTopicPicker)}
|
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">
|
<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" />
|
<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)}
|
onClick={() => toggleInterest(topic)}
|
||||||
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
className={`px-2.5 py-1 text-[11px] font-body rounded-sm border transition-colors ${
|
||||||
interests.includes(topic)
|
interests.includes(topic)
|
||||||
? 'bg-[#E67E22] border-[#E67E22] text-white'
|
? 'bg-[#3b82f6] border-[#3b82f6] text-white'
|
||||||
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#E67E22] hover:text-[#E67E22]'
|
: 'bg-[#1a1a1a] border-[#2a2a2a] text-[#888] hover:border-[#3b82f6] hover:text-[#3b82f6]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{topic}
|
{topic}
|
||||||
@@ -242,7 +242,7 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleApplyTopics}
|
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
|
Update Suggestions
|
||||||
</button>
|
</button>
|
||||||
@@ -280,11 +280,11 @@ export default function AISuggestedArticles({ variant = 'compact' }: Props) {
|
|||||||
sizes="(max-width: 768px) 50vw, 25vw"
|
sizes="(max-width: 768px) 50vw, 25vw"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
<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}
|
{article.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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}
|
{article.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[#555] font-body text-[11px] mt-1">{article.readTime}</p>
|
<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)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded={open}
|
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>ABOUT</span>
|
||||||
<span aria-hidden="true" className="text-[10px]">▾</span>
|
<span aria-hidden="true" className="text-[10px]">▾</span>
|
||||||
@@ -54,7 +54,7 @@ export default function AboutDropdown() {
|
|||||||
href={it.href}
|
href={it.href}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
onClick={() => setOpen(false)}
|
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>{it.label}</span>
|
||||||
<span aria-hidden="true" className="text-[10px] text-[#6b7280]">›</span>
|
<span aria-hidden="true" className="text-[10px] text-[#6b7280]">›</span>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
|||||||
{replyTo && (
|
{replyTo && (
|
||||||
<div className="flex items-center justify-between mb-2 text-xs text-[#888]">
|
<div className="flex items-center justify-between mb-2 text-xs text-[#888]">
|
||||||
<span>Replying to comment…</span>
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
<textarea
|
<textarea
|
||||||
@@ -171,7 +171,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
|||||||
onChange={(e) => setBody(e.target.value)}
|
onChange={(e) => setBody(e.target.value)}
|
||||||
placeholder={replyTo ? "Write your reply…" : "Share your take on this story…"}
|
placeholder={replyTo ? "Write your reply…" : "Share your take on this story…"}
|
||||||
rows={3}
|
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>}
|
{error && <p className="text-[11px] text-red-400 mt-1">{error}</p>}
|
||||||
<div className="flex items-center justify-between mt-2">
|
<div className="flex items-center justify-between mt-2">
|
||||||
@@ -179,7 +179,7 @@ export default function ArticleComments({ articleSlug }: Props) {
|
|||||||
<button
|
<button
|
||||||
onClick={submit}
|
onClick={submit}
|
||||||
disabled={submitting || !body.trim()}
|
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"}
|
{submitting ? "Posting…" : replyTo ? "Post reply" : "Post comment"}
|
||||||
</button>
|
</button>
|
||||||
@@ -193,13 +193,13 @@ export default function ArticleComments({ articleSlug }: Props) {
|
|||||||
<div className="flex gap-3 justify-center">
|
<div className="flex gap-3 justify-center">
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/login?next=${encodeURIComponent(typeof window !== "undefined" ? window.location.pathname : "")}`}
|
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
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={`/forum/register?next=${encodeURIComponent(typeof window !== "undefined" ? window.location.pathname : "")}`}
|
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
|
Create account
|
||||||
</Link>
|
</Link>
|
||||||
@@ -250,14 +250,14 @@ function CommentItem({
|
|||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
/* 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" />
|
<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()}
|
{(comment.author_display_name || "?").trim().charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm font-semibold text-white">
|
<div className="text-sm font-semibold text-white">
|
||||||
{comment.author_display_name || "Member"}
|
{comment.author_display_name || "Member"}
|
||||||
{comment.is_ai_seeded && (
|
{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>
|
</div>
|
||||||
<span className="text-[11px] text-[#666]">· {timeAgo(comment.created_at)}</span>
|
<span className="text-[11px] text-[#666]">· {timeAgo(comment.created_at)}</span>
|
||||||
@@ -288,7 +288,7 @@ function CommentItem({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{canInteract && (
|
{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
|
Reply
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function AskBBAI() {
|
|||||||
aria-label="Ask BB AI"
|
aria-label="Ask BB AI"
|
||||||
>
|
>
|
||||||
<span className="relative inline-block w-3 h-3">
|
<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 className="bb-ring" />
|
||||||
</span>
|
</span>
|
||||||
<span className="font-serif">Ask BB AI</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]">
|
<header className="flex items-center justify-between px-5 py-4 border-b border-[#252525]">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="relative inline-block w-3 h-3">
|
<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 className="bb-ring" />
|
||||||
</span>
|
</span>
|
||||||
<h2 className="font-serif text-lg font-semibold text-white">BB AI</h2>
|
<h2 className="font-serif text-lg font-semibold text-white">BB AI</h2>
|
||||||
@@ -109,7 +109,7 @@ export default function AskBBAI() {
|
|||||||
className={
|
className={
|
||||||
"inline-block max-w-[85%] rounded px-3 py-2 text-sm whitespace-pre-wrap " +
|
"inline-block max-w-[85%] rounded px-3 py-2 text-sm whitespace-pre-wrap " +
|
||||||
(m.role === "user"
|
(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]")
|
: "bg-[#111] border border-[#252525] text-[#e5e7eb]")
|
||||||
}
|
}
|
||||||
dangerouslySetInnerHTML={{ __html: m.role === "assistant" ? linkifyMarkdown(m.content) : escapeHtml(m.content) }}
|
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()}
|
onKeyDown={(e) => e.key === "Enter" && send()}
|
||||||
placeholder="Ask about an event, a vendor, a workflow…"
|
placeholder="Ask about an event, a vendor, a workflow…"
|
||||||
disabled={busy}
|
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
|
<button
|
||||||
onClick={send}
|
onClick={send}
|
||||||
disabled={busy || !input.trim()}
|
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
|
Send
|
||||||
</button>
|
</button>
|
||||||
@@ -161,7 +161,7 @@ function linkifyMarkdown(s: string): string {
|
|||||||
let out = escapeHtml(s);
|
let out = escapeHtml(s);
|
||||||
out = out.replace(
|
out = out.replace(
|
||||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
/\[([^\]]+)\]\(([^)]+)\)/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/>");
|
out = out.replace(/\n/g, "<br/>");
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async function loadTiles(slugs: string[]): Promise<CompanyTile[]> {
|
|||||||
const { data: ads } = await adv
|
const { data: ads } = await adv
|
||||||
.from("active_advertisers_by_property")
|
.from("active_advertisers_by_property")
|
||||||
.select("company_name_lower")
|
.select("company_name_lower")
|
||||||
.eq("property", "broadcastbeat");
|
.eq("property", "avbeat");
|
||||||
advNames = new Set((ads || []).map((r: any) => r.company_name_lower));
|
advNames = new Set((ads || []).map((r: any) => r.company_name_lower));
|
||||||
} catch { /* tolerate */ }
|
} catch { /* tolerate */ }
|
||||||
|
|
||||||
@@ -57,7 +57,7 @@ export default async function CompaniesInThisStory({ slugs }: { slugs: string[]
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#111] border border-[#222] p-5">
|
<div className="bg-[#111] border border-[#222] p-5">
|
||||||
<h3 className="font-body font-bold text-xs text-[#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
|
Companies in this story
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3">
|
<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="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-1 flex-wrap">
|
<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}
|
{t.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -112,12 +112,12 @@ export default function CompanyMentionsHover() {
|
|||||||
/* eslint-disable-next-line @next/next/no-img-element */
|
/* 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" />
|
<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()}
|
{(preview.name || "?").trim().charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<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}
|
{preview.name}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-1.5 mt-1 flex-wrap text-[10px]">
|
<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">
|
<ul className="space-y-1">
|
||||||
{preview.stories.slice(0, 3).map((s) => (
|
{preview.stories.slice(0, 3).map((s) => (
|
||||||
<li key={s.slug}>
|
<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}
|
{s.title}
|
||||||
</Link>
|
</Link>
|
||||||
{s.date && <span className="text-[10px] text-[#666] ml-1">· {new Date(s.date).toISOString().slice(0, 10)}</span>}
|
{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">
|
<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 →
|
Full profile →
|
||||||
</Link>
|
</Link>
|
||||||
{preview.website && (
|
{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(/\/$/, "")} ↗
|
{preview.website.replace(/^https?:\/\//, "").replace(/\/$/, "")} ↗
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export default function CookieConsent() {
|
|||||||
<strong className="text-white">Accept All</strong>, you consent to our use of cookies.{" "}
|
<strong className="text-white">Accept All</strong>, you consent to our use of cookies.{" "}
|
||||||
<Link
|
<Link
|
||||||
href="/privacy"
|
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
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
@@ -86,7 +86,7 @@ export default function CookieConsent() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleDecline}
|
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"
|
aria-label="Decline non-essential cookies"
|
||||||
>
|
>
|
||||||
Decline
|
Decline
|
||||||
@@ -95,7 +95,7 @@ export default function CookieConsent() {
|
|||||||
ref={acceptRef}
|
ref={acceptRef}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAccept}
|
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"
|
aria-label="Accept all cookies"
|
||||||
>
|
>
|
||||||
Accept All
|
Accept All
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default async function DoubleTicker() {
|
|||||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
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 },
|
auth: { persistSession: false },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export default function EventsDropdown() {
|
|||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
aria-expanded={open}
|
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>Show coverage</span>
|
||||||
<span aria-hidden="true" className="text-[10px]">▾</span>
|
<span aria-hidden="true" className="text-[10px]">▾</span>
|
||||||
@@ -113,12 +113,12 @@ export default function EventsDropdown() {
|
|||||||
style={{ borderRadius: "var(--border-radius-md,6px)" }}
|
style={{ borderRadius: "var(--border-radius-md,6px)" }}
|
||||||
>
|
>
|
||||||
<div className="relative p-4">
|
<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 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,#E67E22)]"></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">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<h3 className="font-serif text-base font-semibold">Show coverage</h3>
|
<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" />
|
<span className="bb-pulse-dot" />
|
||||||
BB AI
|
BB AI
|
||||||
</span>
|
</span>
|
||||||
@@ -158,7 +158,7 @@ export default function EventsDropdown() {
|
|||||||
<circle cx="12" cy="12" r="10" />
|
<circle cx="12" cy="12" r="10" />
|
||||||
<polyline points="12 6 12 12 16 14" />
|
<polyline points="12 6 12 12 16 14" />
|
||||||
</svg>
|
</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>
|
</div>
|
||||||
@@ -168,7 +168,7 @@ export default function EventsDropdown() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] font-mono text-[#6b7280] mt-1">
|
<div className="flex items-center gap-2 text-[10px] font-mono text-[#6b7280] mt-1">
|
||||||
<span>{fmtDateRange(e.start_date, e.end_date)}</span>
|
<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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -177,7 +177,7 @@ export default function EventsDropdown() {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div className="mt-3 pt-2 border-t border-[#252525] flex items-center justify-between text-[10px] font-mono text-[#6b7280]">
|
<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 →
|
See all 13 events →
|
||||||
</Link>
|
</Link>
|
||||||
{loadedAt && <span>auto-updated · {ago(loadedAt)}</span>}
|
{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_URL!,
|
||||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
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 },
|
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">
|
<div className="flex items-center gap-3 mb-4 md:mb-5 flex-wrap">
|
||||||
<span className="section-label">Staff Editorial</span>
|
<span className="section-label">Staff Editorial</span>
|
||||||
<div className="flex-1 h-px bg-[#2a2a2a]" />
|
<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 →
|
All featured →
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +89,7 @@ export default async function FeaturedBento() {
|
|||||||
{/* Cinematic 21:9 hero — fills left column, aspect ratio preserved */}
|
{/* Cinematic 21:9 hero — fills left column, aspect ratio preserved */}
|
||||||
<Link
|
<Link
|
||||||
href={`/news/${hero.wp_slug}`}
|
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]">
|
<div className="relative aspect-[21/9] md:aspect-[21/8] bg-[#111]">
|
||||||
<img
|
<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" />
|
<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">
|
<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" />
|
<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>
|
</svg>
|
||||||
@@ -108,11 +108,11 @@ export default async function FeaturedBento() {
|
|||||||
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10">
|
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10">
|
||||||
{hero.category && (
|
{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}
|
{hero.category}
|
||||||
</div>
|
</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}
|
{hero.title}
|
||||||
</h3>
|
</h3>
|
||||||
{hero.excerpt && (
|
{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]">
|
<div className="mt-4 flex flex-wrap items-center gap-3 text-[11px] font-mono text-[#9ca3af]">
|
||||||
<span>{hero.author_name || "AV Beat"}</span>
|
<span>{hero.author_name || "AV Beat"}</span>
|
||||||
<StarRating seed={hero.wp_slug} publishedAt={hero.wp_published_at} size="sm" />
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +140,7 @@ export default async function FeaturedBento() {
|
|||||||
<Link
|
<Link
|
||||||
key={r.wp_slug}
|
key={r.wp_slug}
|
||||||
href={`/news/${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">
|
<div className="aspect-[16/10] bg-[#111] relative flex-shrink-0">
|
||||||
<img
|
<img
|
||||||
@@ -150,10 +150,10 @@ export default async function FeaturedBento() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 flex flex-col gap-1 flex-1 min-h-0 overflow-hidden">
|
<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"}
|
{r.category || "News"}
|
||||||
</div>
|
</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}
|
{r.title}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="mt-auto">
|
<div className="mt-auto">
|
||||||
|
|||||||