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:
parent
b5a80a65b9
commit
c957767e2e
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
"cta": "库管理器",
|
||||
"dismiss": "忽略"
|
||||
},
|
||||
"exportBom": "导出物料清单 (CSV — Pro)"
|
||||
"exportBom": "导出物料清单 (CSV — Pro)",
|
||||
"exportScreenshot": "导出原理图为图片 (PNG — Pro)"
|
||||
},
|
||||
"fileExplorer": {
|
||||
"workspace": "工作区",
|
||||
|
|
|
|||
Loading…
Reference in New Issue