Files
mesh-forge/pages/index/+Page.tsx
2026-02-17 07:48:18 -08:00

214 lines
8.5 KiB
TypeScript

import { DiscordButton } from "@/components/DiscordButton"
import { PluginCard } from "@/components/PluginCard"
import { RedditButton } from "@/components/RedditButton"
import { Button } from "@/components/ui/button"
import { api } from "@/convex/_generated/api"
import registryData from "@/public/registry.json"
import { useQuery } from "convex/react"
import { useEffect, useState } from "react"
import { navigate } from "vike/client/router"
function getGitHubOwnerRepo(repoUrl?: string): { owner: string; repo: string } | null {
if (!repoUrl) return null
try {
const url = new URL(repoUrl)
if (url.hostname === "github.com" || url.hostname === "www.github.com") {
const pathParts = url.pathname.split("/").filter(Boolean)
if (pathParts.length >= 2) {
return { owner: pathParts[0], repo: pathParts[1] }
}
}
} catch {
// Invalid URL
}
return null
}
function QuickBuildIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
role="img"
aria-label="Custom build"
{...props}
>
<title>Custom build</title>
<path fill="currentColor" d="M11 15H6l7-14v8h5l-7 14z" />
</svg>
)
}
function DocsIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
role="img"
aria-label="Docs"
{...props}
>
<title>Docs</title>
<path
fill="currentColor"
d="M6.75 22q-1.125 0-1.937-.763T4 19.35V5.4q0-.95.588-1.7t1.537-.95L16 .8v16l-9.475 1.9q-.225.05-.375.238T6 19.35q0 .275.225.463T6.75 20H18V4h2v18zM7 16.575l2-.4V4.225l-2 .4z"
/>
</svg>
)
}
export default function LandingPage() {
const flashCounts = useQuery(api.plugins.getAll)
const [githubStars, setGithubStars] = useState<Record<string, number>>({})
const featuredPlugins = Object.entries(registryData)
.filter(([, plugin]) => plugin.featured === true)
.sort(([, pluginA], [, pluginB]) => pluginA.name.localeCompare(pluginB.name))
useEffect(() => {
// Fetch GitHub stars for featured plugins
const fetchStars = async () => {
const stars: Record<string, number> = {}
const promises = featuredPlugins.map(async ([slug, plugin]) => {
const ownerRepo = getGitHubOwnerRepo(plugin.repo)
if (!ownerRepo) return
try {
const res = await fetch(`https://api.github.com/repos/${ownerRepo.owner}/${ownerRepo.repo}`)
const data = await res.json()
if (data.stargazers_count !== undefined) {
stars[slug] = data.stargazers_count
}
} catch {
// Silently fail if GitHub API is unavailable
}
})
await Promise.all(promises)
setGithubStars(stars)
}
if (featuredPlugins.length > 0) {
fetchStars()
}
}, [featuredPlugins.length])
const customBuildPlugin = {
id: "custom-build",
name: "Build your own",
description: "Create a custom firmware build with your choice of plugins and modules",
imageUrl: "/custom-build.webp",
featured: false,
}
return (
<div className="min-h-screen bg-slate-950 text-white">
<div className="max-w-7xl mx-auto">
<div className="text-center py-20 px-8">
<h1 className="text-6xl md:text-7xl font-bold mb-6 leading-[1.1]">
<span className="bg-gradient-to-r from-cyan-400 to-blue-600 bg-clip-text text-transparent inline-block pb-2">
Mesh Apps
</span>
</h1>
<p className="text-xl md:text-2xl text-slate-400 max-w-3xl mx-auto mb-10">
Mesh Forge is an open ecosystem of apps, BBS's, custom hardware, games, and more.
</p>
{featuredPlugins.length > 0 && (
<div className="mb-10">
<div className="bg-slate-900/60 border border-slate-800 rounded-2xl p-8 pb-24 md:pb-8 max-w-6xl mx-auto relative">
<h2 className="text-2xl font-bold mb-6 text-center">Popular Builds</h2>
<div className="grid grid-cols-1 sm:grid-cols-[repeat(auto-fit,minmax(312px,312px))] gap-4 auto-rows-fr justify-center">
{featuredPlugins.map(([slug, plugin]) => (
<div key={slug} className="h-full">
<PluginCard
variant="link"
id={slug}
name={plugin.name}
description={plugin.description}
imageUrl={plugin.imageUrl}
featured={false}
repo={plugin.repo}
homepage={plugin.homepage}
version={plugin.version}
downloads={flashCounts?.[slug]}
stars={githubStars[slug]}
/>
</div>
))}
<div className="h-full">
<PluginCard
variant="link"
id={customBuildPlugin.id}
name={customBuildPlugin.name}
description={customBuildPlugin.description}
imageUrl={customBuildPlugin.imageUrl}
featured={false}
href="/builds"
prominent={true}
/>
</div>
</div>
</div>
</div>
)}
<div className="flex flex-col items-center gap-4 mb-10">
<div className="flex flex-wrap items-center justify-center gap-3">
<Button
onClick={() => navigate("/docs")}
size="default"
variant="default"
className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white border-0 shadow-lg shadow-cyan-500/50"
>
<DocsIcon className="mr-2 h-4 w-4" />
Docs
</Button>
<DiscordButton
size="default"
variant="default"
className="bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white border-0 shadow-lg shadow-purple-500/50"
/>
<RedditButton
size="default"
variant="default"
className="bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white border-0 shadow-lg shadow-orange-500/50"
/>
</div>
</div>
{/* Benefits Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-5xl mx-auto mb-10">
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-6 text-left">
<h3 className="text-lg font-semibold text-cyan-400 mb-2">Zero Install</h3>
<p className="text-slate-300 text-sm">No downloads, no toolchains. Everything runs in your browser.</p>
</div>
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-6 text-left">
<h3 className="text-lg font-semibold text-cyan-400 mb-2">Custom Firmware</h3>
<p className="text-slate-300 text-sm">Build bespoke Meshtastic firmware tailored to your exact needs.</p>
</div>
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-6 text-left">
<h3 className="text-lg font-semibold text-cyan-400 mb-2">Community Extensions</h3>
<p className="text-slate-300 text-sm">Include community modules and extensions beyond core Meshtastic.</p>
</div>
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-6 text-left md:col-span-2 lg:col-span-1">
<h3 className="text-lg font-semibold text-cyan-400 mb-2">Share & Remix</h3>
<p className="text-slate-300 text-sm">Publish your build profiles and let others remix your configs.</p>
</div>
<div className="bg-slate-900/60 border border-slate-800 rounded-xl p-6 text-left md:col-span-2 lg:col-span-2">
<h3 className="text-lg font-semibold text-cyan-400 mb-2">Cloud Builds</h3>
<p className="text-slate-300 text-sm">
Compile in the cloud, flash directly to your deviceno local setup required.
</p>
</div>
</div>
</div>
</div>
</div>
)
}