/** * — Newsletter management UI backed by Listmonk API proxy. * * Attributes: space, role * Uses EncryptID access token for auth headers. * Three tabs: Lists, Subscribers, Campaigns. */ import { getAccessToken } from '../../../shared/components/rstack-identity'; type Tab = 'lists' | 'subscribers' | 'campaigns'; export class FolkNewsletterManager extends HTMLElement { private _space = 'demo'; private _role = 'viewer'; private _tab: Tab = 'lists'; private _configured = false; private _loading = true; private _error = ''; // Data private _lists: any[] = []; private _subscribers: any[] = []; private _subscriberTotal = 0; private _subscriberPage = 1; private _campaigns: any[] = []; private _showCreateForm = false; static get observedAttributes() { return ['space', 'role']; } connectedCallback() { if (!this.shadowRoot) this.attachShadow({ mode: 'open' }); this._space = this.getAttribute('space') || 'demo'; this._role = this.getAttribute('role') || 'viewer'; this.render(); this.checkStatus(); } attributeChangedCallback(name: string, _old: string, val: string) { if (name === 'space') this._space = val; if (name === 'role') this._role = val; } private apiBase(): string { return `/${this._space}/rsocials/api/newsletter`; } private async apiFetch(path: string, opts: RequestInit = {}): Promise { const headers = new Headers(opts.headers); const token = getAccessToken(); if (token) headers.set('Authorization', `Bearer ${token}`); if (opts.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json'); return fetch(`${this.apiBase()}${path}`, { ...opts, headers }); } private async checkStatus() { this._loading = true; this.render(); try { const res = await fetch(`${this.apiBase()}/status`); const data = await res.json(); this._configured = data.configured; if (this._configured) await this.loadTab(); } catch (e: any) { this._error = e.message || 'Failed to check status'; } this._loading = false; this.render(); } private async loadTab() { this._error = ''; this._loading = true; this.render(); try { if (this._tab === 'lists') await this.loadLists(); else if (this._tab === 'subscribers') await this.loadSubscribers(); else if (this._tab === 'campaigns') await this.loadCampaigns(); } catch (e: any) { this._error = e.message || 'Request failed'; } this._loading = false; this.render(); } private async loadLists() { const res = await this.apiFetch('/lists'); if (!res.ok) throw new Error(`Failed to load lists (${res.status})`); const data = await res.json(); this._lists = data.data?.results || data.results || []; } private async loadSubscribers() { const res = await this.apiFetch(`/subscribers?page=${this._subscriberPage}&per_page=50`); if (!res.ok) throw new Error(`Failed to load subscribers (${res.status})`); const data = await res.json(); this._subscribers = data.data?.results || data.results || []; this._subscriberTotal = data.data?.total || data.total || 0; } private async loadCampaigns() { const res = await this.apiFetch('/campaigns'); if (!res.ok) throw new Error(`Failed to load campaigns (${res.status})`); const data = await res.json(); this._campaigns = data.data?.results || data.results || []; } private async createCampaign(form: HTMLFormElement) { const fd = new FormData(form); const body = JSON.stringify({ name: fd.get('name'), subject: fd.get('subject'), body: fd.get('body') || '

Newsletter content

', content_type: 'richtext', type: 'regular', lists: this._lists.length > 0 ? [this._lists[0].id] : [], }); const res = await this.apiFetch('/campaigns', { method: 'POST', body }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.message || err.error || 'Failed to create campaign'); } this._showCreateForm = false; await this.loadCampaigns(); } private async setCampaignStatus(id: number, status: string) { const res = await this.apiFetch(`/campaigns/${id}/status`, { method: 'PUT', body: JSON.stringify({ status }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); this._error = err.message || err.error || 'Failed to update status'; this.render(); return; } await this.loadCampaigns(); this.render(); } private get isAdmin(): boolean { return this._role === 'admin'; } // ── Rendering ── private render() { const root = this.shadowRoot!; root.innerHTML = `${this.renderBody()}`; this.attachListeners(); } private renderBody(): string { if (this._loading && !this._configured && this._lists.length === 0) { return `
Loading...
`; } if (!this._configured) { return this.renderSetup(); } return `

Newsletter Manager

Manage mailing lists, subscribers, and email campaigns via Listmonk

${this._error ? `
${this.esc(this._error)}
` : ''}
${this._loading ? '
Loading...
' : this.renderTabContent()} `; } private renderSetup(): string { return `

Newsletter Not Configured

Connect your Listmonk instance to manage newsletters from here.

  1. Open the space settings panel (gear icon in the top bar)
  2. Find rSocials and click the settings gear
  3. Enter your Listmonk URL, username, and password
  4. Click Save Module Config
`; } private renderTabContent(): string { if (this._tab === 'lists') return this.renderLists(); if (this._tab === 'subscribers') return this.renderSubscribers(); return this.renderCampaigns(); } private renderLists(): string { if (this._lists.length === 0) return `
No mailing lists found
`; const rows = this._lists.map(l => ` ${this.esc(l.name)} ${this.esc(l.type)} ${l.subscriber_count ?? '—'} ${l.created_at ? new Date(l.created_at).toLocaleDateString() : '—'} `).join(''); return ` ${rows}
NameTypeSubscribersCreated
`; } private renderSubscribers(): string { if (this._subscribers.length === 0) return `
No subscribers found
`; const rows = this._subscribers.map(s => ` ${this.esc(s.email)} ${this.esc(s.name || '—')} ${this.esc(s.status)} ${(s.lists || []).map((l: any) => this.esc(l.name)).join(', ') || '—'} ${s.created_at ? new Date(s.created_at).toLocaleDateString() : '—'} `).join(''); const totalPages = Math.ceil(this._subscriberTotal / 50); const pagination = totalPages > 1 ? `
Page ${this._subscriberPage} of ${totalPages}
` : ''; return ` ${rows}
EmailNameStatusListsCreated
${pagination} `; } private renderCampaigns(): string { const createBtn = this.isAdmin ? `
` : ''; const form = this._showCreateForm && this.isAdmin ? `
` : ''; if (this._campaigns.length === 0 && !this._showCreateForm) { return `${createBtn}
No campaigns found
`; } const statusLabel = (s: string) => { const map: Record = { draft: 'draft', running: 'running', paused: 'paused', finished: 'finished', scheduled: 'active' }; return map[s] || s; }; const rows = this._campaigns.map(c => { const actions: string[] = []; if (c.status === 'draft' && this.isAdmin) { actions.push(``); } else if (c.status === 'running') { actions.push(``); } else if (c.status === 'paused') { actions.push(``); } return ` ${this.esc(c.name)} ${this.esc(c.subject || '—')} ${this.esc(c.status)} ${c.sent ?? '—'} ${c.created_at ? new Date(c.created_at).toLocaleDateString() : '—'} ${actions.join(' ')} `; }).join(''); return ` ${createBtn} ${form} ${rows}
NameSubjectStatusSentCreatedActions
`; } // ── Event listeners ── private attachListeners() { const root = this.shadowRoot!; // Tab switching root.querySelectorAll('.nl-tab').forEach(btn => { btn.addEventListener('click', () => { this._tab = (btn as HTMLElement).dataset.tab as Tab; this.loadTab(); }); }); // Pagination root.querySelector('[data-action="prev-page"]')?.addEventListener('click', () => { if (this._subscriberPage > 1) { this._subscriberPage--; this.loadTab(); } }); root.querySelector('[data-action="next-page"]')?.addEventListener('click', () => { this._subscriberPage++; this.loadTab(); }); // Create campaign toggle root.querySelectorAll('[data-action="toggle-create"]').forEach(btn => { btn.addEventListener('click', () => { this._showCreateForm = !this._showCreateForm; this.render(); }); }); // Create campaign form const form = root.querySelector('[data-form="create-campaign"]') as HTMLFormElement | null; form?.addEventListener('submit', async (e) => { e.preventDefault(); try { await this.createCampaign(form); this.render(); } catch (err: any) { this._error = err.message; this.render(); } }); // Campaign status actions root.querySelectorAll('[data-action="start-campaign"]').forEach(btn => { btn.addEventListener('click', () => this.setCampaignStatus(Number((btn as HTMLElement).dataset.id), 'running')); }); root.querySelectorAll('[data-action="pause-campaign"]').forEach(btn => { btn.addEventListener('click', () => this.setCampaignStatus(Number((btn as HTMLElement).dataset.id), 'paused')); }); root.querySelectorAll('[data-action="resume-campaign"]').forEach(btn => { btn.addEventListener('click', () => this.setCampaignStatus(Number((btn as HTMLElement).dataset.id), 'running')); }); } private esc(s: string): string { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } } customElements.define('folk-newsletter-manager', FolkNewsletterManager);