/** * — Campaign viewer/editor with import modal. * * Subscribes to Automerge doc for campaign data. Falls back to MYCOFI_CAMPAIGN * demo data when no campaigns exist or space=demo. */ import { socialsSchema, socialsDocId } from '../schemas'; import type { SocialsDoc, Campaign, CampaignPost } from '../schemas'; import type { DocumentId } from '../../../shared/local-first/document'; import { MYCOFI_CAMPAIGN, PLATFORM_ICONS, PLATFORM_COLORS } from '../campaign-data'; export class FolkCampaignManager extends HTMLElement { private _space = 'demo'; private _campaigns: Campaign[] = []; private _offlineUnsub: (() => void) | null = null; static get observedAttributes() { return ['space']; } connectedCallback() { this.attachShadow({ mode: 'open' }); this._space = this.getAttribute('space') || 'demo'; // Start with demo campaign this._campaigns = [{ ...MYCOFI_CAMPAIGN, createdAt: Date.now(), updatedAt: Date.now() }]; this.render(); if (this._space !== 'demo') { this.subscribeOffline(); } } disconnectedCallback() { this._offlineUnsub?.(); this._offlineUnsub = null; } attributeChangedCallback(name: string, _old: string, val: string) { if (name === 'space') this._space = val; } private async subscribeOffline() { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; try { const docId = socialsDocId(this._space) as DocumentId; const doc = await runtime.subscribe(docId, socialsSchema); this.renderFromDoc(doc); this._offlineUnsub = runtime.onChange(docId, (updated: any) => { this.renderFromDoc(updated); }); } catch { // Runtime unavailable — use demo data } } private renderFromDoc(doc: SocialsDoc) { if (!doc?.campaigns || Object.keys(doc.campaigns).length === 0) return; this._campaigns = Object.values(doc.campaigns).sort((a, b) => b.updatedAt - a.updatedAt); this.render(); } private saveCampaignToDoc(campaign: Campaign) { const runtime = (window as any).__rspaceOfflineRuntime; if (!runtime?.isInitialized) return; const docId = socialsDocId(this._space) as DocumentId; runtime.change(docId, `Save campaign ${campaign.title}`, (d: SocialsDoc) => { if (!d.campaigns) d.campaigns = {} as any; campaign.updatedAt = Date.now(); d.campaigns[campaign.id] = campaign; }); } private esc(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } private renderCampaign(c: Campaign): string { const phases = [1, 2, 3]; const phaseIcons = ['📣', '🚀', '📡']; const phaseHTML = phases.map((phaseNum, i) => { const phasePosts = c.posts.filter(p => p.phase === phaseNum); if (!phasePosts.length && !c.phases[i]) return ''; const phaseInfo = c.phases[i] || { label: `Phase ${phaseNum}`, days: '' }; const postsHTML = phasePosts.map(post => { const icon = PLATFORM_ICONS[post.platform] || post.platform; const color = PLATFORM_COLORS[post.platform] || '#64748b'; const statusClass = post.status === 'scheduled' ? 'status--scheduled' : 'status--draft'; const date = new Date(post.scheduledAt); const dateStr = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); const contentPreview = post.content.length > 180 ? this.esc(post.content.substring(0, 180)) + '...' : this.esc(post.content); const tags = post.hashtags.map(h => `#${this.esc(h)}`).join(' '); return `
${icon} ${this.esc(post.status)}
Step ${post.stepNumber}

${contentPreview.replace(/\n/g, '
')}

`; }).join(''); return `

${phaseIcons[i] || '📋'} Phase ${phaseNum}: ${this.esc(phaseInfo.label)} ${this.esc(phaseInfo.days)}

${postsHTML}
`; }).join(''); return `
🍄

${this.esc(c.title)}

${this.esc(c.description)}

📅 ${this.esc(c.duration)} 📱 ${c.platforms.join(', ')} 📝 ${c.posts.length} posts across ${c.phases.length} phases
Open Thread Builder
${phaseHTML}
`; } private render() { if (!this.shadowRoot) return; const c = this._campaigns[0] || { ...MYCOFI_CAMPAIGN, createdAt: Date.now(), updatedAt: Date.now() }; const campaignHTML = this.renderCampaign(c); this.shadowRoot.innerHTML = `
${campaignHTML}
`; this.bindEvents(); } private bindEvents() { if (!this.shadowRoot) return; const modal = this.shadowRoot.getElementById('import-modal') as HTMLElement; const openBtn = this.shadowRoot.getElementById('import-md-btn'); const closeBtn = this.shadowRoot.getElementById('import-modal-close'); const parseBtn = this.shadowRoot.getElementById('import-parse-btn'); const mdInput = this.shadowRoot.getElementById('import-md-textarea') as HTMLTextAreaElement; const platformSel = this.shadowRoot.getElementById('import-platform') as HTMLSelectElement; const importedEl = this.shadowRoot.getElementById('imported-posts'); openBtn?.addEventListener('click', () => { modal.hidden = false; }); closeBtn?.addEventListener('click', () => { modal.hidden = true; }); modal?.addEventListener('click', (e) => { if (e.target === modal) modal.hidden = true; }); parseBtn?.addEventListener('click', () => { const raw = mdInput.value; const tweets = raw.split(/\n---\n/).map(t => t.trim()).filter(Boolean); if (!tweets.length || !importedEl) return; const platform = platformSel.value; const total = tweets.length; // Build imported posts as campaign posts and save to Automerge const posts: CampaignPost[] = tweets.map((text, i) => ({ id: `imported-${Date.now()}-${i}`, platform, postType: 'text', stepNumber: i + 1, content: text, scheduledAt: new Date().toISOString(), status: 'imported', hashtags: [], phase: 1, phaseLabel: 'Imported', })); // Render imported posts inline let html = `

📥 Imported Posts (${total})

`; html += '
'; tweets.forEach((text, i) => { const preview = text.length > 180 ? this.esc(text.substring(0, 180)) + '...' : this.esc(text); html += `
${this.esc(platform.charAt(0).toUpperCase())} imported
Tweet ${i + 1}/${total}

${preview.replace(/\n/g, '
')}

`; }); html += '
'; importedEl.innerHTML = html; modal.hidden = true; // Save to Automerge if runtime available if (this._space !== 'demo') { const c = this._campaigns[0]; if (c) { c.posts = [...c.posts, ...posts]; this.saveCampaignToDoc(c); } } }); } } customElements.define('folk-campaign-manager', FolkCampaignManager);