355 lines
9.7 KiB
TypeScript
355 lines
9.7 KiB
TypeScript
import { FolkShape } from "./folk-shape";
|
||
import { css, html } from "./tags";
|
||
|
||
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: 260px;
|
||
min-height: 120px;
|
||
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, #7c3aed, #a855f7);
|
||
}
|
||
|
||
.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-label {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.3px;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.subject-line {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: var(--rs-text-primary);
|
||
margin-bottom: 8px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.list-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.list-name {
|
||
font-size: 12px;
|
||
color: var(--rs-text-secondary);
|
||
}
|
||
|
||
.subscriber-badge {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
background: rgba(124, 58, 237, 0.1);
|
||
color: #7c3aed;
|
||
}
|
||
|
||
.body-preview {
|
||
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;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.status-badge {
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
padding: 3px 8px;
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.status-badge.draft {
|
||
background: var(--rs-bg-surface-raised);
|
||
color: var(--rs-text-muted);
|
||
}
|
||
|
||
.status-badge.scheduled {
|
||
background: rgba(59, 130, 246, 0.15);
|
||
color: #2563eb;
|
||
}
|
||
|
||
.status-badge.sent {
|
||
background: rgba(34, 197, 94, 0.15);
|
||
color: var(--rs-success);
|
||
}
|
||
|
||
.schedule-info {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
color: var(--rs-text-muted);
|
||
}
|
||
`;
|
||
|
||
declare global {
|
||
interface HTMLElementTagNameMap {
|
||
"folk-social-newsletter": FolkSocialNewsletter;
|
||
}
|
||
}
|
||
|
||
export class FolkSocialNewsletter extends FolkShape {
|
||
static override tagName = "folk-social-newsletter";
|
||
|
||
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;
|
||
}
|
||
|
||
#newsletterId = "";
|
||
#subject = "Untitled Newsletter";
|
||
#listName = "";
|
||
#subscriberCount = 0;
|
||
#status: "draft" | "scheduled" | "sent" = "draft";
|
||
#scheduledAt = "";
|
||
#bodyPreview = "";
|
||
#spaceSlug = "";
|
||
|
||
#subjectEl: HTMLElement | null = null;
|
||
#statusBadgeEl: HTMLElement | null = null;
|
||
#scheduleInfoEl: HTMLElement | null = null;
|
||
|
||
get newsletterId() { return this.#newsletterId; }
|
||
set newsletterId(v: string) { this.#newsletterId = v; this.#dispatchChange(); }
|
||
|
||
get subject() { return this.#subject; }
|
||
set subject(v: string) {
|
||
this.#subject = v;
|
||
if (this.#subjectEl) this.#subjectEl.textContent = v;
|
||
this.requestUpdate("subject");
|
||
this.#dispatchChange();
|
||
}
|
||
|
||
get listName() { return this.#listName; }
|
||
set listName(v: string) { this.#listName = v; this.requestUpdate("listName"); this.#dispatchChange(); }
|
||
|
||
get subscriberCount() { return this.#subscriberCount; }
|
||
set subscriberCount(v: number) { this.#subscriberCount = v; this.requestUpdate("subscriberCount"); this.#dispatchChange(); }
|
||
|
||
get status() { return this.#status; }
|
||
set status(v: "draft" | "scheduled" | "sent") {
|
||
this.#status = v;
|
||
if (this.#statusBadgeEl) {
|
||
this.#statusBadgeEl.className = `status-badge ${v}`;
|
||
this.#statusBadgeEl.textContent = v;
|
||
}
|
||
this.requestUpdate("status");
|
||
this.#dispatchChange();
|
||
}
|
||
|
||
get scheduledAt() { return this.#scheduledAt; }
|
||
set scheduledAt(v: string) {
|
||
this.#scheduledAt = v;
|
||
if (this.#scheduleInfoEl) this.#scheduleInfoEl.textContent = this.#formatSchedule(v);
|
||
this.requestUpdate("scheduledAt");
|
||
this.#dispatchChange();
|
||
}
|
||
|
||
get bodyPreview() { return this.#bodyPreview; }
|
||
set bodyPreview(v: string) { this.#bodyPreview = v; this.requestUpdate("bodyPreview"); 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 subjectAttr = this.getAttribute("subject");
|
||
if (subjectAttr) this.#subject = subjectAttr;
|
||
const statusAttr = this.getAttribute("status") as any;
|
||
if (statusAttr) this.#status = statusAttr;
|
||
|
||
const escSubject = this.#escapeHtml(this.#subject);
|
||
const escListName = this.#escapeHtml(this.#listName || "No list");
|
||
const escPreview = this.#escapeHtml(this.#bodyPreview || "No content preview");
|
||
|
||
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-label">Newsletter</span>
|
||
</div>
|
||
<div class="header-actions">
|
||
<button class="close-btn" title="Remove">×</button>
|
||
</div>
|
||
</div>
|
||
<div class="body">
|
||
<div class="subject-line">${escSubject}</div>
|
||
<div class="list-row">
|
||
<span class="list-name">${escListName}</span>
|
||
<span class="subscriber-badge">${this.#subscriberCount} subscribers</span>
|
||
</div>
|
||
<div class="body-preview">${escPreview}</div>
|
||
</div>
|
||
<div class="footer">
|
||
<span class="status-badge ${this.#status}">${this.#status}</span>
|
||
<span class="schedule-info">${this.#formatSchedule(this.#scheduledAt)}</span>
|
||
</div>
|
||
`;
|
||
|
||
const slot = root.querySelector("slot");
|
||
const containerDiv = slot?.parentElement as HTMLElement;
|
||
if (containerDiv) containerDiv.replaceWith(wrapper);
|
||
|
||
this.#subjectEl = wrapper.querySelector(".subject-line");
|
||
this.#statusBadgeEl = wrapper.querySelector(".status-badge");
|
||
this.#scheduleInfoEl = wrapper.querySelector(".schedule-info");
|
||
|
||
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
||
closeBtn.addEventListener("click", (e) => {
|
||
e.stopPropagation();
|
||
this.dispatchEvent(new CustomEvent("close"));
|
||
});
|
||
|
||
return root;
|
||
}
|
||
|
||
#formatSchedule(dateStr: string): string {
|
||
if (!dateStr) return "Not scheduled";
|
||
try {
|
||
const date = new Date(dateStr);
|
||
if (isNaN(date.getTime())) return dateStr;
|
||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" });
|
||
} catch {
|
||
return dateStr;
|
||
}
|
||
}
|
||
|
||
#escapeHtml(text: string): string {
|
||
const div = document.createElement("div");
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
static override fromData(data: Record<string, any>): FolkSocialNewsletter {
|
||
const shape = FolkShape.fromData(data) as FolkSocialNewsletter;
|
||
if (data.newsletterId) shape.newsletterId = data.newsletterId;
|
||
if (data.subject) shape.subject = data.subject;
|
||
if (data.listName !== undefined) shape.listName = data.listName;
|
||
if (typeof data.subscriberCount === "number") shape.subscriberCount = data.subscriberCount;
|
||
if (data.status) shape.status = data.status;
|
||
if (data.scheduledAt !== undefined) shape.scheduledAt = data.scheduledAt;
|
||
if (data.bodyPreview !== undefined) shape.bodyPreview = data.bodyPreview;
|
||
if (data.spaceSlug) shape.spaceSlug = data.spaceSlug;
|
||
return shape;
|
||
}
|
||
|
||
override applyData(data: Record<string, any>): void {
|
||
super.applyData(data);
|
||
if (data.newsletterId !== undefined && data.newsletterId !== this.newsletterId) this.newsletterId = data.newsletterId;
|
||
if (data.subject !== undefined && data.subject !== this.subject) this.subject = data.subject;
|
||
if (data.listName !== undefined && data.listName !== this.listName) this.listName = data.listName;
|
||
if (typeof data.subscriberCount === "number" && data.subscriberCount !== this.subscriberCount) this.subscriberCount = data.subscriberCount;
|
||
if (data.status !== undefined && data.status !== this.status) this.status = data.status;
|
||
if (data.scheduledAt !== undefined && data.scheduledAt !== this.scheduledAt) this.scheduledAt = data.scheduledAt;
|
||
if (data.bodyPreview !== undefined && data.bodyPreview !== this.bodyPreview) this.bodyPreview = data.bodyPreview;
|
||
if (data.spaceSlug !== undefined && data.spaceSlug !== this.spaceSlug) this.spaceSlug = data.spaceSlug;
|
||
}
|
||
|
||
override toJSON() {
|
||
return {
|
||
...super.toJSON(),
|
||
type: "folk-social-newsletter",
|
||
newsletterId: this.#newsletterId,
|
||
subject: this.#subject,
|
||
listName: this.#listName,
|
||
subscriberCount: this.#subscriberCount,
|
||
status: this.#status,
|
||
scheduledAt: this.#scheduledAt,
|
||
bodyPreview: this.#bodyPreview,
|
||
spaceSlug: this.#spaceSlug,
|
||
};
|
||
}
|
||
}
|