feat: add rChats.online to ecosystem links and EncryptID allowed origins
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ea8f1b3f95
commit
a3572f7a5f
|
|
@ -625,6 +625,19 @@ export class CommunitySync extends EventTarget {
|
||||||
if (data.criteria !== undefined) spider.criteria = data.criteria;
|
if (data.criteria !== undefined) spider.criteria = data.criteria;
|
||||||
if (data.scores !== undefined) spider.scores = data.scores;
|
if (data.scores !== undefined) spider.scores = data.scores;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update social-post properties
|
||||||
|
if (data.type === "folk-social-post") {
|
||||||
|
const post = shape as any;
|
||||||
|
if (data.platform !== undefined && post.platform !== data.platform) post.platform = data.platform;
|
||||||
|
if (data.postType !== undefined && post.postType !== data.postType) post.postType = data.postType;
|
||||||
|
if (data.mediaUrl !== undefined && post.mediaUrl !== data.mediaUrl) post.mediaUrl = data.mediaUrl;
|
||||||
|
if (data.mediaType !== undefined && post.mediaType !== data.mediaType) post.mediaType = data.mediaType;
|
||||||
|
if (data.scheduledAt !== undefined && post.scheduledAt !== data.scheduledAt) post.scheduledAt = data.scheduledAt;
|
||||||
|
if (data.status !== undefined && post.status !== data.status) post.status = data.status;
|
||||||
|
if (data.hashtags !== undefined) post.hashtags = data.hashtags;
|
||||||
|
if (data.stepNumber !== undefined && post.stepNumber !== data.stepNumber) post.stepNumber = data.stepNumber;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,891 @@
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -55,6 +55,9 @@ export * from "./folk-booking";
|
||||||
export * from "./folk-token-mint";
|
export * from "./folk-token-mint";
|
||||||
export * from "./folk-token-ledger";
|
export * from "./folk-token-ledger";
|
||||||
|
|
||||||
|
// Social Media / Campaign Shapes
|
||||||
|
export * from "./folk-social-post";
|
||||||
|
|
||||||
// Decision/Choice Shapes
|
// Decision/Choice Shapes
|
||||||
export * from "./folk-choice-vote";
|
export * from "./folk-choice-vote";
|
||||||
export * from "./folk-choice-rank";
|
export * from "./folk-choice-rank";
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
removeMember,
|
removeMember,
|
||||||
} from "./community-store";
|
} from "./community-store";
|
||||||
import { ensureDemoCommunity } from "./seed-demo";
|
import { ensureDemoCommunity } from "./seed-demo";
|
||||||
|
import { ensureCampaignDemo } from "./seed-campaign";
|
||||||
import type { SpaceVisibility } from "./community-store";
|
import type { SpaceVisibility } from "./community-store";
|
||||||
import {
|
import {
|
||||||
verifyEncryptIDToken,
|
verifyEncryptIDToken,
|
||||||
|
|
@ -553,6 +554,39 @@ async function handleAPI(req: Request, url: URL): Promise<Response> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST /api/communities/campaign-demo/reset - Reset campaign demo
|
||||||
|
if (url.pathname === "/api/communities/campaign-demo/reset" && req.method === "POST") {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastDemoReset < DEMO_RESET_COOLDOWN) {
|
||||||
|
const remaining = Math.ceil((DEMO_RESET_COOLDOWN - (now - lastDemoReset)) / 1000);
|
||||||
|
return Response.json(
|
||||||
|
{ error: `Reset on cooldown. Try again in ${remaining}s` },
|
||||||
|
{ status: 429, headers: corsHeaders }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
lastDemoReset = now;
|
||||||
|
await loadCommunity("campaign-demo");
|
||||||
|
clearShapes("campaign-demo");
|
||||||
|
await ensureCampaignDemo();
|
||||||
|
|
||||||
|
broadcastAutomergeSync("campaign-demo");
|
||||||
|
broadcastJsonSnapshot("campaign-demo");
|
||||||
|
|
||||||
|
return Response.json(
|
||||||
|
{ ok: true, message: "Campaign demo reset to seed data" },
|
||||||
|
{ headers: corsHeaders }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to reset campaign demo:", e);
|
||||||
|
return Response.json(
|
||||||
|
{ error: "Failed to reset campaign demo" },
|
||||||
|
{ status: 500, headers: corsHeaders }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GET /api/communities/:slug - Get community info (respects visibility)
|
// GET /api/communities/:slug - Get community info (respects visibility)
|
||||||
if (url.pathname.startsWith("/api/communities/") && req.method === "GET") {
|
if (url.pathname.startsWith("/api/communities/") && req.method === "GET") {
|
||||||
const slug = url.pathname.split("/")[3];
|
const slug = url.pathname.split("/")[3];
|
||||||
|
|
@ -717,11 +751,17 @@ async function handleAPI(req: Request, url: URL): Promise<Response> {
|
||||||
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure demo community exists on startup
|
// Ensure demo communities exist on startup
|
||||||
ensureDemoCommunity().then(() => {
|
ensureDemoCommunity().then(() => {
|
||||||
console.log("[Demo] Demo community ready");
|
console.log("[Demo] Demo community ready");
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.error("[Demo] Failed to initialize demo community:", e);
|
console.error("[Demo] Failed to initialize demo community:", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ensureCampaignDemo().then(() => {
|
||||||
|
console.log("[Campaign] Campaign demo community ready");
|
||||||
|
}).catch((e) => {
|
||||||
|
console.error("[Campaign] Failed to initialize campaign demo:", e);
|
||||||
|
});
|
||||||
|
|
||||||
console.log(`rSpace server running on http://localhost:${PORT}`);
|
console.log(`rSpace server running on http://localhost:${PORT}`);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,495 @@
|
||||||
|
/**
|
||||||
|
* Social Media Campaign seed — "MycoFi Earth Launch" campaign
|
||||||
|
*
|
||||||
|
* Demonstrates the folk-social-post component with a realistic
|
||||||
|
* multi-platform product launch campaign connected by folk-arrows
|
||||||
|
* in an n8n-style "this then that" flow.
|
||||||
|
*
|
||||||
|
* Flow layout (left-to-right, with parallel branches):
|
||||||
|
*
|
||||||
|
* [Trigger] → [X Teaser] → [LinkedIn Thought] → [IG Carousel] ──┐
|
||||||
|
* ├→ [YT Video] → [X Launch] → [LinkedIn Ann.] ──┐
|
||||||
|
* │ ├→ [IG Reel] → [Threads] → [Bluesky]
|
||||||
|
* └───────────────────────────────────────────────┘
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
addShapes,
|
||||||
|
communityExists,
|
||||||
|
createCommunity,
|
||||||
|
getDocumentData,
|
||||||
|
loadCommunity,
|
||||||
|
} from "./community-store";
|
||||||
|
|
||||||
|
// ── Layout constants ────────────────────────────────────────────
|
||||||
|
const COL_WIDTH = 320;
|
||||||
|
const COL_GAP = 100;
|
||||||
|
const ROW_HEIGHT = 420;
|
||||||
|
const ROW_GAP = 60;
|
||||||
|
const START_X = 60;
|
||||||
|
const START_Y = 60;
|
||||||
|
const NODE_W = 300;
|
||||||
|
const NODE_H = 380;
|
||||||
|
|
||||||
|
function col(n: number): number {
|
||||||
|
return START_X + n * (COL_WIDTH + COL_GAP);
|
||||||
|
}
|
||||||
|
function row(n: number): number {
|
||||||
|
return START_Y + n * (ROW_HEIGHT + ROW_GAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Campaign seed data ──────────────────────────────────────────
|
||||||
|
|
||||||
|
const CAMPAIGN_SHAPES: Record<string, unknown>[] = [
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// TRIGGER NODE (campaign start)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "campaign-trigger",
|
||||||
|
type: "folk-workflow-block",
|
||||||
|
x: col(0),
|
||||||
|
y: row(0) + 100,
|
||||||
|
width: 220,
|
||||||
|
height: 140,
|
||||||
|
rotation: 0,
|
||||||
|
blockType: "trigger",
|
||||||
|
label: "Campaign Start",
|
||||||
|
inputs: [],
|
||||||
|
outputs: [{ name: "launch", type: "trigger" }],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// PHASE 1: Pre-Launch Hype (Days -3 to -1)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "post-x-teaser",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(1),
|
||||||
|
y: row(0),
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "x",
|
||||||
|
postType: "thread",
|
||||||
|
stepNumber: 1,
|
||||||
|
content:
|
||||||
|
"Something is growing in the mycelium... \ud83c\udf44\n\nFor the past 2 years, we've been building the infrastructure for a regenerative economy.\n\nOn Feb 24, we reveal everything.\n\nA thread on why the old financial system is composting itself \ud83e\uddf5\ud83d\udc47",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "",
|
||||||
|
scheduledAt: "2026-02-21T09:00:00",
|
||||||
|
status: "scheduled",
|
||||||
|
hashtags: ["MycoFi", "RegenFinance", "Web3", "ComingSoon"],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-linkedin-thought",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(2),
|
||||||
|
y: row(0),
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "linkedin",
|
||||||
|
postType: "article",
|
||||||
|
stepNumber: 2,
|
||||||
|
content:
|
||||||
|
"The regenerative finance movement isn't just about returns \u2014 it's about redesigning incentive structures from the ground up.\n\nIn this article, I break down why mycelial network theory offers the best model for decentralized economic coordination.\n\n3 key insights from 2 years of building MycoFi Earth...",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "image",
|
||||||
|
scheduledAt: "2026-02-22T11:00:00",
|
||||||
|
status: "scheduled",
|
||||||
|
hashtags: ["RegenerativeFinance", "DeFi", "SystemsThinking", "Leadership"],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-ig-carousel",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(3),
|
||||||
|
y: row(0),
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "instagram",
|
||||||
|
postType: "carousel",
|
||||||
|
stepNumber: 3,
|
||||||
|
content:
|
||||||
|
"5 Ways Mycelium Networks Mirror the Future of Finance \ud83c\udf0d\ud83c\udf44\n\nSlide 1: The problem with extractive finance\nSlide 2: How mycelium redistributes nutrients\nSlide 3: Token-weighted funding circles\nSlide 4: Community governance that actually works\nSlide 5: Join the launch \u2014 Feb 24",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "carousel",
|
||||||
|
scheduledAt: "2026-02-23T14:00:00",
|
||||||
|
status: "scheduled",
|
||||||
|
hashtags: [
|
||||||
|
"MycoFi",
|
||||||
|
"RegenerativeEconomy",
|
||||||
|
"Infographic",
|
||||||
|
"Web3Education",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// PHASE 2: Launch Day (Day 0)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "post-yt-launch",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(4),
|
||||||
|
y: row(0) - 30,
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "youtube",
|
||||||
|
postType: "video",
|
||||||
|
stepNumber: 4,
|
||||||
|
content:
|
||||||
|
"MycoFi Earth \u2014 Official Launch Video\n\nThe regenerative economy starts here. Watch how mycelial intelligence is reshaping finance, governance, and community coordination.\n\nFeaturing interviews with 12 builders from the ecosystem.\n\n[18:42]",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "video",
|
||||||
|
scheduledAt: "2026-02-24T10:00:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: [
|
||||||
|
"MycoFiLaunch",
|
||||||
|
"RegenerativeFinance",
|
||||||
|
"Documentary",
|
||||||
|
"Web3",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-x-launch",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(5),
|
||||||
|
y: row(0) - 60,
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "x",
|
||||||
|
postType: "thread",
|
||||||
|
stepNumber: 5,
|
||||||
|
content:
|
||||||
|
"\ud83c\udf44 MycoFi Earth is LIVE \ud83c\udf44\n\nAfter 2 years of building, the regenerative finance platform is here.\n\nWhat is it?\n\u2022 Token-weighted funding circles\n\u2022 Mycelial governance (no whales)\n\u2022 Composting mechanism for failed proposals\n\u2022 100% on-chain, 100% community-owned\n\n\ud83d\udc47 Full breakdown thread",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "image",
|
||||||
|
scheduledAt: "2026-02-24T10:15:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: [
|
||||||
|
"MycoFi",
|
||||||
|
"Launch",
|
||||||
|
"RegenFinance",
|
||||||
|
"DeFi",
|
||||||
|
"DAO",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-linkedin-launch",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(6),
|
||||||
|
y: row(0) - 30,
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "linkedin",
|
||||||
|
postType: "text",
|
||||||
|
stepNumber: 6,
|
||||||
|
content:
|
||||||
|
"Today we're launching MycoFi Earth \u2014 a regenerative finance platform modeled on mycelial networks.\n\nWhy it matters for the future of organizational design:\n\n1. Composting mechanism: Failed proposals return resources to the network\n2. Nutrient routing: Funds flow to where they're needed most\n3. No single point of failure: True decentralization\n\nFull video + docs in comments \u2193",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "image",
|
||||||
|
scheduledAt: "2026-02-24T11:00:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: [
|
||||||
|
"Launch",
|
||||||
|
"RegenerativeFinance",
|
||||||
|
"Innovation",
|
||||||
|
"FutureOfWork",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// PHASE 3: Post-Launch Amplification (Day +1)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "post-ig-reel",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(7),
|
||||||
|
y: row(0) - 30,
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "instagram",
|
||||||
|
postType: "reel",
|
||||||
|
stepNumber: 7,
|
||||||
|
content:
|
||||||
|
"60-second explainer: How MycoFi Earth works \ud83c\udf44\u2728\n\nVisual breakdown of the token flow from contributor \u2192 funding circle \u2192 project \u2192 compost.\n\nSet to lo-fi beats with mycelium time-lapse footage.\n\nCTA: Link in bio to join the first funding circle.",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "video",
|
||||||
|
scheduledAt: "2026-02-25T12:00:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: [
|
||||||
|
"MycoFi",
|
||||||
|
"RegenFinance",
|
||||||
|
"Explainer",
|
||||||
|
"Web3",
|
||||||
|
"Reels",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-threads-xpost",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(8),
|
||||||
|
y: row(0) - 60,
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "threads",
|
||||||
|
postType: "text",
|
||||||
|
stepNumber: 8,
|
||||||
|
content:
|
||||||
|
"We just launched MycoFi Earth and the response has been incredible \ud83c\udf1f\n\nThe idea is simple: what if finance worked like mycelium?\n\nMycelium doesn't hoard \u2014 it redistributes. MycoFi applies that logic to community funding.\n\nEarly access is open. Come grow with us \ud83c\udf31",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "",
|
||||||
|
scheduledAt: "2026-02-25T14:00:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: ["MycoFi", "RegenEconomy", "Community"],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
id: "post-bluesky-launch",
|
||||||
|
type: "folk-social-post",
|
||||||
|
x: col(9),
|
||||||
|
y: row(0),
|
||||||
|
width: NODE_W,
|
||||||
|
height: NODE_H,
|
||||||
|
rotation: 0,
|
||||||
|
platform: "bluesky",
|
||||||
|
postType: "text",
|
||||||
|
stepNumber: 9,
|
||||||
|
content:
|
||||||
|
"MycoFi Earth just went live \ud83c\udf44\n\nIt's a regenerative finance platform where funding flows like nutrients through a mycelial network.\n\nNo VCs. No whales. Just communities funding what matters.\n\nmycofi.earth",
|
||||||
|
mediaUrl: "",
|
||||||
|
mediaType: "",
|
||||||
|
scheduledAt: "2026-02-25T15:00:00",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: ["MycoFi", "RegenFinance", "Bluesky"],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// CAMPAIGN SUMMARY NODE
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "campaign-summary",
|
||||||
|
type: "folk-markdown",
|
||||||
|
x: col(0),
|
||||||
|
y: row(0) - 200,
|
||||||
|
width: 500,
|
||||||
|
height: 160,
|
||||||
|
rotation: 0,
|
||||||
|
content:
|
||||||
|
"# \ud83c\udf44 MycoFi Earth Launch Campaign\n\n**Duration:** Feb 21\u201325, 2026 (5 days)\n**Platforms:** X, LinkedIn, Instagram, YouTube, Threads, Bluesky\n**Posts:** 9 total across 3 phases\n\n| Phase | Days | Posts |\n|-------|------|-------|\n| Pre-Launch Hype | Day -3 to -1 | 3 posts |\n| Launch Day | Day 0 | 3 posts |\n| Amplification | Day +1 | 3 posts |",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// PHASE LABELS (markdown notes as section headers)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
id: "label-phase1",
|
||||||
|
type: "folk-markdown",
|
||||||
|
x: col(1),
|
||||||
|
y: row(0) - 55,
|
||||||
|
width: COL_WIDTH * 3 + COL_GAP * 2,
|
||||||
|
height: 36,
|
||||||
|
rotation: 0,
|
||||||
|
content:
|
||||||
|
"### \ud83d\udce3 Phase 1: Pre-Launch Hype (Feb 21\u201323)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "label-phase2",
|
||||||
|
type: "folk-markdown",
|
||||||
|
x: col(4),
|
||||||
|
y: row(0) - 85,
|
||||||
|
width: COL_WIDTH * 3 + COL_GAP * 2,
|
||||||
|
height: 36,
|
||||||
|
rotation: 0,
|
||||||
|
content: "### \ud83d\ude80 Phase 2: Launch Day (Feb 24)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "label-phase3",
|
||||||
|
type: "folk-markdown",
|
||||||
|
x: col(7),
|
||||||
|
y: row(0) - 85,
|
||||||
|
width: COL_WIDTH * 3 + COL_GAP * 2,
|
||||||
|
height: 36,
|
||||||
|
rotation: 0,
|
||||||
|
content:
|
||||||
|
"### \ud83d\udce1 Phase 3: Amplification (Feb 25)",
|
||||||
|
},
|
||||||
|
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
// ARROWS (flow connections)
|
||||||
|
// ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Trigger → X Teaser
|
||||||
|
{
|
||||||
|
id: "arrow-trigger-to-teaser",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "campaign-trigger",
|
||||||
|
targetId: "post-x-teaser",
|
||||||
|
color: "#64748b",
|
||||||
|
},
|
||||||
|
|
||||||
|
// X Teaser → LinkedIn Thought Leadership
|
||||||
|
{
|
||||||
|
id: "arrow-teaser-to-linkedin",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-x-teaser",
|
||||||
|
targetId: "post-linkedin-thought",
|
||||||
|
color: "#0A66C2",
|
||||||
|
},
|
||||||
|
|
||||||
|
// LinkedIn Thought → IG Carousel
|
||||||
|
{
|
||||||
|
id: "arrow-linkedin-to-ig",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-linkedin-thought",
|
||||||
|
targetId: "post-ig-carousel",
|
||||||
|
color: "#E4405F",
|
||||||
|
},
|
||||||
|
|
||||||
|
// IG Carousel → YouTube Launch (phase transition)
|
||||||
|
{
|
||||||
|
id: "arrow-ig-to-yt",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-ig-carousel",
|
||||||
|
targetId: "post-yt-launch",
|
||||||
|
color: "#FF0000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// YouTube → X Launch Thread
|
||||||
|
{
|
||||||
|
id: "arrow-yt-to-x-launch",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-yt-launch",
|
||||||
|
targetId: "post-x-launch",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// X Launch → LinkedIn Announcement
|
||||||
|
{
|
||||||
|
id: "arrow-x-to-linkedin-launch",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-x-launch",
|
||||||
|
targetId: "post-linkedin-launch",
|
||||||
|
color: "#0A66C2",
|
||||||
|
},
|
||||||
|
|
||||||
|
// LinkedIn Announcement → IG Reel (phase transition)
|
||||||
|
{
|
||||||
|
id: "arrow-linkedin-to-reel",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-linkedin-launch",
|
||||||
|
targetId: "post-ig-reel",
|
||||||
|
color: "#E4405F",
|
||||||
|
},
|
||||||
|
|
||||||
|
// IG Reel → Threads
|
||||||
|
{
|
||||||
|
id: "arrow-reel-to-threads",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-ig-reel",
|
||||||
|
targetId: "post-threads-xpost",
|
||||||
|
color: "#000000",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Threads → Bluesky
|
||||||
|
{
|
||||||
|
id: "arrow-threads-to-bluesky",
|
||||||
|
type: "folk-arrow",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
rotation: 0,
|
||||||
|
sourceId: "post-threads-xpost",
|
||||||
|
targetId: "post-bluesky-launch",
|
||||||
|
color: "#0085FF",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the campaign demo community exists and is seeded.
|
||||||
|
* Called on server startup alongside the main demo.
|
||||||
|
*/
|
||||||
|
export async function ensureCampaignDemo(): Promise<void> {
|
||||||
|
const slug = "campaign-demo";
|
||||||
|
const exists = await communityExists(slug);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
await createCommunity(
|
||||||
|
"Social Media Campaign Demo",
|
||||||
|
slug,
|
||||||
|
null,
|
||||||
|
"public",
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
"[Campaign] Created campaign-demo community with visibility: public",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await loadCommunity(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if already seeded
|
||||||
|
const data = getDocumentData(slug);
|
||||||
|
const shapeCount = data ? Object.keys(data.shapes || {}).length : 0;
|
||||||
|
|
||||||
|
if (shapeCount === 0) {
|
||||||
|
addShapes(slug, CAMPAIGN_SHAPES);
|
||||||
|
console.log(
|
||||||
|
`[Campaign] Seeded ${CAMPAIGN_SHAPES.length} shapes into campaign-demo`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`[Campaign] campaign-demo already has ${shapeCount} shapes`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -80,6 +80,7 @@ const CONFIG = {
|
||||||
'https://rnetwork.online',
|
'https://rnetwork.online',
|
||||||
'https://rcart.online',
|
'https://rcart.online',
|
||||||
'https://rtube.online',
|
'https://rtube.online',
|
||||||
|
'https://rchats.online',
|
||||||
'https://rstack.online',
|
'https://rstack.online',
|
||||||
'https://rpubs.online',
|
'https://rpubs.online',
|
||||||
'https://rauctions.online',
|
'https://rauctions.online',
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,8 @@
|
||||||
folk-token-ledger,
|
folk-token-ledger,
|
||||||
folk-choice-vote,
|
folk-choice-vote,
|
||||||
folk-choice-rank,
|
folk-choice-rank,
|
||||||
folk-choice-spider {
|
folk-choice-spider,
|
||||||
|
folk-social-post {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,7 +192,8 @@
|
||||||
folk-video-chat, folk-obs-note, folk-workflow-block,
|
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
folk-booking, folk-token-mint, folk-token-ledger,
|
folk-booking, folk-token-mint, folk-token-ledger,
|
||||||
folk-choice-vote, folk-choice-rank, folk-choice-spider) {
|
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
||||||
|
folk-social-post) {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -201,7 +203,8 @@
|
||||||
folk-video-chat, folk-obs-note, folk-workflow-block,
|
folk-video-chat, folk-obs-note, folk-workflow-block,
|
||||||
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
folk-itinerary, folk-destination, folk-budget, folk-packing-list,
|
||||||
folk-booking, folk-token-mint, folk-token-ledger,
|
folk-booking, folk-token-mint, folk-token-ledger,
|
||||||
folk-choice-vote, folk-choice-rank, folk-choice-spider):hover {
|
folk-choice-vote, folk-choice-rank, folk-choice-spider,
|
||||||
|
folk-social-post):hover {
|
||||||
outline: 2px dashed #3b82f6;
|
outline: 2px dashed #3b82f6;
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
}
|
}
|
||||||
|
|
@ -244,6 +247,7 @@
|
||||||
<button id="add-choice-vote" title="Live Poll">☑ Poll</button>
|
<button id="add-choice-vote" title="Live Poll">☑ Poll</button>
|
||||||
<button id="add-choice-rank" title="Rank Choices">📊 Rank</button>
|
<button id="add-choice-rank" title="Rank Choices">📊 Rank</button>
|
||||||
<button id="add-choice-spider" title="Score Matrix">🕸 Spider</button>
|
<button id="add-choice-spider" title="Score Matrix">🕸 Spider</button>
|
||||||
|
<button id="add-social-post" title="Social Media Post">📱 Post</button>
|
||||||
<button id="add-arrow" title="Connect Shapes">↗️ Connect</button>
|
<button id="add-arrow" title="Connect Shapes">↗️ Connect</button>
|
||||||
<button id="zoom-in" title="Zoom In">+</button>
|
<button id="zoom-in" title="Zoom In">+</button>
|
||||||
<button id="zoom-out" title="Zoom Out">-</button>
|
<button id="zoom-out" title="Zoom Out">-</button>
|
||||||
|
|
@ -287,6 +291,7 @@
|
||||||
FolkChoiceVote,
|
FolkChoiceVote,
|
||||||
FolkChoiceRank,
|
FolkChoiceRank,
|
||||||
FolkChoiceSpider,
|
FolkChoiceSpider,
|
||||||
|
FolkSocialPost,
|
||||||
CommunitySync,
|
CommunitySync,
|
||||||
PresenceManager,
|
PresenceManager,
|
||||||
generatePeerId,
|
generatePeerId,
|
||||||
|
|
@ -335,12 +340,14 @@
|
||||||
FolkChoiceVote.define();
|
FolkChoiceVote.define();
|
||||||
FolkChoiceRank.define();
|
FolkChoiceRank.define();
|
||||||
FolkChoiceSpider.define();
|
FolkChoiceSpider.define();
|
||||||
|
FolkSocialPost.define();
|
||||||
|
|
||||||
// Get community info from URL
|
// Get community info from URL
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
const subdomain = hostname.split(".")[0];
|
const subdomain = hostname.split(".")[0];
|
||||||
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1";
|
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1";
|
||||||
const communitySlug = isLocalhost ? "demo" : subdomain;
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const communitySlug = urlParams.get("space") || (isLocalhost ? "demo" : subdomain);
|
||||||
|
|
||||||
// Update UI
|
// Update UI
|
||||||
document.getElementById("community-name").textContent = communitySlug;
|
document.getElementById("community-name").textContent = communitySlug;
|
||||||
|
|
@ -360,7 +367,8 @@
|
||||||
"folk-workflow-block", "folk-itinerary", "folk-destination",
|
"folk-workflow-block", "folk-itinerary", "folk-destination",
|
||||||
"folk-budget", "folk-packing-list", "folk-booking",
|
"folk-budget", "folk-packing-list", "folk-booking",
|
||||||
"folk-token-mint", "folk-token-ledger",
|
"folk-token-mint", "folk-token-ledger",
|
||||||
"folk-choice-vote", "folk-choice-rank", "folk-choice-spider"
|
"folk-choice-vote", "folk-choice-rank", "folk-choice-spider",
|
||||||
|
"folk-social-post"
|
||||||
].join(", ");
|
].join(", ");
|
||||||
|
|
||||||
// Initialize offline store and CommunitySync
|
// Initialize offline store and CommunitySync
|
||||||
|
|
@ -647,6 +655,18 @@
|
||||||
if (data.criteria) shape.criteria = data.criteria;
|
if (data.criteria) shape.criteria = data.criteria;
|
||||||
if (data.scores) shape.scores = data.scores;
|
if (data.scores) shape.scores = data.scores;
|
||||||
break;
|
break;
|
||||||
|
case "folk-social-post":
|
||||||
|
shape = document.createElement("folk-social-post");
|
||||||
|
if (data.platform) shape.platform = data.platform;
|
||||||
|
if (data.postType) shape.postType = data.postType;
|
||||||
|
if (data.content) shape.content = data.content;
|
||||||
|
if (data.mediaUrl) shape.mediaUrl = data.mediaUrl;
|
||||||
|
if (data.mediaType) shape.mediaType = data.mediaType;
|
||||||
|
if (data.scheduledAt) shape.scheduledAt = data.scheduledAt;
|
||||||
|
if (data.status) shape.status = data.status;
|
||||||
|
if (data.hashtags) shape.hashtags = data.hashtags;
|
||||||
|
if (data.stepNumber) shape.stepNumber = data.stepNumber;
|
||||||
|
break;
|
||||||
case "folk-markdown":
|
case "folk-markdown":
|
||||||
default:
|
default:
|
||||||
shape = document.createElement("folk-markdown");
|
shape = document.createElement("folk-markdown");
|
||||||
|
|
@ -715,6 +735,7 @@
|
||||||
"folk-choice-vote": { width: 360, height: 400 },
|
"folk-choice-vote": { width: 360, height: 400 },
|
||||||
"folk-choice-rank": { width: 380, height: 480 },
|
"folk-choice-rank": { width: 380, height: 480 },
|
||||||
"folk-choice-spider": { width: 440, height: 540 },
|
"folk-choice-spider": { width: 440, height: 540 },
|
||||||
|
"folk-social-post": { width: 300, height: 380 },
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the center of the current viewport in canvas coordinates
|
// Get the center of the current viewport in canvas coordinates
|
||||||
|
|
@ -892,6 +913,17 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Social media post
|
||||||
|
document.getElementById("add-social-post").addEventListener("click", () => {
|
||||||
|
createAndAddShape("folk-social-post", {
|
||||||
|
platform: "x",
|
||||||
|
postType: "text",
|
||||||
|
content: "Write your post content here...",
|
||||||
|
status: "draft",
|
||||||
|
hashtags: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Arrow connection mode
|
// Arrow connection mode
|
||||||
let connectMode = false;
|
let connectMode = false;
|
||||||
let connectSource = null;
|
let connectSource = null;
|
||||||
|
|
|
||||||
|
|
@ -555,6 +555,7 @@
|
||||||
<a href="https://rnetwork.online" class="ecosystem-app">🕸 rNetwork</a>
|
<a href="https://rnetwork.online" class="ecosystem-app">🕸 rNetwork</a>
|
||||||
<a href="https://rcart.online" class="ecosystem-app">🛒 rCart</a>
|
<a href="https://rcart.online" class="ecosystem-app">🛒 rCart</a>
|
||||||
<a href="https://rtube.online" class="ecosystem-app">🎬 rTube</a>
|
<a href="https://rtube.online" class="ecosystem-app">🎬 rTube</a>
|
||||||
|
<a href="https://rchats.online" class="ecosystem-app">💬 rChats</a>
|
||||||
<a href="https://rforum.online" class="ecosystem-app">💬 rForum</a>
|
<a href="https://rforum.online" class="ecosystem-app">💬 rForum</a>
|
||||||
<a href="https://rswag.online" class="ecosystem-app">👕 rSwag</a>
|
<a href="https://rswag.online" class="ecosystem-app">👕 rSwag</a>
|
||||||
<a href="https://rdata.online" class="ecosystem-app">📊 rData</a>
|
<a href="https://rdata.online" class="ecosystem-app">📊 rData</a>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue