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
This commit is contained in:
Claude
2026-06-02 15:32:56 +00:00
parent dd93a74bdf
commit d43f78b161
137 changed files with 4728 additions and 4541 deletions

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter, useSearchParams } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
const STATUSES = ['active','completed','cancelled'];
const SCHEDULES = ['single','monthly','quarterly'];

View File

@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); }
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
export default function ReportsPage() {
const { user, loading } = useAuth();

View File

@@ -98,7 +98,7 @@ export default function AdOpsIOsPage() {
</select>
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>

View File

@@ -75,7 +75,7 @@ export default function AdOpsDashboardPage() {
<div className="flex gap-2 flex-wrap">
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Sites</option>
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
</select>
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
<option value="">All Statuses</option>

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
const PRODUCT_TYPES = [
'display_banner_300x250','display_banner_728x90','display_banner_300x600',
'display_banner_970x250','newsletter_banner_600x100','dedicated_email_blast',

View File

@@ -9,7 +9,7 @@ interface BannedTerm {
id: string;
term: string;
ban_level: 'soft' | 'hard';
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
site_scope: 'avbeat' | 'avbeat' | 'both';
is_active: boolean;
notes: string | null;
created_at: string;
@@ -19,7 +19,7 @@ interface BannedTerm {
interface FormState {
term: string;
ban_level: 'soft' | 'hard';
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
site_scope: 'avbeat' | 'avbeat' | 'both';
notes: string;
}
@@ -255,7 +255,7 @@ export default function BannedTermsPage() {
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
<option value="both">Both Sites</option>
<option value="broadcastbeat">Broadcast Beat only</option>
<option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option>
</select>
</div>
@@ -291,7 +291,7 @@ export default function BannedTermsPage() {
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]">
<option value="">All Sites</option>
<option value="broadcastbeat">Broadcast Beat</option>
<option value="avbeat">AV Beat</option>
<option value="avbeat">AV Beat</option>
<option value="both">Both</option>
</select>
@@ -336,7 +336,7 @@ export default function BannedTermsPage() {
</span>
</td>
<td className="px-4 py-3">
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'broadcastbeat' ? 'Broadcast Beat' : 'AV Beat'}</span>
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
</td>
<td className="px-4 py-3">
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
@@ -379,7 +379,7 @@ export default function BannedTermsPage() {
<div className="flex items-center gap-2 mb-1">
<span className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400 font-bold">Hard Ban</span>
</div>
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@broadcastbeat.com for assistance." Admin receives email notification.</p>
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@avbeat.com for assistance." Admin receives email notification.</p>
</div>
</div>
</div>

View File

@@ -68,7 +68,7 @@ type TabType = 'queue' | 'companies' | 'crawl-log' | 'settings';
type QueueStatus = 'generated' | 'pending' | 'approved' | 'published' | 'rejected';
const SITE_CONFIG = {
1: { name: 'Broadcast Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
1: { name: 'AV Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
2: { name: 'AV Beat', badge: 'AV', bgClass: 'bg-[#f97316]/10', textClass: 'text-[#f97316]' },
};
@@ -275,7 +275,7 @@ export default function CompanyTrackerPage() {
<span className="text-[#888] text-sm font-body">Company Tracker</span>
</div>
<h1 className="text-2xl font-display font-bold text-white">Company Story Engine</h1>
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across Broadcast Beat & AV Beat</p>
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across AV Beat & AV Beat</p>
</div>
<div className="flex items-center gap-3">
{/* Auto-Pilot toggle */}
@@ -344,7 +344,7 @@ export default function CompanyTrackerPage() {
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
<option value="">All Sites</option>
<option value="1">Broadcast Beat</option>
<option value="1">AV Beat</option>
<option value="2">AV Beat</option>
</select>
</div>
@@ -412,7 +412,7 @@ export default function CompanyTrackerPage() {
<select onChange={e => handleQueueAction(item.id, 'reassign_site', { site_id: parseInt(e.target.value) })}
defaultValue={item.site_id}
className="bg-[#111] border border-[#333] rounded px-2 py-1 text-xs text-white font-body focus:outline-none">
<option value={1}>Broadcast Beat</option>
<option value={1}>AV Beat</option>
<option value={2}>AV Beat</option>
</select>
</div>
@@ -455,7 +455,7 @@ export default function CompanyTrackerPage() {
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
<option value="both">Both Sites</option>
<option value="broadcastbeat">Broadcast Beat only</option>
<option value="avbeat">AV Beat only</option>
<option value="avbeat">AV Beat only</option>
</select>
</div>
@@ -494,7 +494,7 @@ export default function CompanyTrackerPage() {
<td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td>
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">{company.company_website}</a> : '—'}</td>
<td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td>
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'broadcastbeat' ? 'BB' : 'AV'}</td>
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
<td className="px-4 py-3">
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}>

View File

@@ -47,7 +47,7 @@ interface LaunchChecklist {
// ─── Site labels ──────────────────────────────────────────────────────────────
const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = {
'broadcast-beat': { label: 'Broadcast Beat', color: 'text-blue-400', target: '1525/day' },
'broadcast-beat': { label: 'AV Beat', color: 'text-blue-400', target: '1525/day' },
'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '1015/day' },
'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '1015/day' },
'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '1015/day' },

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter, useSearchParams } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsRow({ label, valid, checkedAt }: { label: string; valid: boolean; checkedAt: string | null }) {
return (

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
return (

View File

@@ -4,7 +4,7 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
export default function SendLogPage() {
const { user, loading } = useAuth();

View File

@@ -4,13 +4,13 @@ import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
const SUPP_TYPES = ['unsubscribe','bounce_hard','bounce_soft','spam_complaint','hostile_reply','manual'];
export default function SuppressionsPage() {
const { user, loading } = useAuth();
const router = useRouter();
const [site, setSite] = useState('broadcastbeat');
const [site, setSite] = useState('avbeat');
const [suppressions, setSuppressions] = useState<any[]>([]);
const [keywords, setKeywords] = useState<any[]>([]);
const [stats, setStats] = useState<any>(null);

View File

@@ -158,8 +158,8 @@ export default function WPImportPage() {
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
<p className="text-[#666] text-sm mt-1">
Import posts from{' '}
<a href="https://www.broadcastbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
broadcastbeat.com
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
avbeat.com
</a>{' '}
via WordPress REST API
</p>
@@ -228,7 +228,7 @@ export default function WPImportPage() {
<div className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div>
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p>
<p className="text-[#666] text-xs mt-0.5">Fetching posts from broadcastbeat.com and saving to database...</p>
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
</div>
</div>
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
@@ -365,11 +365,11 @@ export default function WPImportPage() {
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<a
href={`https://www.broadcastbeat.com/${post.wp_slug}`}
href={`https://www.avbeat.com/${post.wp_slug}`}
target="_blank"
rel="noopener noreferrer"
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1"
title="View on broadcastbeat.com">
title="View on avbeat.com">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
</svg>

View File

@@ -632,7 +632,7 @@ export default function CampaignEditorPage() {
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-medium text-white">Broadcast Beat</span>
<span className="text-xs font-medium text-white">AV Beat</span>
<span className="text-[10px] text-[#444]">now</span>
</div>
<p className="text-xs text-[#888] truncate">{campaign.subject || 'Your subject line…'}</p>
@@ -645,7 +645,7 @@ export default function CampaignEditorPage() {
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
{/* Header */}
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
<p className="text-xs font-bold text-white tracking-widest uppercase">Broadcast Beat</p>
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
{campaign.previewText && (
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
)}
@@ -680,7 +680,7 @@ export default function CampaignEditorPage() {
{/* Footer */}
<div className="text-center pt-2 border-t border-[#1a1a1a]">
<p className="text-[9px] text-[#333]">Broadcast Beat · Unsubscribe · View in browser</p>
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
</div>
</div>
</div>

View File

@@ -689,7 +689,7 @@ export default function AdminNewsletterPage() {
type="text"
value={digestSubject}
onChange={(e) => setDigestSubject(e.target.value)}
placeholder="e.g. Broadcast Beat Weekly — NAB Show Highlights"
placeholder="e.g. AV Beat Weekly — NAB Show Highlights"
className="search-input w-full py-2.5 px-3 text-sm"
/>
</div>

View File

@@ -435,7 +435,7 @@ export default function NewsletterTemplatesPage() {
type="text"
value={form.subject_prefix}
onChange={(e) => setForm((f) => ({ ...f, subject_prefix: e.target.value }))}
placeholder="e.g. Broadcast Beat Weekly —"
placeholder="e.g. AV Beat Weekly —"
className="search-input w-full py-2.5 px-3 text-sm"
/>
<p className="text-xs text-[#555] font-body mt-1">Prepended to the digest subject line when using this template</p>