Files
avbeat-com/src/app/admin/forum-seed/page.tsx
Local Administrator 8042024c4a feat(brand): AV BEAT 2026 rebrand — blue/navy/white identity
- New AvBeatLogo inline-SVG component (rounded-square emblem with
  forward-leaning AV monogram + horizontal beam detail) at
  src/components/AvBeatLogo.tsx, three variants (full/wordmark/icon).
- Header.tsx now uses the responsive AV BEAT logo lockup in the
  top-left, links to /, full lockup on desktop (emblem+wordmark+tagline),
  emblem+wordmark on tablet, emblem-only on mobile.
- Removed DualSpeedTicker (the 'RECENT FORUM POSTS' ticker strip);
  the news ticker / glow chrome does not match the new aesthetic and the
  forum row was the explicit removal target.
- Tailwind theme + globals.css now expose the AV BEAT 2026 palette as
  semantic tokens: --brand-blue (#1D4ED8), --brand-blue-bright (#38BDF8),
  --brand-navy (#0F172A), --brand-bg (#F8FAFC), --brand-surface, --brand-border,
  --brand-text, --brand-text-muted. Legacy --color-* aliases re-point to the
  new tokens so any inline-styled component re-themes for free.
- Site-wide hex sweep migrates 2,769 hardcoded color literals across 144
  files from the old dark-broadcast palette to the new tokens (orange
  -> blue, dark-brown -> white surface / navy text, cream -> navy).
- Layout body class flipped from 'bb-neon' to 'bg-brand-bg text-brand-text'
  so the dark glow chrome no longer leaks through the new light theme.
2026-06-03 12:07:18 +00:00

299 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import React, { useState } from 'react';
import Link from 'next/link';
import Header from '@/components/Header';
import Footer from '@/components/Footer';
interface SeedThread {
category_slug: string;
title: string;
body: string;
author_name: string;
replies: Array<{ author_name: string; body: string }>;
}
const SEED_TEMPLATES: SeedThread[] = [
{
category_slug: 'live-production',
title: 'Comparing production switchers for large-scale live events',
author_name: 'ProductionEngineer_A',
body: 'We are evaluating production switchers for a major live event series. Looking at Ross, Grass Valley, and Sony options. What are the key differentiators teams are finding in 2025? Particularly interested in IP integration and graphics playout capabilities.',
replies: [
{ author_name: 'LiveProdVet', body: 'Ross has the best ecosystem integration if you are already in their world. The XPression graphics integration is seamless.' },
{ author_name: 'SwitcherPro', body: 'Grass Valley Korona is worth a serious look for large-scale. The multiviewer flexibility alone is worth the evaluation time.' },
],
},
{
category_slug: 'ip-cloud',
title: 'PTP grandmaster redundancy strategies for ST 2110 facilities',
author_name: 'NetworkBroadcast_T',
body: 'We are designing our PTP infrastructure for a new ST 2110 facility. Looking for advice on grandmaster redundancy — specifically whether to use BMCA automatic failover or manual switchover with monitoring. What are people deploying in production environments?',
replies: [
{ author_name: 'TimingExpert', body: 'BMCA failover works but the transition can cause brief sync issues. We use manual switchover with automated alerting for our tier-1 facilities.' },
],
},
{
category_slug: 'streaming',
title: 'Low-latency streaming for live sports — architecture deep dive',
author_name: 'StreamArchitect_B',
body: 'Building a low-latency streaming architecture for a sports rights holder. Target is under 3 seconds glass-to-glass. Currently evaluating LL-HLS vs DASH-LL vs WebRTC for the last mile. What are the real-world trade-offs at scale?',
replies: [
{ author_name: 'CDNExpert', body: 'LL-HLS is the most practical for scale right now. WebRTC gets you lower latency but the infrastructure complexity at 100k+ concurrent viewers is significant.' },
{ author_name: 'StreamingEng', body: 'We run LL-HLS with a 2-second target and achieve it consistently. The key is segment duration — we use 500ms segments with 3-segment playlist.' },
],
},
{
category_slug: 'audio',
title: 'Immersive audio for broadcast — Dolby Atmos workflow questions',
author_name: 'AudioMixer_C',
body: 'We are adding Dolby Atmos delivery to our broadcast workflow. Looking for advice on monitoring setups, DAW integration, and the loudness compliance implications of object-based audio. Anyone running Atmos in a live broadcast environment?',
replies: [
{ author_name: 'AtmosEngineer', body: 'Live Atmos is challenging but doable. We use the Dolby Atmos Production Suite with a 7.1.4 monitoring setup. The loudness compliance is handled by the Dolby encoder — it normalizes the bed and objects separately.' },
],
},
{
category_slug: 'ai-automation',
title: 'AI-powered sports highlights generation — production workflow',
author_name: 'SportsTechPro',
body: 'We are evaluating AI highlight generation tools for a sports network. The promise is automated clip selection and packaging within minutes of live events ending. Has anyone deployed Grabyo, WSC Sports, or similar platforms in production? What is the editorial quality like?',
replies: [
{ author_name: 'DigitalSports_M', body: 'WSC Sports is genuinely impressive for structured sports like basketball and soccer. The AI understands game events well. For less structured sports the results are more variable.' },
{ author_name: 'SportsTechPro', body: 'Good to know. We cover a mix of sports. Did you find the customization options adequate for brand compliance?' },
],
},
];
export default function AdminForumSeedPage() {
const [seedData, setSeedData] = useState(JSON.stringify(SEED_TEMPLATES, null, 2));
const [importing, setImporting] = useState(false);
const [results, setResults] = useState<Array<{ title: string; status: 'success' | 'error'; message: string }>>([]);
const [activeTab, setActiveTab] = useState<'import' | 'json'>('import');
const handleImport = async () => {
setImporting(true);
setResults([]);
let threads: SeedThread[] = [];
try {
threads = JSON.parse(seedData);
} catch {
setResults([{ title: 'Parse Error', status: 'error', message: 'Invalid JSON. Please check your seed data.' }]);
setImporting(false);
return;
}
// Fetch categories first
const catRes = await fetch('/api/forum/categories');
const catData = await catRes.json();
const categories: Record<string, string> = {};
(catData.categories || []).forEach((c: any) => { categories[c.slug] = c.id; });
const newResults: typeof results = [];
for (const thread of threads) {
const categoryId = categories[thread.category_slug];
if (!categoryId) {
newResults.push({ title: thread.title, status: 'error', message: `Category "${thread.category_slug}" not found` });
continue;
}
try {
// Create thread
const threadRes = await fetch('/api/forum/threads', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
category_id: categoryId,
title: thread.title,
body: thread.body,
author_name: thread.author_name,
}),
});
const threadData = await threadRes.json();
if (!threadData.thread) throw new Error(threadData.error || 'Failed to create thread');
// Create replies
for (const reply of thread.replies || []) {
await fetch('/api/forum/replies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
thread_id: threadData.thread.id,
body: reply.body,
author_name: reply.author_name,
}),
});
}
newResults.push({ title: thread.title, status: 'success', message: `Created with ${thread.replies?.length || 0} replies` });
} catch (err: any) {
newResults.push({ title: thread.title, status: 'error', message: err.message });
}
}
setResults(newResults);
setImporting(false);
};
const successCount = results.filter(r => r.status === 'success').length;
const errorCount = results.filter(r => r.status === 'error').length;
return (
<>
<Header />
<main className="min-h-screen bg-[#F8FAFC]">
<div className="bg-[#FFFFFF] border-b border-[#2a3a50]">
<div className="max-w-container mx-auto px-4 py-6">
<nav className="text-sm font-body text-[#666] mb-2">
<Link href="/admin" className="hover:text-[#1D4ED8] transition-colors">Admin</Link>
<span className="mx-2"></span>
<Link href="/forum" className="hover:text-[#1D4ED8] transition-colors">Forum</Link>
<span className="mx-2"></span>
<span className="text-[#aab4c4]">Seed Tool</span>
</nav>
<h1 className="text-2xl font-heading font-bold text-white">Forum Seed Tool</h1>
<p className="text-[#aab4c4] font-body text-sm mt-1">
Import editorial starter threads in bulk. Edit the JSON below or paste your own seed data.
</p>
</div>
</div>
<div className="max-w-container mx-auto px-4 py-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Editor */}
<div className="lg:col-span-2">
<div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg overflow-hidden">
<div className="flex border-b border-[#DCE6F2]">
<button
onClick={() => setActiveTab('import')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'import' ? 'text-white border-b-2 border-[#1D4ED8]' : 'text-[#666] hover:text-[#aaa]'}`}
>
Import Editor
</button>
<button
onClick={() => setActiveTab('json')}
className={`px-4 py-3 font-body text-sm font-semibold transition-colors ${activeTab === 'json' ? 'text-white border-b-2 border-[#1D4ED8]' : 'text-[#666] hover:text-[#aaa]'}`}
>
JSON Schema
</button>
</div>
{activeTab === 'import' && (
<div className="p-4">
<p className="text-[#888] font-body text-xs mb-3">
Edit the JSON array below. Each object creates one thread with replies. Category slugs must match existing forum categories.
</p>
<textarea
value={seedData}
onChange={e => setSeedData(e.target.value)}
rows={24}
className="w-full bg-[#F8FAFC] border border-[#333] rounded-lg px-3 py-3 text-[#d0d0d0] font-mono text-xs focus:outline-none focus:border-[#1D4ED8] resize-none"
spellCheck={false}
/>
<div className="flex gap-3 mt-3">
<button
onClick={handleImport}
disabled={importing}
className="bg-[#1D4ED8] hover:bg-[#1E3A8A] disabled:opacity-50 text-white font-body font-semibold text-sm px-5 py-2 rounded-lg transition-colors"
>
{importing ? 'Importing...' : 'Import Threads'}
</button>
<button
onClick={() => setSeedData(JSON.stringify(SEED_TEMPLATES, null, 2))}
className="bg-[#DCE6F2] hover:bg-[#333] text-[#aaa] font-body text-sm px-4 py-2 rounded-lg transition-colors"
>
Reset to Templates
</button>
</div>
</div>
)}
{activeTab === 'json' && (
<div className="p-4">
<p className="text-[#888] font-body text-xs mb-3">Required JSON structure for seed data:</p>
<pre className="bg-[#F8FAFC] border border-[#333] rounded-lg p-4 text-[#d0d0d0] font-mono text-xs overflow-auto">
{`[
{
"category_slug": "live-production",
"title": "Thread title here",
"author_name": "AuthorUsername",
"body": "Thread body content...",
"replies": [
{
"author_name": "ReplyAuthor",
"body": "Reply content..."
}
]
}
]`}
</pre>
<div className="mt-4">
<h4 className="text-white font-body font-semibold text-sm mb-2">Available Category Slugs</h4>
<div className="flex flex-wrap gap-2">
{['live-production','ip-cloud','audio','cameras','storage-mam','streaming','ai-automation','post-production','nab-ibc','career-jobs','gear-reviews','general'].map(slug => (
<code key={slug} className="bg-[#F8FAFC] border border-[#333] text-[#1D4ED8] font-mono text-xs px-2 py-1 rounded">{slug}</code>
))}
</div>
</div>
</div>
)}
</div>
</div>
{/* Right: Results + Info */}
<div className="space-y-4">
{/* Import Results */}
{results.length > 0 && (
<div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Import Results</h3>
<div className="flex gap-4 mb-3">
<div className="text-center">
<div className="text-2xl font-bold text-green-400">{successCount}</div>
<div className="text-[#666] font-body text-xs">Imported</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-red-400">{errorCount}</div>
<div className="text-[#666] font-body text-xs">Failed</div>
</div>
</div>
<div className="space-y-2 max-h-64 overflow-y-auto">
{results.map((r, i) => (
<div key={i} className={`rounded p-2 text-xs font-body ${r.status === 'success' ? 'bg-green-900/20 border border-green-800/30' : 'bg-red-900/20 border border-red-800/30'}`}>
<div className={`font-semibold ${r.status === 'success' ? 'text-green-400' : 'text-red-400'}`}>
{r.status === 'success' ? '✓' : '✗'} {r.title.slice(0, 50)}{r.title.length > 50 ? '...' : ''}
</div>
<div className="text-[#888] mt-0.5">{r.message}</div>
</div>
))}
</div>
</div>
)}
{/* Quick Links */}
<div className="bg-[#FFFFFF] border border-[#DCE6F2] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-3">Quick Links</h3>
<div className="space-y-2">
<Link href="/forum" className="flex items-center gap-2 text-[#1D4ED8] hover:underline font-body text-sm">
<span></span> View Forum
</Link>
<Link href="/admin" className="flex items-center gap-2 text-[#1D4ED8] hover:underline font-body text-sm">
<span></span> Admin Dashboard
</Link>
</div>
</div>
{/* Tips */}
<div className="bg-[#FFFFFF] border border-[#2a3a50] rounded-lg p-4">
<h3 className="text-white font-heading font-semibold mb-2 text-sm">Tips</h3>
<ul className="text-[#aab4c4] font-body text-xs space-y-1.5">
<li> Threads are imported as real posts they appear immediately in the forum</li>
<li> Use realistic author names that fit the broadcast engineering community</li>
<li> Include 25 replies per thread for a natural conversation feel</li>
<li> Vary author names across threads to avoid repetition</li>
<li> Content should read as authentic community discussion</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<Footer />
</>
);
}