892 lines
21 KiB
TypeScript
892 lines
21 KiB
TypeScript
import { FolkShape } from "./folk-shape";
|
|
import { css, html } from "./tags";
|
|
|
|
const styles = css`
|
|
:host {
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
min-width: 280px;
|
|
min-height: 140px;
|
|
overflow: hidden;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
}
|
|
|
|
:host(:hover) {
|
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
:host([data-status="posted"]) {
|
|
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
:host([data-status="scheduled"]) {
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
:host([data-status="failed"]) {
|
|
box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.4), 0 2px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 14px;
|
|
cursor: move;
|
|
color: white;
|
|
border-radius: 12px 12px 0 0;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.platform-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;
|
|
}
|
|
|
|
.platform-name {
|
|
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;
|
|
}
|
|
|
|
.post-type {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
padding: 2px 8px;
|
|
border-radius: 10px;
|
|
background: #f1f5f9;
|
|
color: #64748b;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.content-preview {
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
color: #1e293b;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 3;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.media-preview {
|
|
width: 100%;
|
|
height: 100px;
|
|
border-radius: 8px;
|
|
background: #f1f5f9;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-bottom: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.media-preview img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.media-placeholder {
|
|
color: #94a3b8;
|
|
font-size: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.media-placeholder .icon {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.hashtags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 4px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.hashtag {
|
|
font-size: 11px;
|
|
color: #3b82f6;
|
|
background: #eff6ff;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 8px 14px;
|
|
background: #f8fafc;
|
|
border-top: 1px solid #e2e8f0;
|
|
border-radius: 0 0 12px 12px;
|
|
}
|
|
|
|
.schedule {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 11px;
|
|
color: #64748b;
|
|
}
|
|
|
|
.schedule-icon {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.schedule-time {
|
|
font-weight: 500;
|
|
color: #475569;
|
|
}
|
|
|
|
.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: #f1f5f9;
|
|
color: #64748b;
|
|
}
|
|
|
|
.status-badge.scheduled {
|
|
background: #dbeafe;
|
|
color: #2563eb;
|
|
}
|
|
|
|
.status-badge.posted {
|
|
background: #dcfce7;
|
|
color: #16a34a;
|
|
}
|
|
|
|
.status-badge.failed {
|
|
background: #fee2e2;
|
|
color: #dc2626;
|
|
}
|
|
|
|
.step-number {
|
|
position: absolute;
|
|
top: -8px;
|
|
left: -8px;
|
|
width: 24px;
|
|
height: 24px;
|
|
border-radius: 50%;
|
|
background: #1e293b;
|
|
color: white;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1;
|
|
border: 2px solid white;
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
}
|
|
|
|
.edit-overlay {
|
|
display: none;
|
|
position: absolute;
|
|
inset: 0;
|
|
background: white;
|
|
z-index: 10;
|
|
border-radius: 12px;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.edit-overlay.active {
|
|
display: flex;
|
|
}
|
|
|
|
.edit-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.edit-header span {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.edit-body {
|
|
flex: 1;
|
|
padding: 12px 14px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.edit-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.edit-field label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.edit-field input,
|
|
.edit-field textarea,
|
|
.edit-field select {
|
|
padding: 8px 10px;
|
|
border: 1px solid #e2e8f0;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
outline: none;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.edit-field input:focus,
|
|
.edit-field textarea:focus,
|
|
.edit-field select:focus {
|
|
border-color: #3b82f6;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
}
|
|
|
|
.edit-field textarea {
|
|
min-height: 80px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.edit-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
border-top: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.edit-actions button {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.edit-actions .save-btn {
|
|
background: #3b82f6;
|
|
color: white;
|
|
border-color: #3b82f6;
|
|
}
|
|
|
|
.edit-actions .save-btn:hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
.edit-actions .cancel-btn {
|
|
background: white;
|
|
color: #64748b;
|
|
}
|
|
|
|
.edit-actions .cancel-btn:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
`;
|
|
|
|
export type SocialPlatform =
|
|
| "x"
|
|
| "linkedin"
|
|
| "instagram"
|
|
| "youtube"
|
|
| "threads"
|
|
| "bluesky"
|
|
| "tiktok"
|
|
| "facebook";
|
|
|
|
export type PostStatus = "draft" | "scheduled" | "posted" | "failed";
|
|
|
|
export type PostType =
|
|
| "text"
|
|
| "image"
|
|
| "video"
|
|
| "carousel"
|
|
| "story"
|
|
| "reel"
|
|
| "thread"
|
|
| "article"
|
|
| "short";
|
|
|
|
const PLATFORM_CONFIG: Record<
|
|
SocialPlatform,
|
|
{ icon: string; label: string; color: string }
|
|
> = {
|
|
x: { icon: "\ud835\udd4f", label: "X", color: "#000000" },
|
|
linkedin: { icon: "in", label: "LinkedIn", color: "#0A66C2" },
|
|
instagram: { icon: "\ud83d\udcf7", label: "Instagram", color: "#E4405F" },
|
|
youtube: { icon: "\u25b6", label: "YouTube", color: "#FF0000" },
|
|
threads: { icon: "@", label: "Threads", color: "#000000" },
|
|
bluesky: { icon: "\ud83e\ude77", label: "Bluesky", color: "#0085FF" },
|
|
tiktok: { icon: "\u266b", label: "TikTok", color: "#010101" },
|
|
facebook: { icon: "f", label: "Facebook", color: "#1877F2" },
|
|
};
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"folk-social-post": FolkSocialPost;
|
|
}
|
|
}
|
|
|
|
export class FolkSocialPost extends FolkShape {
|
|
static override tagName = "folk-social-post";
|
|
|
|
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;
|
|
}
|
|
|
|
#platform: SocialPlatform = "x";
|
|
#postType: PostType = "text";
|
|
#content = "";
|
|
#mediaUrl = "";
|
|
#mediaType = ""; // "image", "video", "carousel"
|
|
#scheduledAt = "";
|
|
#status: PostStatus = "draft";
|
|
#hashtags: string[] = [];
|
|
#stepNumber = 0;
|
|
#isEditing = false;
|
|
|
|
// DOM references
|
|
#contentPreviewEl: HTMLElement | null = null;
|
|
#statusBadgeEl: HTMLElement | null = null;
|
|
#scheduleTimeEl: HTMLElement | null = null;
|
|
#editOverlay: HTMLElement | null = null;
|
|
#mediaPreviewEl: HTMLElement | null = null;
|
|
#hashtagsEl: HTMLElement | null = null;
|
|
#postTypeEl: HTMLElement | null = null;
|
|
#stepNumberEl: HTMLElement | null = null;
|
|
|
|
get platform() {
|
|
return this.#platform;
|
|
}
|
|
set platform(value: SocialPlatform) {
|
|
this.#platform = value;
|
|
this.requestUpdate("platform");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get postType() {
|
|
return this.#postType;
|
|
}
|
|
set postType(value: PostType) {
|
|
this.#postType = value;
|
|
if (this.#postTypeEl) this.#postTypeEl.textContent = value;
|
|
this.requestUpdate("postType");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get content() {
|
|
return this.#content;
|
|
}
|
|
set content(value: string) {
|
|
this.#content = value;
|
|
if (this.#contentPreviewEl) this.#contentPreviewEl.textContent = value;
|
|
this.requestUpdate("content");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get mediaUrl() {
|
|
return this.#mediaUrl;
|
|
}
|
|
set mediaUrl(value: string) {
|
|
this.#mediaUrl = value;
|
|
this.#renderMedia();
|
|
this.requestUpdate("mediaUrl");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get mediaType() {
|
|
return this.#mediaType;
|
|
}
|
|
set mediaType(value: string) {
|
|
this.#mediaType = value;
|
|
this.#renderMedia();
|
|
this.requestUpdate("mediaType");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get scheduledAt() {
|
|
return this.#scheduledAt;
|
|
}
|
|
set scheduledAt(value: string) {
|
|
this.#scheduledAt = value;
|
|
if (this.#scheduleTimeEl)
|
|
this.#scheduleTimeEl.textContent = this.#formatSchedule(value);
|
|
this.requestUpdate("scheduledAt");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get status(): PostStatus {
|
|
return this.#status;
|
|
}
|
|
set status(value: PostStatus) {
|
|
this.#status = value;
|
|
this.setAttribute("data-status", value);
|
|
if (this.#statusBadgeEl) {
|
|
this.#statusBadgeEl.className = `status-badge ${value}`;
|
|
this.#statusBadgeEl.textContent = value;
|
|
}
|
|
this.requestUpdate("status");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get hashtags(): string[] {
|
|
return this.#hashtags;
|
|
}
|
|
set hashtags(value: string[]) {
|
|
this.#hashtags = value;
|
|
this.#renderHashtags();
|
|
this.requestUpdate("hashtags");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
get stepNumber(): number {
|
|
return this.#stepNumber;
|
|
}
|
|
set stepNumber(value: number) {
|
|
this.#stepNumber = value;
|
|
if (this.#stepNumberEl) {
|
|
this.#stepNumberEl.textContent = String(value);
|
|
this.#stepNumberEl.style.display = value > 0 ? "flex" : "none";
|
|
}
|
|
this.requestUpdate("stepNumber");
|
|
this.#dispatchChange();
|
|
}
|
|
|
|
#dispatchChange() {
|
|
this.dispatchEvent(
|
|
new CustomEvent("content-change", {
|
|
detail: this.toJSON(),
|
|
}),
|
|
);
|
|
}
|
|
|
|
override createRenderRoot() {
|
|
const root = super.createRenderRoot();
|
|
|
|
// Parse attributes
|
|
const platformAttr = this.getAttribute("platform") as SocialPlatform;
|
|
if (platformAttr && platformAttr in PLATFORM_CONFIG)
|
|
this.#platform = platformAttr;
|
|
const postTypeAttr = this.getAttribute("post-type") as PostType;
|
|
if (postTypeAttr) this.#postType = postTypeAttr;
|
|
const contentAttr = this.getAttribute("content");
|
|
if (contentAttr) this.#content = contentAttr;
|
|
const statusAttr = this.getAttribute("status") as PostStatus;
|
|
if (statusAttr) this.#status = statusAttr;
|
|
const stepAttr = this.getAttribute("step");
|
|
if (stepAttr) this.#stepNumber = parseInt(stepAttr, 10);
|
|
|
|
const config = PLATFORM_CONFIG[this.#platform];
|
|
|
|
const wrapper = document.createElement("div");
|
|
wrapper.style.position = "relative";
|
|
wrapper.style.height = "100%";
|
|
|
|
wrapper.innerHTML = html`
|
|
<span
|
|
class="step-number"
|
|
style="display: ${this.#stepNumber > 0 ? "flex" : "none"}"
|
|
>${this.#stepNumber}</span
|
|
>
|
|
<div class="header" style="background: ${config.color}">
|
|
<div class="header-left">
|
|
<span class="platform-icon">${config.icon}</span>
|
|
<span class="platform-name">${config.label}</span>
|
|
</div>
|
|
<div class="header-actions">
|
|
<button class="edit-btn" title="Edit">\u270F</button>
|
|
<button class="close-btn" title="Remove">\u00D7</button>
|
|
</div>
|
|
</div>
|
|
<div class="body">
|
|
<span class="post-type">${this.#escapeHtml(this.#postType)}</span>
|
|
<div class="content-preview">
|
|
${this.#escapeHtml(this.#content) || "No content yet..."}
|
|
</div>
|
|
<div class="media-preview">
|
|
<div class="media-placeholder">
|
|
<span class="icon">\ud83d\uddbc</span>
|
|
<span>No media</span>
|
|
</div>
|
|
</div>
|
|
<div class="hashtags"></div>
|
|
</div>
|
|
<div class="footer">
|
|
<div class="schedule">
|
|
<span class="schedule-icon">\ud83d\udcc5</span>
|
|
<span class="schedule-time"
|
|
>${this.#formatSchedule(this.#scheduledAt)}</span
|
|
>
|
|
</div>
|
|
<span class="status-badge ${this.#status}">${this.#status}</span>
|
|
</div>
|
|
<div class="edit-overlay">
|
|
<div class="edit-header">
|
|
<span>Edit Post</span>
|
|
</div>
|
|
<div class="edit-body">
|
|
<div class="edit-field">
|
|
<label>Platform</label>
|
|
<select class="edit-platform">
|
|
<option value="x">X (Twitter)</option>
|
|
<option value="linkedin">LinkedIn</option>
|
|
<option value="instagram">Instagram</option>
|
|
<option value="youtube">YouTube</option>
|
|
<option value="threads">Threads</option>
|
|
<option value="bluesky">Bluesky</option>
|
|
<option value="tiktok">TikTok</option>
|
|
<option value="facebook">Facebook</option>
|
|
</select>
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Post Type</label>
|
|
<select class="edit-post-type">
|
|
<option value="text">Text</option>
|
|
<option value="image">Image</option>
|
|
<option value="video">Video</option>
|
|
<option value="carousel">Carousel</option>
|
|
<option value="story">Story</option>
|
|
<option value="reel">Reel</option>
|
|
<option value="thread">Thread</option>
|
|
<option value="article">Article</option>
|
|
<option value="short">Short</option>
|
|
</select>
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Content</label>
|
|
<textarea
|
|
class="edit-content"
|
|
placeholder="Write your post..."
|
|
></textarea>
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Media URL</label>
|
|
<input
|
|
type="text"
|
|
class="edit-media-url"
|
|
placeholder="https://..."
|
|
/>
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Scheduled At</label>
|
|
<input type="datetime-local" class="edit-scheduled" />
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Hashtags (comma-separated)</label>
|
|
<input
|
|
type="text"
|
|
class="edit-hashtags"
|
|
placeholder="#launch, #product"
|
|
/>
|
|
</div>
|
|
<div class="edit-field">
|
|
<label>Status</label>
|
|
<select class="edit-status">
|
|
<option value="draft">Draft</option>
|
|
<option value="scheduled">Scheduled</option>
|
|
<option value="posted">Posted</option>
|
|
<option value="failed">Failed</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="edit-actions">
|
|
<button class="cancel-btn">Cancel</button>
|
|
<button class="save-btn">Save</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Replace the container div (slot's parent) with our wrapper
|
|
const slot = root.querySelector("slot");
|
|
const containerDiv = slot?.parentElement as HTMLElement;
|
|
if (containerDiv) {
|
|
containerDiv.replaceWith(wrapper);
|
|
}
|
|
|
|
// Cache DOM refs
|
|
this.#contentPreviewEl = wrapper.querySelector(".content-preview");
|
|
this.#statusBadgeEl = wrapper.querySelector(".status-badge");
|
|
this.#scheduleTimeEl = wrapper.querySelector(".schedule-time");
|
|
this.#editOverlay = wrapper.querySelector(".edit-overlay");
|
|
this.#mediaPreviewEl = wrapper.querySelector(".media-preview");
|
|
this.#hashtagsEl = wrapper.querySelector(".hashtags");
|
|
this.#postTypeEl = wrapper.querySelector(".post-type");
|
|
this.#stepNumberEl = wrapper.querySelector(".step-number");
|
|
|
|
// Set initial attribute state
|
|
this.setAttribute("data-status", this.#status);
|
|
|
|
// Edit button
|
|
const editBtn = wrapper.querySelector(".edit-btn") as HTMLButtonElement;
|
|
editBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.#openEditor();
|
|
});
|
|
|
|
// Close button
|
|
const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement;
|
|
closeBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.dispatchEvent(new CustomEvent("close"));
|
|
});
|
|
|
|
// Save button
|
|
const saveBtn = wrapper.querySelector(".save-btn") as HTMLButtonElement;
|
|
saveBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.#saveEdit(wrapper);
|
|
});
|
|
|
|
// Cancel button
|
|
const cancelBtn = wrapper.querySelector(
|
|
".cancel-btn",
|
|
) as HTMLButtonElement;
|
|
cancelBtn.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
this.#closeEditor();
|
|
});
|
|
|
|
// Render media and hashtags
|
|
this.#renderMedia();
|
|
this.#renderHashtags();
|
|
|
|
return root;
|
|
}
|
|
|
|
#openEditor() {
|
|
if (!this.#editOverlay) return;
|
|
this.#isEditing = true;
|
|
this.#editOverlay.classList.add("active");
|
|
|
|
const root = this.#editOverlay.closest("div") as HTMLElement;
|
|
const platformSel = root.querySelector(
|
|
".edit-platform",
|
|
) as HTMLSelectElement;
|
|
const postTypeSel = root.querySelector(
|
|
".edit-post-type",
|
|
) as HTMLSelectElement;
|
|
const contentArea = root.querySelector(
|
|
".edit-content",
|
|
) as HTMLTextAreaElement;
|
|
const mediaInput = root.querySelector(
|
|
".edit-media-url",
|
|
) as HTMLInputElement;
|
|
const scheduledInput = root.querySelector(
|
|
".edit-scheduled",
|
|
) as HTMLInputElement;
|
|
const hashtagsInput = root.querySelector(
|
|
".edit-hashtags",
|
|
) as HTMLInputElement;
|
|
const statusSel = root.querySelector(
|
|
".edit-status",
|
|
) as HTMLSelectElement;
|
|
|
|
if (platformSel) platformSel.value = this.#platform;
|
|
if (postTypeSel) postTypeSel.value = this.#postType;
|
|
if (contentArea) contentArea.value = this.#content;
|
|
if (mediaInput) mediaInput.value = this.#mediaUrl;
|
|
if (scheduledInput) scheduledInput.value = this.#scheduledAt;
|
|
if (hashtagsInput) hashtagsInput.value = this.#hashtags.join(", ");
|
|
if (statusSel) statusSel.value = this.#status;
|
|
}
|
|
|
|
#closeEditor() {
|
|
if (!this.#editOverlay) return;
|
|
this.#isEditing = false;
|
|
this.#editOverlay.classList.remove("active");
|
|
}
|
|
|
|
#saveEdit(wrapper: HTMLElement) {
|
|
const platformSel = wrapper.querySelector(
|
|
".edit-platform",
|
|
) as HTMLSelectElement;
|
|
const postTypeSel = wrapper.querySelector(
|
|
".edit-post-type",
|
|
) as HTMLSelectElement;
|
|
const contentArea = wrapper.querySelector(
|
|
".edit-content",
|
|
) as HTMLTextAreaElement;
|
|
const mediaInput = wrapper.querySelector(
|
|
".edit-media-url",
|
|
) as HTMLInputElement;
|
|
const scheduledInput = wrapper.querySelector(
|
|
".edit-scheduled",
|
|
) as HTMLInputElement;
|
|
const hashtagsInput = wrapper.querySelector(
|
|
".edit-hashtags",
|
|
) as HTMLInputElement;
|
|
const statusSel = wrapper.querySelector(
|
|
".edit-status",
|
|
) as HTMLSelectElement;
|
|
|
|
if (platformSel)
|
|
this.#platform = platformSel.value as SocialPlatform;
|
|
if (postTypeSel) this.postType = postTypeSel.value as PostType;
|
|
if (contentArea) this.content = contentArea.value;
|
|
if (mediaInput) this.mediaUrl = mediaInput.value;
|
|
if (scheduledInput) this.scheduledAt = scheduledInput.value;
|
|
if (hashtagsInput)
|
|
this.hashtags = hashtagsInput.value
|
|
.split(",")
|
|
.map((t) => t.trim())
|
|
.filter(Boolean);
|
|
if (statusSel) this.status = statusSel.value as PostStatus;
|
|
|
|
// Update header color for new platform
|
|
const header = wrapper.querySelector(".header") as HTMLElement;
|
|
const config = PLATFORM_CONFIG[this.#platform];
|
|
if (header && config) {
|
|
header.style.background = config.color;
|
|
const iconEl = header.querySelector(".platform-icon");
|
|
const nameEl = header.querySelector(".platform-name");
|
|
if (iconEl) iconEl.textContent = config.icon;
|
|
if (nameEl) nameEl.textContent = config.label;
|
|
}
|
|
|
|
this.#closeEditor();
|
|
}
|
|
|
|
#renderMedia() {
|
|
if (!this.#mediaPreviewEl) return;
|
|
|
|
if (this.#mediaUrl) {
|
|
this.#mediaPreviewEl.innerHTML = `<img src="${this.#escapeHtml(this.#mediaUrl)}" alt="Post media" onerror="this.parentElement.innerHTML='<div class=\\'media-placeholder\\'><span class=\\'icon\\'>⚠️</span><span>Failed to load</span></div>'" />`;
|
|
} else {
|
|
const mediaIcons: Record<string, string> = {
|
|
image: "\ud83d\uddbc",
|
|
video: "\ud83c\udfac",
|
|
carousel: "\ud83d\udcf8",
|
|
reel: "\ud83c\udfac",
|
|
short: "\ud83c\udfac",
|
|
story: "\ud83d\udcf1",
|
|
};
|
|
const icon = mediaIcons[this.#postType] || "\ud83d\uddbc";
|
|
const label =
|
|
this.#postType === "text" ? "No media" : `${this.#postType} media`;
|
|
this.#mediaPreviewEl.innerHTML = `<div class="media-placeholder"><span class="icon">${icon}</span><span>${label}</span></div>`;
|
|
}
|
|
}
|
|
|
|
#renderHashtags() {
|
|
if (!this.#hashtagsEl) return;
|
|
|
|
if (this.#hashtags.length === 0) {
|
|
this.#hashtagsEl.innerHTML = "";
|
|
return;
|
|
}
|
|
|
|
this.#hashtagsEl.innerHTML = this.#hashtags
|
|
.map((tag) => {
|
|
const t = tag.startsWith("#") ? tag : `#${tag}`;
|
|
return `<span class="hashtag">${this.#escapeHtml(t)}</span>`;
|
|
})
|
|
.join("");
|
|
}
|
|
|
|
#formatSchedule(dateStr: string): string {
|
|
if (!dateStr) return "Not scheduled";
|
|
try {
|
|
const date = new Date(dateStr);
|
|
if (isNaN(date.getTime())) return dateStr;
|
|
const opts: Intl.DateTimeFormatOptions = {
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
};
|
|
return date.toLocaleDateString("en-US", opts);
|
|
} catch {
|
|
return dateStr;
|
|
}
|
|
}
|
|
|
|
#escapeHtml(text: string): string {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
override toJSON() {
|
|
return {
|
|
...super.toJSON(),
|
|
type: "folk-social-post",
|
|
platform: this.#platform,
|
|
postType: this.#postType,
|
|
content: this.#content,
|
|
mediaUrl: this.#mediaUrl,
|
|
mediaType: this.#mediaType,
|
|
scheduledAt: this.#scheduledAt,
|
|
status: this.#status,
|
|
hashtags: this.#hashtags,
|
|
stepNumber: this.#stepNumber,
|
|
};
|
|
}
|
|
}
|