import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const PLATFORM_ICONS: Record = { x: "𝕏", linkedin: "in", instagram: "📷", youtube: "▶", threads: "@", bluesky: "🩷", tiktok: "♫", facebook: "f", }; const styles = css` :host { background: var(--rs-bg-surface, #fff); color: var(--rs-text-primary, #1e293b); border-radius: 12px; box-shadow: var(--rs-shadow-sm); min-width: 280px; min-height: 140px; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } :host(:hover) { box-shadow: var(--rs-shadow-md); } .header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; cursor: move; color: white; border-radius: 12px 12px 0 0; background: linear-gradient(135deg, #6366f1, #8b5cf6); } .header-left { display: flex; align-items: center; gap: 8px; } .header-icon { font-size: 18px; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; background: rgba(255, 255, 255, 0.2); border-radius: 6px; } .header-title { font-size: 13px; font-weight: 600; letter-spacing: 0.3px; max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .header-actions { display: flex; gap: 4px; } .header-actions button { background: rgba(255, 255, 255, 0.2); border: none; color: white; width: 24px; height: 24px; border-radius: 4px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; } .header-actions button:hover { background: rgba(255, 255, 255, 0.3); } .body { padding: 12px 14px; } .description { font-size: 13px; line-height: 1.4; color: var(--rs-text-secondary); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 10px; } .platform-row { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; } .platform-chip { font-size: 12px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; background: var(--rs-bg-surface-raised); border-radius: 6px; border: 1px solid var(--rs-border); } .progress-section { margin-bottom: 10px; } .progress-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--rs-text-muted); margin-bottom: 4px; } .progress-bar { height: 6px; background: var(--rs-bg-surface-raised); border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #8b5cf6); border-radius: 3px; transition: width 0.3s ease; } .stats-row { display: flex; gap: 12px; } .stat { display: flex; align-items: center; gap: 4px; font-size: 11px; color: var(--rs-text-muted); } .stat-count { font-weight: 600; color: var(--rs-text-primary); } .stat-dot { width: 6px; height: 6px; border-radius: 50%; } .stat-dot.draft { background: #94a3b8; } .stat-dot.scheduled { background: #3b82f6; } .stat-dot.published { background: #22c55e; } .footer { display: flex; align-items: center; justify-content: space-between; padding: 8px 14px; background: var(--rs-bg-surface-raised); border-top: 1px solid var(--rs-border); border-radius: 0 0 12px 12px; } .duration-label { font-size: 11px; color: var(--rs-text-muted); font-weight: 500; } .open-link { font-size: 11px; color: var(--rs-primary); cursor: pointer; text-decoration: none; font-weight: 500; } .open-link:hover { text-decoration: underline; } `; declare global { interface HTMLElementTagNameMap { "folk-social-campaign": FolkSocialCampaign; } } export class FolkSocialCampaign extends FolkShape { static override tagName = "folk-social-campaign"; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules).map((r) => r.cssText).join("\n"); const childRules = Array.from(styles.cssRules).map((r) => r.cssText).join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #campaignId = ""; #title = "Untitled Campaign"; #description = ""; #platforms: string[] = []; #phases: { name: string; label: string; days: string }[] = []; #postCount = 0; #draftCount = 0; #scheduledCount = 0; #publishedCount = 0; #duration = ""; #spaceSlug = ""; get campaignId() { return this.#campaignId; } set campaignId(v: string) { this.#campaignId = v; this.#dispatchChange(); } get title() { return this.#title; } set title(v: string) { this.#title = v; this.requestUpdate("title"); this.#dispatchChange(); } get description() { return this.#description; } set description(v: string) { this.#description = v; this.requestUpdate("description"); this.#dispatchChange(); } get platforms(): string[] { return this.#platforms; } set platforms(v: string[]) { this.#platforms = v; this.requestUpdate("platforms"); this.#dispatchChange(); } get phases() { return this.#phases; } set phases(v: { name: string; label: string; days: string }[]) { this.#phases = v; this.requestUpdate("phases"); this.#dispatchChange(); } get postCount() { return this.#postCount; } set postCount(v: number) { this.#postCount = v; this.requestUpdate("postCount"); this.#dispatchChange(); } get draftCount() { return this.#draftCount; } set draftCount(v: number) { this.#draftCount = v; this.requestUpdate("draftCount"); this.#dispatchChange(); } get scheduledCount() { return this.#scheduledCount; } set scheduledCount(v: number) { this.#scheduledCount = v; this.requestUpdate("scheduledCount"); this.#dispatchChange(); } get publishedCount() { return this.#publishedCount; } set publishedCount(v: number) { this.#publishedCount = v; this.requestUpdate("publishedCount"); this.#dispatchChange(); } get duration() { return this.#duration; } set duration(v: string) { this.#duration = v; this.requestUpdate("duration"); this.#dispatchChange(); } get spaceSlug() { return this.#spaceSlug; } set spaceSlug(v: string) { this.#spaceSlug = v; this.#dispatchChange(); } #dispatchChange() { this.dispatchEvent(new CustomEvent("content-change", { detail: this.toJSON() })); } override createRenderRoot() { const root = super.createRenderRoot(); const titleAttr = this.getAttribute("title"); if (titleAttr) this.#title = titleAttr; const total = this.#postCount || 1; const publishedPct = Math.round((this.#publishedCount / total) * 100); const escTitle = this.#escapeHtml(this.#title); const escDesc = this.#escapeHtml(this.#description || "No description"); const platformChips = this.#platforms .map((p) => `${PLATFORM_ICONS[p] || p[0]?.toUpperCase() || "?"}`) .join(""); const wrapper = document.createElement("div"); wrapper.style.position = "relative"; wrapper.style.height = "100%"; wrapper.innerHTML = html`
📢 ${escTitle}
${escDesc}
${platformChips || 'No platforms'}
Phase Progress
${this.#draftCount} draft
${this.#scheduledCount} sched
${this.#publishedCount} pub
`; const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) containerDiv.replaceWith(wrapper); const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); const openLink = wrapper.querySelector(".open-link") as HTMLElement; openLink.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("navigate-to-module", { bubbles: true, composed: true, detail: { path: `/${this.#spaceSlug}/rsocials/campaigns?id=${this.#campaignId}` }, })); }); return root; } #escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } static override fromData(data: Record): FolkSocialCampaign { const shape = FolkShape.fromData(data) as FolkSocialCampaign; if (data.campaignId) shape.campaignId = data.campaignId; if (data.title) shape.title = data.title; if (data.description !== undefined) shape.description = data.description; if (Array.isArray(data.platforms)) shape.platforms = data.platforms; if (Array.isArray(data.phases)) shape.phases = data.phases; if (typeof data.postCount === "number") shape.postCount = data.postCount; if (typeof data.draftCount === "number") shape.draftCount = data.draftCount; if (typeof data.scheduledCount === "number") shape.scheduledCount = data.scheduledCount; if (typeof data.publishedCount === "number") shape.publishedCount = data.publishedCount; if (data.duration !== undefined) shape.duration = data.duration; if (data.spaceSlug) shape.spaceSlug = data.spaceSlug; return shape; } override applyData(data: Record): void { super.applyData(data); if (data.campaignId !== undefined && data.campaignId !== this.campaignId) this.campaignId = data.campaignId; if (data.title !== undefined && data.title !== this.title) this.title = data.title; if (data.description !== undefined && data.description !== this.description) this.description = data.description; if (Array.isArray(data.platforms) && JSON.stringify(data.platforms) !== JSON.stringify(this.platforms)) this.platforms = data.platforms; if (Array.isArray(data.phases) && JSON.stringify(data.phases) !== JSON.stringify(this.phases)) this.phases = data.phases; if (typeof data.postCount === "number" && data.postCount !== this.postCount) this.postCount = data.postCount; if (typeof data.draftCount === "number" && data.draftCount !== this.draftCount) this.draftCount = data.draftCount; if (typeof data.scheduledCount === "number" && data.scheduledCount !== this.scheduledCount) this.scheduledCount = data.scheduledCount; if (typeof data.publishedCount === "number" && data.publishedCount !== this.publishedCount) this.publishedCount = data.publishedCount; if (data.duration !== undefined && data.duration !== this.duration) this.duration = data.duration; if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug; } override toJSON() { return { ...super.toJSON(), type: "folk-social-campaign", campaignId: this.#campaignId, title: this.#title, description: this.#description, platforms: this.#platforms, phases: this.#phases, postCount: this.#postCount, draftCount: this.#draftCount, scheduledCount: this.#scheduledCount, publishedCount: this.#publishedCount, duration: this.#duration, spaceSlug: this.#spaceSlug, }; } }