396 lines
12 KiB
TypeScript
396 lines
12 KiB
TypeScript
import { FolkShape } from "./folk-shape";
|
||
import { css, html } from "./tags";
|
||
|
||
const PLATFORM_ICONS: Record<string, string> = {
|
||
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) => `<span class="platform-chip" title="${this.#escapeHtml(p)}">${PLATFORM_ICONS[p] || p[0]?.toUpperCase() || "?"}</span>`)
|
||
.join("");
|
||
|
||
const wrapper = document.createElement("div");
|
||
wrapper.style.position = "relative";
|
||
wrapper.style.height = "100%";
|
||
|
||
wrapper.innerHTML = html`
|
||
<div class="header">
|
||
<div class="header-left">
|
||
<span class="header-icon">📢</span>
|
||
<span class="header-title">${escTitle}</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="close-btn" title="Remove">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="description">${escDesc}</div>
|
||
<div class="platform-row">${platformChips || '<span style="font-size:11px;color:var(--rs-text-muted)">No platforms</span>'}</div>
|
||
<div class="progress-section">
|
||
<div class="progress-label">Phase Progress</div>
|
||
<div class="progress-bar"><div class="progress-fill" style="width: ${publishedPct}%"></div></div>
|
||
</div>
|
||
<div class="stats-row">
|
||
<div class="stat"><span class="stat-dot draft"></span><span class="stat-count">${this.#draftCount}</span> draft</div>
|
||
<div class="stat"><span class="stat-dot scheduled"></span><span class="stat-count">${this.#scheduledCount}</span> sched</div>
|
||
<div class="stat"><span class="stat-dot published"></span><span class="stat-count">${this.#publishedCount}</span> pub</div>
|
||
</div>
|
||
</div>
|
||
<div class="footer">
|
||
<span class="duration-label">${this.#escapeHtml(this.#duration || "No duration set")}</span>
|
||
<span class="open-link">Open Campaign →</span>
|
||
</div>
|
||
`;
|
||
|
||
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<string, any>): 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<string, any>): 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,
|
||
};
|
||
}
|
||
}
|