- )}
+ {/* Library install overlay used to live here — moved to
+ ExampleEditorPage now that loading runs at /example/. */}
);
};
diff --git a/frontend/src/pages/ExampleEditorPage.tsx b/frontend/src/pages/ExampleEditorPage.tsx
new file mode 100644
index 00000000..cfc58a08
--- /dev/null
+++ b/frontend/src/pages/ExampleEditorPage.tsx
@@ -0,0 +1,160 @@
+/**
+ * ExampleEditorPage — route `/example/:exampleId`.
+ *
+ * Paralelo a ProjectByIdPage (`/project/`) but for the built-in
+ * example projects. Loads the example into the editor + simulator
+ * stores AND keeps the URL pinned to `/example/` while the user
+ * runs / edits. That makes example links:
+ *
+ * - Shareable: copy the URL, send it, recipient lands on the same
+ * example pre-loaded.
+ * - Bookmarkable: a tab title and back-button history that point
+ * at the example, not at a generic `/editor`.
+ * - SEO-friendly: each example gets its own URL the same way
+ * /examples/ already gave it a landing page. The two co-
+ * exist on purpose — `/examples/` (plural) is the marketing
+ * landing with preview + description, `/example/` (singular)
+ * is the live editor with the example pre-loaded.
+ *
+ * If the user starts editing and clicks "Save", the pro overlay's
+ * save modal asks for a name and creates a NEW project (no project
+ * id is set on useProjectStore, so it can't overwrite anything).
+ */
+
+import { useEffect, useRef, useState } from 'react';
+import { useParams } from 'react-router-dom';
+import { exampleProjects } from '../data/examples';
+import { loadExample, type LibraryInstallProgress } from '../utils/loadExample';
+import { EditorPage } from './EditorPage';
+import { AppHeader } from '../components/layout/AppHeader';
+import { useSEO } from '../utils/useSEO';
+
+const DOMAIN = 'https://velxio.dev';
+
+export const ExampleEditorPage: React.FC = () => {
+ const { exampleId } = useParams<{ exampleId: string }>();
+ const [ready, setReady] = useState(false);
+ const [error, setError] = useState(false);
+ const [installing, setInstalling] = useState(null);
+ // Guard so React strict-mode (which fires effects twice in dev) doesn't
+ // run loadExample twice — and so the user can keep editing without the
+ // example reloading on every store-triggered re-render.
+ const loadedIdRef = useRef(null);
+
+ const example = exampleId
+ ? exampleProjects.find((e) => e.id === exampleId)
+ : null;
+
+ useSEO({
+ title: example
+ ? `${example.title} — Velxio Arduino Simulator`
+ : 'Example — Velxio',
+ description:
+ example?.description ?? 'Arduino example running on Velxio.',
+ url: example
+ ? `${DOMAIN}/example/${example.id}`
+ : `${DOMAIN}/examples`,
+ });
+
+ useEffect(() => {
+ if (!exampleId) {
+ setError(true);
+ return;
+ }
+ if (!example) {
+ setError(true);
+ return;
+ }
+ if (loadedIdRef.current === exampleId) return;
+ loadedIdRef.current = exampleId;
+
+ let cancelled = false;
+ setReady(false);
+ setError(false);
+ (async () => {
+ try {
+ await loadExample(example, setInstalling);
+ } catch {
+ // loadExample's internal failures (library install network errors)
+ // are swallowed inside ensureLibraries — anything that DOES bubble
+ // up here means the stores are partially populated. Surfacing a
+ // clean error is more useful than rendering an empty editor.
+ if (!cancelled) setError(true);
+ return;
+ }
+ if (!cancelled) setReady(true);
+ })();
+
+ return () => {
+ cancelled = true;
+ };
+ }, [exampleId, example]);
+
+ if (error) {
+ return (
+