Merge pull request #73 from davidmonterocrespo24/copilot/add-google-analytics-key-events-tracking

Copilot/add google analytics key events tracking
pull/74/head
David Montero Crespo 2026-03-25 03:07:11 -03:00 committed by GitHub
commit 91e9c2d5f6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 146 additions and 40 deletions

View File

@ -11,7 +11,7 @@ import { InstallLibrariesModal } from '../simulator/InstallLibrariesModal';
import { parseCompileResult } from '../../utils/compilationLogger'; import { parseCompileResult } from '../../utils/compilationLogger';
import type { CompilationLog } from '../../utils/compilationLogger'; import type { CompilationLog } from '../../utils/compilationLogger';
import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip'; import { exportToWokwiZip, importFromWokwiZip } from '../../utils/wokwiZip';
import { trackCompileCode, trackRunSimulation } from '../../utils/analytics'; import { trackCompileCode, trackRunSimulation, trackStopSimulation, trackResetSimulation, trackOpenLibraryManager } from '../../utils/analytics';
import './EditorToolbar.css'; import './EditorToolbar.css';
interface EditorToolbarProps { interface EditorToolbarProps {
@ -160,6 +160,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
const board = boards.find((b) => b.id === activeBoardId); const board = boards.find((b) => b.id === activeBoardId);
const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3'; const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3';
if (isQemuBoard || board?.compiledProgram) { if (isQemuBoard || board?.compiledProgram) {
trackRunSimulation(board?.boardKind);
startBoard(activeBoardId); startBoard(activeBoardId);
setMessage(null); setMessage(null);
return; return;
@ -176,12 +177,14 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
}; };
const handleStop = () => { const handleStop = () => {
trackStopSimulation();
if (activeBoardId) stopBoard(activeBoardId); if (activeBoardId) stopBoard(activeBoardId);
else stopSimulation(); else stopSimulation();
setMessage(null); setMessage(null);
}; };
const handleReset = () => { const handleReset = () => {
trackResetSimulation();
if (activeBoardId) resetBoard(activeBoardId); if (activeBoardId) resetBoard(activeBoardId);
else resetSimulation(); else resetSimulation();
setMessage(null); setMessage(null);
@ -431,7 +434,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
{/* Library Manager — always visible with label */} {/* Library Manager — always visible with label */}
<button <button
onClick={() => setLibManagerOpen(true)} onClick={() => { trackOpenLibraryManager(); setLibManagerOpen(true); }}
className="tb-btn-libraries" className="tb-btn-libraries"
title="Search and install Arduino libraries" title="Search and install Arduino libraries"
> >
@ -516,7 +519,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
<line x1="12" y1="16" x2="12.01" y2="16" /> <line x1="12" y1="16" x2="12.01" y2="16" />
</svg> </svg>
<span>Missing library? Install it from the</span> <span>Missing library? Install it from the</span>
<button className="tb-lib-hint-btn" onClick={() => { setLibManagerOpen(true); setMissingLibHint(false); }}> <button className="tb-lib-hint-btn" onClick={() => { trackOpenLibraryManager(); setLibManagerOpen(true); setMissingLibHint(false); }}>
Library Manager Library Manager
</button> </button>
<button className="tb-lib-hint-close" onClick={() => setMissingLibHint(false)} title="Dismiss"> <button className="tb-lib-hint-close" onClick={() => setMissingLibHint(false)} title="Dismiss">

View File

@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate, useLocation } from 'react-router-dom'; import { Link, useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../../store/useAuthStore'; import { useAuthStore } from '../../store/useAuthStore';
import { trackVisitGitHub } from '../../utils/analytics'; import { trackVisitGitHub, trackVisitDiscord } from '../../utils/analytics';
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio'; const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
const DISCORD_URL = 'https://discord.gg/rCScB9cG'; const DISCORD_URL = 'https://discord.gg/rCScB9cG';
@ -64,13 +64,13 @@ export const AppHeader: React.FC<AppHeaderProps> = () => {
<Link to="/docs" className={'header-nav-link' + isActive('/docs')}>Documentation</Link> <Link to="/docs" className={'header-nav-link' + isActive('/docs')}>Documentation</Link>
<Link to="/examples" className={'header-nav-link' + isActive('/examples')}>Examples</Link> <Link to="/examples" className={'header-nav-link' + isActive('/examples')}>Examples</Link>
<Link to="/editor" className={'header-nav-link' + isActive('/editor')}>Editor</Link> <Link to="/editor" className={'header-nav-link' + isActive('/editor')}>Editor</Link>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="header-nav-link"> <a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="header-nav-link" onClick={trackVisitGitHub}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}>
<path d="M12 2C6.477 2 2 6.484 2 12.021c0 4.428 2.865 8.185 6.839 9.504.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.342-3.369-1.342-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.026 2.747-1.026.546 1.378.202 2.397.1 2.65.64.7 1.028 1.595 1.028 2.688 0 3.848-2.338 4.695-4.566 4.944.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.203 22 16.447 22 12.021 22 6.484 17.523 2 12 2z" /> <path d="M12 2C6.477 2 2 6.484 2 12.021c0 4.428 2.865 8.185 6.839 9.504.5.092.682-.217.682-.482 0-.237-.009-.868-.013-1.703-2.782.605-3.369-1.342-3.369-1.342-.454-1.154-1.11-1.462-1.11-1.462-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.026 2.747-1.026.546 1.378.202 2.397.1 2.65.64.7 1.028 1.595 1.028 2.688 0 3.848-2.338 4.695-4.566 4.944.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.203 22 16.447 22 12.021 22 6.484 17.523 2 12 2z" />
</svg> </svg>
GitHub GitHub
</a> </a>
<a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" className="header-nav-link header-nav-discord"> <a href={DISCORD_URL} target="_blank" rel="noopener noreferrer" className="header-nav-link header-nav-discord" onClick={trackVisitDiscord}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}> <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" style={{ flexShrink: 0 }}>
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.032.053a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/> <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057c.002.022.015.043.032.053a19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg> </svg>

View File

@ -4,7 +4,7 @@ import { useEditorStore } from '../../store/useEditorStore';
import { useSimulatorStore } from '../../store/useSimulatorStore'; import { useSimulatorStore } from '../../store/useSimulatorStore';
import { useProjectStore } from '../../store/useProjectStore'; import { useProjectStore } from '../../store/useProjectStore';
import { createProject, updateProject } from '../../services/projectService'; import { createProject, updateProject } from '../../services/projectService';
import { trackCreateProject } from '../../utils/analytics'; import { trackCreateProject, trackSaveProject } from '../../utils/analytics';
interface SaveProjectModalProps { interface SaveProjectModalProps {
onClose: () => void; onClose: () => void;
@ -55,6 +55,7 @@ export const SaveProjectModal: React.FC<SaveProjectModalProps> = ({ onClose }) =
let saved; let saved;
if (isUpdate && currentProject) { if (isUpdate && currentProject) {
saved = await updateProject(currentProject.id, payload); saved = await updateProject(currentProject.id, payload);
trackSaveProject();
} else { } else {
saved = await createProject(payload); saved = await createProject(payload);
trackCreateProject(); trackCreateProject();

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { searchLibraries, installLibrary, getInstalledLibraries } from '../../services/libraryService'; import { searchLibraries, installLibrary, getInstalledLibraries } from '../../services/libraryService';
import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService'; import type { ArduinoLibrary, InstalledLibrary } from '../../services/libraryService';
import { trackInstallLibrary } from '../../utils/analytics';
import './LibraryManagerModal.css'; import './LibraryManagerModal.css';
interface LibraryManagerModalProps { interface LibraryManagerModalProps {
@ -79,6 +80,7 @@ export const LibraryManagerModal: React.FC<LibraryManagerModalProps> = ({ isOpen
try { try {
const result = await installLibrary(libName); const result = await installLibrary(libName);
if (result.success) { if (result.success) {
trackInstallLibrary(libName);
setStatusMsg({ type: 'success', text: `"${libName}" installed successfully!` }); setStatusMsg({ type: 'success', text: `"${libName}" installed successfully!` });
fetchInstalled(); // Refresh installed list so search tab reflects new state fetchInstalled(); // Refresh installed list so search tab reflects new state
} else { } else {

View File

@ -27,6 +27,7 @@ import type { ComponentMetadata } from '../../types/component-metadata';
import type { BoardKind } from '../../types/board'; import type { BoardKind } from '../../types/board';
import { BOARD_KIND_LABELS } from '../../types/board'; import { BOARD_KIND_LABELS } from '../../types/board';
import { useOscilloscopeStore } from '../../store/useOscilloscopeStore'; import { useOscilloscopeStore } from '../../store/useOscilloscopeStore';
import { trackSelectBoard, trackAddComponent, trackCreateWire, trackToggleSerialMonitor } from '../../utils/analytics';
import './SimulatorCanvas.css'; import './SimulatorCanvas.css';
export const SimulatorCanvas = () => { export const SimulatorCanvas = () => {
@ -747,6 +748,7 @@ export const SimulatorCanvas = () => {
const y = 100 + (row * gridSize); const y = 100 + (row * gridSize);
const component = createComponentFromMetadata(metadata, x, y); const component = createComponentFromMetadata(metadata, x, y);
trackAddComponent(metadata.id);
addComponent(component as any); addComponent(component as any);
setShowComponentPicker(false); setShowComponentPicker(false);
}; };
@ -1001,6 +1003,7 @@ export const SimulatorCanvas = () => {
if (wireInProgress) { if (wireInProgress) {
// Finish wire: connect to this pin // Finish wire: connect to this pin
finishWireCreation({ componentId, pinName, x, y }); finishWireCreation({ componentId, pinName, x, y });
trackCreateWire();
} else { } else {
// Start wire: auto-detect color from pin name // Start wire: auto-detect color from pin name
startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName)); startWireCreation({ componentId, pinName, x, y }, autoWireColor(pinName));
@ -1183,7 +1186,7 @@ export const SimulatorCanvas = () => {
{/* Serial Monitor toggle */} {/* Serial Monitor toggle */}
<button <button
onClick={toggleSerialMonitor} onClick={() => { toggleSerialMonitor(); trackToggleSerialMonitor(!serialMonitorOpen); }}
className={`canvas-serial-btn${serialMonitorOpen ? ' canvas-serial-btn-active' : ''}`} className={`canvas-serial-btn${serialMonitorOpen ? ' canvas-serial-btn-active' : ''}`}
title="Toggle Serial Monitor" title="Toggle Serial Monitor"
> >
@ -1405,6 +1408,7 @@ export const SimulatorCanvas = () => {
onClose={() => setShowComponentPicker(false)} onClose={() => setShowComponentPicker(false)}
onSelectComponent={handleSelectComponent} onSelectComponent={handleSelectComponent}
onSelectBoard={(kind: BoardKind) => { onSelectBoard={(kind: BoardKind) => {
trackSelectBoard(kind);
const sameKind = boards.filter((b) => b.boardKind === kind); const sameKind = boards.filter((b) => b.boardKind === kind);
const newBoardId = sameKind.length === 0 ? kind : `${kind}-${sameKind.length + 1}`; const newBoardId = sameKind.length === 0 ? kind : `${kind}-${sameKind.length + 1}`;
const x = boardPosition.x + boards.length * 60 + 420; const x = boardPosition.x + boards.length * 60 + 420;

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import './SEOPage.css'; import './SEOPage.css';
const META = getSeoMeta('/arduino-emulator')!; const META = getSeoMeta('/arduino-emulator')!;
@ -86,7 +87,7 @@ export const ArduinoEmulatorPage: React.FC = () => {
executed at 16 MHz the same silicon behavior as real hardware, without buying hardware. executed at 16 MHz the same silicon behavior as real hardware, without buying hardware.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open Emulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-emulator', '/editor')}>Open Emulator </Link>
<Link to="/docs/emulator" className="seo-btn-secondary">Emulation Details</Link> <Link to="/docs/emulator" className="seo-btn-secondary">Emulation Details</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · Built on avr8js &amp; rp2040js · No signup required</p> <p className="seo-trust">Free &amp; open-source · Built on avr8js &amp; rp2040js · No signup required</p>
@ -171,7 +172,7 @@ export const ArduinoEmulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Start emulating Arduino today</h2> <h2>Start emulating Arduino today</h2>
<p>Open the editor and execute your firmware against a real emulated CPU no hardware purchase, no cloud, no limits.</p> <p>Open the editor and execute your firmware against a real emulated CPU no hardware purchase, no cloud, no limits.</p>
<Link to="/editor" className="seo-btn-primary">Launch Emulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-emulator', '/editor')}>Launch Emulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/arduino-simulator">Arduino Simulator</Link> <Link to="/arduino-simulator">Arduino Simulator</Link>
<Link to="/atmega328p-simulator">ATmega328P Simulator</Link> <Link to="/atmega328p-simulator">ATmega328P Simulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import './SEOPage.css'; import './SEOPage.css';
const META = getSeoMeta('/arduino-mega-simulator')!; const META = getSeoMeta('/arduino-mega-simulator')!;
@ -86,7 +87,7 @@ export const ArduinoMegaSimulatorPage: React.FC = () => {
pins, 16 analog inputs, 4 serial ports, and 6 timers. Free and open-source. pins, 16 analog inputs, 4 serial ports, and 6 timers. Free and open-source.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open Mega 2560 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-mega-simulator', '/editor')}>Open Mega 2560 Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">Browse Examples</Link> <Link to="/examples" className="seo-btn-secondary">Browse Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · No signup required · Full ATmega2560 emulation</p> <p className="seo-trust">Free &amp; open-source · No signup required · Full ATmega2560 emulation</p>
@ -168,7 +169,7 @@ export const ArduinoMegaSimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Simulate your Arduino Mega project</h2> <h2>Simulate your Arduino Mega project</h2>
<p>Select the Mega 2560 board in the editor and start simulating full ATmega2560 emulation, no hardware purchase needed.</p> <p>Select the Mega 2560 board in the editor and start simulating full ATmega2560 emulation, no hardware purchase needed.</p>
<Link to="/editor" className="seo-btn-primary">Launch Mega 2560 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-mega-simulator', '/editor')}>Launch Mega 2560 Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/arduino-simulator">Arduino Simulator</Link> <Link to="/arduino-simulator">Arduino Simulator</Link>
<Link to="/arduino-emulator">Arduino Emulator</Link> <Link to="/arduino-emulator">Arduino Emulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import './SEOPage.css'; import './SEOPage.css';
const META = getSeoMeta('/arduino-simulator')!; const META = getSeoMeta('/arduino-simulator')!;
@ -86,7 +87,7 @@ export const ArduinoSimulatorPage: React.FC = () => {
electronic components. No install, no cloud, no account required. electronic components. No install, no cloud, no account required.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open Arduino Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-simulator', '/editor')}>Open Arduino Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">Browse Examples</Link> <Link to="/examples" className="seo-btn-secondary">Browse Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · No signup required · Runs 100% in your browser</p> <p className="seo-trust">Free &amp; open-source · No signup required · Runs 100% in your browser</p>
@ -167,7 +168,7 @@ export const ArduinoSimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate your Arduino?</h2> <h2>Ready to simulate your Arduino?</h2>
<p>Open the editor and start coding in seconds no setup, no install, no account needed.</p> <p>Open the editor and start coding in seconds no setup, no install, no account needed.</p>
<Link to="/editor" className="seo-btn-primary">Launch Arduino Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('arduino-simulator', '/editor')}>Launch Arduino Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/examples">Example Projects</Link> <Link to="/examples">Example Projects</Link>
<Link to="/arduino-emulator">Arduino Emulator</Link> <Link to="/arduino-emulator">Arduino Emulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import './SEOPage.css'; import './SEOPage.css';
const META = getSeoMeta('/atmega328p-simulator')!; const META = getSeoMeta('/atmega328p-simulator')!;
@ -86,7 +87,7 @@ export const AtmegaSimulatorPage: React.FC = () => {
16 MHz with full GPIO, timer, ADC, and USART emulation. No hardware, no install. 16 MHz with full GPIO, timer, ADC, and USART emulation. No hardware, no install.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open ATmega328P Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('atmega-simulator', '/editor')}>Open ATmega328P Simulator </Link>
<Link to="/docs/emulator" className="seo-btn-secondary">Technical Details</Link> <Link to="/docs/emulator" className="seo-btn-secondary">Technical Details</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · Genuine AVR8 emulation · Runs 100% in your browser</p> <p className="seo-trust">Free &amp; open-source · Genuine AVR8 emulation · Runs 100% in your browser</p>
@ -172,7 +173,7 @@ export const AtmegaSimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Simulate your ATmega328P code now</h2> <h2>Simulate your ATmega328P code now</h2>
<p>Open the editor, paste your sketch, and click Simulate no setup, no hardware purchase required.</p> <p>Open the editor, paste your sketch, and click Simulate no setup, no hardware purchase required.</p>
<Link to="/editor" className="seo-btn-primary">Launch ATmega328P Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('atmega-simulator', '/editor')}>Launch ATmega328P Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/arduino-simulator">Arduino Simulator</Link> <Link to="/arduino-simulator">Arduino Simulator</Link>
<Link to="/arduino-emulator">Arduino Emulator</Link> <Link to="/arduino-emulator">Arduino Emulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import esp32C3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-c3-devkitm-1/board.svg?url'; import esp32C3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-c3-devkitm-1/board.svg?url';
import './SEOPage.css'; import './SEOPage.css';
@ -86,7 +87,7 @@ export const Esp32C3SimulatorPage: React.FC = () => {
RV32IMC at 160 MHz with 48+ interactive components. RV32IMC at 160 MHz with 48+ interactive components.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open ESP32-C3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-c3-simulator', '/editor')}>Open ESP32-C3 Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">C3 Examples</Link> <Link to="/examples" className="seo-btn-secondary">C3 Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · 100% browser-native · No backend required</p> <p className="seo-trust">Free &amp; open-source · 100% browser-native · No backend required</p>
@ -168,7 +169,7 @@ export const Esp32C3SimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate ESP32-C3?</h2> <h2>Ready to simulate ESP32-C3?</h2>
<p>Open the editor, pick an ESP32-C3 board, and start coding runs instantly in your browser.</p> <p>Open the editor, pick an ESP32-C3 board, and start coding runs instantly in your browser.</p>
<Link to="/editor" className="seo-btn-primary">Launch ESP32-C3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-c3-simulator', '/editor')}>Launch ESP32-C3 Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/esp32-simulator">ESP32 Simulator</Link> <Link to="/esp32-simulator">ESP32 Simulator</Link>
<Link to="/esp32-s3-simulator">ESP32-S3 Simulator</Link> <Link to="/esp32-s3-simulator">ESP32-S3 Simulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import esp32S3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-s3-devkitc-1/board.svg?url'; import esp32S3SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-s3-devkitc-1/board.svg?url';
import './SEOPage.css'; import './SEOPage.css';
@ -86,7 +87,7 @@ export const Esp32S3SimulatorPage: React.FC = () => {
USB OTG, vector extensions, 45 GPIOs. Write, compile, and run in seconds. USB OTG, vector extensions, 45 GPIOs. Write, compile, and run in seconds.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open ESP32-S3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-s3-simulator', '/editor')}>Open ESP32-S3 Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">Browse Examples</Link> <Link to="/examples" className="seo-btn-secondary">Browse Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · QEMU Xtensa LX7 · No account needed</p> <p className="seo-trust">Free &amp; open-source · QEMU Xtensa LX7 · No account needed</p>
@ -150,7 +151,7 @@ export const Esp32S3SimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate ESP32-S3?</h2> <h2>Ready to simulate ESP32-S3?</h2>
<p>Open the editor, select an ESP32-S3 board, and run your code instantly.</p> <p>Open the editor, select an ESP32-S3 board, and run your code instantly.</p>
<Link to="/editor" className="seo-btn-primary">Launch ESP32-S3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-s3-simulator', '/editor')}>Launch ESP32-S3 Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/esp32-simulator">ESP32 Simulator</Link> <Link to="/esp32-simulator">ESP32 Simulator</Link>
<Link to="/esp32-c3-simulator">ESP32-C3 Simulator</Link> <Link to="/esp32-c3-simulator">ESP32-C3 Simulator</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import esp32SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-devkit-v1/board.svg?url'; import esp32SvgUrl from '../../../wokwi-libs/wokwi-boards/boards/esp32-devkit-v1/board.svg?url';
import './SEOPage.css'; import './SEOPage.css';
@ -88,7 +89,7 @@ export const Esp32SimulatorPage: React.FC = () => {
48+ interactive components, Serial Monitor, no install required. 48+ interactive components, Serial Monitor, no install required.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open ESP32 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-simulator', '/editor')}>Open ESP32 Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">ESP32 Examples</Link> <Link to="/examples" className="seo-btn-secondary">ESP32 Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · No signup · QEMU-powered emulation</p> <p className="seo-trust">Free &amp; open-source · No signup · QEMU-powered emulation</p>
@ -183,7 +184,7 @@ export const Esp32SimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate your ESP32?</h2> <h2>Ready to simulate your ESP32?</h2>
<p>Open the editor, select an ESP32 board, and start coding no setup, no install, no account needed.</p> <p>Open the editor, select an ESP32 board, and start coding no setup, no install, no account needed.</p>
<Link to="/editor" className="seo-btn-primary">Launch ESP32 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('esp32-simulator', '/editor')}>Launch ESP32 Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/examples">Example Projects</Link> <Link to="/examples">Example Projects</Link>
<Link to="/docs/esp32-emulation">ESP32 Docs</Link> <Link to="/docs/esp32-emulation">ESP32 Docs</Link>

View File

@ -38,9 +38,6 @@ export const ExamplesPage: React.FC = () => {
const missing = libs.filter((l) => !installedNames.has(l.toLowerCase())); const missing = libs.filter((l) => !installedNames.has(l.toLowerCase()));
if (missing.length === 0) return; if (missing.length === 0) return;
const handleLoadExample = (example: ExampleProject) => {
console.log('Loading example:', example.title);
trackOpenExample(example.title);
setInstalling({ total: missing.length, done: 0, current: missing[0] }); setInstalling({ total: missing.length, done: 0, current: missing[0] });
for (let i = 0; i < missing.length; i++) { for (let i = 0; i < missing.length; i++) {
setInstalling({ total: missing.length, done: i, current: missing[i] }); setInstalling({ total: missing.length, done: i, current: missing[i] });
@ -54,6 +51,7 @@ export const ExamplesPage: React.FC = () => {
}; };
const handleLoadExample = async (example: ExampleProject) => { const handleLoadExample = async (example: ExampleProject) => {
trackOpenExample(example.title);
// Auto-install required libraries before loading // Auto-install required libraries before loading
if (example.libraries && example.libraries.length > 0) { if (example.libraries && example.libraries.length > 0) {
await ensureLibraries(example.libraries); await ensureLibraries(example.libraries);

View File

@ -1,7 +1,7 @@
import { useState, useRef, useEffect } from 'react'; import { useState, useRef, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom'; import { Link, useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
import { trackVisitGitHub } from '../utils/analytics'; import { trackVisitGitHub, trackClickCTA } from '../utils/analytics';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
@ -496,7 +496,7 @@ export const LandingPage: React.FC = () => {
Raspberry Pi Pico, Raspberry Pi 3, and more. No hardware, no cloud, no limits. Raspberry Pi Pico, Raspberry Pi 3, and more. No hardware, no cloud, no limits.
</p> </p>
<div className="hero-ctas"> <div className="hero-ctas">
<Link to="/editor" className="cta-primary"> <Link to="/editor" className="cta-primary" onClick={() => trackClickCTA('landing', '/editor')}>
<IcoZap /> <IcoZap />
Try Simulator Free Try Simulator Free
</Link> </Link>

View File

@ -3,6 +3,7 @@ import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { login, initiateGoogleLogin } from '../services/authService'; import { login, initiateGoogleLogin } from '../services/authService';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { trackLogin } from '../utils/analytics';
export const LoginPage: React.FC = () => { export const LoginPage: React.FC = () => {
useSEO({ useSEO({
@ -25,6 +26,7 @@ export const LoginPage: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const user = await login(email, password); const user = await login(email, password);
trackLogin('email');
setUser(user); setUser(user);
navigate(searchParams.get('redirect') || '/'); navigate(searchParams.get('redirect') || '/');
} catch (err: any) { } catch (err: any) {
@ -71,7 +73,7 @@ export const LoginPage: React.FC = () => {
<div className="ap-divider">or</div> <div className="ap-divider">or</div>
<button onClick={initiateGoogleLogin} className="ap-btn-white"> <button onClick={() => { trackLogin('google'); initiateGoogleLogin(); }} className="ap-btn-white">
<svg width="18" height="18" viewBox="0 0 48 48"> <svg width="18" height="18" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/> <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/> <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import piPicoSvgUrl from '../../../wokwi-libs/wokwi-boards/boards/pi-pico/board.svg?url'; import piPicoSvgUrl from '../../../wokwi-libs/wokwi-boards/boards/pi-pico/board.svg?url';
import './SEOPage.css'; import './SEOPage.css';
@ -85,7 +86,7 @@ export const RaspberryPiPicoSimulatorPage: React.FC = () => {
at 133 MHz. 26 GPIO pins, I2C, SPI, UART, ADC. No hardware needed. at 133 MHz. 26 GPIO pins, I2C, SPI, UART, ADC. No hardware needed.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open Pico Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('rpi-pico-simulator', '/editor')}>Open Pico Simulator </Link>
<Link to="/examples" className="seo-btn-secondary">Pico Examples</Link> <Link to="/examples" className="seo-btn-secondary">Pico Examples</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · rp2040js emulation · No account needed</p> <p className="seo-trust">Free &amp; open-source · rp2040js emulation · No account needed</p>
@ -163,7 +164,7 @@ export const RaspberryPiPicoSimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate Raspberry Pi Pico?</h2> <h2>Ready to simulate Raspberry Pi Pico?</h2>
<p>Open the editor, select a Pico board, and start coding no Raspberry Pi hardware required.</p> <p>Open the editor, select a Pico board, and start coding no Raspberry Pi hardware required.</p>
<Link to="/editor" className="seo-btn-primary">Launch Pico Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('rpi-pico-simulator', '/editor')}>Launch Pico Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/examples">Example Projects</Link> <Link to="/examples">Example Projects</Link>
<Link to="/docs/rp2040-emulation">RP2040 Docs</Link> <Link to="/docs/rp2040-emulation">RP2040 Docs</Link>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg'; import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './SEOPage.css'; import './SEOPage.css';
@ -86,7 +87,7 @@ export const RaspberryPiSimulatorPage: React.FC = () => {
Write Python, control GPIO, install packages. No hardware needed. Write Python, control GPIO, install packages. No hardware needed.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary">Open Pi 3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('rpi-simulator', '/editor')}>Open Pi 3 Simulator </Link>
<Link to="/docs/raspberry-pi3-emulation" className="seo-btn-secondary">Read the Docs</Link> <Link to="/docs/raspberry-pi3-emulation" className="seo-btn-secondary">Read the Docs</Link>
</div> </div>
<p className="seo-trust">Free &amp; open-source · QEMU ARM64 · Full Raspberry Pi OS</p> <p className="seo-trust">Free &amp; open-source · QEMU ARM64 · Full Raspberry Pi OS</p>
@ -155,7 +156,7 @@ export const RaspberryPiSimulatorPage: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Ready to simulate Raspberry Pi 3?</h2> <h2>Ready to simulate Raspberry Pi 3?</h2>
<p>Open the editor, select Raspberry Pi 3, and boot into Linux right in your browser.</p> <p>Open the editor, select Raspberry Pi 3, and boot into Linux right in your browser.</p>
<Link to="/editor" className="seo-btn-primary">Launch Pi 3 Simulator </Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('rpi-simulator', '/editor')}>Launch Pi 3 Simulator </Link>
<div className="seo-internal-links"> <div className="seo-internal-links">
<Link to="/raspberry-pi-pico-simulator">Pico Simulator</Link> <Link to="/raspberry-pi-pico-simulator">Pico Simulator</Link>
<Link to="/esp32-simulator">ESP32 Simulator</Link> <Link to="/esp32-simulator">ESP32 Simulator</Link>

View File

@ -4,6 +4,7 @@ import { register, initiateGoogleLogin } from '../services/authService';
import { useAuthStore } from '../store/useAuthStore'; import { useAuthStore } from '../store/useAuthStore';
import { RESERVED_USERNAMES } from '../utils/reservedUsernames'; import { RESERVED_USERNAMES } from '../utils/reservedUsernames';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { trackSignUp } from '../utils/analytics';
export const RegisterPage: React.FC = () => { export const RegisterPage: React.FC = () => {
useSEO({ useSEO({
@ -37,6 +38,7 @@ export const RegisterPage: React.FC = () => {
setLoading(true); setLoading(true);
try { try {
const user = await register(username.toLowerCase(), email, password); const user = await register(username.toLowerCase(), email, password);
trackSignUp('email');
setUser(user); setUser(user);
navigate('/editor'); navigate('/editor');
} catch (err: any) { } catch (err: any) {
@ -95,7 +97,7 @@ export const RegisterPage: React.FC = () => {
<div className="ap-divider">or</div> <div className="ap-divider">or</div>
<button onClick={initiateGoogleLogin} className="ap-btn-white"> <button onClick={() => { trackSignUp('google'); initiateGoogleLogin(); }} className="ap-btn-white">
<svg width="18" height="18" viewBox="0 0 48 48"> <svg width="18" height="18" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/> <path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/> <path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>

View File

@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader'; import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO'; import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes'; import { getSeoMeta } from '../seoRoutes';
import { trackClickCTA } from '../utils/analytics';
import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg'; import raspberryPi3Svg from '../assets/Raspberry_Pi_3_illustration.svg';
import './SEOPage.css'; import './SEOPage.css';
import './Velxio2Page.css'; import './Velxio2Page.css';
@ -265,7 +266,7 @@ export const Velxio2Page: React.FC = () => {
Free, open-source, no install needed. Free, open-source, no install needed.
</p> </p>
<div className="seo-cta-group"> <div className="seo-cta-group">
<Link to="/editor" className="seo-btn-primary"> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('velxio-v2', '/editor')}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" /></svg> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" /></svg>
Try Velxio 2.0 Try Velxio 2.0
</Link> </Link>
@ -603,7 +604,7 @@ export const Velxio2Page: React.FC = () => {
<div className="seo-bottom"> <div className="seo-bottom">
<h2>Try Velxio 2.0 now</h2> <h2>Try Velxio 2.0 now</h2>
<p>Open the editor and start simulating 19 boards, 68+ examples, zero setup.</p> <p>Open the editor and start simulating 19 boards, 68+ examples, zero setup.</p>
<Link to="/editor" className="seo-btn-primary">Launch Simulator</Link> <Link to="/editor" className="seo-btn-primary" onClick={() => trackClickCTA('velxio-v2', '/editor')}>Launch Simulator</Link>
<div className="v2-bottom-community"> <div className="v2-bottom-community">
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="v2-community-btn v2-star-btn"> <a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="v2-community-btn v2-star-btn">

View File

@ -13,11 +13,55 @@ function fireEvent(eventName: string, params: Record<string, string | number | b
} }
} }
// ── Simulation ──────────────────────────────────────────────────────────────
/** Fired when the user starts a simulation (clicks Run). */ /** Fired when the user starts a simulation (clicks Run). */
export function trackRunSimulation(): void { export function trackRunSimulation(board?: string): void {
fireEvent('run_simulation', { event_category: 'engagement' }); fireEvent('run_simulation', {
event_category: 'engagement',
...(board ? { board } : {}),
});
} }
/** Fired when the user stops a running simulation. */
export function trackStopSimulation(): void {
fireEvent('stop_simulation', { event_category: 'engagement' });
}
/** Fired when the user resets a simulation. */
export function trackResetSimulation(): void {
fireEvent('reset_simulation', { event_category: 'engagement' });
}
// ── Editor / Canvas ─────────────────────────────────────────────────────────
/** Fired when code compilation starts. */
export function trackCompileCode(): void {
fireEvent('compile_code', { event_category: 'development' });
}
/** Fired when the user changes the board type. */
export function trackSelectBoard(board: string): void {
fireEvent('select_board', { event_category: 'engagement', board });
}
/** Fired when the user adds a component to the canvas. */
export function trackAddComponent(componentType: string): void {
fireEvent('add_component', { event_category: 'engagement', component_type: componentType });
}
/** Fired when the user finishes creating a wire between two pins. */
export function trackCreateWire(): void {
fireEvent('create_wire', { event_category: 'engagement' });
}
/** Fired when the user opens or closes the Serial Monitor. */
export function trackToggleSerialMonitor(open: boolean): void {
fireEvent('toggle_serial_monitor', { event_category: 'engagement', state: open ? 'open' : 'close' });
}
// ── Examples ────────────────────────────────────────────────────────────────
/** Fired when a user loads a sample project from the examples gallery. */ /** Fired when a user loads a sample project from the examples gallery. */
export function trackOpenExample(exampleTitle?: string): void { export function trackOpenExample(exampleTitle?: string): void {
fireEvent('open_example', { fireEvent('open_example', {
@ -26,17 +70,57 @@ export function trackOpenExample(exampleTitle?: string): void {
}); });
} }
// ── Projects ────────────────────────────────────────────────────────────────
/** Fired when a user successfully creates a new project. */ /** Fired when a user successfully creates a new project. */
export function trackCreateProject(): void { export function trackCreateProject(): void {
fireEvent('create_project', { event_category: 'engagement' }); fireEvent('create_project', { event_category: 'engagement' });
} }
/** Fired when code compilation starts. */ /** Fired when a user saves/updates an existing project. */
export function trackCompileCode(): void { export function trackSaveProject(): void {
fireEvent('compile_code', { event_category: 'development' }); fireEvent('save_project', { event_category: 'engagement' });
} }
// ── Auth ────────────────────────────────────────────────────────────────────
/** Fired on successful sign-up. */
export function trackSignUp(method: 'email' | 'google'): void {
fireEvent('sign_up', { event_category: 'auth', method });
}
/** Fired on successful login. */
export function trackLogin(method: 'email' | 'google'): void {
fireEvent('login', { event_category: 'auth', method });
}
// ── External Links ──────────────────────────────────────────────────────────
/** Fired when a user clicks any GitHub repository link. */ /** Fired when a user clicks any GitHub repository link. */
export function trackVisitGitHub(): void { export function trackVisitGitHub(): void {
fireEvent('visit_github', { event_category: 'external_link' }); fireEvent('visit_github', { event_category: 'external_link' });
} }
/** Fired when a user clicks any Discord invite link. */
export function trackVisitDiscord(): void {
fireEvent('visit_discord', { event_category: 'external_link' });
}
// ── CTA / Conversion ────────────────────────────────────────────────────────
/** Fired when a user clicks a CTA button on a landing/SEO page. */
export function trackClickCTA(source: string, destination: string): void {
fireEvent('click_cta', { event_category: 'conversion', source, destination });
}
// ── Library Manager ─────────────────────────────────────────────────────────
/** Fired when a user opens the Library Manager panel. */
export function trackOpenLibraryManager(): void {
fireEvent('open_library_manager', { event_category: 'engagement' });
}
/** Fired when a user installs a library. */
export function trackInstallLibrary(libraryName: string): void {
fireEvent('install_library', { event_category: 'development', library_name: libraryName });
}