/** * Popup entry point (vanilla TypeScript, no framework). * * Renders: * - Tab: Clip — title, space selector, parent page, selection preview, clip button * - Tab: Settings — API URL, token, default workspace/space IDs * * Flow: * 1. On open: load settings, query the active tab for page data. * 2. User fills in form and clicks "Clip". * 3. sendClip() hits POST /api/v1/clipper/import. * 4. On success: show result link. On error: show typed error message. */ import { getSettings, saveSettings } from '../lib/storage'; import { sendClip, ClipPayload } from '../lib/api-client'; import { t } from '../i18n/messages'; interface PageData { url: string; title: string; htmlSelection: string; } // --------------------------------------------------------------------------- // State // --------------------------------------------------------------------------- let pageData: PageData = { url: '', title: '', htmlSelection: '' }; // --------------------------------------------------------------------------- // DOM helpers // --------------------------------------------------------------------------- function el(id: string): T { const node = document.getElementById(id); if (!node) throw new Error(`Element #${id} not found in popup DOM`); return node as T; } function setText(id: string, text: string): void { el(id).textContent = text; } function setHidden(id: string, hidden: boolean): void { el(id).hidden = hidden; } // --------------------------------------------------------------------------- // Boot // --------------------------------------------------------------------------- async function init(): Promise { renderSkeleton(); const settings = await getSettings(); bindSettingsTab(settings); await loadPageData(); bindClipTab(settings); bindTabs(); } // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- function renderSkeleton(): void { const app = document.getElementById('app')!; app.innerHTML = `
${t('title')}
${t('selectionEmpty')}
`; } // --------------------------------------------------------------------------- // Page data // --------------------------------------------------------------------------- async function loadPageData(): Promise { try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab?.id) return; const response = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_DATA', }); if (response) { pageData = response as PageData; } } catch { // Content script not yet injected — use tab URL as fallback const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); pageData = { url: tab?.url ?? '', title: tab?.title ?? '', htmlSelection: '', }; } } // --------------------------------------------------------------------------- // Clip tab binding // --------------------------------------------------------------------------- function bindClipTab(settings: Awaited>): void { const titleInput = el('inp-title'); const spaceInput = el('inp-space'); const parentInput = el('inp-parent'); const selBadge = el('sel-badge'); const btnClip = el('btn-clip'); titleInput.value = pageData.title; spaceInput.value = settings.defaultSpaceId; if (pageData.htmlSelection) { selBadge.textContent = t('selectionPresent'); selBadge.classList.add('has-selection'); } const canClip = () => !!titleInput.value.trim() && !!spaceInput.value.trim() && !!settings.apiToken && !!settings.defaultWorkspaceId; const updateButton = () => { btnClip.disabled = !canClip(); }; titleInput.addEventListener('input', updateButton); spaceInput.addEventListener('input', updateButton); updateButton(); btnClip.addEventListener('click', async () => { const errorEl = el('clip-error'); const successEl = el('clip-success'); errorEl.hidden = true; successEl.hidden = true; btnClip.disabled = true; btnClip.innerHTML = ` ${t('btnClipping')}`; const payload: ClipPayload = { url: pageData.url, title: titleInput.value.trim(), html_selection: pageData.htmlSelection || undefined, target_workspace_id: settings.defaultWorkspaceId, target_space_id: spaceInput.value.trim(), target_parent_page_id: parentInput.value.trim() || undefined, }; try { const result = await sendClip(settings.apiUrl, settings.apiToken, payload); const pageUrl = `${settings.apiUrl}/p/${result.slugId}`; successEl.innerHTML = ` ${t('successTitle')} ${t('successOpen')} `; successEl.hidden = false; } catch (err: unknown) { const statusCode = err != null && typeof err === 'object' && 'statusCode' in err ? (err as { statusCode: number }).statusCode : 0; let msg = t('errorGeneric'); if (statusCode === 401 || statusCode === 403) { msg = t('errorUnauthorized'); } else if (err instanceof Error && err.message.startsWith('Network')) { msg = t('errorNetwork'); } else if (err != null && typeof err === 'object' && 'message' in err) { msg = String((err as { message: string }).message); } errorEl.textContent = msg; errorEl.hidden = false; } finally { btnClip.disabled = !canClip(); btnClip.textContent = t('btnClip'); } }); } // --------------------------------------------------------------------------- // Settings tab binding // --------------------------------------------------------------------------- function bindSettingsTab( settings: Awaited>, ): void { el('cfg-url').value = settings.apiUrl; el('cfg-token').value = settings.apiToken; el('cfg-ws').value = settings.defaultWorkspaceId; el('cfg-space').value = settings.defaultSpaceId; el('btn-save').addEventListener('click', async () => { await saveSettings({ apiUrl: el('cfg-url').value.trim(), apiToken: el('cfg-token').value.trim(), defaultWorkspaceId: el('cfg-ws').value.trim(), defaultSpaceId: el('cfg-space').value.trim(), }); const savedEl = el('settings-saved'); savedEl.hidden = false; setTimeout(() => { savedEl.hidden = true; }, 2000); }); } // --------------------------------------------------------------------------- // Tab switching // --------------------------------------------------------------------------- function bindTabs(): void { document.querySelectorAll('.tab-btn').forEach((btn) => { btn.addEventListener('click', () => { const target = btn.dataset['tab']; if (!target) return; document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'), ); document.querySelectorAll('.tab-panel').forEach((p) => p.classList.remove('active'), ); btn.classList.add('active'); document.getElementById(`tab-${target}`)?.classList.add('active'); }); }); } // --------------------------------------------------------------------------- // Entry // --------------------------------------------------------------------------- document.addEventListener('DOMContentLoaded', () => { init().catch(console.error); });