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:
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const STATUSES = ['active','completed','cancelled'];
|
||||
const SCHEDULES = ['single','monthly','quarterly'];
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
|
||||
|
||||
function fmt(cents: number) { return '$' + (cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2 }); }
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
export default function ReportsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function AdOpsIOsPage() {
|
||||
</select>
|
||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function AdOpsDashboardPage() {
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<select value={filterSite} onChange={e => setFilterSite(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
{['broadcastbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
{['avbeat','avbeat','backlotbeat','broadcastengineering'].map(s => <option key={s} value={s}>{s}</option>)}
|
||||
</select>
|
||||
<select value={filterStatus} onChange={e => setFilterStatus(e.target.value)} className="px-2 py-1 bg-[#111] border border-[#252525] rounded text-xs text-[#888] focus:outline-none">
|
||||
<option value="">All Statuses</option>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const PRODUCT_TYPES = [
|
||||
'display_banner_300x250','display_banner_728x90','display_banner_300x600',
|
||||
'display_banner_970x250','newsletter_banner_600x100','dedicated_email_blast',
|
||||
|
||||
@@ -9,7 +9,7 @@ interface BannedTerm {
|
||||
id: string;
|
||||
term: string;
|
||||
ban_level: 'soft' | 'hard';
|
||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
||||
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||
is_active: boolean;
|
||||
notes: string | null;
|
||||
created_at: string;
|
||||
@@ -19,7 +19,7 @@ interface BannedTerm {
|
||||
interface FormState {
|
||||
term: string;
|
||||
ban_level: 'soft' | 'hard';
|
||||
site_scope: 'broadcastbeat' | 'avbeat' | 'both';
|
||||
site_scope: 'avbeat' | 'avbeat' | 'both';
|
||||
notes: string;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ export default function BannedTermsPage() {
|
||||
<select value={form.site_scope} onChange={e => setForm(f => ({ ...f, site_scope: e.target.value as any }))}
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -291,7 +291,7 @@ export default function BannedTermsPage() {
|
||||
<select value={filterSiteScope} onChange={e => setFilterSiteScope(e.target.value)}
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-2 text-[#888] text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
||||
<option value="">All Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat</option>
|
||||
<option value="avbeat">AV Beat</option>
|
||||
<option value="avbeat">AV Beat</option>
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
@@ -336,7 +336,7 @@ export default function BannedTermsPage() {
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'broadcastbeat' ? 'Broadcast Beat' : 'AV Beat'}</span>
|
||||
<span className="text-xs text-[#888] font-body capitalize">{term.site_scope === 'both' ? 'Both Sites' : term.site_scope === 'avbeat' ? 'AV Beat' : 'AV Beat'}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full font-body ${term.is_active ? 'bg-green-500/10 text-green-400' : 'bg-[#1a1a1a] text-[#555]'}`}>
|
||||
@@ -379,7 +379,7 @@ export default function BannedTermsPage() {
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-red-500/10 text-red-400 font-bold">Hard Ban</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@broadcastbeat.com for assistance." Admin receives email notification.</p>
|
||||
<p className="text-xs text-[#555] font-body">Submission is blocked. Post saved as draft automatically. Contributor sees only: "This post could not be submitted. Please contact the editorial team at editor@avbeat.com for assistance." Admin receives email notification.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@ type TabType = 'queue' | 'companies' | 'crawl-log' | 'settings';
|
||||
type QueueStatus = 'generated' | 'pending' | 'approved' | 'published' | 'rejected';
|
||||
|
||||
const SITE_CONFIG = {
|
||||
1: { name: 'Broadcast Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
|
||||
1: { name: 'AV Beat', badge: 'BB', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' },
|
||||
2: { name: 'AV Beat', badge: 'AV', bgClass: 'bg-[#f97316]/10', textClass: 'text-[#f97316]' },
|
||||
};
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function CompanyTrackerPage() {
|
||||
<span className="text-[#888] text-sm font-body">Company Tracker</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-display font-bold text-white">Company Story Engine</h1>
|
||||
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across Broadcast Beat & AV Beat</p>
|
||||
<p className="text-[#555] text-sm font-body mt-1">Automated story generation from company mentions across AV Beat & AV Beat</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Auto-Pilot toggle */}
|
||||
@@ -344,7 +344,7 @@ export default function CompanyTrackerPage() {
|
||||
<select value={queueSiteFilter} onChange={e => setQueueSiteFilter(e.target.value)}
|
||||
className="bg-[#111] border border-[#252525] rounded-lg px-3 py-1.5 text-[#555] text-xs font-body focus:outline-none">
|
||||
<option value="">All Sites</option>
|
||||
<option value="1">Broadcast Beat</option>
|
||||
<option value="1">AV Beat</option>
|
||||
<option value="2">AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -412,7 +412,7 @@ export default function CompanyTrackerPage() {
|
||||
<select onChange={e => handleQueueAction(item.id, 'reassign_site', { site_id: parseInt(e.target.value) })}
|
||||
defaultValue={item.site_id}
|
||||
className="bg-[#111] border border-[#333] rounded px-2 py-1 text-xs text-white font-body focus:outline-none">
|
||||
<option value={1}>Broadcast Beat</option>
|
||||
<option value={1}>AV Beat</option>
|
||||
<option value={2}>AV Beat</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ export default function CompanyTrackerPage() {
|
||||
<select value={addCompanyForm.site_scope} onChange={e => setAddCompanyForm(f => ({ ...f, site_scope: e.target.value }))}
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-3 py-2.5 text-white text-sm font-body focus:outline-none focus:border-[#3b82f6]">
|
||||
<option value="both">Both Sites</option>
|
||||
<option value="broadcastbeat">Broadcast Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
<option value="avbeat">AV Beat only</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -494,7 +494,7 @@ export default function CompanyTrackerPage() {
|
||||
<td className="px-4 py-3 text-sm font-body font-semibold text-white">{company.company_name}</td>
|
||||
<td className="px-4 py-3 text-xs text-[#555] font-body">{company.company_website ? <a href={`https://${company.company_website}`} target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">{company.company_website}</a> : '—'}</td>
|
||||
<td className="px-4 py-3 text-sm font-body text-white font-bold">{company.mention_count}</td>
|
||||
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'broadcastbeat' ? 'BB' : 'AV'}</td>
|
||||
<td className="px-4 py-3 text-xs text-[#888] font-body capitalize">{company.site_scope === 'both' ? 'Both' : company.site_scope === 'avbeat' ? 'BB' : 'AV'}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button onClick={() => handleToggleCompanyAutoStory(company.id, !company.auto_story_enabled)} disabled={actionLoading === company.id}
|
||||
className={`relative w-9 h-5 rounded-full transition-colors ${company.auto_story_enabled ? 'bg-[#3b82f6]' : 'bg-[#333]'}`}>
|
||||
|
||||
@@ -47,7 +47,7 @@ interface LaunchChecklist {
|
||||
// ─── Site labels ──────────────────────────────────────────────────────────────
|
||||
|
||||
const SITE_LABELS: Record<string, { label: string; color: string; target: string }> = {
|
||||
'broadcast-beat': { label: 'Broadcast Beat', color: 'text-blue-400', target: '15–25/day' },
|
||||
'broadcast-beat': { label: 'AV Beat', color: 'text-blue-400', target: '15–25/day' },
|
||||
'av-beat': { label: 'AV Beat', color: 'text-purple-400', target: '10–15/day' },
|
||||
'backlot-beat': { label: 'Backlot Beat', color: 'text-amber-400', target: '10–15/day' },
|
||||
'broadcast-engineering': { label: 'BroadcastEngineering.com', color: 'text-green-400', target: '10–15/day' },
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
function DnsRow({ label, valid, checkedAt }: { label: string; valid: boolean; checkedAt: string | null }) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
function DnsCheck({ label, valid }: { label: string; valid: boolean | null }) {
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
|
||||
export default function SendLogPage() {
|
||||
const { user, loading } = useAuth();
|
||||
|
||||
@@ -4,13 +4,13 @@ import Link from 'next/link';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
const SITES = ['broadcastbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SITES = ['avbeat','avbeat','backlotbeat','broadcastengineering'];
|
||||
const SUPP_TYPES = ['unsubscribe','bounce_hard','bounce_soft','spam_complaint','hostile_reply','manual'];
|
||||
|
||||
export default function SuppressionsPage() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [site, setSite] = useState('broadcastbeat');
|
||||
const [site, setSite] = useState('avbeat');
|
||||
const [suppressions, setSuppressions] = useState<any[]>([]);
|
||||
const [keywords, setKeywords] = useState<any[]>([]);
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
|
||||
@@ -158,8 +158,8 @@ export default function WPImportPage() {
|
||||
<h1 className="text-2xl font-bold text-white font-heading">WordPress Post Importer</h1>
|
||||
<p className="text-[#666] text-sm mt-1">
|
||||
Import posts from{' '}
|
||||
<a href="https://www.broadcastbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
||||
broadcastbeat.com
|
||||
<a href="https://www.avbeat.com" target="_blank" rel="noopener noreferrer" className="text-[#3b82f6] hover:underline">
|
||||
avbeat.com
|
||||
</a>{' '}
|
||||
via WordPress REST API
|
||||
</p>
|
||||
@@ -228,7 +228,7 @@ export default function WPImportPage() {
|
||||
<div className="w-5 h-5 border-2 border-[#3b82f6] border-t-transparent rounded-full animate-spin flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-[#3b82f6] text-sm font-bold">Import in progress</p>
|
||||
<p className="text-[#666] text-xs mt-0.5">Fetching posts from broadcastbeat.com and saving to database...</p>
|
||||
<p className="text-[#666] text-xs mt-0.5">Fetching posts from avbeat.com and saving to database...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 h-1.5 bg-[#1a2535] rounded-full overflow-hidden">
|
||||
@@ -365,11 +365,11 @@ export default function WPImportPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<a
|
||||
href={`https://www.broadcastbeat.com/${post.wp_slug}`}
|
||||
href={`https://www.avbeat.com/${post.wp_slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#555] hover:text-[#3b82f6] transition-colors p-1"
|
||||
title="View on broadcastbeat.com">
|
||||
title="View on avbeat.com">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /><polyline points="15 3 21 3 21 9" /><line x1="10" y1="14" x2="21" y2="3" />
|
||||
</svg>
|
||||
|
||||
@@ -632,7 +632,7 @@ export default function CampaignEditorPage() {
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-white">Broadcast Beat</span>
|
||||
<span className="text-xs font-medium text-white">AV Beat</span>
|
||||
<span className="text-[10px] text-[#444]">now</span>
|
||||
</div>
|
||||
<p className="text-xs text-[#888] truncate">{campaign.subject || 'Your subject line…'}</p>
|
||||
@@ -645,7 +645,7 @@ export default function CampaignEditorPage() {
|
||||
<div className="p-4 space-y-3 max-h-[400px] overflow-y-auto">
|
||||
{/* Header */}
|
||||
<div className="bg-[#0a0a0a] rounded-lg p-3 text-center border border-[#1a1a1a]">
|
||||
<p className="text-xs font-bold text-white tracking-widest uppercase">Broadcast Beat</p>
|
||||
<p className="text-xs font-bold text-white tracking-widest uppercase">AV Beat</p>
|
||||
{campaign.previewText && (
|
||||
<p className="text-[10px] text-[#555] mt-1">{campaign.previewText}</p>
|
||||
)}
|
||||
@@ -680,7 +680,7 @@ export default function CampaignEditorPage() {
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center pt-2 border-t border-[#1a1a1a]">
|
||||
<p className="text-[9px] text-[#333]">Broadcast Beat · Unsubscribe · View in browser</p>
|
||||
<p className="text-[9px] text-[#333]">AV Beat · Unsubscribe · View in browser</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -689,7 +689,7 @@ export default function AdminNewsletterPage() {
|
||||
type="text"
|
||||
value={digestSubject}
|
||||
onChange={(e) => setDigestSubject(e.target.value)}
|
||||
placeholder="e.g. Broadcast Beat Weekly — NAB Show Highlights"
|
||||
placeholder="e.g. AV Beat Weekly — NAB Show Highlights"
|
||||
className="search-input w-full py-2.5 px-3 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -435,7 +435,7 @@ export default function NewsletterTemplatesPage() {
|
||||
type="text"
|
||||
value={form.subject_prefix}
|
||||
onChange={(e) => setForm((f) => ({ ...f, subject_prefix: e.target.value }))}
|
||||
placeholder="e.g. Broadcast Beat Weekly —"
|
||||
placeholder="e.g. AV Beat Weekly —"
|
||||
className="search-input w-full py-2.5 px-3 text-sm"
|
||||
/>
|
||||
<p className="text-xs text-[#555] font-body mt-1">Prepended to the digest subject line when using this template</p>
|
||||
|
||||
Reference in New Issue
Block a user