/** * — Campaign gallery grid. * * Lists all campaign flows and renders mini SVG previews. Click a card to * open that flow in the planner. Also surfaces the AI wizard (which sets up * a new flow) and a blank "+ New" affordance. * * Attributes: * space — space slug (default "demo") */ import { TourEngine } from '../../../shared/tour-engine'; import type { TourStep } from '../../../shared/tour-engine'; import type { CampaignFlow, CampaignPlannerNode, CampaignEdge, PhaseNodeData, } from '../schemas'; // ── Mini SVG constants ── const NODE_COLORS: Record = { post: '#3b82f6', thread: '#8b5cf6', platform: '#10b981', audience: '#f59e0b', phase: '#64748b', goal: '#6366f1', message: '#6366f1', tone: '#8b5cf6', brief: '#ec4899', }; function nodeSize(n: CampaignPlannerNode): { w: number; h: number } { switch (n.type) { case 'post': return { w: 240, h: 120 }; case 'thread': return { w: 240, h: 100 }; case 'platform': return { w: 180, h: 80 }; case 'audience': return { w: 180, h: 80 }; case 'phase': { const d = n.data as PhaseNodeData; return { w: d.size?.w || 400, h: d.size?.h || 300 }; } case 'goal': return { w: 240, h: 130 }; case 'message': return { w: 220, h: 100 }; case 'tone': return { w: 220, h: 110 }; case 'brief': return { w: 260, h: 150 }; default: return { w: 200, h: 100 }; } } function esc(s: string): string { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; } // ── Mini SVG renderer ── function renderMiniFlowSVG(nodes: CampaignPlannerNode[], edges: CampaignEdge[]): string { if (nodes.length === 0) { return ` Empty flow `; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const n of nodes) { const s = nodeSize(n); minX = Math.min(minX, n.position.x); minY = Math.min(minY, n.position.y); maxX = Math.max(maxX, n.position.x + s.w); maxY = Math.max(maxY, n.position.y + s.h); } const pad = 40; const vx = minX - pad; const vy = minY - pad; const vw = (maxX - minX) + pad * 2; const vh = (maxY - minY) + pad * 2; // Edges (simple line midpoint-to-midpoint; kept light) const edgePaths = edges.map(e => { const from = nodes.find(n => n.id === e.from); const to = nodes.find(n => n.id === e.to); if (!from || !to) return ''; const fs = nodeSize(from); const ts = nodeSize(to); const x1 = from.position.x + fs.w; const y1 = from.position.y + fs.h / 2; const x2 = to.position.x; const y2 = to.position.y + ts.h / 2; const dx = Math.abs(x2 - x1) * 0.5; return ``; }).join(''); // Phase rects behind const phaseRects = nodes.filter(n => n.type === 'phase').map(n => { const s = nodeSize(n); const d = n.data as PhaseNodeData; return ``; }).join(''); // Content nodes as solid rects const contentRects = nodes.filter(n => n.type !== 'phase').map(n => { const s = nodeSize(n); const color = NODE_COLORS[n.type] || '#666'; return ``; }).join(''); return ` ${phaseRects} ${edgePaths} ${contentRects} `; } // ── Component ── const DASHBOARD_TOUR_STEPS: TourStep[] = [ { target: '#btn-wizard', title: 'Campaign Wizard', message: 'AI-guided campaign setup — answer a few questions and the wizard builds the flow for you.' }, { target: '#btn-new', title: 'New Campaign', message: 'Start a blank campaign flow and lay out your own posts, platforms, and audiences.' }, { target: '.cd-card', title: 'Campaign Cards', message: 'Click any card to open its flow in the planner.' }, ]; class FolkCampaignsDashboard extends HTMLElement { private shadow: ShadowRoot; private space = ''; private flows: CampaignFlow[] = []; private loading = true; private _tour!: TourEngine; private get basePath() { const host = window.location.hostname; if (host.endsWith('.rspace.online') || host.endsWith('.rsocials.online')) return '/rsocials/'; return `/${this.space}/rsocials/`; } constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }); this._tour = new TourEngine( this.shadow, DASHBOARD_TOUR_STEPS, 'rsocials_dashboard_tour_done', () => this.shadow.querySelector('.cd-root') as HTMLElement, ); } connectedCallback() { this.space = this.getAttribute('space') || 'demo'; this.render(); this.loadFlows(); } private async loadFlows() { try { const res = await fetch(`${this.basePath}api/campaign/flows`); if (res.ok) { const data = await res.json(); this.flows = data.results || []; } } catch { console.warn('[CampaignsDashboard] Failed to load flows'); } this.loading = false; this.render(); if (!localStorage.getItem('rsocials_dashboard_tour_done')) { setTimeout(() => this._tour.start(), 800); } } startTour() { this._tour.start(); } private async createFlow() { try { const res = await fetch(`${this.basePath}api/campaign/flows`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'New Campaign' }), }); if (res.ok) { const flow = await res.json(); this.navigateToFlow(flow.id); } } catch { console.error('[CampaignsDashboard] Failed to create flow'); } } private navigateToFlow(id: string) { window.location.href = `${this.basePath}campaign-flow?id=${encodeURIComponent(id)}`; } private formatDate(ts: number | null): string { if (!ts) return '—'; const d = new Date(ts); const now = Date.now(); const diff = now - ts; if (diff < 60000) return 'just now'; if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`; if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`; return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); } private render() { const cards = this.flows.map(flow => { const nodeCount = flow.nodes.length; const postCount = flow.nodes.filter(n => n.type === 'post').length; const platformCount = flow.nodes.filter(n => n.type === 'platform').length; return `
${renderMiniFlowSVG(flow.nodes, flow.edges)}
${esc(flow.name)}
${nodeCount} node${nodeCount !== 1 ? 's' : ''} ${postCount > 0 ? `${postCount} post${postCount !== 1 ? 's' : ''}` : ''} ${platformCount > 0 ? `${platformCount} platform${platformCount !== 1 ? 's' : ''}` : ''}
Updated ${this.formatDate(flow.updatedAt)}
`; }).join(''); const emptyState = !this.loading && this.flows.length === 0 ? `
📢
No campaigns yet
Use the AI wizard to set up a flow from a brief, or start a blank canvas
` : ''; const loadingState = this.loading ? `
Loading campaigns…
` : ''; this.shadow.innerHTML = `

Campaigns

Plan, wire up, and schedule multi-platform campaigns
${!this.loading && this.flows.length > 0 ? '' : ''}
${loadingState} ${emptyState} ${!this.loading && this.flows.length > 0 ? `
${cards}
` : ''}
`; this._tour.renderOverlay(); this.attachListeners(); } private attachListeners() { this.shadow.querySelectorAll('.cd-card').forEach(card => { card.addEventListener('click', () => { const id = (card as HTMLElement).dataset.flowId; if (id) this.navigateToFlow(id); }); }); const btnNew = this.shadow.getElementById('btn-new'); if (btnNew) btnNew.addEventListener('click', () => this.createFlow()); const btnNewEmpty = this.shadow.querySelector('.cd-btn--new-empty:not(#btn-wizard-empty)'); if (btnNewEmpty) btnNewEmpty.addEventListener('click', () => this.createFlow()); const wizardUrl = `${this.basePath}campaign-wizard`; const btnWizard = this.shadow.getElementById('btn-wizard'); if (btnWizard) btnWizard.addEventListener('click', () => { window.location.href = wizardUrl; }); const btnWizardEmpty = this.shadow.getElementById('btn-wizard-empty'); if (btnWizardEmpty) btnWizardEmpty.addEventListener('click', () => { window.location.href = wizardUrl; }); this.shadow.getElementById('btn-tour')?.addEventListener('click', () => this.startTour()); } } customElements.define('folk-campaigns-dashboard', FolkCampaignsDashboard);