merge(dev): rsocials post detail overlay
CI/CD / deploy (push) Successful in 2m40s
Details
CI/CD / deploy (push) Successful in 2m40s
Details
This commit is contained in:
commit
a3f3e67cdb
|
|
@ -25,6 +25,13 @@ interface DraftPostCard {
|
|||
type StatusFilter = 'draft' | 'scheduled' | 'published';
|
||||
const STATUS_STORAGE_KEY = 'rsocials:gallery:status-filter';
|
||||
|
||||
interface EditDraft {
|
||||
content: string;
|
||||
scheduledAt: string;
|
||||
hashtags: string;
|
||||
status: 'draft' | 'scheduled' | 'published';
|
||||
}
|
||||
|
||||
export class FolkThreadGallery extends HTMLElement {
|
||||
private _space = 'demo';
|
||||
private _threads: ThreadData[] = [];
|
||||
|
|
@ -33,6 +40,11 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
private _subscribedDocIds: string[] = [];
|
||||
private _isDemoFallback = false;
|
||||
private _statusFilter: StatusFilter = 'draft';
|
||||
private _expandedPostId: string | null = null;
|
||||
private _editMode = false;
|
||||
private _editDraft: EditDraft | null = null;
|
||||
private _savingIndicator = '';
|
||||
private _boundKeyDown: ((e: KeyboardEvent) => void) | null = null;
|
||||
|
||||
static get observedAttributes() { return ['space']; }
|
||||
|
||||
|
|
@ -46,6 +58,17 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
this._statusFilter = saved;
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
this._boundKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && this._expandedPostId) {
|
||||
e.preventDefault();
|
||||
if (this._editMode) {
|
||||
this.exitEditMode();
|
||||
} else {
|
||||
this.closeOverlay();
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this._boundKeyDown);
|
||||
this.render();
|
||||
this.subscribeOffline();
|
||||
}
|
||||
|
|
@ -58,6 +81,8 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
for (const id of this._subscribedDocIds) runtime.unsubscribe(id);
|
||||
}
|
||||
this._subscribedDocIds = [];
|
||||
if (this._boundKeyDown) document.removeEventListener('keydown', this._boundKeyDown);
|
||||
this._boundKeyDown = null;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _old: string, val: string) {
|
||||
|
|
@ -192,7 +217,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
|
||||
const threadCardsHTML = filteredPosts.length === 0 && threadsVisible.length === 0
|
||||
? `<div class="empty">
|
||||
<p>${filter === 'all' ? 'No posts or threads yet.' : `No ${filter} posts.`} Create your first post or thread.</p>
|
||||
<p>No ${filter} posts. Create your first post or thread.</p>
|
||||
<div style="display:flex;gap:.5rem;justify-content:center;margin-top:1rem;flex-wrap:wrap">
|
||||
<a href="${this.basePath}thread-editor" class="btn btn--success">Create Thread</a>
|
||||
<a href="${this.basePath}campaigns" class="btn btn--primary">Open Campaigns</a>
|
||||
|
|
@ -205,10 +230,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
const statusClass = `badge badge--${p.status}`;
|
||||
const statusLabel = p.status.charAt(0).toUpperCase() + p.status.slice(1);
|
||||
const cardClass = `card card--${p.status}`;
|
||||
const href = p.threadId
|
||||
? `${this.basePath}thread-editor/${this.esc(p.threadId)}/edit`
|
||||
: `${this.basePath}campaign`;
|
||||
return `<a href="${href}" class="${cardClass}">
|
||||
return `<button type="button" class="${cardClass}" data-post-id="${this.esc(p.id)}">
|
||||
<div class="card__badges">
|
||||
<span class="${statusClass}">${statusLabel}</span>
|
||||
<span class="badge badge--campaign">${this.esc(p.campaignTitle)}</span>
|
||||
|
|
@ -219,7 +241,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
${p.hashtags.length ? `<span>${p.hashtags.slice(0, 3).join(' ')}</span>` : ''}
|
||||
${schedDate ? `<span>${schedDate}</span>` : ''}
|
||||
</div>
|
||||
</a>`;
|
||||
</button>`;
|
||||
}).join('')}
|
||||
${threadsVisible.map(t => {
|
||||
const initial = (t.name || '?').charAt(0).toUpperCase();
|
||||
|
|
@ -269,8 +291,11 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
padding: 1.25rem; transition: border-color 0.15s, transform 0.15s;
|
||||
display: flex; flex-direction: column; gap: 0.75rem;
|
||||
text-decoration: none; color: inherit;
|
||||
font: inherit; text-align: left; cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
.card:hover { border-color: var(--rs-primary); transform: translateY(-2px); }
|
||||
.card:focus-visible { outline: 2px solid #14b8a6; outline-offset: 2px; }
|
||||
.card--draft { border-left: 3px solid #f59e0b; }
|
||||
.card--scheduled { border-left: 3px solid #60a5fa; }
|
||||
.card--published { border-left: 3px solid #22c55e; }
|
||||
|
|
@ -327,6 +352,121 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
}
|
||||
.card__image { border-radius: 8px; overflow: hidden; border: 1px solid var(--rs-input-border, #334155); margin-bottom: 0.25rem; }
|
||||
.card__image img { display: block; width: 100%; height: 120px; object-fit: cover; }
|
||||
|
||||
/* ── Post detail overlay ── */
|
||||
.overlay-backdrop {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: rgba(8, 12, 24, 0.6);
|
||||
backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 2rem; animation: fadeIn 0.15s ease-out;
|
||||
}
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
.overlay-card {
|
||||
background: var(--rs-bg-surface, #1e293b);
|
||||
border: 1px solid var(--rs-input-border, #334155);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 24px 80px -20px rgba(0, 0, 0, 0.6);
|
||||
width: min(720px, 100%); max-height: calc(100vh - 4rem);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
transform-origin: center center;
|
||||
}
|
||||
.overlay-header {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 1rem 1.25rem; border-bottom: 1px solid var(--rs-input-border, #334155);
|
||||
}
|
||||
.overlay-platform {
|
||||
font-size: 1.25rem; line-height: 1;
|
||||
width: 2.25rem; height: 2.25rem; border-radius: 8px;
|
||||
background: rgba(20, 184, 166, 0.12);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.overlay-headline {
|
||||
flex: 1; display: flex; flex-direction: column; gap: 0.15rem; min-width: 0;
|
||||
}
|
||||
.overlay-title {
|
||||
font-size: 1rem; font-weight: 700; color: var(--rs-text-primary, #f1f5f9);
|
||||
line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.overlay-sub {
|
||||
display: flex; align-items: center; gap: 0.4rem; flex-wrap: wrap;
|
||||
font-size: 0.75rem; color: var(--rs-text-muted, #64748b);
|
||||
}
|
||||
.overlay-close {
|
||||
background: transparent; border: 1px solid var(--rs-input-border, #334155);
|
||||
color: var(--rs-text-secondary, #94a3b8);
|
||||
width: 2rem; height: 2rem; border-radius: 8px;
|
||||
font-size: 1.1rem; line-height: 1; cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.overlay-close:hover { background: rgba(239,68,68,0.1); color: #fca5a5; border-color: #ef4444; }
|
||||
.overlay-body {
|
||||
padding: 1.5rem 1.25rem;
|
||||
overflow-y: auto;
|
||||
display: flex; flex-direction: column; gap: 1rem;
|
||||
}
|
||||
.overlay-content {
|
||||
font-size: 1rem; line-height: 1.6;
|
||||
color: var(--rs-text-primary, #f1f5f9);
|
||||
white-space: pre-wrap; word-wrap: break-word;
|
||||
}
|
||||
.overlay-meta-grid {
|
||||
display: grid; grid-template-columns: auto 1fr; gap: 0.5rem 1rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.overlay-meta-label {
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.7rem; font-weight: 600;
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
.overlay-meta-value { color: var(--rs-text-secondary, #94a3b8); }
|
||||
.overlay-hashtags { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
.overlay-hashtags span {
|
||||
background: rgba(99,102,241,0.15); color: #a5b4fc;
|
||||
padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.72rem;
|
||||
}
|
||||
.overlay-footer {
|
||||
display: flex; justify-content: flex-end; gap: 0.5rem;
|
||||
padding: 0.85rem 1.25rem; border-top: 1px solid var(--rs-input-border, #334155);
|
||||
}
|
||||
.overlay-btn {
|
||||
padding: 0.5rem 1rem; border-radius: 8px;
|
||||
border: 1px solid var(--rs-input-border, #334155);
|
||||
background: transparent; color: var(--rs-text-primary, #e2e8f0);
|
||||
font-size: 0.85rem; font-weight: 600; cursor: pointer;
|
||||
font-family: inherit;
|
||||
transition: background 0.12s, border-color 0.12s;
|
||||
}
|
||||
.overlay-btn:hover { background: rgba(255,255,255,0.05); }
|
||||
.overlay-btn--primary {
|
||||
background: linear-gradient(135deg,#14b8a6,#0d9488); color: #fff; border-color: #0d9488;
|
||||
}
|
||||
.overlay-btn--primary:hover { filter: brightness(1.1); }
|
||||
.overlay-btn--danger { border-color: #ef4444; color: #fca5a5; }
|
||||
.overlay-btn--danger:hover { background: rgba(239,68,68,0.1); }
|
||||
/* Edit-mode controls */
|
||||
.edit-field { display: flex; flex-direction: column; gap: 0.3rem; }
|
||||
.edit-label {
|
||||
font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
|
||||
color: var(--rs-text-muted, #64748b);
|
||||
}
|
||||
.edit-input, .edit-textarea, .edit-select {
|
||||
background: var(--rs-bg-surface-raised, #0f172a);
|
||||
border: 1px solid var(--rs-input-border, #334155);
|
||||
border-radius: 8px; color: var(--rs-text-primary, #f1f5f9);
|
||||
padding: 0.6rem 0.75rem; font-size: 0.9rem; font-family: inherit;
|
||||
}
|
||||
.edit-input:focus, .edit-textarea:focus, .edit-select:focus {
|
||||
outline: none; border-color: #14b8a6;
|
||||
box-shadow: 0 0 0 3px rgba(20,184,166,0.15);
|
||||
}
|
||||
.edit-textarea {
|
||||
min-height: 120px; resize: vertical; line-height: 1.6;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.edit-saving { font-size: 0.75rem; color: #94a3b8; margin-right: auto; align-self: center; }
|
||||
</style>
|
||||
<div class="gallery">
|
||||
<div class="header">
|
||||
|
|
@ -336,6 +476,7 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
${filterBar}
|
||||
${threadCardsHTML}
|
||||
</div>
|
||||
${this.renderOverlay()}
|
||||
`;
|
||||
|
||||
// Attach chip click handlers after render
|
||||
|
|
@ -348,6 +489,268 @@ export class FolkThreadGallery extends HTMLElement {
|
|||
this.render();
|
||||
});
|
||||
});
|
||||
|
||||
// Card click → expand
|
||||
this.shadowRoot.querySelectorAll('button.card[data-post-id]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const id = (btn as HTMLElement).dataset.postId;
|
||||
if (id) this.expandPost(id, btn as HTMLElement);
|
||||
});
|
||||
});
|
||||
|
||||
// Overlay wiring
|
||||
this.wireOverlay();
|
||||
}
|
||||
|
||||
// ── Overlay rendering ──
|
||||
|
||||
private renderOverlay(): string {
|
||||
if (!this._expandedPostId) return '';
|
||||
const post = this._allPosts.find(p => p.id === this._expandedPostId);
|
||||
if (!post) return '';
|
||||
|
||||
const platformLabel = post.platform.charAt(0).toUpperCase() + post.platform.slice(1);
|
||||
const statusLabel = post.status.charAt(0).toUpperCase() + post.status.slice(1);
|
||||
const schedDate = post.scheduledAt
|
||||
? new Date(post.scheduledAt).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' })
|
||||
: '—';
|
||||
|
||||
const header = `<div class="overlay-header">
|
||||
<div class="overlay-platform">${this.platformIcon(post.platform)}</div>
|
||||
<div class="overlay-headline">
|
||||
<div class="overlay-title">${this.esc(platformLabel)} Post</div>
|
||||
<div class="overlay-sub">
|
||||
<span class="badge badge--${post.status}">${statusLabel}</span>
|
||||
<span class="badge badge--campaign">${this.esc(post.campaignTitle)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="overlay-close" data-action="close" aria-label="Close">×</button>
|
||||
</div>`;
|
||||
|
||||
let body = '';
|
||||
let footer = '';
|
||||
|
||||
if (this._editMode && this._editDraft) {
|
||||
const d = this._editDraft;
|
||||
// datetime-local needs YYYY-MM-DDTHH:mm in local time
|
||||
const dtLocal = d.scheduledAt ? toDatetimeLocal(d.scheduledAt) : '';
|
||||
body = `<div class="overlay-body">
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-content">Content</label>
|
||||
<textarea id="f-content" class="edit-textarea" data-field="content">${this.esc(d.content)}</textarea>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-status">Status</label>
|
||||
<select id="f-status" class="edit-select" data-field="status">
|
||||
<option value="draft"${d.status === 'draft' ? ' selected' : ''}>Draft</option>
|
||||
<option value="scheduled"${d.status === 'scheduled' ? ' selected' : ''}>Scheduled</option>
|
||||
<option value="published"${d.status === 'published' ? ' selected' : ''}>Published</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-sched">Scheduled at</label>
|
||||
<input id="f-sched" type="datetime-local" class="edit-input" data-field="scheduledAt" value="${this.esc(dtLocal)}" />
|
||||
</div>
|
||||
<div class="edit-field">
|
||||
<label class="edit-label" for="f-tags">Hashtags (space separated)</label>
|
||||
<input id="f-tags" class="edit-input" data-field="hashtags" value="${this.esc(d.hashtags)}" placeholder="#launch #campaign" />
|
||||
</div>
|
||||
</div>`;
|
||||
footer = `<div class="overlay-footer">
|
||||
<span class="edit-saving">${this.esc(this._savingIndicator)}</span>
|
||||
<button type="button" class="overlay-btn" data-action="cancel-edit">Cancel</button>
|
||||
<button type="button" class="overlay-btn overlay-btn--primary" data-action="save">Save</button>
|
||||
</div>`;
|
||||
} else {
|
||||
body = `<div class="overlay-body">
|
||||
<div class="overlay-content">${this.esc(post.content) || '<em style="color:#64748b">Empty post</em>'}</div>
|
||||
<div class="overlay-meta-grid">
|
||||
<div class="overlay-meta-label">Schedule</div>
|
||||
<div class="overlay-meta-value">${this.esc(schedDate)}</div>
|
||||
<div class="overlay-meta-label">Hashtags</div>
|
||||
<div class="overlay-hashtags">${post.hashtags.length ? post.hashtags.map(h => `<span>${this.esc(h)}</span>`).join('') : '<span style="background:transparent;color:#64748b">—</span>'}</div>
|
||||
<div class="overlay-meta-label">Campaign</div>
|
||||
<div class="overlay-meta-value">${this.esc(post.campaignTitle)}</div>
|
||||
<div class="overlay-meta-label">Characters</div>
|
||||
<div class="overlay-meta-value">${post.content.length}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
const openHref = post.threadId
|
||||
? `${this.basePath}thread-editor/${this.esc(post.threadId)}/edit`
|
||||
: `${this.basePath}campaign`;
|
||||
footer = `<div class="overlay-footer">
|
||||
<a href="${openHref}" class="overlay-btn">Open in Campaign ↗</a>
|
||||
<button type="button" class="overlay-btn overlay-btn--primary" data-action="edit">Edit</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `<div class="overlay-backdrop" data-action="backdrop">
|
||||
<div class="overlay-card" id="overlay-card" role="dialog" aria-modal="true" aria-label="Post details">
|
||||
${header}
|
||||
${body}
|
||||
${footer}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private wireOverlay(): void {
|
||||
if (!this.shadowRoot) return;
|
||||
const backdrop = this.shadowRoot.querySelector('.overlay-backdrop') as HTMLElement | null;
|
||||
if (!backdrop) return;
|
||||
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.dataset.action === 'backdrop') {
|
||||
if (this._editMode) this.exitEditMode();
|
||||
else this.closeOverlay();
|
||||
return;
|
||||
}
|
||||
const actionEl = target.closest<HTMLElement>('[data-action]');
|
||||
if (!actionEl) return;
|
||||
const action = actionEl.dataset.action;
|
||||
if (action === 'close') this.closeOverlay();
|
||||
else if (action === 'edit') this.enterEditMode();
|
||||
else if (action === 'cancel-edit') this.exitEditMode();
|
||||
else if (action === 'save') this.saveEdit();
|
||||
});
|
||||
|
||||
// Bind input listeners in edit mode so the draft stays fresh
|
||||
if (this._editMode) {
|
||||
backdrop.querySelectorAll<HTMLElement>('[data-field]').forEach(el => {
|
||||
el.addEventListener('input', (e) => {
|
||||
const input = e.currentTarget as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
||||
const field = input.dataset.field;
|
||||
if (!field || !this._editDraft) return;
|
||||
(this._editDraft as any)[field] = input.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// FLIP animation entry (only on first mount of this overlay)
|
||||
const card = backdrop.querySelector('#overlay-card') as HTMLElement | null;
|
||||
if (card && !card.dataset.animated) {
|
||||
card.dataset.animated = '1';
|
||||
const origin = this._flipOrigin;
|
||||
if (origin) {
|
||||
const target = card.getBoundingClientRect();
|
||||
const dx = origin.left + origin.width / 2 - (target.left + target.width / 2);
|
||||
const dy = origin.top + origin.height / 2 - (target.top + target.height / 2);
|
||||
const sx = origin.width / target.width;
|
||||
const sy = origin.height / target.height;
|
||||
card.animate([
|
||||
{ transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`, opacity: 0.6 },
|
||||
{ transform: 'translate(0, 0) scale(1, 1)', opacity: 1 },
|
||||
], { duration: 220, easing: 'cubic-bezier(0.2, 0.8, 0.2, 1)' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Overlay state transitions ──
|
||||
|
||||
private _flipOrigin: DOMRect | null = null;
|
||||
|
||||
private expandPost(id: string, sourceEl?: HTMLElement): void {
|
||||
this._flipOrigin = sourceEl?.getBoundingClientRect() ?? null;
|
||||
this._expandedPostId = id;
|
||||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
this._savingIndicator = '';
|
||||
this.render();
|
||||
}
|
||||
|
||||
private closeOverlay(): void {
|
||||
this._expandedPostId = null;
|
||||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
this._flipOrigin = null;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private enterEditMode(): void {
|
||||
if (!this._expandedPostId) return;
|
||||
if (this._isDemoFallback) return; // no runtime = read-only
|
||||
const post = this._allPosts.find(p => p.id === this._expandedPostId);
|
||||
if (!post) return;
|
||||
this._editDraft = {
|
||||
content: post.content,
|
||||
scheduledAt: post.scheduledAt,
|
||||
hashtags: (post.hashtags || []).join(' '),
|
||||
status: post.status as 'draft' | 'scheduled' | 'published',
|
||||
};
|
||||
this._editMode = true;
|
||||
this.render();
|
||||
// Focus content textarea
|
||||
requestAnimationFrame(() => {
|
||||
const ta = this.shadowRoot?.getElementById('f-content') as HTMLTextAreaElement | null;
|
||||
ta?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private exitEditMode(): void {
|
||||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
this._savingIndicator = '';
|
||||
this.render();
|
||||
}
|
||||
|
||||
private async saveEdit(): Promise<void> {
|
||||
if (!this._editDraft || !this._expandedPostId) return;
|
||||
const runtime = (window as any).__rspaceOfflineRuntime;
|
||||
if (!runtime?.isInitialized) {
|
||||
this._savingIndicator = 'Offline runtime unavailable';
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
const draft = this._editDraft;
|
||||
const postId = this._expandedPostId;
|
||||
const hashtags = draft.hashtags
|
||||
.split(/[\s,]+/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(s => s.startsWith('#') ? s : `#${s}`);
|
||||
// datetime-local string → ISO
|
||||
const scheduledAtIso = draft.scheduledAt ? new Date(draft.scheduledAt).toISOString() : '';
|
||||
|
||||
this._savingIndicator = 'Saving…';
|
||||
this.render();
|
||||
|
||||
try {
|
||||
const docId = socialsDocId(this._space) as DocumentId;
|
||||
await runtime.changeDoc(docId, 'edit post', (d: SocialsDoc) => {
|
||||
if (!d.campaigns) return;
|
||||
for (const campaign of Object.values(d.campaigns)) {
|
||||
const post = (campaign.posts || []).find((p: CampaignPost) => p.id === postId);
|
||||
if (post) {
|
||||
post.content = draft.content;
|
||||
post.status = draft.status;
|
||||
post.scheduledAt = scheduledAtIso;
|
||||
post.hashtags = hashtags;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
this._savingIndicator = 'Saved';
|
||||
this._editMode = false;
|
||||
this._editDraft = null;
|
||||
this.render();
|
||||
setTimeout(() => { this._savingIndicator = ''; this.render(); }, 1500);
|
||||
} catch (err) {
|
||||
console.warn('[folk-thread-gallery] save failed', err);
|
||||
this._savingIndicator = 'Save failed';
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toDatetimeLocal(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return '';
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3026,7 +3026,7 @@ routes.get("/threads", (c) => {
|
|||
theme: "dark",
|
||||
body: `<folk-thread-gallery space="${escapeHtml(space)}"></folk-thread-gallery>`,
|
||||
styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=3"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=4"></script>`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
@ -3181,7 +3181,7 @@ routes.get("/", (c) => {
|
|||
theme: "dark",
|
||||
styles: `<link rel="stylesheet" href="/modules/rsocials/socials.css">`,
|
||||
body: `<folk-thread-gallery space="${escapeHtml(space)}"></folk-thread-gallery>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=3"></script>`,
|
||||
scripts: `<script type="module" src="/modules/rsocials/folk-thread-gallery.js?v=4"></script>`,
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue