mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-07-06 09:52:12 +02:00
feat: add BuildProgress component and enhance BuildDownloadButton with compact mode
This commit is contained in:
+50
-2
@@ -1,8 +1,30 @@
|
||||
import { PluginCard } from "@/components/PluginCard"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import registryData from "@/public/registry.json"
|
||||
import { PluginDisplay } from "@/types"
|
||||
import { useQuery } from "convex/react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export default function PluginsPage() {
|
||||
const flashCounts = useQuery(api.plugins.getAll)
|
||||
const [githubStars, setGithubStars] = useState<Record<string, number>>({})
|
||||
|
||||
const plugins = Object.entries(registryData).sort(([, pluginA], [, pluginB]) => {
|
||||
// Featured plugins first
|
||||
const featuredA = pluginA.featured ?? false
|
||||
@@ -14,6 +36,32 @@ export default function PluginsPage() {
|
||||
return pluginA.name.localeCompare(pluginB.name)
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch GitHub stars for all plugins
|
||||
const fetchStars = async () => {
|
||||
const stars: Record<string, number> = {}
|
||||
const promises = plugins.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)
|
||||
}
|
||||
|
||||
fetchStars()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-6 md:p-10">
|
||||
<div className="max-w-6xl mx-auto space-y-8">
|
||||
@@ -40,8 +88,8 @@ export default function PluginsPage() {
|
||||
repo={pluginDisplay.repo}
|
||||
homepage={pluginDisplay.homepage}
|
||||
version={pluginDisplay.version}
|
||||
downloads={pluginDisplay.downloads}
|
||||
stars={pluginDisplay.stars}
|
||||
downloads={flashCounts?.[slug]}
|
||||
stars={githubStars[slug]}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import registryData from "@/public/registry.json"
|
||||
import { PluginDisplay } from "@/types"
|
||||
import { useQuery } from "convex/react"
|
||||
import { Download, Github, Home, Star } from "lucide-react"
|
||||
import { usePageContext } from "vike-react/usePageContext"
|
||||
import { navigate } from "vike/client/router"
|
||||
|
||||
function getGitHubStarsBadgeUrl(repoUrl?: string): 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) {
|
||||
const owner = pathParts[0]
|
||||
const repo = pathParts[1]
|
||||
return `https://img.shields.io/github/stars/${owner}/${repo}?style=flat&logo=github&logoColor=white&labelColor=rgb(0,0,0,0)&color=rgb(30,30,30)&label=★`
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Invalid URL
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default function PluginPage() {
|
||||
const pageContext = usePageContext()
|
||||
const slug = pageContext.routeParams?.slug as string | undefined
|
||||
const pluginStats = useQuery(api.plugins.get, slug ? { slug } : "skip")
|
||||
|
||||
if (!slug) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<p className="text-slate-300">Plugin slug missing.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const plugin = (registryData as Record<string, PluginDisplay>)[slug]
|
||||
const starsBadgeUrl = getGitHubStarsBadgeUrl(plugin?.repo)
|
||||
|
||||
if (!plugin) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-4">
|
||||
<div className="bg-slate-900/60 border border-slate-800 rounded-lg p-6">
|
||||
<p className="text-slate-300">
|
||||
Plugin <span className="font-mono">{slug}</span> not found.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
<div className="bg-slate-900/60 rounded-lg border border-slate-800 p-6 space-y-6">
|
||||
<div className="flex items-start gap-6">
|
||||
{plugin.imageUrl && (
|
||||
<img
|
||||
src={plugin.imageUrl}
|
||||
alt={`${plugin.name} logo`}
|
||||
className="w-24 h-24 rounded-lg object-contain shrink-0"
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h1 className="text-3xl font-bold">{plugin.name}</h1>
|
||||
{plugin.featured && (
|
||||
<span className="px-2 py-1 text-xs font-medium text-yellow-400 bg-yellow-400/10 border border-yellow-400/20 rounded">
|
||||
<Star className="w-3 h-3 inline mr-1 fill-yellow-400" />
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-300 text-lg mb-4">{plugin.description}</p>
|
||||
<div className="flex flex-wrap items-center gap-4 text-sm text-slate-400">
|
||||
{plugin.version && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-slate-500">Version:</span>
|
||||
<span className="font-mono text-white">v{plugin.version}</span>
|
||||
</div>
|
||||
)}
|
||||
{plugin.author && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-slate-500">Author:</span>
|
||||
<span className="text-white">{plugin.author}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="w-4 h-4" />
|
||||
<span>{(pluginStats?.flashCount ?? 0).toLocaleString()}</span>
|
||||
</div>
|
||||
{starsBadgeUrl && plugin.repo && (
|
||||
<a
|
||||
href={plugin.repo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img src={starsBadgeUrl} alt="GitHub stars" className="h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-slate-800">
|
||||
{plugin.repo && (
|
||||
<Button
|
||||
onClick={() => window.open(plugin.repo, "_blank", "noopener,noreferrer")}
|
||||
variant="outline"
|
||||
className="border-slate-600 hover:bg-slate-800"
|
||||
>
|
||||
<Github className="w-4 h-4 mr-2" />
|
||||
View Repository
|
||||
</Button>
|
||||
)}
|
||||
{plugin.homepage && plugin.homepage !== plugin.repo && (
|
||||
<Button
|
||||
onClick={() => window.open(plugin.homepage, "_blank", "noopener,noreferrer")}
|
||||
variant="outline"
|
||||
className="border-slate-600 hover:bg-slate-800"
|
||||
>
|
||||
<Home className="w-4 h-4 mr-2" />
|
||||
Homepage
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => navigate(`/builds/new?plugin=${slug}`)} className="bg-cyan-600 hover:bg-cyan-700">
|
||||
Build with this Plugin
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{plugin.dependencies && Object.keys(plugin.dependencies).length > 0 && (
|
||||
<div className="pt-4 border-t border-slate-800">
|
||||
<h2 className="text-lg font-semibold mb-3">Dependencies</h2>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(plugin.dependencies).map(([depName, depVersion]) => (
|
||||
<div key={depName} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-slate-400">{depName}:</span>
|
||||
<span className="font-mono text-white">{depVersion}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{plugin.includes && plugin.includes.length > 0 && (
|
||||
<div className="pt-4 border-t border-slate-800">
|
||||
<h2 className="text-lg font-semibold mb-3">Supported Platforms</h2>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{plugin.includes.map(platform => (
|
||||
<span
|
||||
key={platform}
|
||||
className="px-2 py-1 text-xs font-medium text-cyan-400 bg-cyan-400/10 border border-cyan-400/20 rounded"
|
||||
>
|
||||
{platform}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export default {
|
||||
prerender: false,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user