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

Copilot/add google analytics key events tracking
pull/78/head^2
David Montero Crespo 2026-03-28 16:27:28 -03:00 committed by GitHub
commit 7095717dc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 930 additions and 10 deletions

View File

@ -440,6 +440,12 @@ For commercial licensing inquiries: [davidmonterocrespo24@gmail.com](mailto:davi
See [LICENSE](LICENSE) and [COMMERCIAL_LICENSE.md](COMMERCIAL_LICENSE.md) for full terms.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=davidmonterocrespo24/velxio&type=Date)](https://star-history.com/#davidmonterocrespo24/velxio&Date)
---
## References
- [Wokwi](https://wokwi.com) — Inspiration

View File

@ -20,6 +20,7 @@ import { Esp32C3SimulatorPage } from './pages/Esp32C3SimulatorPage';
import { RaspberryPiPicoSimulatorPage } from './pages/RaspberryPiPicoSimulatorPage';
import { RaspberryPiSimulatorPage } from './pages/RaspberryPiSimulatorPage';
import { Velxio2Page } from './pages/Velxio2Page';
import { AboutPage } from './pages/AboutPage';
import { useAuthStore } from './store/useAuthStore';
import './App.css';
@ -52,6 +53,7 @@ function App() {
<Route path="/raspberry-pi-pico-simulator" element={<RaspberryPiPicoSimulatorPage />} />
<Route path="/raspberry-pi-simulator" element={<RaspberryPiSimulatorPage />} />
<Route path="/v2" element={<Velxio2Page />} />
<Route path="/about" element={<AboutPage />} />
{/* Canonical project URL by ID */}
<Route path="/project/:id" element={<ProjectByIdPage />} />
{/* Legacy slug route — redirects to /project/:id */}

View File

@ -40,6 +40,7 @@ interface BoardOnCanvasProps {
isActive?: boolean;
onMouseDown: (e: React.MouseEvent) => void;
onPinClick: (componentId: string, pinName: string, x: number, y: number) => void;
zoom?: number;
}
export const BoardOnCanvas = ({
@ -49,6 +50,7 @@ export const BoardOnCanvas = ({
isActive = false,
onMouseDown,
onPinClick,
zoom = 1,
}: BoardOnCanvasProps) => {
const { id, boardKind, x, y } = board;
const size = BOARD_SIZE[boardKind] ?? { w: 300, h: 200 };
@ -156,6 +158,7 @@ export const BoardOnCanvas = ({
showPins={true}
wrapperOffsetX={0}
wrapperOffsetY={0}
zoom={zoom}
/>
</>
);

View File

@ -3,10 +3,22 @@
*
* Renders clickable pin indicators over components to enable wire creation.
* Shows when hovering over a component or when creating a wire.
*
* On touch devices the hit-target is scaled up inversely to the canvas zoom
* so the *screen-space* tap area stays at least ~40px regardless of zoom level.
*/
import React, { useEffect, useState } from 'react';
/** Detect touch-capable device once */
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
/** Minimum visual pin size in *world* pixels at zoom 1 */
const PIN_VISUAL = 12;
/** Desired minimum screen-space hit-target size for touch (px) */
const TOUCH_MIN_SCREEN_PX = 44;
interface PinInfo {
name: string;
x: number; // CSS pixels
@ -23,6 +35,8 @@ interface PinOverlayProps {
/** Extra offset to compensate for wrapper padding/border. Default: 4 (x), 6 (y) for component wrappers. Pass 0 when the element has no wrapper. */
wrapperOffsetX?: number;
wrapperOffsetY?: number;
/** Current canvas zoom level — used to keep touch targets usable at any zoom */
zoom?: number;
}
export const PinOverlay: React.FC<PinOverlayProps> = ({
@ -33,6 +47,7 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
showPins,
wrapperOffsetX = 4,
wrapperOffsetY = 6,
zoom = 1,
}) => {
const [pins, setPins] = useState<PinInfo[]>([]);
@ -56,6 +71,13 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
return null;
}
// On touch devices, compute world-space size so the pin is at least
// TOUCH_MIN_SCREEN_PX on screen. On desktop, keep the original 12px.
const pinSize = isTouchDevice
? Math.max(PIN_VISUAL, TOUCH_MIN_SCREEN_PX / zoom)
: PIN_VISUAL;
const pinHalf = pinSize / 2;
return (
<div
style={{
@ -67,7 +89,6 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
}}
>
{pins.map((pin, index) => {
// Pin coordinates are already in CSS pixels
const pinX = pin.x;
const pinY = pin.y;
@ -81,20 +102,22 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
}}
onTouchEnd={(e) => {
e.stopPropagation();
e.preventDefault();
onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY);
}}
style={{
position: 'absolute',
left: `${pinX - 6}px`,
top: `${pinY - 6}px`,
width: '12px',
height: '12px',
left: `${pinX - pinHalf}px`,
top: `${pinY - pinHalf}px`,
width: `${pinSize}px`,
height: `${pinSize}px`,
borderRadius: '3px',
backgroundColor: 'rgba(0, 200, 255, 0.8)',
border: '1.5px solid white',
cursor: 'crosshair',
pointerEvents: 'all',
transition: 'all 0.15s',
touchAction: 'none',
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'rgba(0, 255, 100, 1)';

View File

@ -23,6 +23,9 @@ import {
renderedToWaypoints,
renderedPointsToPath,
} from '../../utils/wireHitDetection';
/** Detect touch-capable device once */
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
import type { ComponentMetadata } from '../../types/component-metadata';
import type { BoardKind } from '../../types/board';
import { BOARD_KIND_LABELS } from '../../types/board';
@ -396,6 +399,18 @@ export const SimulatorCanvas = () => {
if (e.touches.length !== 1) return;
const touch = e.touches[0];
// ── Segment drag (wire editing) via touch ──
if (segmentDragRef.current) {
const world = toWorld(touch.clientX, touch.clientY);
const sd = segmentDragRef.current;
sd.isDragging = true;
const newValue = sd.axis === 'horizontal' ? world.y : world.x;
const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
const overridePath = renderedPointsToPath(newPts);
setSegmentDragPreview({ wireId: sd.wireId, overridePath });
return;
}
// ── Wire preview: update position as finger moves ──
if (wireInProgressRef.current && !isPanningRef.current && !touchDraggedComponentIdRef.current) {
const world = toWorld(touch.clientX, touch.clientY);
@ -464,6 +479,24 @@ export const SimulatorCanvas = () => {
if (e.touches.length > 0) return; // Still fingers on screen
// ── Finish segment drag (wire editing) via touch ──
if (segmentDragRef.current) {
const sd = segmentDragRef.current;
if (sd.isDragging) {
segmentDragJustCommittedRef.current = true;
const changed = e.changedTouches[0];
if (changed) {
const world = toWorld(changed.clientX, changed.clientY);
const newValue = sd.axis === 'horizontal' ? world.y : world.x;
const newPts = moveSegment(sd.renderedPts, sd.segIndex, sd.axis, newValue);
updateWire(sd.wireId, { waypoints: renderedToWaypoints(newPts) });
}
}
segmentDragRef.current = null;
setSegmentDragPreview(null);
return;
}
// ── Finish panning ──
let wasPanning = false;
if (isPanningRef.current) {
@ -480,7 +513,7 @@ export const SimulatorCanvas = () => {
const dx = changed.clientX - touchClickStartPosRef.current.x;
const dy = changed.clientY - touchClickStartPosRef.current.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const isShortTap = dist < 10 && elapsed < 400;
const isShortTap = dist < 20 && elapsed < 400;
// If we actually panned (moved significantly), don't process as tap
if (wasPanning && !isShortTap) return;
@ -530,7 +563,8 @@ export const SimulatorCanvas = () => {
if (isShortTap) {
const now = Date.now();
const world = toWorld(changed.clientX, changed.clientY);
const threshold = 8 / zoomRef.current;
const baseThreshold = isTouchDevice ? 20 : 8;
const threshold = baseThreshold / zoomRef.current;
const wire = findWireNearPoint(wiresRef.current, world.x, world.y, threshold);
// Double-tap → delete wire
@ -961,6 +995,28 @@ export const SimulatorCanvas = () => {
[selectedWireId],
);
// Handle touchstart on a segment handle circle (mobile wire editing)
const handleHandleTouchStart = useCallback(
(e: React.TouchEvent, segIndex: number) => {
e.stopPropagation();
if (!selectedWireId) return;
const wire = wiresRef.current.find((w) => w.id === selectedWireId);
if (!wire) return;
const segments = getRenderedSegments(wire);
const seg = segments[segIndex];
if (!seg) return;
const expandedPts = getRenderedPoints(wire);
segmentDragRef.current = {
wireId: wire.id,
segIndex,
axis: seg.axis,
renderedPts: expandedPts,
isDragging: false,
};
},
[selectedWireId],
);
// Zoom centered on cursor
const handleWheel = (e: React.WheelEvent) => {
e.preventDefault();
@ -1135,6 +1191,7 @@ export const SimulatorCanvas = () => {
componentY={component.y}
onPinClick={handlePinClick}
showPins={showPinsForComponent}
zoom={zoom}
/>
)}
</React.Fragment>
@ -1325,6 +1382,7 @@ export const SimulatorCanvas = () => {
segmentDragPreview={segmentDragPreview}
segmentHandles={segmentHandles}
onHandleMouseDown={handleHandleMouseDown}
onHandleTouchStart={handleHandleTouchStart}
/>
{/* All boards on canvas */}
@ -1343,6 +1401,7 @@ export const SimulatorCanvas = () => {
setDragOffset({ x: world.x - board.x, y: world.y - board.y });
}}
onPinClick={handlePinClick}
zoom={zoom}
/>
))}

View File

@ -3,6 +3,8 @@ import { useSimulatorStore } from '../../store/useSimulatorStore';
import { WireRenderer } from './WireRenderer';
import { WireInProgressRenderer } from './WireInProgressRenderer';
const isTouchDevice = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
export interface SegmentHandle {
segIndex: number;
axis: 'horizontal' | 'vertical';
@ -18,6 +20,8 @@ interface WireLayerProps {
segmentHandles: SegmentHandle[];
/** Called when user starts dragging a handle (passes segIndex) */
onHandleMouseDown: (e: React.MouseEvent, segIndex: number) => void;
/** Called when user starts dragging a handle via touch (passes segIndex) */
onHandleTouchStart?: (e: React.TouchEvent, segIndex: number) => void;
}
export const WireLayer: React.FC<WireLayerProps> = ({
@ -25,6 +29,7 @@ export const WireLayer: React.FC<WireLayerProps> = ({
segmentDragPreview,
segmentHandles,
onHandleMouseDown,
onHandleTouchStart,
}) => {
const wires = useSimulatorStore((s) => s.wires);
const wireInProgress = useSimulatorStore((s) => s.wireInProgress);
@ -64,12 +69,13 @@ export const WireLayer: React.FC<WireLayerProps> = ({
key={handle.segIndex}
cx={handle.mx}
cy={handle.my}
r={7}
r={isTouchDevice ? 14 : 7}
fill="white"
stroke="#007acc"
strokeWidth={2}
style={{ pointerEvents: 'all', cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize' }}
style={{ pointerEvents: 'all', cursor: handle.axis === 'horizontal' ? 'ns-resize' : 'ew-resize', touchAction: 'none' }}
onMouseDown={(e) => onHandleMouseDown(e, handle.segIndex)}
onTouchStart={(e) => onHandleTouchStart?.(e, handle.segIndex)}
/>
))}

View File

@ -0,0 +1,490 @@
/* ── AboutPage ────────────────────────────────────────── */
.about-page {
min-height: 100vh;
background: var(--bg, #0a0a0a);
color: var(--text, #e5e5e7);
font-family: var(--font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
-webkit-font-smoothing: antialiased;
}
/* ── Hero ─────────────────────────────────────────────── */
.about-hero {
padding: 100px 40px 60px;
text-align: center;
background: linear-gradient(180deg, rgba(0,113,227,0.08) 0%, transparent 100%);
border-bottom: 1px solid var(--border, #1d1d1f);
}
.about-hero-inner {
max-width: 700px;
margin: 0 auto;
}
.about-hero-title {
font-size: 48px;
font-weight: 700;
letter-spacing: -1.5px;
margin: 0 0 16px;
background: linear-gradient(135deg, #fff 0%, #999 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.about-hero-sub {
font-size: 18px;
color: #999;
line-height: 1.6;
margin: 0;
}
/* ── Sections ────────────────────────────────────────── */
.about-section {
padding: 72px 40px;
}
.about-section-alt {
background: #0f0f0f;
}
.about-container {
max-width: 800px;
margin: 0 auto;
}
.about-heading {
font-size: 32px;
font-weight: 700;
letter-spacing: -0.8px;
margin: 0 0 28px;
color: #fff;
}
.about-section p,
.about-story p {
font-size: 16px;
line-height: 1.75;
color: #aaa;
margin: 0 0 16px;
}
.about-section a {
color: #0071e3;
text-decoration: none;
}
.about-section a:hover {
text-decoration: underline;
}
.about-section strong {
color: #ddd;
}
/* ── Credits list ────────────────────────────────────── */
.about-credits-list {
list-style: none;
padding: 0;
margin: 16px 0 24px;
}
.about-credits-list li {
position: relative;
padding-left: 20px;
margin-bottom: 10px;
font-size: 15px;
color: #aaa;
line-height: 1.5;
}
.about-credits-list li::before {
content: '';
position: absolute;
left: 0;
top: 8px;
width: 8px;
height: 8px;
border-radius: 2px;
background: #0071e3;
}
/* ── Architecture grid ───────────────────────────────── */
.about-arch-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.about-arch-card {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 12px;
padding: 28px;
transition: border-color 0.2s;
}
.about-arch-card:hover {
border-color: #0071e3;
}
.about-arch-icon {
width: 36px;
height: 36px;
margin-bottom: 14px;
color: #0071e3;
}
.about-arch-icon svg {
width: 100%;
height: 100%;
}
.about-arch-card h3 {
font-size: 17px;
font-weight: 600;
color: #fff;
margin: 0 0 8px;
}
.about-arch-card p {
font-size: 14px;
line-height: 1.6;
color: #888;
margin: 0;
}
/* ── Creator ─────────────────────────────────────────── */
.about-creator {
display: flex;
gap: 40px;
align-items: flex-start;
}
.about-creator-photo {
flex-shrink: 0;
}
.about-creator-avatar {
width: 120px;
height: 120px;
border-radius: 50%;
background: linear-gradient(135deg, #0071e3 0%, #005bb5 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
font-weight: 700;
color: #fff;
letter-spacing: 1px;
}
.about-creator-name {
font-size: 26px;
font-weight: 700;
color: #fff;
margin: 0 0 6px;
}
.about-creator-role {
font-size: 15px;
color: #888;
margin: 0 0 18px;
}
.about-creator-bio {
font-size: 15px;
line-height: 1.7;
color: #aaa;
margin: 0 0 14px;
}
.about-creator-stack {
margin-top: 24px;
}
.about-creator-stack h4 {
font-size: 14px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 1px;
margin: 0 0 12px;
}
.about-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.about-tag {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 6px;
padding: 4px 12px;
font-size: 13px;
color: #aaa;
}
.about-creator-links {
display: flex;
gap: 16px;
margin-top: 24px;
}
.about-social-link {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
color: #ccc;
font-size: 14px;
font-weight: 500;
text-decoration: none;
transition: border-color 0.2s, color 0.2s;
}
.about-social-link:hover {
border-color: #0071e3;
color: #fff;
text-decoration: none;
}
.about-social-link svg {
width: 16px;
height: 16px;
}
/* ── Quote ────────────────────────────────────────────── */
.about-quote {
border-left: 3px solid #0071e3;
padding: 24px 32px;
margin: 0;
background: rgba(0,113,227,0.04);
border-radius: 0 12px 12px 0;
}
.about-quote p {
font-size: 17px;
line-height: 1.8;
color: #bbb;
margin: 0 0 12px;
font-style: italic;
}
.about-quote em {
color: #ddd;
}
.about-quote strong {
color: #fff;
}
.about-quote cite {
display: block;
font-size: 14px;
color: #666;
font-style: normal;
margin-top: 16px;
}
/* ── Stats ────────────────────────────────────────────── */
.about-stats-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.about-stat {
text-align: center;
padding: 28px 16px;
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 12px;
}
.about-stat-number {
display: block;
font-size: 36px;
font-weight: 700;
color: #0071e3;
letter-spacing: -1px;
}
.about-stat-label {
display: block;
font-size: 13px;
color: #888;
margin-top: 6px;
}
/* ── Press ────────────────────────────────────────────── */
.about-press {
text-align: center;
}
.about-press > p {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
.about-press-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.about-press-badge {
background: #1a1a1a;
border: 1px solid #2a2a2a;
border-radius: 8px;
padding: 8px 18px;
font-size: 14px;
color: #aaa;
text-decoration: none;
transition: border-color 0.2s;
}
a.about-press-badge:hover {
border-color: #0071e3;
color: #fff;
text-decoration: none;
}
/* ── CTA ─────────────────────────────────────────────── */
.about-cta {
text-align: center;
padding: 72px 40px;
border-top: 1px solid var(--border, #1d1d1f);
}
.about-cta h2 {
font-size: 32px;
font-weight: 700;
color: #fff;
margin: 0 0 12px;
}
.about-cta p {
font-size: 16px;
color: #888;
margin: 0 0 28px;
}
.about-cta-btns {
display: flex;
gap: 14px;
justify-content: center;
}
.about-btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
border-radius: 10px;
background: #0071e3;
color: #fff;
font-size: 15px;
font-weight: 600;
text-decoration: none;
transition: background 0.2s;
}
.about-btn-primary:hover {
background: #0077ed;
}
.about-btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 28px;
border-radius: 10px;
background: #1a1a1a;
border: 1px solid #333;
color: #ccc;
font-size: 15px;
font-weight: 500;
text-decoration: none;
transition: border-color 0.2s;
}
.about-btn-secondary:hover {
border-color: #555;
color: #fff;
}
.about-btn-secondary svg {
width: 16px;
height: 16px;
}
/* ── Mobile ──────────────────────────────────────────── */
@media (max-width: 768px) {
.about-hero {
padding: 80px 20px 48px;
}
.about-hero-title {
font-size: 32px;
}
.about-section {
padding: 48px 20px;
}
.about-arch-grid {
grid-template-columns: 1fr;
}
.about-creator {
flex-direction: column;
align-items: center;
text-align: center;
}
.about-creator-links {
justify-content: center;
}
.about-tags {
justify-content: center;
}
.about-stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.about-cta-btns {
flex-direction: column;
align-items: center;
}
}
@media (max-width: 480px) {
.about-hero-title {
font-size: 28px;
}
.about-stats-grid {
grid-template-columns: 1fr 1fr;
gap: 12px;
}
.about-creator-links {
flex-direction: column;
align-items: stretch;
}
.about-social-link {
justify-content: center;
}
}

View File

@ -0,0 +1,317 @@
import { Link } from 'react-router-dom';
import { AppHeader } from '../components/layout/AppHeader';
import { useSEO } from '../utils/useSEO';
import { getSeoMeta } from '../seoRoutes';
import './AboutPage.css';
const GITHUB_URL = 'https://github.com/davidmonterocrespo24/velxio';
const LINKEDIN_URL = 'https://www.linkedin.com/in/davidmonterocrespo24';
const GITHUB_PROFILE = 'https://github.com/davidmonterocrespo24';
const MEDIUM_URL = 'https://medium.com/@davidmonterocrespo24';
const HN_THREAD_V2 = 'https://news.ycombinator.com/item?id=43484227';
/* ── Icons ──────────────────────────────────────────── */
const IcoChip = () => (
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="5" y="5" width="14" height="14" rx="2" />
<rect x="9" y="9" width="6" height="6" />
<path d="M9 1v4M15 1v4M9 19v4M15 19v4M1 9h4M1 15h4M19 9h4M19 15h4" />
</svg>
);
const IcoGitHub = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.3 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61-.546-1.385-1.335-1.755-1.335-1.755-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 21.795 24 17.295 24 12c0-6.63-5.37-12-12-12z" />
</svg>
);
const IcoLinkedIn = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
);
const IcoMedium = () => (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M13.54 12a6.8 6.8 0 01-6.77 6.82A6.8 6.8 0 010 12a6.8 6.8 0 016.77-6.82A6.8 6.8 0 0113.54 12zM20.96 12c0 3.54-1.51 6.42-3.38 6.42-1.87 0-3.39-2.88-3.39-6.42s1.52-6.42 3.39-6.42 3.38 2.88 3.38 6.42M24 12c0 3.17-.53 5.75-1.19 5.75-.66 0-1.19-2.58-1.19-5.75s.53-5.75 1.19-5.75C23.47 6.25 24 8.83 24 12z"/>
</svg>
);
/* ── Component ──────────────────────────────────────── */
export const AboutPage: React.FC = () => {
useSEO({
...getSeoMeta('/about')!,
jsonLd: {
'@context': 'https://schema.org',
'@type': 'AboutPage',
name: 'About Velxio',
description: 'Learn about Velxio and its creator David Montero Crespo.',
url: 'https://velxio.dev/about',
},
});
return (
<div className="about-page">
<AppHeader />
{/* Hero */}
<section className="about-hero">
<div className="about-hero-inner">
<h1 className="about-hero-title">About Velxio</h1>
<p className="about-hero-sub">
A free, open-source embedded systems emulator built by a single developer with a passion for hardware and open source.
</p>
</div>
</section>
{/* The Story */}
<section className="about-section">
<div className="about-container">
<div className="about-story">
<h2 className="about-heading">The Story</h2>
<p>
Velxio started as a personal exploration into how microcontroller emulators work internally CPU instructions,
memory management, peripheral timing, and low-level architecture. What began as a learning project during
a vacation quickly grew into something bigger.
</p>
<p>
The idea was simple: <strong>what if anyone could simulate Arduino, ESP32, and Raspberry Pi boards
directly in the browser, without buying hardware, without installing toolchains, without cloud accounts?</strong>
</p>
<p>
Velxio v1 launched on Product Hunt and Hacker News, supporting Arduino Uno and Raspberry Pi Pico.
The feedback from the maker and embedded community was incredible and it pushed the project forward.
</p>
<p>
<strong>Velxio 2.0</strong> shipped with ESP32 emulation via QEMU (using the lcgamboa fork), a Raspberry Pi 3
running real Linux, RISC-V support for ESP32-C3 and CH32V003, realistic sensor simulation (DHT22, HC-SR04,
WS2812B NeoPixel), and 19 boards across 5 CPU architectures all running real compiled code.
</p>
<p>
The v2 launch hit the <strong>front page of Hacker News for over 20 hours</strong>, reaching nearly 600 GitHub
stars and thousands of visitors in less than 24 hours. It's now being used by students, professors, and makers
around the world.
</p>
</div>
</div>
</section>
{/* Architecture overview */}
<section className="about-section about-section-alt">
<div className="about-container">
<h2 className="about-heading">How It Works</h2>
<div className="about-arch-grid">
<div className="about-arch-card">
<div className="about-arch-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="4" y="4" width="16" height="16" rx="2"/><rect x="8" y="8" width="8" height="8"/><path d="M10 2v2M14 2v2M10 20v2M14 20v2M2 10h2M2 14h2M20 10h2M20 14h2"/></svg>
</div>
<h3>AVR8 &amp; RP2040</h3>
<p>Cycle-accurate emulation runs entirely in your browser using avr8js and rp2040js. No backend needed for simulation.</p>
</div>
<div className="about-arch-card">
<div className="about-arch-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<h3>ESP32 via QEMU</h3>
<p>Xtensa ESP32 and ESP32-S3 run on backend QEMU (lcgamboa fork) with real flash images, GPIO, ADC, and timers.</p>
</div>
<div className="about-arch-card">
<div className="about-arch-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
</div>
<h3>RISC-V In-Browser</h3>
<p>ESP32-C3 and CH32V003 run on a custom RV32IMC core written in TypeScript entirely client-side, no QEMU.</p>
</div>
<div className="about-arch-card">
<div className="about-arch-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
</div>
<h3>Raspberry Pi 3</h3>
<p>Full ARM Cortex-A53 Linux via QEMU raspi3b boots real Raspberry Pi OS and runs Python with RPi.GPIO.</p>
</div>
</div>
</div>
</section>
{/* Open Source Philosophy */}
<section className="about-section">
<div className="about-container">
<h2 className="about-heading">Open Source Philosophy</h2>
<p>
Velxio is <strong>100% open source</strong> under the AGPLv3 license. No cloud dependency, no student accounts,
no data leaving your network. Universities and bootcamps can deploy it on their own servers with a single Docker
command and give every student access to a complete embedded development environment for free.
</p>
<p>
The project builds on top of amazing open-source work from the community:
</p>
<ul className="about-credits-list">
<li><a href="https://github.com/wokwi/avr8js" target="_blank" rel="noopener noreferrer">avr8js</a> AVR8 CPU emulation by Wokwi</li>
<li><a href="https://github.com/wokwi/rp2040js" target="_blank" rel="noopener noreferrer">rp2040js</a> RP2040 emulation by Wokwi</li>
<li><a href="https://github.com/wokwi/wokwi-elements" target="_blank" rel="noopener noreferrer">wokwi-elements</a> 48+ visual electronic components by Wokwi</li>
<li><a href="https://github.com/lcgamboa/qemu" target="_blank" rel="noopener noreferrer">QEMU lcgamboa fork</a> ESP32 and Raspberry Pi QEMU emulation</li>
<li><a href="https://arduino.github.io/arduino-cli/" target="_blank" rel="noopener noreferrer">arduino-cli</a> Arduino compilation toolchain</li>
</ul>
<p>
Velxio was inspired by <a href="https://wokwi.com" target="_blank" rel="noopener noreferrer">Wokwi</a>, which is a
fantastic tool. The goal of Velxio is to take a different path: fully open source, self-hostable, and supporting
multiple heterogeneous boards in the same circuit.
</p>
</div>
</section>
{/* Creator */}
<section className="about-section about-section-alt">
<div className="about-container">
<h2 className="about-heading">The Creator</h2>
<div className="about-creator">
<div className="about-creator-photo">
<div className="about-creator-avatar">DMC</div>
</div>
<div className="about-creator-info">
<h3 className="about-creator-name">David Montero Crespo</h3>
<p className="about-creator-role">Application Architect @ IBM &middot; Montevideo, Uruguay</p>
<p className="about-creator-bio">
Application Architect with over 10 years of experience leading the development of large-scale
enterprise ecosystems. Specialist in Generative AI (LLMs, RAG, LangChain), Cloud-Native architectures
(OpenShift, Kubernetes), and certified Odoo ERP expert. Currently at IBM working on enterprise applications
for the Uruguayan State Insurance Bank (BSE).
</p>
<p className="about-creator-bio">
Studied Computer Science Engineering at Universidad de Oriente in Cuba (2012-2017), then moved to
Uruguay where he built a career spanning roles at Quanam (Odoo implementations for government institutions),
and IBM (enterprise architecture for BPS and BSE).
</p>
<p className="about-creator-bio">
Programming and robotics enthusiast. Creator of other open-source projects including
a <a href="https://github.com/davidmonterocrespo24" target="_blank" rel="noopener noreferrer">3D racing game running on an ESP32</a> (viral
on Reddit with 40K+ views) and an iPod Classic clone for Raspberry Pi.
</p>
<div className="about-creator-stack">
<h4>Tech Stack</h4>
<div className="about-tags">
<span className="about-tag">Java</span>
<span className="about-tag">Python</span>
<span className="about-tag">TypeScript</span>
<span className="about-tag">React</span>
<span className="about-tag">Angular</span>
<span className="about-tag">Node.js</span>
<span className="about-tag">FastAPI</span>
<span className="about-tag">Docker</span>
<span className="about-tag">Kubernetes</span>
<span className="about-tag">OpenShift</span>
<span className="about-tag">LangChain</span>
<span className="about-tag">watsonx.ai</span>
<span className="about-tag">Odoo</span>
<span className="about-tag">Arduino</span>
<span className="about-tag">ESP32</span>
<span className="about-tag">Raspberry Pi</span>
</div>
</div>
<div className="about-creator-links">
<a href={LINKEDIN_URL} target="_blank" rel="noopener noreferrer" className="about-social-link">
<IcoLinkedIn /> LinkedIn
</a>
<a href={GITHUB_PROFILE} target="_blank" rel="noopener noreferrer" className="about-social-link">
<IcoGitHub /> GitHub
</a>
<a href={MEDIUM_URL} target="_blank" rel="noopener noreferrer" className="about-social-link">
<IcoMedium /> Medium
</a>
</div>
</div>
</div>
</div>
</section>
{/* Personal story quote */}
<section className="about-section">
<div className="about-container">
<blockquote className="about-quote">
<p>
"I studied Computer Science in Cuba from 2012 to 2017, and when I moved to Uruguay, the most
common question was: <em>'How did you graduate as an engineer without internet, without a computer,
without YouTube?'</em>
</p>
<p>
I only had 4 words for the answer: <strong>Perseverance! Perseverance! Perseverance! Perseverance!</strong>
</p>
<p>
There is no better motivation than not having a plan B."
</p>
<cite> David Montero Crespo</cite>
</blockquote>
</div>
</section>
{/* Community & Press */}
<section className="about-section about-section-alt">
<div className="about-container">
<h2 className="about-heading">Community &amp; Press</h2>
<div className="about-stats-grid">
<div className="about-stat">
<span className="about-stat-number">600+</span>
<span className="about-stat-label">GitHub Stars</span>
</div>
<div className="about-stat">
<span className="about-stat-number">97+</span>
<span className="about-stat-label">Countries</span>
</div>
<div className="about-stat">
<span className="about-stat-number">19</span>
<span className="about-stat-label">Supported Boards</span>
</div>
<div className="about-stat">
<span className="about-stat-number">5</span>
<span className="about-stat-label">CPU Architectures</span>
</div>
</div>
<div className="about-press">
<p>Featured on:</p>
<div className="about-press-list">
<a href={HN_THREAD_V2} target="_blank" rel="noopener noreferrer" className="about-press-badge">Hacker News (Front Page)</a>
<span className="about-press-badge">Product Hunt</span>
<span className="about-press-badge">Reddit r/arduino</span>
<span className="about-press-badge">Hackster.io</span>
<span className="about-press-badge">XDA Developers</span>
</div>
</div>
</div>
</section>
{/* CTA */}
<section className="about-cta">
<div className="about-container">
<h2>Ready to try Velxio?</h2>
<p>No signup required. Runs 100% in your browser. Free and open source.</p>
<div className="about-cta-btns">
<Link to="/editor" className="about-btn-primary">Open Editor</Link>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="about-btn-secondary">
<IcoGitHub /> View on GitHub
</a>
</div>
</div>
</section>
{/* Footer */}
<footer className="landing-footer">
<div className="footer-brand">
<IcoChip />
<span>Velxio</span>
</div>
<div className="footer-links">
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer">GitHub</a>
<Link to="/docs">Docs</Link>
<Link to="/examples">Examples</Link>
<Link to="/editor">Editor</Link>
<Link to="/about">About</Link>
</div>
<p className="footer-copy">
MIT License &middot; Powered by <a href="https://github.com/wokwi/avr8js" target="_blank" rel="noopener noreferrer">avr8js</a> &amp; <a href="https://github.com/wokwi/wokwi-elements" target="_blank" rel="noopener noreferrer">wokwi-elements</a>
</p>
</footer>
</div>
);
};

View File

@ -739,6 +739,7 @@ export const LandingPage: React.FC = () => {
<Link to="/docs">Docs</Link>
<Link to="/examples">Examples</Link>
<Link to="/editor">Editor</Link>
<Link to="/about">About</Link>
</div>
<p className="footer-copy">
MIT License · Powered by <a href="https://github.com/wokwi/avr8js" target="_blank" rel="noopener noreferrer">avr8js</a> &amp; <a href="https://github.com/wokwi/wokwi-elements" target="_blank" rel="noopener noreferrer">wokwi-elements</a>

View File

@ -191,6 +191,18 @@ export const SEO_ROUTES: SeoRoute[] = [
},
},
// ── About
{
path: '/about',
priority: 0.7,
changefreq: 'monthly',
seoMeta: {
title: 'About Velxio — Open Source Embedded Emulator by David Montero Crespo',
description: 'Learn about Velxio, the free open-source multi-board embedded emulator, and its creator David Montero Crespo — Application Architect at IBM, programming and robotics enthusiast.',
url: `${DOMAIN}/about`,
},
},
// ── Auth / admin (noindex)
{ path: '/login', noindex: true },
{ path: '/register', noindex: true },

View File

@ -5,9 +5,10 @@
* Each function maps to an event that should be marked as a Key Event in GA4.
*/
declare function gtag(command: 'event', eventName: string, eventParams?: Record<string, unknown>): void;
type GtagFn = (command: 'event', eventName: string, eventParams?: Record<string, unknown>) => void;
function fireEvent(eventName: string, params: Record<string, string | number | boolean>): void {
const gtag = (window as unknown as { gtag?: GtagFn }).gtag;
if (typeof gtag === 'function') {
gtag('event', eventName, params);
}