diff --git a/src/App.tsx b/src/App.tsx index a2caba6..c205ca3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,19 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { get, set } from 'idb-keyval' import { api, apiBlob } from './api' -import type { Block, BlockType, TemplateDocument, TemplateRecord, TemplateSummary, TemplateType } from './types' +import type { + ApiKeySummary, + Block, + BlockType, + TemplateDocument, + TemplateRecord, + TemplateSummary, + TemplateType +} from './types' -type Screen = 'templates' | 'editor' | 'preview' | 'data' +type Screen = 'templates' | 'editor' | 'preview' | 'data' | 'use' type SaveState = 'saved' | 'saving' | 'offline' +type Filter = 'all' | TemplateType type Props = { user: { @@ -14,6 +23,10 @@ type Props = { } } +type CreatedApiKey = ApiKeySummary & { + api_key: string +} + const defaults: Record> = { heading: { text: 'New heading' }, text: { text: 'Write something useful.' }, @@ -58,6 +71,11 @@ function blockSummary(block: Block) { return String(block.props.text || block.props.alt || blockLabels[block.type]) } +function formatDate(value?: string | null) { + if (!value) return 'Never' + return new Date(value).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }) +} + export function App({ user }: Props) { const [screen, setScreen] = useState('templates') const [templates, setTemplates] = useState([]) @@ -74,16 +92,41 @@ export function App({ user }: Props) { const [previewDevice, setPreviewDevice] = useState<'mobile' | 'desktop'>('mobile') const [dataText, setDataText] = useState('{}') const [error, setError] = useState('') + const [filter, setFilter] = useState('all') + const [search, setSearch] = useState('') + const [apiKeys, setApiKeys] = useState([]) + const [newKeyName, setNewKeyName] = useState('') + const [createdKey, setCreatedKey] = useState(null) + const [copied, setCopied] = useState('') + const loadedId = useRef(null) const timer = useRef(undefined) const selected = templateDoc.blocks.find(block => block.id === selectedId) || null const paths = useMemo(() => flatten(sampleData), [sampleData]) + const visibleTemplates = useMemo(() => { + const needle = search.trim().toLowerCase() + return templates.filter(template => { + if (filter !== 'all' && template.type !== filter) return false + if (!needle) return true + return `${template.name} ${template.template_key}`.toLowerCase().includes(needle) + }) + }, [templates, filter, search]) + + const counts = useMemo(() => ({ + total: templates.length, + report: templates.filter(template => template.type === 'report').length, + email: templates.filter(template => template.type === 'email').length + }), [templates]) async function refreshTemplates() { setTemplates(await api('/api/templates')) } + async function refreshApiKeys() { + setApiKeys(await api('/api/api-keys')) + } + useEffect(() => { void refreshTemplates().catch(err => setError(String(err))) }, []) @@ -126,6 +169,7 @@ export function App({ user }: Props) { setSampleData(local?.sampleData || remote.sample_data || {}) setDataText(JSON.stringify(local?.sampleData || remote.sample_data || {}, null, 2)) setSelectedId(null) + setCreatedKey(null) setScreen('editor') } @@ -202,6 +246,15 @@ export function App({ user }: Props) { } } + async function goUse() { + setScreen('use') + try { + await refreshApiKeys() + } catch (err) { + setError(String(err)) + } + } + async function downloadPdf() { if (!active) return await saveNow() @@ -241,6 +294,29 @@ export function App({ user }: Props) { updateBlock(selected.id, { [field]: `${String(selected.props[field] || '')}{{${path}}}` }) } + async function createApiKey() { + if (!newKeyName.trim()) return + const created = await api('/api/api-keys', { + method: 'POST', + body: JSON.stringify({ name: newKeyName.trim() }) + }) + setCreatedKey(created) + setNewKeyName('') + await refreshApiKeys() + } + + async function revokeApiKey(id: string) { + await api(`/api/api-keys/${id}`, { method: 'DELETE' }) + setCreatedKey(current => current?.id === id ? null : current) + await refreshApiKeys() + } + + async function copy(value: string, label: string) { + await navigator.clipboard.writeText(value) + setCopied(label) + window.setTimeout(() => setCopied(''), 1400) + } + if (screen === 'templates') { return (
@@ -252,23 +328,53 @@ export function App({ user }: Props) { -
-

Build email templates and reports from reusable blocks, right from your phone.

+
+
+ Design once. Pull anywhere. +

Email and report templates for your apps.

+

Build on mobile, publish a version, then consume it over an authenticated API.

+
+
+ + + +
+
+ +
+
+ ⌕ + setSearch(event.target.value)} placeholder="Search templates" /> +
+
+ {(['all', 'report', 'email'] as Filter[]).map(value => ( + + ))} +
{error &&
{error}
}
- {templates.map(template => ( - ))} - {!templates.length &&
No templates yet. Tap + to build your first one.
} + {!visibleTemplates.length &&
No templates match this view.
}