/** * — 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(); // Re-check status when module is configured inline this.addEventListener('module-configured', () => { this._configured = false; this._loading = true; 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 ``; } 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);