/** * Examples Gallery Component * * Displays a gallery of example Arduino projects that users can load and run */ import React, { useState, useCallback } from 'react'; import { exampleProjects, type ExampleProject } from '../../data/examples'; import { CircuitPreview } from './CircuitPreview'; import './ExamplesGallery.css'; interface ExamplesGalleryProps { onLoadExample: (example: ExampleProject) => void; } // ── Board config ─────────────────────────────────────────────────────────── interface BoardTab { id: string; label: string; color: string; bg: string; } const BOARD_TABS: BoardTab[] = [ { id: 'all', label: 'All', color: '#ffffff', bg: '#444444' }, { id: 'arduino-uno', label: 'Arduino Uno', color: '#ffffff', bg: '#007acc' }, { id: 'arduino-nano', label: 'Arduino Nano', color: '#ffffff', bg: '#0055aa' }, { id: 'arduino-mega', label: 'Arduino Mega', color: '#ffffff', bg: '#003388' }, { id: 'raspberry-pi-pico', label: 'Pico', color: '#ffffff', bg: '#c11c31' }, { id: 'pi-pico-w', label: 'Pico W (Wi-Fi)', color: '#ffffff', bg: '#8c0e1e' }, { id: 'esp32', label: 'ESP32 (Xtensa)', color: '#ffffff', bg: '#e77d11' }, { id: 'esp32-cam', label: 'ESP32-CAM', color: '#ffffff', bg: '#d35400' }, { id: 'esp32-c3', label: 'ESP32-C3 (RISC-V)', color: '#ffffff', bg: '#27ae60' }, { id: 'attiny85', label: 'ATtiny85', color: '#ffffff', bg: '#5d4037' }, { id: 'multi', label: 'Multi-Board', color: '#ffffff', bg: '#7b2d8b' }, { id: 'analog', label: 'Analog', color: '#ffffff', bg: '#0ea5a5' }, ]; function getBoardFilter(example: ExampleProject): string { if (example.boards) return 'multi'; if ((example as any).boardFilter) return (example as any).boardFilter; return example.boardType ?? 'arduino-uno'; } export const ExamplesGallery: React.FC = ({ onLoadExample }) => { const [selectedBoard, setSelectedBoard] = useState('all'); const [selectedCategory, setSelectedCategory] = useState( 'all', ); const [selectedDifficulty, setSelectedDifficulty] = useState< ExampleProject['difficulty'] | 'all' >('all'); const [copiedId, setCopiedId] = useState(null); const [search, setSearch] = useState(''); 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); }); }, []); // Pre-tokenise the search string once per keystroke. Each token must match // somewhere in the example's haystack, so users can type "esp32 oled dht" // and find every project that hits all three. const searchTokens = search .trim() .toLowerCase() .split(/\s+/) .filter(Boolean); const exampleHaystack = (example: ExampleProject): string => [ example.title, example.description, example.category, example.difficulty, getBoardFilter(example), ...(example.tags ?? []), ...example.components.map((c) => c.type), ] .join(' ') .toLowerCase(); const filteredExamples = exampleProjects.filter((example) => { const boardMatch = selectedBoard === 'all' || getBoardFilter(example) === selectedBoard; const catMatch = selectedCategory === 'all' || example.category === selectedCategory; const diffMatch = selectedDifficulty === 'all' || example.difficulty === selectedDifficulty; if (!boardMatch || !catMatch || !diffMatch) return false; if (searchTokens.length === 0) return true; const hay = exampleHaystack(example); return searchTokens.every((tok) => hay.includes(tok)); }); // Count per board for tab badges const boardCounts: Record = { all: exampleProjects.length }; exampleProjects.forEach((ex) => { const b = getBoardFilter(ex); boardCounts[b] = (boardCounts[b] ?? 0) + 1; }); const getCategoryIcon = (category: ExampleProject['category']): React.ReactNode => { const svgProps = { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const, style: { display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }, }; const icons: Record = { basics: ( ), sensors: ( ), displays: ( ), communication: ( ), games: ( ), robotics: ( ), circuits: ( ), }; return icons[category]; }; const getDifficultyColor = (difficulty: ExampleProject['difficulty']): string => ({ beginner: '#4ade80', intermediate: '#fbbf24', advanced: '#f87171', })[difficulty]; const getBoardBadge = ( example: ExampleProject, ): { label: string; color: string; bg: string } | null => { const bf = getBoardFilter(example); const tab = BOARD_TABS.find((t) => t.id === bf); if (!tab || tab.id === 'all') return null; return { label: tab.label, color: tab.color, bg: tab.bg }; }; return (

Featured Projects

Explore and run example projects — organized by board

{/* Search */}
setSearch(e.target.value)} aria-label="Search examples" /> {search && ( )}
{searchTokens.length > 0 && ( {filteredExamples.length} match {filteredExamples.length === 1 ? '' : 'es'} )}
{/* Board tabs */}
{BOARD_TABS.map((tab) => ( ))}
{/* Category + Difficulty filters */}
{( [ 'all', 'basics', 'sensors', 'displays', 'communication', 'games', 'robotics', 'circuits', ] as const ).map((cat) => ( ))}
{(['all', 'beginner', 'intermediate', 'advanced'] as const).map((diff) => ( ))}
{/* Examples Grid */}
{filteredExamples.map((example) => { const boardBadge = getBoardBadge(example); return (
onLoadExample(example)}>
{example.thumbnail ? ( {example.title} ) : ( )}

{example.title}

{example.description}

{example.difficulty} {getCategoryIcon(example.category)} {example.category} {boardBadge && ( {boardBadge.label} )}
); })}
{filteredExamples.length === 0 && (

No examples found {searchTokens.length > 0 ? ` for "${search.trim()}"` : ''} with the selected filters

{(searchTokens.length > 0 || selectedBoard !== 'all' || selectedCategory !== 'all' || selectedDifficulty !== 'all') && ( )}
)}
); };