fix(examples): one malformed example must not take the gallery down

Typing anything into the /examples search box threw "Cannot read
properties of undefined (reading 'map')" and blanked the page. The
haystack the search matches against is built from
`example.components.map(...)`, and `components` was undefined on 27
examples.

`components` is required by `ExampleProject`, so this reads as
impossible — but the gallery is assembled from overlay example modules
whose factories end in `as unknown as ExampleProject`, and the compiler
stops checking there. The field is a promise the type makes and cannot
keep, so the three places that read it now cope with its absence.

CircuitPreview is the one that matters most: it renders as the fallback
whenever an example's thumbnail 404s, so it meets exactly the examples
nobody has generated art for yet.
This commit is contained in:
David Montero Crespo 2026-08-24 15:22:04 +02:00
parent 8cff0b31e4
commit b33f83b149
3 changed files with 18 additions and 8 deletions

View File

@ -274,17 +274,22 @@ export const CircuitPreview: React.FC<CircuitPreviewProps> = ({
const boardKind = example.boardType ?? (isBoardless ? null : 'arduino-uno');
const boardDef = boardKind ? BOARD_DEFS[boardKind] : undefined;
// Same defensive read as the gallery's search haystack: overlay example
// factories cast their result, so a missing `components` reaches here as
// undefined and this preview is exactly where it lands — it renders as the
// fallback whenever an example's thumbnail 404s.
const components = example.components ?? [];
// Check if the board is explicitly listed in components[]
const boardInComponents = example.components.some((c) => isBoardType(c.type));
const boardInComponents = components.some((c) => isBoardType(c.type));
if (boardDef && !boardInComponents && !isBoardless) {
// Board is implicit (pico, esp32, nano examples) — inject at default position
// Position it to the left of the other components
const minCompX =
example.components.length > 0 ? Math.min(...example.components.map((c) => c.x)) : 400;
const minCompX = components.length > 0 ? Math.min(...components.map((c) => c.x)) : 400;
const avgCompY =
example.components.length > 0
? example.components.reduce((s, c) => s + c.y, 0) / example.components.length
components.length > 0
? components.reduce((s, c) => s + c.y, 0) / components.length
: 150;
const boardX = Math.max(40, minCompX - boardDef.w - 60);
const boardY = Math.max(40, avgCompY - boardDef.h / 2);
@ -292,7 +297,7 @@ export const CircuitPreview: React.FC<CircuitPreviewProps> = ({
}
// Add all components from the example
example.components.forEach((c) => {
components.forEach((c) => {
// For board components that ARE in components[], use the board SVG def
const def =
isBoardType(c.type) && boardDef ? boardDef : getCompDef(c.type, c.properties ?? {});

View File

@ -133,7 +133,12 @@ export const ExamplesGallery: React.FC<ExamplesGalleryProps> = ({ onLoadExample
example.difficulty,
getBoardFilter(example),
...(example.tags ?? []),
...example.components.map((c) => c.type),
// Defensive. `components` is required by ExampleProject, but overlay
// example sets are built by factories that cast their result, so the
// compiler is not actually guarding this. One entry that omitted it took
// the entire gallery down with a TypeError the first time anyone typed in
// the search box — a whole page lost to one malformed example.
...(example.components ?? []).map((c) => c.type),
]
.join(' ')
.toLowerCase();

View File

@ -300,7 +300,7 @@ export const ExampleDetailPage: React.FC = () => {
}}
>
<span style={{ color: '#4fc3f7', fontWeight: 700 }}>✓</span>
{example.components.length > 0
{(example.components?.length ?? 0) > 0
? `${example.components.length} interactive component${example.components.length > 1 ? 's' : ''} on the canvas`
: 'Interactive simulation canvas'}
</li>