diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index 11a785ac..64c6d072 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -881,6 +881,53 @@ export const EditorToolbar = ({ } }; + // Phase 3 D3.1 — BOM export. Pro-tier-gated by the backend (402 if not pro). + // We let everyone click; the 402 response feeds the upgrade prompt below + // so free/maker users hit the funnel naturally instead of an obviously- + // locked button (which they'd just dismiss). + const handleExportBom = async () => { + const projectId = currentProject?.id; + if (!projectId) { + setMessage({ type: 'error', text: 'Save the project before exporting a BOM.' }); + return; + } + try { + const resp = await fetch(`/api/pro/projects/${projectId}/bom.csv`, { + credentials: 'include', + }); + if (resp.status === 402) { + // Pro-required. Bounce to /pricing with a hint so the page can + // surface the right upgrade message. (The backend returned a + // structured `detail.error = 'pro_required'` payload but for the + // MVP we just route to pricing.) + window.location.href = '/pricing?from=bom_export'; + return; + } + if (resp.status === 401) { + window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`; + return; + } + if (!resp.ok) { + setMessage({ type: 'error', text: 'BOM export failed.' }); + return; + } + const blob = await resp.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + // Filename comes from Content-Disposition; pick a fallback. + const cd = resp.headers.get('Content-Disposition') || ''; + const m = /filename="?([^"]+)"?/.exec(cd); + a.download = m ? m[1] : `bom-${projectId}.csv`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch { + setMessage({ type: 'error', text: 'BOM export failed.' }); + } + }; + const handleFirmwareUpload = async (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (firmwareInputRef.current) firmwareInputRef.current.value = ''; @@ -1212,6 +1259,19 @@ export const EditorToolbar = ({ +