From 3235864063dc016abeae19cfd77e522462d07c24 Mon Sep 17 00:00:00 2001 From: Ben Allfree Date: Fri, 17 Apr 2026 02:53:44 -0700 Subject: [PATCH] feat: featured projects --- bun.lock | 12 ++++++ components/FeaturedProjects.tsx | 68 +++++++++++++++++++++++++++++++++ package.json | 5 ++- projects.yaml | 21 ++++++++++ scripts/sync-featured-logos.ts | 45 ++++++++++++++++++++++ src/index.css | 7 +++- src/lib/featuredProjects.ts | 8 ++++ src/pages/HomePage.tsx | 13 +++++-- src/pages/RepoPage.tsx | 2 +- src/types/featured.ts | 11 ++++++ src/vite-env.d.ts | 7 +++- vendor/meshtastic-web | 1 + vite.config.ts | 3 +- 13 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 components/FeaturedProjects.tsx create mode 100644 projects.yaml create mode 100644 scripts/sync-featured-logos.ts create mode 100644 src/lib/featuredProjects.ts create mode 100644 src/types/featured.ts create mode 160000 vendor/meshtastic-web diff --git a/bun.lock b/bun.lock index 7c81fe0..0df0d49 100644 --- a/bun.lock +++ b/bun.lock @@ -27,6 +27,7 @@ "@mdx-js/rollup": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-slot": "^1.2.4", + "@rollup/plugin-yaml": "^4.1.2", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "@types/node": "^24.10.1", @@ -47,6 +48,7 @@ "typescript": "^5.9.3", "vite": "^7.2.6", "wrangler": "^4.51.0", + "yaml": "^2.8.3", }, }, }, @@ -375,6 +377,8 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.47", "", {}, "sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw=="], + "@rollup/plugin-yaml": ["@rollup/plugin-yaml@4.1.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "js-yaml": "^4.1.0", "tosource": "^2.0.0-alpha.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], @@ -605,6 +609,8 @@ "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -771,6 +777,8 @@ "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -1045,6 +1053,8 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tosource": ["tosource@2.0.0-alpha.3", "", {}, "sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug=="], + "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -1099,6 +1109,8 @@ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], diff --git a/components/FeaturedProjects.tsx b/components/FeaturedProjects.tsx new file mode 100644 index 0000000..55175e0 --- /dev/null +++ b/components/FeaturedProjects.tsx @@ -0,0 +1,68 @@ +import { getFeaturedProjects } from "@/src/lib/featuredProjects" +import type { FeaturedProject } from "@/src/types/featured" +import { Check, Github } from "lucide-react" +import { useState } from "react" + +function FeaturedAvatar({ src, title }: { src: string; title: string }) { + const [ok, setOk] = useState(true) + if (!ok) { + return ( +
+ +
+ ) + } + return ( + setOk(false)} + title={title} + /> + ) +} + +function FeaturedTile({ project, onOpenRepoUrl }: { project: FeaturedProject; onOpenRepoUrl: (url: string) => void }) { + return ( + + ) +} + +export function FeaturedProjects({ onOpenRepoUrl }: { onOpenRepoUrl: (url: string) => void }) { + const projects = getFeaturedProjects() + if (!projects.length) return null + + return ( +
+

Featured projects

+
+ {projects.map(p => ( + + ))} +
+
+ ) +} diff --git a/package.json b/package.json index b477df1..cb6e3fa 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "scripts": { "generate:versions": "node scripts/generate-versions.js && biome format src/constants/versions.ts --write", "generate:architecture": "node scripts/generate-architecture-hierarchy.js", + "sync:featured": "bun scripts/sync-featured-logos.ts", "dev": "vite", "build": "vite build", "preview": "vite preview", @@ -44,6 +45,7 @@ "@mdx-js/rollup": "^3.1.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-slot": "^1.2.4", + "@rollup/plugin-yaml": "^4.1.2", "@tailwindcss/postcss": "^4.2.2", "@tailwindcss/typography": "^0.5.19", "@types/node": "^24.10.1", @@ -63,7 +65,8 @@ "tailwindcss-animate": "^1.0.7", "typescript": "^5.9.3", "vite": "^7.2.6", - "wrangler": "^4.51.0" + "wrangler": "^4.51.0", + "yaml": "^2.8.3" }, "type": "module" } diff --git a/projects.yaml b/projects.yaml new file mode 100644 index 0000000..71d8b44 --- /dev/null +++ b/projects.yaml @@ -0,0 +1,21 @@ +projects: + - title: Lotato + subtitle: 100% on-device Potato Mesh ingestor for MeshCore. + url: https://github.com/MeshEnvy/MeshCore-lotato + highlighted: false + logo: https://avatars.githubusercontent.com/u/237130814?v=4&s=128 + - title: Mesh64 + subtitle: A 64-hop Meshtastic prototype by Baymesh. + url: https://github.com/RCGV1/firmware-Fork/tree/baymesh-refactor + highlighted: true + logo: https://avatars.githubusercontent.com/u/119711889?v=4&s=128 + - title: MeshCore + subtitle: Open mesh stack and device firmware for LoRa-class radios—companion nodes, repeaters, and app-friendly transports. + url: https://github.com/meshcore-dev/MeshCore + highlighted: false + logo: https://avatars.githubusercontent.com/u/210668307?v=4&s=128 + - title: Meshtastic + subtitle: Official open-source device firmware—encrypted text, position, and telemetry over a decentralized LoRa mesh. + url: https://github.com/meshtastic/firmware + highlighted: false + logo: https://avatars.githubusercontent.com/u/61627050?v=4&s=128 diff --git a/scripts/sync-featured-logos.ts b/scripts/sync-featured-logos.ts new file mode 100644 index 0000000..6dbc8b3 --- /dev/null +++ b/scripts/sync-featured-logos.ts @@ -0,0 +1,45 @@ +import { readFileSync, writeFileSync } from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" +import { parse, stringify } from "yaml" +import { parseGithubUrl } from "../src/lib/parseGithubUrl" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const root = path.resolve(__dirname, "..") +const yamlPath = path.join(root, "projects.yaml") + +type Row = { + title: string + url: string + logo?: string + subtitle?: string + highlighted?: boolean +} + +const text = readFileSync(yamlPath, "utf8") +const doc = parse(text) as { projects: Row[] } + +if (!doc?.projects || !Array.isArray(doc.projects)) { + console.error("projects.yaml: expected top-level `projects` array") + process.exit(1) +} + +for (const p of doc.projects) { + const parsed = parseGithubUrl(p.url) + if (!parsed) { + console.error("Invalid url:", p.url) + process.exit(1) + } + const res = await fetch(`https://api.github.com/repos/${parsed.owner}/${parsed.repo}`) + if (!res.ok) { + console.error(`${parsed.owner}/${parsed.repo}:`, res.status, await res.text()) + process.exit(1) + } + const json = (await res.json()) as { owner: { avatar_url: string } } + const base = json.owner.avatar_url + const sep = base.includes("?") ? "&" : "?" + p.logo = `${base}${sep}s=128` +} + +writeFileSync(yamlPath, stringify(doc, { lineWidth: 120, indent: 2 }) + "\n", "utf8") +console.log("Wrote logos to", yamlPath) diff --git a/src/index.css b/src/index.css index 37bbcf8..73d554f 100644 --- a/src/index.css +++ b/src/index.css @@ -75,9 +75,14 @@ } .prose.prose-invert a { - color: oklch(0.488 0.243 264.376); + color: oklch(0.82 0.12 195); font-weight: 500; text-decoration: underline; + text-underline-offset: 0.15em; +} + +.prose.prose-invert a:hover { + color: oklch(0.88 0.1 195); } .prose.prose-invert code { diff --git a/src/lib/featuredProjects.ts b/src/lib/featuredProjects.ts new file mode 100644 index 0000000..d97cf5b --- /dev/null +++ b/src/lib/featuredProjects.ts @@ -0,0 +1,8 @@ +import type { FeaturedProjectsFile } from "@/src/types/featured" +import rawFeatured from "../../projects.yaml" + +const data = rawFeatured as FeaturedProjectsFile + +export function getFeaturedProjects(): FeaturedProjectsFile["projects"] { + return data.projects +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index a3b374e..1cd1015 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,4 +1,5 @@ import logo from "@/assets/logo.png" +import { FeaturedProjects } from "@/components/FeaturedProjects" import { Button } from "@/components/ui/button" import { useState } from "react" import { useNavigate } from "react-router-dom" @@ -13,8 +14,10 @@ export default function HomePage() { const [input, setInput] = useState("") const [error, setError] = useState(null) - const go = () => { - const parsed = parseGithubUrl(input) + const openRepoUrl = (raw: string) => { + const trimmed = raw.trim() + setInput(trimmed) + const parsed = parseGithubUrl(trimmed) if (!parsed) { setError("Paste a GitHub URL like https://github.com/owner/repo or owner/repo") return @@ -30,8 +33,10 @@ export default function HomePage() { } } + const go = () => openRepoUrl(input) + return ( -
+
@@ -49,6 +54,8 @@ export default function HomePage() {
+ +

{owner}

- + {repo} {!ghAboutHomepage ? ( diff --git a/src/types/featured.ts b/src/types/featured.ts new file mode 100644 index 0000000..3efbaa9 --- /dev/null +++ b/src/types/featured.ts @@ -0,0 +1,11 @@ +export type FeaturedProject = { + title: string + subtitle?: string + url: string + highlighted?: boolean + logo: string +} + +export type FeaturedProjectsFile = { + projects: FeaturedProject[] +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index b5b4546..26dcfa8 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,6 +1,11 @@ /// -declare module '*.md?raw' { +declare module "*.md?raw" { const src: string export default src } + +declare module "*.yaml" { + const data: unknown + export default data +} diff --git a/vendor/meshtastic-web b/vendor/meshtastic-web new file mode 160000 index 0000000..6535c96 --- /dev/null +++ b/vendor/meshtastic-web @@ -0,0 +1 @@ +Subproject commit 6535c96e8116f4d3006dca18b9e3b57d0e42a51e diff --git a/vite.config.ts b/vite.config.ts index 0fb7dc3..a8e375b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,11 @@ +import yaml from "@rollup/plugin-yaml" import react from "@vitejs/plugin-react" import path from "node:path" import { defineConfig } from "vite" /** Tailwind runs via PostCSS (`postcss.config.mjs`), not `@tailwindcss/vite` — the Vite plugin was stalling builds (no stdout) on large workspaces. */ export default defineConfig({ - plugins: [react()], + plugins: [react(), yaml()], logLevel: "info", resolve: { alias: {