mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-05-15 13:55:49 +02:00
feat: featured projects
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-xl bg-slate-800 ring-1 ring-white/10">
|
||||
<Github className="h-8 w-8 text-slate-400" aria-hidden />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className="h-14 w-14 shrink-0 rounded-xl object-cover ring-1 ring-white/10"
|
||||
onError={() => setOk(false)}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function FeaturedTile({ project, onOpenRepoUrl }: { project: FeaturedProject; onOpenRepoUrl: (url: string) => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onOpenRepoUrl(project.url)}
|
||||
aria-label={`Open ${project.title} in Mesh Forge`}
|
||||
className="group flex w-full cursor-pointer items-center gap-3 rounded-xl border border-slate-700/80 bg-slate-900/60 p-3 text-left ring-1 ring-white/5 transition hover:border-cyan-700/60 hover:bg-slate-800/80 hover:ring-cyan-500/20"
|
||||
>
|
||||
<FeaturedAvatar src={project.logo} title={project.title} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="truncate font-semibold text-white group-hover:text-cyan-100">{project.title}</span>
|
||||
{project.highlighted ? (
|
||||
<span className="inline-flex shrink-0 items-center gap-0.5 rounded-full bg-emerald-500/15 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-emerald-300 ring-1 ring-emerald-500/30">
|
||||
<Check className="h-3 w-3" aria-hidden />
|
||||
pick
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
{project.subtitle ? (
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-snug text-slate-400">{project.subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeaturedProjects({ onOpenRepoUrl }: { onOpenRepoUrl: (url: string) => void }) {
|
||||
const projects = getFeaturedProjects()
|
||||
if (!projects.length) return null
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-3 text-left">
|
||||
<h2 className="text-center text-sm font-semibold uppercase tracking-wide text-slate-400">Featured projects</h2>
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{projects.map(p => (
|
||||
<FeaturedTile key={p.url} project={p} onOpenRepoUrl={onOpenRepoUrl} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+4
-1
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
+6
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
+10
-3
@@ -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<string | null>(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 (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-white flex flex-col items-center justify-center px-6 py-16">
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-950 via-slate-900 to-slate-950 text-white flex flex-col items-center justify-start px-6 py-12 md:py-16">
|
||||
<div className="max-w-xl w-full text-center space-y-8">
|
||||
<div className="space-y-5">
|
||||
<div className="flex justify-center">
|
||||
@@ -49,6 +54,8 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FeaturedProjects onOpenRepoUrl={openRepoUrl} />
|
||||
|
||||
<div className="space-y-3 text-left">
|
||||
<input
|
||||
id="gh-url"
|
||||
|
||||
@@ -728,7 +728,7 @@ export default function RepoPage() {
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 mb-1">{owner}</p>
|
||||
<h3 className="flex flex-wrap items-center gap-1.5 text-xl font-bold text-white leading-tight">
|
||||
<a className="hover:text-cyan-400" href={ghRepoRoot} target="_blank" rel="noreferrer">
|
||||
<a className="text-white hover:text-cyan-400" href={ghRepoRoot} target="_blank" rel="noreferrer">
|
||||
{repo}
|
||||
</a>
|
||||
{!ghAboutHomepage ? (
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export type FeaturedProject = {
|
||||
title: string
|
||||
subtitle?: string
|
||||
url: string
|
||||
highlighted?: boolean
|
||||
logo: string
|
||||
}
|
||||
|
||||
export type FeaturedProjectsFile = {
|
||||
projects: FeaturedProject[]
|
||||
}
|
||||
Vendored
+6
-1
@@ -1,6 +1,11 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.md?raw' {
|
||||
declare module "*.md?raw" {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module "*.yaml" {
|
||||
const data: unknown
|
||||
export default data
|
||||
}
|
||||
|
||||
+1
Submodule vendor/meshtastic-web added at 6535c96e81
+2
-1
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user