'use client'; import React, { useState, useEffect, useCallback } from 'react'; import Link from 'next/link'; import { useAuth } from '@/contexts/AuthContext'; import { useRouter } from 'next/navigation'; // ─── Types ──────────────────────────────────────────────────────────────────── interface ShowEvent { id: string; event_name: string; event_type: string; site_id: number; city: string | null; venue: string | null; country: string | null; start_date: string | null; end_date: string | null; story_engine_start_date: string | null; story_engine_end_date: string | null; official_site: string | null; seo_tier: number; status: string; year: number; phase1_volume_min: number; phase1_volume_max: number; phase2_volume_min: number; phase2_volume_max: number; phase3_volume_min: number; phase3_volume_max: number; phase4_volume_min: number; phase4_volume_max: number; engine_paused: boolean; notes: string | null; current_phase: string; } interface StoryQueueItem { id: string; company_name: string; story_type: string; show_story_type: string | null; show_phase: string | null; assigned_pen_name: string; site_id: number; status: string; generated_title: string | null; generated_content: string | null; generated_excerpt: string | null; quality_confidence: number | null; source_show: string | null; seo_meta_description: string | null; created_at: string; } interface Exhibitor { id: string; company_name: string; booth_number: string | null; preview_story_status: string; announcement_count: number; is_av_eligible: boolean; created_at: string; } type TabType = 'calendar' | 'queue' | 'exhibitors' | 'style-profiles' | 'settings'; type SiteFilter = '' | '1' | '2'; const SITE_CONFIG = { 1: { name: 'AV Beat', badge: 'BB', color: '#0ea5e9', bgClass: 'bg-[#0ea5e9]/10', textClass: 'text-[#0ea5e9]' }, 2: { name: 'AV Beat', badge: 'AV', color: '#f97316', bgClass: 'bg-[#f97316]/10', textClass: 'text-[#f97316]' }, }; const SEO_TIER_LABELS: Record = { 1: { label: 'Tier 1 — Max SEO', color: 'text-yellow-400' }, 2: { label: 'Tier 2 — High', color: 'text-blue-400' }, 3: { label: 'Tier 3 — Standard',color: 'text-gray-400' }, 4: { label: 'Tier 4 — Light', color: 'text-gray-500' }, }; const PHASE_LABELS: Record = { phase1_buildup: { label: 'Phase 1 — Buildup', color: 'text-blue-400' }, phase2_surge: { label: 'Phase 2 — Surge', color: 'text-yellow-400' }, phase3_show_week: { label: 'Phase 3 — Show Week', color: 'text-green-400' }, phase4_post_show: { label: 'Phase 4 — Post-Show', color: 'text-orange-400' }, phase5_off_season: { label: 'Phase 5 — Off-Season',color: 'text-gray-500' }, }; function SiteBadge({ siteId }: { siteId: number }) { const cfg = SITE_CONFIG[siteId as 1 | 2] || SITE_CONFIG[1]; return ( {cfg.badge} ); } function formatDate(d: string | null): string { if (!d) return '—'; return new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); } export default function ShowCalendarPage() { const { user, loading } = useAuth(); const router = useRouter(); const [activeTab, setActiveTab] = useState('calendar'); const [siteFilter, setSiteFilter] = useState(''); const [showFilter, setShowFilter] = useState(''); const [queueStatus, setQueueStatus] = useState('generated'); const [events, setEvents] = useState([]); const [queue, setQueue] = useState([]); const [exhibitors, setExhibitors] = useState([]); const [styleProfiles, setStyleProfiles] = useState([]); const [loadingData, setLoadingData] = useState(true); const [actionLoading, setActionLoading] = useState(null); const [expandedItem, setExpandedItem] = useState(null); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); // Add/Edit event modal const [showEventModal, setShowEventModal] = useState(false); const [editingEvent, setEditingEvent] = useState | null>(null); // Generate story modal const [showGenerateModal, setShowGenerateModal] = useState(false); const [generateForm, setGenerateForm] = useState({ event_id: '', story_type: 'exhibitor_preview', company_name: '', source_content: '', }); const showToast = (message: string, type: 'success' | 'error' = 'success') => { setToast({ message, type }); setTimeout(() => setToast(null), 4000); }; const fetchData = useCallback(async () => { setLoadingData(true); try { const params = new URLSearchParams({ tab: activeTab }); if (siteFilter) params.set('site', siteFilter); if (showFilter) params.set('show', showFilter); if (activeTab === 'queue' && queueStatus) params.set('status', queueStatus); const res = await fetch(`/api/admin/show-calendar?${params}`); const data = await res.json(); if (activeTab === 'calendar') setEvents(data.events || []); else if (activeTab === 'queue') setQueue(data.queue || []); else if (activeTab === 'exhibitors') setExhibitors(data.exhibitors || []); else if (activeTab === 'style-profiles') setStyleProfiles(data.profiles || []); } catch { showToast('Failed to load data', 'error'); } finally { setLoadingData(false); } }, [activeTab, siteFilter, showFilter, queueStatus]); useEffect(() => { if (!loading && !user) router.push('/login'); }, [user, loading, router]); useEffect(() => { if (user) fetchData(); }, [user, fetchData]); const handleToggleEngine = async (eventId: string, paused: boolean) => { setActionLoading(eventId); try { const res = await fetch('/api/admin/show-calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'toggle_engine', event_id: eventId, paused }), }); if ((await res.json()).success) { showToast(paused ? 'Engine paused' : 'Engine resumed'); fetchData(); } } catch { showToast('Failed', 'error'); } finally { setActionLoading(null); } }; const handleExtractStyle = async (eventName: string) => { setActionLoading('style-' + eventName); try { const res = await fetch('/api/admin/show-calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'extract_style', event_name: eventName }), }); const data = await res.json(); if (data.success) { showToast(`Style extracted from ${data.articleCount} articles`); fetchData(); } else showToast(data.error || 'Failed', 'error'); } catch { showToast('Failed', 'error'); } finally { setActionLoading(null); } }; const handleGenerateStory = async (e: React.FormEvent) => { e.preventDefault(); setActionLoading('generate'); try { const siteId = siteFilter === '2' ? 2 : 1; const res = await fetch('/api/admin/show-calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'generate_story', ...generateForm, site_id: siteId }), }); const data = await res.json(); if (data.story) { showToast(`Story generated — assigned to ${data.penName}`); setShowGenerateModal(false); setActiveTab('queue'); fetchData(); } else showToast(data.error || 'Failed', 'error'); } catch { showToast('Failed', 'error'); } finally { setActionLoading(null); } }; const handleQueueAction = async (id: string, action: string, extra?: any) => { setActionLoading(id + action); try { const res = await fetch('/api/admin/show-calendar', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, action, ...extra }), }); const data = await res.json(); if (data.success) { showToast(action === 'approve_story' ? 'Story published' : 'Story rejected'); fetchData(); } else showToast(data.error || 'Failed', 'error'); } catch { showToast('Failed', 'error'); } finally { setActionLoading(null); } }; const handleSaveEvent = async (e: React.FormEvent) => { e.preventDefault(); if (!editingEvent) return; setActionLoading('save-event'); try { const res = await fetch('/api/admin/show-calendar', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'upsert_event', ...editingEvent }), }); const data = await res.json(); if (data.event) { showToast('Event saved'); setShowEventModal(false); setEditingEvent(null); fetchData(); } else showToast(data.error || 'Failed', 'error'); } catch { showToast('Failed', 'error'); } finally { setActionLoading(null); } }; if (loading) { return (
); } const tabs: { id: TabType; label: string }[] = [ { id: 'calendar', label: 'Show Calendar' }, { id: 'queue', label: 'Story Queue' }, { id: 'exhibitors', label: 'Exhibitor List' }, { id: 'style-profiles',label: 'Style Profiles' }, { id: 'settings', label: 'Settings' }, ]; return (
{/* Toast */} {toast && (
{toast.message}
)} {/* Header */}
← Dashboard

Show Calendar Engine

AV Beat + AV Beat — Global Show Coverage

{/* Site Filter */}
Site: {(['', '1', '2'] as SiteFilter[]).map(s => ( ))}
{/* Tabs */}
{tabs.map(tab => ( ))}
{loadingData ? (
) : ( <> {/* ── SHOW CALENDAR TAB ── */} {activeTab === 'calendar' && (
{events.length === 0 && (
No events found. Add events to get started.
)} {events.map(event => { const siteCfg = SITE_CONFIG[event.site_id as 1 | 2] || SITE_CONFIG[1]; const tierInfo = SEO_TIER_LABELS[event.seo_tier] || SEO_TIER_LABELS[3]; const phaseInfo = PHASE_LABELS[event.current_phase] || PHASE_LABELS.phase5_off_season; return (
{tierInfo.label} {phaseInfo.label} {event.engine_paused && ( Engine Paused )}

{event.event_name}

{[event.city, event.venue, event.country].filter(Boolean).join(' · ')}

Show: {formatDate(event.start_date)} – {formatDate(event.end_date)} Engine: {formatDate(event.story_engine_start_date)} – {formatDate(event.story_engine_end_date)} {event.official_site && ( {event.official_site} )}
Ph1: {event.phase1_volume_min}–{event.phase1_volume_max}/day Ph2: {event.phase2_volume_min}–{event.phase2_volume_max}/day Ph3: {event.phase3_volume_min}–{event.phase3_volume_max}/day Ph4: {event.phase4_volume_min}–{event.phase4_volume_max}/day
{event.notes && (

{event.notes}

)}
); })}
)} {/* ── STORY QUEUE TAB ── */} {activeTab === 'queue' && (
Status: {['generated', 'in_review', 'approved', 'published', 'rejected', 'all'].map(s => ( ))}
{queue.length === 0 && (
No stories in queue.
)} {queue.map(item => (
{item.source_show && ( {item.source_show} )} {item.show_phase && ( {PHASE_LABELS[item.show_phase]?.label || item.show_phase} )} {item.status} {item.quality_confidence !== null && ( = 0.85 ? 'text-green-400' : item.quality_confidence >= 0.7 ? 'text-yellow-400' : 'text-red-400'}`}> {Math.round((item.quality_confidence || 0) * 100)}% confidence )}

{item.generated_title || '(No title generated)'}

By {item.assigned_pen_name} · {item.show_story_type || item.story_type}

{item.generated_excerpt && (

{item.generated_excerpt}

)} {/* Expanded content */} {expandedItem === item.id && item.generated_content && (
1500 ? '...' : '') }} />
)}
{item.status === 'generated' && ( <> )}
))}
)} {/* ── EXHIBITOR LIST TAB ── */} {activeTab === 'exhibitors' && (
Filter by show:
{exhibitors.length === 0 && ( )} {exhibitors.map(ex => ( ))}
Company Booth Preview Story AV Eligible Announcements
No exhibitors found.
{ex.company_name} {ex.booth_number || '—'} {ex.preview_story_status} {ex.is_av_eligible ? ( AV Beat ) : ( BB only )} {ex.announcement_count}
)} {/* ── STYLE PROFILES TAB ── */} {activeTab === 'style-profiles' && (

Style profiles are extracted from Ryan Salazar's existing NAB Show and event coverage articles. These profiles are injected into every show story generation prompt so output matches the publication's established voice.

{styleProfiles.length === 0 && (
No style profiles yet. Use "Extract Style" on any show in the Calendar tab.
)} {styleProfiles.map(profile => (

{profile.event_name || profile.event_type}

Author: {profile.author_username} · {profile.source_article_count} reference articles · Updated: {formatDate(profile.last_updated)}

{expandedItem === profile.id && profile.style_guide_text && (

{profile.style_guide_text}

)}
))}
)} {/* ── SETTINGS TAB ── */} {activeTab === 'settings' && (

Show Engine Settings

Annual Date Discovery

Runs automatically on January 1 each year. Searches official show websites and Google for confirmed dates. Sends admin notification when dates are set or when manual review is needed.

Pen Name Rosters

AV Beat (12 writers): Michael Strand, David Harlow, Karen Fielding, James Mercer (primary NAB), Peter Calloway, Sandra Voss, Brian Kowalski, Laura Pennington, Thomas Reeves, Christine Vale, Marcus Webb, Ellen Forsythe

AV Beat (7 writers): Rex Chandler, Dana Flux, Derek Wainwright, Sloane Rigging, Chip Crosspoint, Blair Presenter, Jordan Lumen

SEO Tier Priority

Tier 1 (Max): NAB Las Vegas, IBC, InfoComm, ISE · Tier 2 (High): NAB NY, BroadcastAsia, CABSAT, AES, InfoComm EDGE/China/LatAm · Tier 3 (Standard): MPTS, SET Expo, NATPE, InfoComm India, FORTÉ LIVE · Tier 4 (Light): NewsTECHForum, TAB, SVVS, Hamburg Open, AVIXA Regional

NAB Cross-Post to AV Beat

NAB exhibitors with AV-eligible products (projectors, displays, conferencing, control systems, digital signage, installed audio, AV-over-IP) automatically generate stories on AV Beat using AV Beat pen names.

Blocked Competitor Sources

BB: tvtechnology.com, broadcastingcable.com, sportsvideo.org, digitaltveurope.com, nexttv.com · AV: ravepubs.com, avnetwork.com, commercialintegrator.com, systemscontractor.com, avinteractive.com · Note: infocommshow.org, iseurope.org, nabshow.com, avixa.org are PRIMARY sources — not blocked.

)} )}
{/* ── Add/Edit Event Modal ── */} {showEventModal && editingEvent && (

{editingEvent.id ? 'Edit Event' : 'Add Event'}

setEditingEvent(prev => ({ ...prev, event_name: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, start_date: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, end_date: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, story_engine_start_date: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, story_engine_end_date: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, city: e.target.value }))} className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />
setEditingEvent(prev => ({ ...prev, official_site: e.target.value }))} placeholder="nabshow.com" className="w-full bg-[#0a0a0a] border border-[#333] text-white text-sm rounded px-3 py-2" />