diff --git a/src/App.tsx b/src/App.tsx index fd98b47..592bbb1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,8 @@ import type { TemplateSummary, TemplateType, ThemeSummary, - EmailRenderResult + EmailRenderResult, + FolderSummary } from './types' type Screen = 'templates' | 'editor' | 'preview' | 'data' | 'use' @@ -84,6 +85,11 @@ function formatDate(value?: string | null) { export function App({ user }: Props) { const [screen, setScreen] = useState('templates') const [templates, setTemplates] = useState([]) + const [folders, setFolders] = useState([]) + const [currentFolderId, setCurrentFolderId] = useState(null) + const [createFolderOpen, setCreateFolderOpen] = useState(false) + const [createFolderName, setCreateFolderName] = useState('') + const [moveTemplate, setMoveTemplate] = useState(null) const [active, setActive] = useState(null) const [templateDoc, setTemplateDoc] = useState({ blocks: [] }) const [sampleData, setSampleData] = useState>({}) @@ -120,11 +126,48 @@ export function App({ user }: Props) { const visibleTemplates = useMemo(() => { const needle = search.trim().toLowerCase() return templates.filter(template => { + if ((template.folder_id || null) !== currentFolderId) return false if (filter !== 'all' && template.type !== filter) return false if (!needle) return true return `${template.name} ${template.template_key}`.toLowerCase().includes(needle) }) - }, [templates, filter, search]) + }, [templates, filter, search, currentFolderId]) + + const visibleFolders = useMemo( + () => folders + .filter(folder => (folder.parent_id || null) === currentFolderId) + .sort((a, b) => a.name.localeCompare(b.name)), + [folders, currentFolderId] + ) + + const folderOptions = useMemo(() => { + const options: Array = [] + const walk = (parentId: string | null, depth: number) => { + folders + .filter(folder => (folder.parent_id || null) === parentId) + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach(folder => { + options.push({ ...folder, depth }) + walk(folder.id, depth + 1) + }) + } + walk(null, 0) + return options + }, [folders]) + + const breadcrumbs = useMemo(() => { + const result: FolderSummary[] = [] + let id = currentFolderId + const seen = new Set() + while (id && !seen.has(id)) { + seen.add(id) + const folder = folders.find(item => item.id === id) + if (!folder) break + result.unshift(folder) + id = folder.parent_id || null + } + return result + }, [folders, currentFolderId]) const counts = useMemo(() => ({ total: templates.length, @@ -136,6 +179,14 @@ export function App({ user }: Props) { setTemplates(await api('/api/templates')) } + async function refreshFolders() { + setFolders(await api('/api/folders')) + } + + async function refreshLibrary() { + await Promise.all([refreshTemplates(), refreshFolders()]) + } + async function refreshApiKeys() { setApiKeys(await api('/api/api-keys')) } @@ -145,7 +196,7 @@ export function App({ user }: Props) { } useEffect(() => { - void refreshTemplates().catch(err => setError(String(err))) + void refreshLibrary().catch(err => setError(String(err))) }, []) async function saveNow() { @@ -203,14 +254,42 @@ export function App({ user }: Props) { if (!createName.trim()) return const created = await api('/api/templates', { method: 'POST', - body: JSON.stringify({ name: createName.trim(), type: createType }) + body: JSON.stringify({ name: createName.trim(), type: createType, folderId: currentFolderId }) }) setCreateOpen(false) setCreateName('') - await refreshTemplates() + await refreshLibrary() await openTemplate(created.id) } + async function createFolder() { + if (!createFolderName.trim()) return + try { + await api('/api/folders', { + method: 'POST', + body: JSON.stringify({ name: createFolderName.trim(), parentId: currentFolderId }) + }) + setCreateFolderName('') + setCreateFolderOpen(false) + await refreshFolders() + } catch (err) { + setError(String(err)) + } + } + + async function moveTemplateTo(template: TemplateSummary, folderId: string | null) { + try { + await api(`/api/templates/${template.id}/folder`, { + method: 'PATCH', + body: JSON.stringify({ folderId }) + }) + setMoveTemplate(null) + await refreshTemplates() + } catch (err) { + setError(String(err)) + } + } + function addBlock(type: BlockType) { const block: Block = { id: crypto.randomUUID(), type, props: structuredClone(defaults[type]) } setTemplateDoc(current => ({ ...current, blocks: [...current.blocks, block] })) @@ -449,7 +528,13 @@ export function App({ user }: Props) { '/api/template-packs/import', file ) - await refreshTemplates() + if (currentFolderId) { + await api(`/api/templates/${imported.template.id}/folder`, { + method: 'PATCH', + body: JSON.stringify({ folderId: currentFolderId }) + }) + } + await refreshLibrary() await openTemplate(imported.template.id) } catch (err) { setError(String(err)) @@ -514,26 +599,77 @@ export function App({ user }: Props) { +
+
+ + +
+ + {!!visibleFolders.length && ( +
+ {visibleFolders.map(folder => { + const childFolders = folders.filter(item => (item.parent_id || null) === folder.id).length + return ( + + ) + })} +
+ )} +
+ {error &&
{error}
}
{visibleTemplates.map(template => ( - + › + + + ))} - {!visibleTemplates.length &&
No templates match this view.
} + {!visibleTemplates.length && !visibleFolders.length &&
This folder is empty.
}