diff --git a/.gitignore b/.gitignore index 1214e917..8aa73488 100644 --- a/.gitignore +++ b/.gitignore @@ -114,3 +114,10 @@ backend/tests/test_wifi_webserver_e2e.py # Compiled binary test fixtures (large, should be built not committed) frontend/src/__tests__/fixtures/avr-blink/ frontend/src/__tests__/fixtures/rp2040-blink/ + +# Arduino compilation byproducts in test fixtures +**/*.ino.eep +**/*.ino.with_bootloader.bin +**/*.ino.with_bootloader.hex +**/*.ino.map +**/*.ino.uf2 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 58bb1384..37bacf44 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import { UserProfilePage } from './pages/UserProfilePage'; import { ProjectPage } from './pages/ProjectPage'; import { ProjectByIdPage } from './pages/ProjectByIdPage'; import { AdminPage } from './pages/AdminPage'; +import { ExampleLoaderPage } from './pages/ExampleLoaderPage'; import { ArduinoSimulatorPage } from './pages/ArduinoSimulatorPage'; import { ArduinoEmulatorPage } from './pages/ArduinoEmulatorPage'; import { AtmegaSimulatorPage } from './pages/AtmegaSimulatorPage'; @@ -37,6 +38,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/examples/ExamplesGallery.css b/frontend/src/components/examples/ExamplesGallery.css index e89ae53d..435289ec 100644 --- a/frontend/src/components/examples/ExamplesGallery.css +++ b/frontend/src/components/examples/ExamplesGallery.css @@ -267,6 +267,23 @@ } /* Empty state */ +.example-copy-link { + margin-left: auto; + background: none; + border: 1px solid transparent; + border-radius: 4px; + padding: 3px 5px; + cursor: pointer; + color: #888; + display: flex; + align-items: center; + transition: color 0.15s, border-color 0.15s; +} +.example-copy-link:hover { + color: #4fc3f7; + border-color: #4fc3f7; +} + .examples-empty { max-width: 1200px; margin: 80px auto; diff --git a/frontend/src/components/examples/ExamplesGallery.tsx b/frontend/src/components/examples/ExamplesGallery.tsx index f50d8724..d47b4b4f 100644 --- a/frontend/src/components/examples/ExamplesGallery.tsx +++ b/frontend/src/components/examples/ExamplesGallery.tsx @@ -4,7 +4,7 @@ * Displays a gallery of example Arduino projects that users can load and run */ -import React, { useState } from 'react'; +import React, { useState, useCallback } from 'react'; import { exampleProjects, type ExampleProject } from '../../data/examples'; import './ExamplesGallery.css'; @@ -42,6 +42,16 @@ export const ExamplesGallery: React.FC = ({ onLoadExample const [selectedBoard, setSelectedBoard] = useState('all'); const [selectedCategory, setSelectedCategory] = useState('all'); const [selectedDifficulty, setSelectedDifficulty] = useState('all'); + const [copiedId, setCopiedId] = useState(null); + + const handleCopyLink = useCallback((e: React.MouseEvent, exampleId: string) => { + e.stopPropagation(); // Don't trigger card click + const url = `${window.location.origin}/examples/${exampleId}`; + navigator.clipboard.writeText(url).then(() => { + setCopiedId(exampleId); + setTimeout(() => setCopiedId(null), 2000); + }); + }, []); const filteredExamples = exampleProjects.filter((example) => { const boardMatch = selectedBoard === 'all' || getBoardFilter(example) === selectedBoard; @@ -237,6 +247,22 @@ export const ExamplesGallery: React.FC = ({ onLoadExample {boardBadge.label} )} + diff --git a/frontend/src/components/layout/AppHeader.tsx b/frontend/src/components/layout/AppHeader.tsx index c36c4a90..130536c1 100644 --- a/frontend/src/components/layout/AppHeader.tsx +++ b/frontend/src/components/layout/AppHeader.tsx @@ -1,6 +1,8 @@ import { useState, useRef, useEffect } from 'react'; import { Link, useNavigate, useLocation } from 'react-router-dom'; import { useAuthStore } from '../../store/useAuthStore'; +import { useProjectStore } from '../../store/useProjectStore'; +import { ShareModal } from './ShareModal'; import { trackVisitGitHub, trackVisitDiscord } from '../../utils/analytics'; const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio'; @@ -13,8 +15,10 @@ export const AppHeader: React.FC = () => { const logout = useAuthStore((s) => s.logout); const navigate = useNavigate(); const location = useLocation(); + const currentProject = useProjectStore((s) => s.currentProject); const [dropdownOpen, setDropdownOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false); + const [showShareModal, setShowShareModal] = useState(false); const dropdownRef = useRef(null); useEffect(() => { @@ -80,8 +84,30 @@ export const AppHeader: React.FC = () => { - {/* Right: auth + mobile hamburger */} + {/* Right: share + auth + mobile hamburger */}
+ {/* Share button — visible when a project is loaded */} + {currentProject && location.pathname === '/editor' && ( + + )} + {/* Auth UI */} {user ? (
@@ -138,6 +164,8 @@ export const AppHeader: React.FC = () => {
+ + {showShareModal && setShowShareModal(false)} />} ); }; diff --git a/frontend/src/components/layout/SaveProjectModal.tsx b/frontend/src/components/layout/SaveProjectModal.tsx index 4d0b0730..365635fd 100644 --- a/frontend/src/components/layout/SaveProjectModal.tsx +++ b/frontend/src/components/layout/SaveProjectModal.tsx @@ -116,14 +116,35 @@ export const SaveProjectModal: React.FC = ({ onClose }) = placeholder="Optional" /> - +
setIsPublic(!isPublic)} + role="button" + tabIndex={0} + > +
+ {isPublic ? ( + + + + + + ) : ( + + + + + )} +
+
+ {isPublic ? 'Public' : 'Private'} +
+
+ {isPublic ? 'Anyone with the link can view' : 'Only you can see this'} +
+
+
+
+
+ + {/* Share link */} +
+ (e.target as HTMLInputElement).select()} + /> + +
+ + {!isPublic && ( +
+ This project is private. Others will see a 403 error when opening this link. +
+ )} + +
+ +
+ + + ); +}; + +const styles: Record = { + overlay: { + position: 'fixed', inset: 0, background: 'rgba(0,0,0,.6)', + display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, + }, + modal: { + background: '#252526', border: '1px solid #3c3c3c', borderRadius: 8, + padding: '1.75rem', width: 440, display: 'flex', flexDirection: 'column', gap: 16, + }, + title: { color: '#ccc', margin: 0, fontSize: 18, fontWeight: 600 }, + visibilityRow: { + display: 'flex', alignItems: 'center', justifyContent: 'space-between', + gap: 12, padding: '10px 12px', background: '#1e1e1e', + border: '1px solid #333', borderRadius: 6, + }, + visibilityInfo: { + display: 'flex', flexDirection: 'column', gap: 4, + }, + toggleBtn: { + background: 'transparent', border: '1px solid #555', borderRadius: 4, + color: '#ccc', padding: '6px 12px', fontSize: 12, cursor: 'pointer', + whiteSpace: 'nowrap', flexShrink: 0, + }, + linkRow: { display: 'flex', gap: 6 }, + linkInput: { + flex: 1, background: '#1e1e1e', border: '1px solid #444', borderRadius: 4, + padding: '8px 10px', color: '#4fc3f7', fontSize: 13, fontFamily: 'monospace', + outline: 'none', + }, + copyBtn: { + background: '#0e639c', border: 'none', borderRadius: 4, + color: '#fff', padding: '8px 16px', fontSize: 13, cursor: 'pointer', + fontWeight: 500, display: 'flex', alignItems: 'center', + }, + warning: { + background: '#3d2e00', border: '1px solid #f59e0b44', borderRadius: 4, + color: '#f59e0b', padding: '8px 12px', fontSize: 12, + }, + actions: { display: 'flex', justifyContent: 'flex-end' }, + closeBtn: { + background: 'transparent', border: '1px solid #555', borderRadius: 4, + color: '#ccc', padding: '8px 16px', fontSize: 13, cursor: 'pointer', + }, +}; diff --git a/frontend/src/pages/ExampleLoaderPage.tsx b/frontend/src/pages/ExampleLoaderPage.tsx new file mode 100644 index 00000000..92b88716 --- /dev/null +++ b/frontend/src/pages/ExampleLoaderPage.tsx @@ -0,0 +1,91 @@ +/** + * ExampleLoaderPage — loads an example by ID from the URL and redirects to the editor. + * + * Route: /examples/:exampleId + * Example: /examples/blink-led + */ + +import React, { useEffect, useState } from 'react'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { exampleProjects } from '../data/examples'; +import { loadExample, type LibraryInstallProgress } from '../utils/loadExample'; +import { AppHeader } from '../components/layout/AppHeader'; + +export const ExampleLoaderPage: React.FC = () => { + const { exampleId } = useParams<{ exampleId: string }>(); + const navigate = useNavigate(); + const [error, setError] = useState(false); + const [installing, setInstalling] = useState(null); + + useEffect(() => { + if (!exampleId) { setError(true); return; } + + const example = exampleProjects.find((e) => e.id === exampleId); + if (!example) { setError(true); return; } + + let cancelled = false; + (async () => { + await loadExample(example, setInstalling); + if (!cancelled) navigate('/editor', { replace: true }); + })(); + + return () => { cancelled = true; }; + }, [exampleId, navigate]); + + if (error) { + return ( +
+ +
+
404
+
+ Example "{exampleId}" not found. +
+ + Browse all examples + +
+
+ ); + } + + return ( +
+
+
+ Loading example... +
+ {installing && ( +
+
+ Installing libraries ({installing.done + 1}/{installing.total}) +
+
+ {installing.current} +
+
+
+
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/pages/ExamplesPage.tsx b/frontend/src/pages/ExamplesPage.tsx index 934357ef..32e8d818 100644 --- a/frontend/src/pages/ExamplesPage.tsx +++ b/frontend/src/pages/ExamplesPage.tsx @@ -10,165 +10,17 @@ import { ExamplesGallery } from '../components/examples/ExamplesGallery'; import { AppHeader } from '../components/layout/AppHeader'; import { useSEO } from '../utils/useSEO'; import { getSeoMeta } from '../seoRoutes'; -import { useEditorStore } from '../store/useEditorStore'; -import { useSimulatorStore } from '../store/useSimulatorStore'; -import { useVfsStore } from '../store/useVfsStore'; -import { isBoardComponent } from '../utils/boardPinMapping'; -import { getInstalledLibraries, installLibrary } from '../services/libraryService'; +import { loadExample, type LibraryInstallProgress } from '../utils/loadExample'; import type { ExampleProject } from '../data/examples'; -import { trackOpenExample } from '../utils/analytics'; -import type { BoardKind } from '../types/board'; export const ExamplesPage: React.FC = () => { useSEO(getSeoMeta('/examples')!); const navigate = useNavigate(); - const { setCode } = useEditorStore(); - const { setComponents, setWires, setBoardType, activeBoardId, boards, addBoard, removeBoard, setActiveBoardId } = useSimulatorStore(); - const [installing, setInstalling] = useState<{ total: number; done: number; current: string } | null>(null); - - /** Install any missing libraries required by the example (non-blocking UI). */ - const ensureLibraries = async (libs: string[]): Promise => { - if (libs.length === 0) return; - try { - const installed = await getInstalledLibraries(); - const installedNames = new Set( - installed.map((l) => (l.library?.name ?? l.name ?? '').toLowerCase()) - ); - const missing = libs.filter((l) => !installedNames.has(l.toLowerCase())); - if (missing.length === 0) return; - - setInstalling({ total: missing.length, done: 0, current: missing[0] }); - for (let i = 0; i < missing.length; i++) { - setInstalling({ total: missing.length, done: i, current: missing[i] }); - await installLibrary(missing[i]); - } - setInstalling(null); - } catch { - // If install fails (e.g. offline), continue anyway — compile will show the error - setInstalling(null); - } - }; + const [installing, setInstalling] = useState(null); const handleLoadExample = async (example: ExampleProject) => { - trackOpenExample(example.title); - // Auto-install required libraries before loading - if (example.libraries && example.libraries.length > 0) { - await ensureLibraries(example.libraries); - } - - if (example.boards && example.boards.length > 0) { - // ── Multi-board loading ─────────────────────────────────────────────── - // 1. Remove all current boards - const currentIds = boards.map((b) => b.id); - currentIds.forEach((id) => removeBoard(id)); - - // 2. Add each board from the example; addBoard returns deterministic IDs - example.boards.forEach((eb) => { - addBoard(eb.boardKind as BoardKind, eb.x, eb.y); - }); - - // 3. Load code + VFS per board - const { boards: newBoards } = useSimulatorStore.getState(); - example.boards.forEach((eb) => { - const boardId = eb.boardKind; // predictable: first board of each kind = boardKind string - const board = newBoards.find((b) => b.id === boardId); - if (!board) return; - - if (eb.code) { - const filename = boardId === 'arduino-uno' || boardId === 'arduino-nano' || boardId === 'arduino-mega' - ? 'sketch.ino' - : 'main.cpp'; - // loadFiles reads activeGroupId internally — switch to this board's group first - useEditorStore.getState().setActiveGroup(board.activeFileGroupId); - useEditorStore.getState().loadFiles([{ name: filename, content: eb.code }]); - } - - if (eb.vfsFiles && boardId === 'raspberry-pi-3') { - // Update VFS files by name (default tree has script.py and hello.sh) - const vfsState = useVfsStore.getState(); - const tree = vfsState.getTree(boardId); - for (const [nodeId, node] of Object.entries(tree)) { - if (node.type === 'file' && eb.vfsFiles[node.name] !== undefined) { - vfsState.setContent(boardId, nodeId, eb.vfsFiles[node.name]); - } - } - } - }); - - // 4. Set active board to the first non-Pi board (so editor shows Arduino code) - const firstArduino = example.boards.find((eb) => - eb.boardKind !== 'raspberry-pi-3' && eb.boardKind !== 'esp32' && - eb.boardKind !== 'esp32-s3' && eb.boardKind !== 'esp32-c3' - ); - if (firstArduino) { - setActiveBoardId(firstArduino.boardKind); - } - - // 5. Load components (filter out board components — they're placed via boards[]) - const componentsWithoutBoard = example.components.filter( - (comp) => - !comp.type.includes('arduino') && - !comp.type.includes('pico') && - !comp.type.includes('raspberry') && - !comp.type.includes('esp32') - ); - setComponents( - componentsWithoutBoard.map((comp) => ({ - id: comp.id, - metadataId: comp.type.replace('wokwi-', ''), - x: comp.x, - y: comp.y, - properties: comp.properties, - })) - ); - - // 6. Load wires — componentIds already match board instance IDs - setWires( - example.wires.map((wire) => ({ - id: wire.id, - start: { componentId: wire.start.componentId, pinName: wire.start.pinName, x: 0, y: 0 }, - end: { componentId: wire.end.componentId, pinName: wire.end.pinName, x: 0, y: 0 }, - color: wire.color, - waypoints: [], - })) - ); - } else { - // ── Single-board loading (original behaviour) ───────────────────────── - const targetBoard = example.boardType || 'arduino-uno'; - setBoardType(targetBoard); - setCode(example.code); - - const componentsWithoutBoard = example.components.filter( - (comp) => - !comp.type.includes('arduino') && - !comp.type.includes('pico') && - !comp.type.includes('esp32') - ); - setComponents( - componentsWithoutBoard.map((comp) => ({ - id: comp.id, - metadataId: comp.type.replace('wokwi-', ''), - x: comp.x, - y: comp.y, - properties: comp.properties, - })) - ); - - const boardInstanceId = activeBoardId ?? 'arduino-uno'; - const remapBoardId = (id: string) => isBoardComponent(id) ? boardInstanceId : id; - - setWires( - example.wires.map((wire) => ({ - id: wire.id, - start: { componentId: remapBoardId(wire.start.componentId), pinName: wire.start.pinName, x: 0, y: 0 }, - end: { componentId: remapBoardId(wire.end.componentId), pinName: wire.end.pinName, x: 0, y: 0 }, - color: wire.color, - waypoints: [], - })) - ); - } - + await loadExample(example, setInstalling); navigate('/editor'); }; diff --git a/frontend/src/pages/UserProfilePage.css b/frontend/src/pages/UserProfilePage.css index 5408b9d2..732cc3e9 100644 --- a/frontend/src/pages/UserProfilePage.css +++ b/frontend/src/pages/UserProfilePage.css @@ -128,6 +128,23 @@ font-family: var(--mono); } +.profile-share-btn { + background: none; + border: 1px solid transparent; + border-radius: 4px; + padding: 2px 4px; + cursor: pointer; + color: #888; + display: flex; + align-items: center; + margin-left: 4px; + transition: color 0.15s, border-color 0.15s; +} +.profile-share-btn:hover { + color: #4fc3f7; + border-color: #4fc3f7; +} + .profile-muted { color: var(--text-muted); font-size: 15px; diff --git a/frontend/src/pages/UserProfilePage.tsx b/frontend/src/pages/UserProfilePage.tsx index b77bb0fe..74a606cd 100644 --- a/frontend/src/pages/UserProfilePage.tsx +++ b/frontend/src/pages/UserProfilePage.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { Link, useParams } from 'react-router-dom'; import { getUserProjects, type ProjectResponse } from '../services/projectService'; import { useAuthStore } from '../store/useAuthStore'; @@ -30,6 +30,17 @@ export const UserProfilePage: React.FC = () => { }, [username]); const isOwn = user?.username === username; + const [copiedId, setCopiedId] = useState(null); + + const handleCopyLink = useCallback((e: React.MouseEvent, projectId: string) => { + e.preventDefault(); // Don't navigate via the + e.stopPropagation(); + const url = `${window.location.origin}/project/${projectId}`; + navigator.clipboard.writeText(url).then(() => { + setCopiedId(projectId); + setTimeout(() => setCopiedId(null), 2000); + }); + }, []); return (
@@ -58,6 +69,24 @@ export const UserProfilePage: React.FC = () => { {p.board_type} {!p.is_public && Private} {new Date(p.updated_at).toLocaleDateString()} + {p.is_public && ( + + )}
))} diff --git a/frontend/src/store/useProjectStore.ts b/frontend/src/store/useProjectStore.ts index 41c94106..ea934d71 100644 --- a/frontend/src/store/useProjectStore.ts +++ b/frontend/src/store/useProjectStore.ts @@ -11,10 +11,15 @@ interface ProjectState { currentProject: CurrentProject | null; setCurrentProject: (project: CurrentProject) => void; clearCurrentProject: () => void; + setVisibility: (isPublic: boolean) => void; } export const useProjectStore = create((set) => ({ currentProject: null, setCurrentProject: (project) => set({ currentProject: project }), clearCurrentProject: () => set({ currentProject: null }), + setVisibility: (isPublic) => + set((s) => + s.currentProject ? { currentProject: { ...s.currentProject, isPublic } } : s, + ), })); diff --git a/frontend/src/utils/loadExample.ts b/frontend/src/utils/loadExample.ts new file mode 100644 index 00000000..3a852f21 --- /dev/null +++ b/frontend/src/utils/loadExample.ts @@ -0,0 +1,176 @@ +/** + * Shared utility to load an example project into the editor and simulator stores. + * Used by both ExamplesPage (gallery click) and ExampleLoaderPage (direct URL). + */ + +import type { ExampleProject } from '../data/examples'; +import type { BoardKind } from '../types/board'; +import { useEditorStore } from '../store/useEditorStore'; +import { useSimulatorStore } from '../store/useSimulatorStore'; +import { useVfsStore } from '../store/useVfsStore'; +import { isBoardComponent } from './boardPinMapping'; +import { getInstalledLibraries, installLibrary } from '../services/libraryService'; +import { trackOpenExample } from './analytics'; + +export interface LibraryInstallProgress { + total: number; + done: number; + current: string; +} + +/** + * Install any missing Arduino libraries required by an example. + * Calls onProgress for UI updates; silently continues on failure. + */ +export async function ensureLibraries( + libs: string[], + onProgress?: (progress: LibraryInstallProgress | null) => void, +): Promise { + if (libs.length === 0) return; + try { + const installed = await getInstalledLibraries(); + const installedNames = new Set( + installed.map((l) => (l.library?.name ?? l.name ?? '').toLowerCase()), + ); + const missing = libs.filter((l) => !installedNames.has(l.toLowerCase())); + if (missing.length === 0) return; + + onProgress?.({ total: missing.length, done: 0, current: missing[0] }); + for (let i = 0; i < missing.length; i++) { + onProgress?.({ total: missing.length, done: i, current: missing[i] }); + await installLibrary(missing[i]); + } + onProgress?.(null); + } catch { + onProgress?.(null); + } +} + +/** + * Load an example project into the editor + simulator stores. + * Does NOT navigate — the caller is responsible for navigation. + */ +export async function loadExample( + example: ExampleProject, + onLibraryProgress?: (progress: LibraryInstallProgress | null) => void, +): Promise { + trackOpenExample(example.title); + + // Auto-install required libraries + if (example.libraries && example.libraries.length > 0) { + await ensureLibraries(example.libraries, onLibraryProgress); + } + + const { + setComponents, setWires, setBoardType, + activeBoardId, boards, addBoard, removeBoard, setActiveBoardId, + } = useSimulatorStore.getState(); + + if (example.boards && example.boards.length > 0) { + // ── Multi-board loading ─────────────────────────────────────────────── + const currentIds = boards.map((b) => b.id); + currentIds.forEach((id) => removeBoard(id)); + + example.boards.forEach((eb) => { + addBoard(eb.boardKind as BoardKind, eb.x, eb.y); + }); + + const { boards: newBoards } = useSimulatorStore.getState(); + example.boards.forEach((eb) => { + const boardId = eb.boardKind; + const board = newBoards.find((b) => b.id === boardId); + if (!board) return; + + if (eb.code) { + const filename = + boardId === 'arduino-uno' || boardId === 'arduino-nano' || boardId === 'arduino-mega' + ? 'sketch.ino' + : 'main.cpp'; + useEditorStore.getState().setActiveGroup(board.activeFileGroupId); + useEditorStore.getState().loadFiles([{ name: filename, content: eb.code }]); + } + + if (eb.vfsFiles && boardId === 'raspberry-pi-3') { + const vfsState = useVfsStore.getState(); + const tree = vfsState.getTree(boardId); + for (const [nodeId, node] of Object.entries(tree)) { + if (node.type === 'file' && eb.vfsFiles[node.name] !== undefined) { + vfsState.setContent(boardId, nodeId, eb.vfsFiles[node.name]); + } + } + } + }); + + const firstArduino = example.boards.find( + (eb) => + eb.boardKind !== 'raspberry-pi-3' && + eb.boardKind !== 'esp32' && + eb.boardKind !== 'esp32-s3' && + eb.boardKind !== 'esp32-c3', + ); + if (firstArduino) { + setActiveBoardId(firstArduino.boardKind); + } + + const componentsWithoutBoard = example.components.filter( + (comp) => + !comp.type.includes('arduino') && + !comp.type.includes('pico') && + !comp.type.includes('raspberry') && + !comp.type.includes('esp32'), + ); + setComponents( + componentsWithoutBoard.map((comp) => ({ + id: comp.id, + metadataId: comp.type.replace('wokwi-', ''), + x: comp.x, + y: comp.y, + properties: comp.properties, + })), + ); + + setWires( + example.wires.map((wire) => ({ + id: wire.id, + start: { componentId: wire.start.componentId, pinName: wire.start.pinName, x: 0, y: 0 }, + end: { componentId: wire.end.componentId, pinName: wire.end.pinName, x: 0, y: 0 }, + color: wire.color, + waypoints: [], + })), + ); + } else { + // ── Single-board loading ───────────────────────────────────────────── + const targetBoard = example.boardType || 'arduino-uno'; + setBoardType(targetBoard); + useEditorStore.getState().setCode(example.code); + + const componentsWithoutBoard = example.components.filter( + (comp) => + !comp.type.includes('arduino') && + !comp.type.includes('pico') && + !comp.type.includes('esp32'), + ); + setComponents( + componentsWithoutBoard.map((comp) => ({ + id: comp.id, + metadataId: comp.type.replace('wokwi-', ''), + x: comp.x, + y: comp.y, + properties: comp.properties, + })), + ); + + const boardInstanceId = activeBoardId ?? 'arduino-uno'; + const remapBoardId = (id: string) => (isBoardComponent(id) ? boardInstanceId : id); + + setWires( + example.wires.map((wire) => ({ + id: wire.id, + start: { componentId: remapBoardId(wire.start.componentId), pinName: wire.start.pinName, x: 0, y: 0 }, + end: { componentId: remapBoardId(wire.end.componentId), pinName: wire.end.pinName, x: 0, y: 0 }, + color: wire.color, + waypoints: [], + })), + ); + } +}