feat(editor): add Export-Screenshot button (Phase 3 D3.2)

Front-end half of the schematic image export. New camera-icon button in
the editor toolbar between BOM and Upload-Firmware. Handler:

  1. POSTs to /api/pro/projects/{id}/screenshot.png (server renders the
     canvas with headless chromium, returns a PNG).
  2. 402 → /pricing?from=screenshot_export
  3. 401 → /login with redirect-back
  4. 422 → friendly "add at least one component" toast
  5. 200 → blob download with Content-Disposition filename
  6. The "rendering..." toast surfaces during the 5-10 s of headless
     chromium time so users know to wait, not click again.

i18n key editor.toolbar.exportScreenshot added in en/es/pt-br/zh-cn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
David Montero 2026-05-29 04:18:43 +02:00
parent b5a80a65b9
commit c957767e2e
5 changed files with 67 additions and 4 deletions

View File

@ -881,6 +881,54 @@ export const EditorToolbar = ({
}
};
// Phase 3 D3.2 — Schematic screenshot. Pro-tier-gated by the backend.
// Same UX pattern as BOM export: everyone can click; 402 redirects to
// /pricing. The server-side headless chromium renders the canvas and
// returns a PNG, which we trigger a download for.
const handleExportScreenshot = async () => {
const projectId = currentProject?.id;
if (!projectId) {
setMessage({ type: 'error', text: 'Save the project before exporting an image.' });
return;
}
setMessage({ type: 'info', text: 'Rendering screenshot — may take 5-10 seconds…' });
try {
const resp = await fetch(`/api/pro/projects/${projectId}/screenshot.png`, {
credentials: 'include',
});
if (resp.status === 402) {
window.location.href = '/pricing?from=screenshot_export';
return;
}
if (resp.status === 401) {
window.location.href = `/login?redirect=${encodeURIComponent(window.location.pathname)}`;
return;
}
if (resp.status === 422) {
setMessage({ type: 'error', text: 'Add at least one component to export an image.' });
return;
}
if (!resp.ok) {
setMessage({ type: 'error', text: 'Screenshot export failed.' });
return;
}
const blob = await resp.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const cd = resp.headers.get('Content-Disposition') || '';
const m = /filename="?([^"]+)"?/.exec(cd);
a.download = m ? m[1] : `velxio-${projectId}.png`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
setMessage({ type: 'success', text: 'Screenshot downloaded.' });
} catch {
setMessage({ type: 'error', text: 'Screenshot export failed.' });
}
};
// 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-
@ -1272,6 +1320,17 @@ export const EditorToolbar = ({
<line x1="9" y1="4" x2="9" y2="20" />
</svg>
</button>
<button
onClick={() => handleExportScreenshot()}
className="tb-btn"
title={t('editor.toolbar.exportScreenshot')}
>
{/* Camera/image icon — circuit-to-image export */}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
</button>
<button
onClick={() => firmwareInputRef.current?.click()}
className="tb-btn"

View File

@ -30,7 +30,8 @@
"cta": "Library Manager",
"dismiss": "Dismiss"
},
"exportBom": "Export Bill of Materials (CSV — Pro)"
"exportBom": "Export Bill of Materials (CSV — Pro)",
"exportScreenshot": "Export schematic as image (PNG — Pro)"
},
"fileExplorer": {
"workspace": "WORKSPACE",

View File

@ -30,7 +30,8 @@
"cta": "Gestor de librerías",
"dismiss": "Descartar"
},
"exportBom": "Exportar lista de materiales (CSV — Pro)"
"exportBom": "Exportar lista de materiales (CSV — Pro)",
"exportScreenshot": "Exportar esquemático como imagen (PNG — Pro)"
},
"fileExplorer": {
"workspace": "ESPACIO DE TRABAJO",

View File

@ -30,7 +30,8 @@
"cta": "Gerenciador de Bibliotecas",
"dismiss": "Dispensar"
},
"exportBom": "Exportar lista de materiais (CSV — Pro)"
"exportBom": "Exportar lista de materiais (CSV — Pro)",
"exportScreenshot": "Exportar esquemático como imagem (PNG — Pro)"
},
"fileExplorer": {
"workspace": "ESPAÇO DE TRABALHO",

View File

@ -30,7 +30,8 @@
"cta": "库管理器",
"dismiss": "忽略"
},
"exportBom": "导出物料清单 (CSV — Pro)"
"exportBom": "导出物料清单 (CSV — Pro)",
"exportScreenshot": "导出原理图为图片 (PNG — Pro)"
},
"fileExplorer": {
"workspace": "工作区",