diff --git a/modules/rsocials/components/folk-thread-gallery.ts b/modules/rsocials/components/folk-thread-gallery.ts index 2b47e6c1..28810041 100644 --- a/modules/rsocials/components/folk-thread-gallery.ts +++ b/modules/rsocials/components/folk-thread-gallery.ts @@ -22,19 +22,30 @@ interface DraftPostCard { threadId?: string; } +type StatusFilter = 'all' | 'draft' | 'scheduled' | 'published'; +const STATUS_STORAGE_KEY = 'rsocials:gallery:status-filter'; + export class FolkThreadGallery extends HTMLElement { private _space = 'demo'; private _threads: ThreadData[] = []; - private _draftPosts: DraftPostCard[] = []; + private _allPosts: DraftPostCard[] = []; private _offlineUnsub: (() => void) | null = null; private _subscribedDocIds: string[] = []; private _isDemoFallback = false; + private _statusFilter: StatusFilter = 'all'; static get observedAttributes() { return ['space']; } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._space = this.getAttribute('space') || 'demo'; + // Restore last status filter + try { + const saved = localStorage.getItem(STATUS_STORAGE_KEY); + if (saved === 'draft' || saved === 'scheduled' || saved === 'published' || saved === 'all') { + this._statusFilter = saved; + } + } catch { /* ignore */ } this.render(); this.subscribeOffline(); } @@ -100,24 +111,23 @@ export class FolkThreadGallery extends HTMLElement { this._isDemoFallback = false; this._threads = Object.values(doc.threads).sort((a, b) => b.updatedAt - a.updatedAt); - // Extract draft/scheduled posts from campaigns - this._draftPosts = []; + // Extract all campaign posts across statuses so the status filter + // can choose between drafts / scheduled / published. + this._allPosts = []; if (doc.campaigns) { for (const campaign of Object.values(doc.campaigns)) { for (const post of campaign.posts || []) { - if (post.status === 'draft' || post.status === 'scheduled') { - this._draftPosts.push({ - id: post.id, - campaignId: campaign.id, - campaignTitle: campaign.title, - platform: post.platform, - content: post.content, - scheduledAt: post.scheduledAt, - status: post.status, - hashtags: post.hashtags || [], - threadId: post.threadId, - }); - } + this._allPosts.push({ + id: post.id, + campaignId: campaign.id, + campaignTitle: campaign.title, + platform: post.platform, + content: post.content, + scheduledAt: post.scheduledAt, + status: post.status, + hashtags: post.hashtags || [], + threadId: post.threadId, + }); } } } @@ -147,28 +157,63 @@ export class FolkThreadGallery extends HTMLElement { private render() { if (!this.shadowRoot) return; - const space = this._space; const threads = this._threads; - const drafts = this._draftPosts; + const filter = this._statusFilter; - const threadCardsHTML = threads.length === 0 && drafts.length === 0 + // Filter + sort posts by status + recency + const filteredPosts = this._allPosts + .filter(p => filter === 'all' || p.status === filter) + .sort((a, b) => { + const aT = a.scheduledAt ? new Date(a.scheduledAt).getTime() : 0; + const bT = b.scheduledAt ? new Date(b.scheduledAt).getTime() : 0; + return bT - aT; + }); + + // Status counts for chip badges + const counts = { + all: this._allPosts.length, + draft: this._allPosts.filter(p => p.status === 'draft').length, + scheduled: this._allPosts.filter(p => p.status === 'scheduled').length, + published: this._allPosts.filter(p => p.status === 'published').length, + }; + + const chip = (key: StatusFilter, label: string) => + ``; + + const filterBar = ``; + + // Threads are shown only when status is "all" or "published" — they don't have draft state in this schema. + const showThreads = filter === 'all' || filter === 'published'; + const threadsVisible = showThreads ? threads : []; + + const threadCardsHTML = filteredPosts.length === 0 && threadsVisible.length === 0 ? `
-

No posts or threads yet. Create your first thread!

- Create Thread +

${filter === 'all' ? 'No posts or threads yet.' : `No ${filter} posts.`} Create your first post or thread.

+
+ Create Thread + Open Campaigns +
` : `
- ${drafts.map(p => { + ${filteredPosts.map(p => { const preview = this.esc(p.content.substring(0, 200)); const schedDate = p.scheduledAt ? new Date(p.scheduledAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : ''; - const statusBadge = p.status === 'scheduled' - ? 'Scheduled' - : 'Draft'; + 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 ` + return `
- ${statusBadge} + ${statusLabel} ${this.esc(p.campaignTitle)}

${this.platformIcon(p.platform)} ${this.esc(p.platform)} Post

@@ -179,7 +224,7 @@ export class FolkThreadGallery extends HTMLElement {
`; }).join('')} - ${threads.map(t => { + ${threadsVisible.map(t => { const initial = (t.name || '?').charAt(0).toUpperCase(); const preview = this.esc((t.tweets[0] || '').substring(0, 200)); const dateStr = new Date(t.updatedAt).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); @@ -230,6 +275,8 @@ export class FolkThreadGallery extends HTMLElement { } .card:hover { border-color: var(--rs-primary); transform: translateY(-2px); } .card--draft { border-left: 3px solid #f59e0b; } + .card--scheduled { border-left: 3px solid #60a5fa; } + .card--published { border-left: 3px solid #22c55e; } .card__badges { display: flex; gap: 0.4rem; flex-wrap: wrap; } .badge { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; @@ -237,7 +284,37 @@ export class FolkThreadGallery extends HTMLElement { } .badge--draft { background: rgba(245,158,11,0.15); color: #f59e0b; } .badge--scheduled { background: rgba(59,130,246,0.15); color: #60a5fa; } + .badge--published { background: rgba(34,197,94,0.15); color: #22c55e; } .badge--campaign { background: rgba(99,102,241,0.15); color: #a5b4fc; } + .filter-bar { + display: flex; gap: 0.4rem; flex-wrap: wrap; margin-bottom: 1.25rem; + } + .chip { + display: inline-flex; align-items: center; gap: 0.4rem; + padding: 0.4rem 0.85rem; border-radius: 999px; + background: var(--rs-bg-surface, #1e293b); + border: 1px solid var(--rs-input-border, #334155); + color: var(--rs-text-secondary, #94a3b8); + font-size: 0.8rem; font-weight: 600; + cursor: pointer; transition: background 0.12s, color 0.12s, border-color 0.12s; + font-family: inherit; + } + .chip:hover { background: rgba(20, 184, 166, 0.08); color: var(--rs-text-primary, #e2e8f0); } + .chip--active { + background: rgba(20, 184, 166, 0.18); + border-color: #14b8a6; + color: #5eead4; + } + .chip__count { + font-size: 0.7rem; font-weight: 700; + background: rgba(148, 163, 184, 0.2); + padding: 0.1rem 0.45rem; border-radius: 999px; + min-width: 1.2rem; text-align: center; + } + .chip--active .chip__count { + background: rgba(94, 234, 212, 0.2); + color: #5eead4; + } .card__title { font-size: 1rem; font-weight: 700; color: var(--rs-text-primary, #f1f5f9); margin: 0; line-height: 1.3; } .card__preview { font-size: 0.85rem; color: var(--rs-text-secondary, #94a3b8); line-height: 1.5; @@ -258,9 +335,21 @@ export class FolkThreadGallery extends HTMLElement {

Posts & Threads

New Thread + ${filterBar} ${threadCardsHTML} `; + + // Attach chip click handlers after render + this.shadowRoot.querySelectorAll('.chip').forEach(btn => { + btn.addEventListener('click', () => { + const s = (btn as HTMLElement).dataset.status as StatusFilter | undefined; + if (!s || s === this._statusFilter) return; + this._statusFilter = s; + try { localStorage.setItem(STATUS_STORAGE_KEY, s); } catch { /* ignore */ } + this.render(); + }); + }); } } diff --git a/modules/rsocials/mod.ts b/modules/rsocials/mod.ts index 8365bb2e..25d0dcd6 100644 --- a/modules/rsocials/mod.ts +++ b/modules/rsocials/mod.ts @@ -3026,7 +3026,7 @@ routes.get("/threads", (c) => { theme: "dark", body: ``, styles: ``, - scripts: ``, + scripts: ``, })); }); @@ -3169,7 +3169,7 @@ routes.get("/landing", (c) => { })); }); -// ── Default: rSocials hub with navigation ── +// ── Default: rSocials landing — posts gallery with draft/scheduled/published filter ── routes.get("/", (c) => { const space = c.req.param("space") || "demo"; @@ -3182,44 +3182,19 @@ routes.get("/", (c) => { spaceSlug: space, modules: getModuleInfoList(), theme: "dark", - styles: ``, - body: `
-

rSocials

-

Social media tools for your community

- -
`, + body: ` +`, + scripts: ``, })); });