diff --git a/modules/rsocials/components/campaign-planner.css b/modules/rsocials/components/campaign-planner.css index bcd4ef9..92d1469 100644 --- a/modules/rsocials/components/campaign-planner.css +++ b/modules/rsocials/components/campaign-planner.css @@ -470,3 +470,392 @@ folk-campaign-planner { flex-wrap: wrap; } } + +/* ── View Switcher ── */ +.cp-view-switcher { + display: flex; + gap: 2px; + background: var(--rs-input-bg, #16162a); + border: 1px solid var(--rs-input-border, #3d3d5c); + border-radius: 8px; + padding: 2px; +} + +.cp-view-btn { + width: 30px; + height: 28px; + border: none; + border-radius: 6px; + background: transparent; + color: var(--rs-text-muted, #94a3b8); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, color 0.15s; +} + +.cp-view-btn:hover { + background: var(--rs-bg-surface-raised, #252545); + color: var(--rs-text-primary, #e2e8f0); +} + +.cp-view-btn.active { + background: #6366f133; + color: #818cf8; +} + +/* ── Alt view container ── */ +.cp-alt-view { + display: none; + flex: 1; + overflow: auto; + background: var(--rs-canvas-bg, #0f0f23); +} + +/* ── Timeline View ── */ +.cp-timeline { + display: flex; + flex-direction: column; + height: 100%; +} + +.cp-tl-scroll { + flex: 1; + overflow: auto; + padding: 16px; +} + +.cp-tl-grid { + display: grid; + gap: 0; + min-width: max-content; +} + +.cp-tl-header { + display: contents; +} + +.cp-tl-day { + padding: 8px 12px; + font-size: 11px; + font-weight: 600; + color: var(--rs-text-muted, #94a3b8); + border-bottom: 1px solid var(--rs-border, #2d2d44); + position: sticky; + top: 0; + background: var(--rs-canvas-bg, #0f0f23); + z-index: 2; + text-align: center; +} + +.cp-tl-phases { + display: flex; + gap: 4px; + padding: 8px 0; +} + +.cp-tl-phase { + padding: 4px 10px; + border-radius: 6px; + white-space: nowrap; +} + +.cp-tl-body { + display: grid; + gap: 0; +} + +.cp-tl-col { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px 6px; + border-right: 1px solid var(--rs-border, #2d2d44); + min-height: 60px; +} + +.cp-tl-col:last-child { + border-right: none; +} + +.cp-tl-card { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + background: var(--rs-bg-surface, #1a1a2e); + border: 1px solid var(--rs-border, #2d2d44); + border-radius: 6px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; + font-size: 11px; +} + +.cp-tl-card:hover { + border-color: #6366f1; + background: var(--rs-bg-surface-raised, #252545); +} + +.cp-tl-card__icon { + font-size: 12px; + flex-shrink: 0; +} + +.cp-tl-card__label { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--rs-text-primary, #e2e8f0); +} + +.cp-tl-card__dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; +} + +.cp-tl-card__time { + font-size: 9px; + color: var(--rs-text-muted, #94a3b8); + white-space: nowrap; +} + +/* ── Platform View ── */ +.cp-platform-view { + display: flex; + gap: 16px; + padding: 16px; + height: 100%; + overflow-x: auto; +} + +.cp-pv-column { + flex: 1; + min-width: 220px; + max-width: 320px; + display: flex; + flex-direction: column; + background: var(--rs-bg-surface, #1a1a2e); + border: 1px solid var(--rs-border, #2d2d44); + border-radius: 10px; + overflow: hidden; +} + +.cp-pv-column__header { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; + border-bottom: 1px solid var(--rs-border, #2d2d44); +} + +.cp-pv-column__title { + font-size: 13px; + font-weight: 600; + color: var(--rs-text-primary, #e2e8f0); + flex: 1; +} + +.cp-pv-column__count { + font-size: 10px; + background: var(--rs-bg-surface-raised, #252545); + color: var(--rs-text-muted, #94a3b8); + padding: 2px 6px; + border-radius: 10px; + font-weight: 600; +} + +.cp-pv-column__body { + flex: 1; + overflow-y: auto; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; + max-height: calc(100vh - 180px); +} + +.cp-pv-card { + padding: 10px; + background: var(--rs-input-bg, #16162a); + border: 1px solid var(--rs-border, #2d2d44); + border-radius: 8px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} + +.cp-pv-card:hover { + border-color: #6366f1; + background: var(--rs-bg-surface-raised, #252545); +} + +.cp-pv-card__header { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 4px; +} + +.cp-pv-card__label { + font-size: 12px; + font-weight: 600; + color: var(--rs-text-primary, #e2e8f0); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cp-pv-card__dot { + width: 7px; + height: 7px; + border-radius: 50%; + flex-shrink: 0; +} + +.cp-pv-card__preview { + font-size: 11px; + color: var(--rs-text-muted, #94a3b8); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 6px; +} + +.cp-pv-card__meta { + display: flex; + justify-content: space-between; + font-size: 10px; + color: #64748b; +} + +.cp-pv-card__type { + text-transform: capitalize; +} + +.cp-pv-card__tags { + display: flex; + gap: 4px; + margin-top: 6px; + flex-wrap: wrap; +} + +.cp-pv-tag { + font-size: 9px; + padding: 1px 5px; + border-radius: 4px; + background: #6366f122; + color: #818cf8; +} + +/* ── Table View ── */ +.cp-table-view { + padding: 16px; + overflow: auto; + height: 100%; +} + +.cp-tv-table { + width: 100%; + border-collapse: collapse; + font-size: 12px; +} + +.cp-tv-table thead th { + position: sticky; + top: 0; + background: var(--rs-bg-surface, #1a1a2e); + text-align: left; + padding: 8px 12px; + font-size: 11px; + font-weight: 600; + color: var(--rs-text-muted, #94a3b8); + border-bottom: 1px solid var(--rs-border-strong, #3d3d5c); + white-space: nowrap; + z-index: 2; +} + +.cp-tv-row { + cursor: pointer; + transition: background 0.1s; +} + +.cp-tv-row:hover { + background: var(--rs-bg-surface-raised, #252545); +} + +.cp-tv-row td { + padding: 8px 12px; + border-bottom: 1px solid var(--rs-border, #2d2d44); + color: var(--rs-text-primary, #e2e8f0); + white-space: nowrap; +} + +.cp-tv-content { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap !important; + color: var(--rs-text-muted, #94a3b8) !important; +} + +.cp-tv-dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; + margin-right: 6px; + vertical-align: middle; +} + +.cp-tv-tag { + font-size: 9px; + padding: 1px 5px; + border-radius: 4px; + background: #6366f122; + color: #818cf8; + margin-right: 3px; +} + +/* ── Mobile: alt views ── */ +@media (max-width: 768px) { + .cp-view-switcher { + order: -1; + width: 100%; + justify-content: center; + } + + .cp-platform-view { + flex-direction: column; + gap: 12px; + } + + .cp-pv-column { + max-width: 100%; + min-width: 0; + } + + .cp-pv-column__body { + max-height: 300px; + } + + .cp-tl-card { + font-size: 10px; + padding: 4px 6px; + } + + .cp-tv-table { + font-size: 11px; + } + + .cp-tv-row td { + padding: 6px 8px; + } + + .cp-tv-content { + max-width: 150px; + } +} diff --git a/modules/rsocials/components/folk-campaign-planner.ts b/modules/rsocials/components/folk-campaign-planner.ts index 9df0aab..3dd5ce0 100644 --- a/modules/rsocials/components/folk-campaign-planner.ts +++ b/modules/rsocials/components/folk-campaign-planner.ts @@ -120,6 +120,7 @@ function getUsername(): string | null { class FolkCampaignPlanner extends HTMLElement { private shadow: ShadowRoot; private space = ''; + private currentView: 'canvas' | 'timeline' | 'platform' | 'table' = 'canvas'; private get basePath() { const host = window.location.hostname; @@ -228,7 +229,11 @@ class FolkCampaignPlanner extends HTMLElement { if (flow) { this.nodes = flow.nodes.map(n => ({ ...n, position: { ...n.position }, data: { ...n.data } })); this.edges = flow.edges.map(e => ({ ...e, waypoint: e.waypoint ? { ...e.waypoint } : undefined })); - this.drawCanvasContent(); + if (this.currentView === 'canvas') { + this.drawCanvasContent(); + } else { + this.switchView(this.currentView); + } } }); @@ -909,6 +914,316 @@ class FolkCampaignPlanner extends HTMLElement { this.shadow.getElementById('cp-context-menu')?.remove(); } + // ── View switching ── + + private switchView(view: 'canvas' | 'timeline' | 'platform' | 'table') { + this.currentView = view; + const canvasEl = this.shadow.getElementById('cp-canvas'); + const altEl = this.shadow.getElementById('cp-alt-view'); + const zoomControls = this.shadow.querySelector('.cp-zoom-controls') as HTMLElement | null; + + // Update active button + this.shadow.querySelectorAll('.cp-view-btn').forEach(btn => { + btn.classList.toggle('active', btn.getAttribute('data-view') === view); + }); + + if (view === 'canvas') { + if (canvasEl) canvasEl.style.display = ''; + if (altEl) { altEl.innerHTML = ''; altEl.style.display = 'none'; } + if (zoomControls) zoomControls.style.display = ''; + return; + } + + // Hide canvas SVG (preserves state) + if (canvasEl) canvasEl.style.display = 'none'; + if (zoomControls) zoomControls.style.display = 'none'; + if (!altEl) return; + altEl.style.display = ''; + + switch (view) { + case 'timeline': altEl.innerHTML = this.renderTimeline(); break; + case 'platform': altEl.innerHTML = this.renderPlatformView(); break; + case 'table': altEl.innerHTML = this.renderTableView(); break; + } + this.attachAltViewListeners(); + } + + // ── Timeline view ── + + private renderTimeline(): string { + const posts = this.nodes.filter(n => n.type === 'post' || n.type === 'thread'); + const phases = this.nodes.filter(n => n.type === 'phase'); + + // Collect all dates; group posts by day + const dated = posts.map(n => { + const d = n.data as PostNodeData; + const dt = d.scheduledAt ? new Date(d.scheduledAt) : null; + return { node: n, date: dt }; + }).sort((a, b) => { + if (!a.date && !b.date) return 0; + if (!a.date) return 1; + if (!b.date) return -1; + return a.date.getTime() - b.date.getTime(); + }); + + // Build day buckets + const dayMap = new Map(); + for (const item of dated) { + const key = item.date ? item.date.toISOString().split('T')[0] : 'unscheduled'; + if (!dayMap.has(key)) dayMap.set(key, []); + dayMap.get(key)!.push(item); + } + + // Determine date range for columns + const allDates = dated.filter(d => d.date).map(d => d.date!); + if (allDates.length === 0) { + return '
No scheduled posts yet. Add dates in canvas view.
'; + } + + const minDate = new Date(Math.min(...allDates.map(d => d.getTime()))); + const maxDate = new Date(Math.max(...allDates.map(d => d.getTime()))); + minDate.setDate(minDate.getDate() - 1); + maxDate.setDate(maxDate.getDate() + 1); + + const days: string[] = []; + const cur = new Date(minDate); + while (cur <= maxDate) { + days.push(cur.toISOString().split('T')[0]); + cur.setDate(cur.getDate() + 1); + } + + // Phase bars + let phaseBars = ''; + for (const phase of phases) { + const pd = phase.data as PhaseNodeData; + if (!pd.dateRange) continue; + const parts = pd.dateRange.split(/\s*[-–]\s*/); + if (parts.length < 2) continue; + const start = days.indexOf(parts[0].trim()); + const end = days.indexOf(parts[1].trim()); + if (start < 0 || end < 0) continue; + phaseBars += `
+ ${esc(pd.label)} +
`; + } + + // Day headers + cards + const headers = days.map(d => { + const dt = new Date(d + 'T12:00:00'); + const label = dt.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); + return `
${label}
`; + }).join(''); + + const columns = days.map(dayKey => { + const items = dayMap.get(dayKey) || []; + const cards = items.map(item => { + const d = item.node.data as PostNodeData; + const platform = d.platform || 'x'; + const color = PLATFORM_COLORS[platform] || '#888'; + const icon = PLATFORM_ICONS[platform] || platform.charAt(0); + const statusColor = d.status === 'published' ? '#22c55e' : d.status === 'scheduled' ? '#3b82f6' : '#f59e0b'; + const time = item.date ? item.date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }) : ''; + return `
+ ${icon} + ${esc(d.label || (d.content || '').split('\n')[0].substring(0, 30) || 'Post')} + + ${time ? `${time}` : ''} +
`; + }).join(''); + return `
${cards}
`; + }).join(''); + + // Unscheduled section + let unscheduledHtml = ''; + const unscheduled = dayMap.get('unscheduled'); + if (unscheduled && unscheduled.length > 0) { + const cards = unscheduled.map(item => { + const d = item.node.data as PostNodeData; + const platform = d.platform || 'x'; + const color = PLATFORM_COLORS[platform] || '#888'; + const icon = PLATFORM_ICONS[platform] || platform.charAt(0); + return `
+ ${icon} + ${esc(d.label || 'Post')} + +
`; + }).join(''); + unscheduledHtml = `
+
Unscheduled
+
${cards}
+
`; + } + + return `
+
+
+
${headers}
+
${phaseBars}
+
${columns}
+
+
+ ${unscheduledHtml} +
`; + } + + // ── Platform view ── + + private renderPlatformView(): string { + const posts = this.nodes.filter(n => n.type === 'post'); + + // Group by platform + const platformMap = new Map(); + for (const node of posts) { + const d = node.data as PostNodeData; + const p = d.platform || 'x'; + if (!platformMap.has(p)) platformMap.set(p, []); + platformMap.get(p)!.push(node); + } + + // Sort each platform's posts by date + platformMap.forEach(items => { + items.sort((a, b) => { + const da = (a.data as PostNodeData).scheduledAt; + const db = (b.data as PostNodeData).scheduledAt; + if (!da && !db) return 0; + if (!da) return 1; + if (!db) return -1; + return new Date(da).getTime() - new Date(db).getTime(); + }); + }); + + if (platformMap.size === 0) { + return '
No posts yet. Add posts in canvas view.
'; + } + + const columns = Array.from(platformMap.entries()).map(([platform, items]) => { + const color = PLATFORM_COLORS[platform] || '#888'; + const icon = PLATFORM_ICONS[platform] || platform.charAt(0); + + const cards = items.map(node => { + const d = node.data as PostNodeData; + const statusColor = d.status === 'published' ? '#22c55e' : d.status === 'scheduled' ? '#3b82f6' : '#f59e0b'; + const dateStr = d.scheduledAt ? new Date(d.scheduledAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'Unscheduled'; + const preview = (d.content || '').split('\n')[0].substring(0, 60); + const tags = d.hashtags.slice(0, 3).map(h => + `${esc(h.startsWith('#') ? h : '#' + h)}` + ).join(''); + + return `
+
+ ${esc(d.label || preview || 'Post')} + +
+ ${preview ? `
${esc(preview)}
` : ''} +
+ ${esc(d.postType)} + ${dateStr} +
+ ${tags ? `
${tags}
` : ''} +
`; + }).join(''); + + return `
+
+ ${icon} + ${platform.charAt(0).toUpperCase() + platform.slice(1)} + ${items.length} +
+
${cards}
+
`; + }).join(''); + + return `
${columns}
`; + } + + // ── Table view ── + + private renderTableView(): string { + const posts = this.nodes.filter(n => n.type === 'post' || n.type === 'thread'); + + posts.sort((a, b) => { + const da = (a.data as PostNodeData).scheduledAt || ''; + const db = (b.data as PostNodeData).scheduledAt || ''; + if (!da && !db) return 0; + if (!da) return 1; + if (!db) return -1; + return da.localeCompare(db); + }); + + if (posts.length === 0) { + return '
No posts yet.
'; + } + + const rows = posts.map(node => { + const d = node.data as PostNodeData; + const platform = d.platform || 'x'; + const color = PLATFORM_COLORS[platform] || '#888'; + const icon = PLATFORM_ICONS[platform] || platform.charAt(0); + const statusColor = d.status === 'published' ? '#22c55e' : d.status === 'scheduled' ? '#3b82f6' : '#f59e0b'; + const statusLabel = d.status ? d.status.charAt(0).toUpperCase() + d.status.slice(1) : 'Draft'; + const dateStr = d.scheduledAt ? new Date(d.scheduledAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : '—'; + const preview = (d.content || '').split('\n')[0].substring(0, 50); + const tags = (d.hashtags || []).slice(0, 3).map(h => + `${esc(h.startsWith('#') ? h : '#' + h)}` + ).join(''); + + return ` + ${statusLabel} + ${icon} ${platform.charAt(0).toUpperCase() + platform.slice(1)} + ${esc(d.postType || node.type)} + ${esc(preview)} + ${dateStr} + ${tags} + `; + }).join(''); + + return `
+ + + + + ${rows} +
StatusPlatformTypeContentScheduledHashtags
+
`; + } + + // ── Alt view click-to-navigate ── + + private attachAltViewListeners() { + const altEl = this.shadow.getElementById('cp-alt-view'); + if (!altEl) return; + altEl.querySelectorAll('[data-nav-node]').forEach(el => { + el.addEventListener('click', () => { + const nodeId = el.getAttribute('data-nav-node'); + if (!nodeId) return; + this.navigateToCanvasNode(nodeId); + }); + }); + } + + private navigateToCanvasNode(nodeId: string) { + this.switchView('canvas'); + const node = this.nodes.find(n => n.id === nodeId); + if (!node) return; + + this.selectedNodeId = nodeId; + this.updateSelectionHighlight(); + + // Center node in view + const svg = this.shadow.getElementById('cp-svg') as SVGSVGElement | null; + if (svg) { + const rect = svg.getBoundingClientRect(); + const s = getNodeSize(node); + const cx = node.position.x + s.w / 2; + const cy = node.position.y + s.h / 2; + this.canvasPanX = rect.width / 2 - cx * this.canvasZoom; + this.canvasPanY = rect.height / 2 - cy * this.canvasZoom; + this.updateCanvasTransform(); + } + + this.enterInlineEdit(nodeId); + } + // ── Rendering ── private render() { @@ -922,6 +1237,20 @@ class FolkCampaignPlanner extends HTMLElement { 📢 ${esc(this.flowName || 'Campaign Planner')} ${this.space === 'demo' ? 'Demo' : ''} +
+ + + + +
@@ -951,6 +1280,7 @@ class FolkCampaignPlanner extends HTMLElement {
+
@@ -1208,6 +1538,14 @@ class FolkCampaignPlanner extends HTMLElement { const canvas = this.shadow.getElementById('cp-canvas'); if (!svg || !canvas) return; + // View switcher buttons + this.shadow.querySelectorAll('.cp-view-btn').forEach(btn => { + btn.addEventListener('click', () => { + const view = btn.getAttribute('data-view') as 'canvas' | 'timeline' | 'platform' | 'table'; + if (view) this.switchView(view); + }); + }); + // Toolbar add buttons this.shadow.getElementById('add-post')?.addEventListener('click', () => { const rect = svg.getBoundingClientRect(); @@ -1481,6 +1819,22 @@ class FolkCampaignPlanner extends HTMLElement { const tag = (e.target as HTMLElement)?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + // Escape: in alt views → switch back to canvas + if (e.key === 'Escape') { + if (this.currentView !== 'canvas') { + this.switchView('canvas'); + return; + } + if (this.wiringActive) this.cancelWiring(); + else if (this.inlineEditNodeId) this.exitInlineEdit(); + else if (this.selectedNodeId) { this.selectedNodeId = null; this.updateSelectionHighlight(); } + else if (this.selectedEdgeKey) { this.selectedEdgeKey = null; this.redrawEdges(); } + return; + } + + // Canvas-only shortcuts + if (this.currentView !== 'canvas') return; + if (e.key === 'Delete' || e.key === 'Backspace') { if (this.selectedNodeId) { this.deleteNode(this.selectedNodeId); @@ -1489,11 +1843,6 @@ class FolkCampaignPlanner extends HTMLElement { } } else if (e.key === 'f' || e.key === 'F') { this.fitView(); - } else if (e.key === 'Escape') { - if (this.wiringActive) this.cancelWiring(); - else if (this.inlineEditNodeId) this.exitInlineEdit(); - else if (this.selectedNodeId) { this.selectedNodeId = null; this.updateSelectionHighlight(); } - else if (this.selectedEdgeKey) { this.selectedEdgeKey = null; this.redrawEdges(); } } else if (e.key === '+' || e.key === '=') { const rect = svg.getBoundingClientRect(); this.zoomAt(rect.width / 2, rect.height / 2, 1.15);