153 lines
4.1 KiB
TypeScript
153 lines
4.1 KiB
TypeScript
/**
|
|
* CommunityProjectsGrid — second section on /examples beneath the
|
|
* hardcoded official examples. Fetches `/api/projects/featured` (a
|
|
* pro-overlay-only endpoint that returns the top public projects ranked
|
|
* by run_count).
|
|
*
|
|
* Renders nothing when the fetch fails / returns empty so the OSS build
|
|
* (which has no projects route at all) degrades to its old layout
|
|
* without surfacing an error.
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { useLocalizedHref } from '../../i18n/useLocalizedNavigate';
|
|
|
|
type FeaturedProject = {
|
|
id: string;
|
|
name: string;
|
|
description: string | null;
|
|
slug: string;
|
|
owner_username: string;
|
|
board_type: string;
|
|
run_count: number;
|
|
compile_count: number;
|
|
};
|
|
|
|
const BOARD_LABELS: Record<string, string> = {
|
|
'arduino-uno': 'Arduino Uno',
|
|
'arduino-nano': 'Arduino Nano',
|
|
'arduino-mega': 'Arduino Mega',
|
|
'esp32': 'ESP32',
|
|
'esp32-s3': 'ESP32-S3',
|
|
'esp32-c3': 'ESP32-C3',
|
|
'raspberry-pi-pico': 'Pico',
|
|
'raspberry-pi-3': 'Pi 3',
|
|
'attiny85': 'ATtiny85',
|
|
};
|
|
|
|
export const CommunityProjectsGrid = () => {
|
|
const [projects, setProjects] = useState<FeaturedProject[] | null>(null);
|
|
const [hidden, setHidden] = useState(false);
|
|
const localize = useLocalizedHref();
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
fetch('/api/projects/featured?limit=12', { credentials: 'include' })
|
|
.then(async (r) => {
|
|
if (!r.ok) throw new Error(`status ${r.status}`);
|
|
return (await r.json()) as FeaturedProject[];
|
|
})
|
|
.then((rows) => {
|
|
if (cancelled) return;
|
|
if (!rows.length) {
|
|
setHidden(true);
|
|
return;
|
|
}
|
|
setProjects(rows);
|
|
})
|
|
.catch(() => { if (!cancelled) setHidden(true); });
|
|
return () => { cancelled = true; };
|
|
}, []);
|
|
|
|
if (hidden || !projects) return null;
|
|
|
|
return (
|
|
<section style={styles.shell}>
|
|
<div style={styles.header}>
|
|
<h2 style={styles.title}>Featured community projects</h2>
|
|
<p style={styles.sub}>
|
|
The most-run public circuits on Velxio. Open one to see how it's wired,
|
|
remix the code, and run it live.
|
|
</p>
|
|
</div>
|
|
<div style={styles.grid}>
|
|
{projects.map((p) => (
|
|
<Link
|
|
key={p.id}
|
|
to={localize(`/${p.owner_username}/${p.slug}`)}
|
|
style={styles.card}
|
|
className="velxio-community-card"
|
|
>
|
|
<div style={styles.cardName}>{p.name || 'Untitled'}</div>
|
|
<div style={styles.cardMeta}>
|
|
<span>{BOARD_LABELS[p.board_type] || p.board_type}</span>
|
|
<span>·</span>
|
|
<span>{p.run_count.toLocaleString()} runs</span>
|
|
</div>
|
|
<div style={styles.cardOwner}>by @{p.owner_username}</div>
|
|
{p.description && (
|
|
<div style={styles.cardDesc}>{p.description.slice(0, 140)}</div>
|
|
)}
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
const styles: Record<string, React.CSSProperties> = {
|
|
shell: {
|
|
maxWidth: 1280,
|
|
margin: '40px auto 60px',
|
|
padding: '0 24px',
|
|
color: '#ddd',
|
|
},
|
|
header: { marginBottom: 24 },
|
|
title: { fontSize: 24, fontWeight: 600, color: '#fff', margin: 0 },
|
|
sub: { color: '#aaa', fontSize: 14, margin: '6px 0 0 0', maxWidth: 640 },
|
|
grid: {
|
|
display: 'grid',
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(260px, 1fr))',
|
|
gap: 14,
|
|
},
|
|
card: {
|
|
display: 'block',
|
|
background: '#1e1e1e',
|
|
border: '1px solid #2a2a2a',
|
|
borderRadius: 8,
|
|
padding: '14px 16px',
|
|
textDecoration: 'none',
|
|
color: 'inherit',
|
|
transition: 'border-color 0.12s, transform 0.12s',
|
|
},
|
|
cardName: {
|
|
fontSize: 15,
|
|
fontWeight: 600,
|
|
color: '#fff',
|
|
marginBottom: 6,
|
|
},
|
|
cardMeta: {
|
|
display: 'flex',
|
|
gap: 6,
|
|
alignItems: 'center',
|
|
color: '#888',
|
|
fontSize: 12,
|
|
},
|
|
cardOwner: {
|
|
color: '#4fc3f7',
|
|
fontSize: 12,
|
|
marginTop: 6,
|
|
},
|
|
cardDesc: {
|
|
color: '#aaa',
|
|
fontSize: 13,
|
|
marginTop: 8,
|
|
lineHeight: 1.45,
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 2,
|
|
WebkitBoxOrient: 'vertical',
|
|
overflow: 'hidden',
|
|
},
|
|
};
|