mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-07-01 07:21:03 +02:00
feat: add route for shared builds and enhance BuildNew and BuildProgress components for loading and sharing functionality
This commit is contained in:
@@ -40,6 +40,7 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/browse" element={<Browse />} />
|
||||
<Route path="/builds/new/:buildHash" element={<BuildNew />} />
|
||||
<Route path="/builds/new" element={<BuildNew />} />
|
||||
<Route path="/builds/:buildHash" element={<BuildProgress />} />
|
||||
<Route path="/profiles/:id" element={<ProfileDetail />} />
|
||||
@@ -53,6 +54,7 @@ function App() {
|
||||
<Route path="/" element={<LandingPage />} />
|
||||
<Route path="/browse" element={<Browse />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/builds/new/:buildHash" element={<BuildNew />} />
|
||||
<Route path="/builds/new" element={<BuildNew />} />
|
||||
<Route path="/builds/:buildHash" element={<BuildProgress />} />
|
||||
<Route
|
||||
|
||||
+81
-1
@@ -1,7 +1,7 @@
|
||||
import { useMutation, useQuery } from 'convex/react'
|
||||
import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { ModuleToggle } from '@/components/ModuleToggle'
|
||||
import { PluginToggle } from '@/components/PluginToggle'
|
||||
@@ -35,8 +35,13 @@ const DEFAULT_TARGET =
|
||||
|
||||
export default function BuildNew() {
|
||||
const navigate = useNavigate()
|
||||
const { buildHash: buildHashParam } = useParams<{ buildHash?: string }>()
|
||||
const ensureBuildFromConfig = useMutation(api.builds.ensureBuildFromConfig)
|
||||
const pluginFlashCounts = useQuery(api.plugins.getAll) ?? {}
|
||||
const sharedBuild = useQuery(
|
||||
api.builds.getByHash,
|
||||
buildHashParam ? { buildHash: buildHashParam } : 'skip'
|
||||
)
|
||||
|
||||
const STORAGE_KEY = 'quick_build_target'
|
||||
const persistTargetSelection = (targetId: string) => {
|
||||
@@ -59,6 +64,7 @@ export default function BuildNew() {
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [showModuleOverrides, setShowModuleOverrides] = useState(false)
|
||||
const [showPlugins, setShowPlugins] = useState(true)
|
||||
const [isLoadingSharedBuild, setIsLoadingSharedBuild] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeCategory && TARGET_CATEGORIES.length > 0) {
|
||||
@@ -111,6 +117,67 @@ export default function BuildNew() {
|
||||
}
|
||||
}, [selectedTarget])
|
||||
|
||||
// Pre-populate form from shared build
|
||||
useEffect(() => {
|
||||
if (!buildHashParam) return
|
||||
if (sharedBuild === undefined) {
|
||||
setIsLoadingSharedBuild(true)
|
||||
return
|
||||
}
|
||||
setIsLoadingSharedBuild(false)
|
||||
|
||||
if (!sharedBuild) {
|
||||
setErrorMessage(
|
||||
'Build not found. The shared build may have been deleted.'
|
||||
)
|
||||
toast.error('Build not found', {
|
||||
description: 'The shared build could not be loaded.',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const config = sharedBuild.config
|
||||
|
||||
// Set target and category
|
||||
if (config.target && TARGETS[config.target]) {
|
||||
setSelectedTarget(config.target)
|
||||
const category = TARGETS[config.target].category || 'Other'
|
||||
if (TARGET_CATEGORIES.includes(category)) {
|
||||
setActiveCategory(category)
|
||||
}
|
||||
}
|
||||
|
||||
// Set version
|
||||
if (
|
||||
config.version &&
|
||||
(VERSIONS as readonly string[]).includes(config.version)
|
||||
) {
|
||||
setSelectedVersion(config.version as (typeof VERSIONS)[number])
|
||||
}
|
||||
|
||||
// Set module config (already in the correct format)
|
||||
if (config.modulesExcluded) {
|
||||
setModuleConfig(config.modulesExcluded)
|
||||
if (Object.keys(config.modulesExcluded).length > 0) {
|
||||
setShowModuleOverrides(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Set plugin config (convert array to object format)
|
||||
if (config.pluginsEnabled && config.pluginsEnabled.length > 0) {
|
||||
const pluginObj: Record<string, boolean> = {}
|
||||
config.pluginsEnabled.forEach((pluginId) => {
|
||||
// Extract slug from "slug@version" format if present
|
||||
const slug = pluginId.includes('@') ? pluginId.split('@')[0] : pluginId
|
||||
if (slug in registryData) {
|
||||
pluginObj[slug] = true
|
||||
}
|
||||
})
|
||||
setPluginConfig(pluginObj)
|
||||
setShowPlugins(true)
|
||||
}
|
||||
}, [buildHashParam, sharedBuild])
|
||||
|
||||
const moduleCount = Object.keys(moduleConfig).length
|
||||
const pluginCount = Object.keys(pluginConfig).filter(
|
||||
(id) => pluginConfig[id] === true
|
||||
@@ -176,6 +243,19 @@ export default function BuildNew() {
|
||||
|
||||
const isFlashDisabled = !selectedTarget || isFlashing
|
||||
|
||||
if (isLoadingSharedBuild) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-6 md:p-10 flex items-center justify-center">
|
||||
<div className="text-center space-y-4">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-cyan-500 mx-auto" />
|
||||
<p className="text-slate-400">
|
||||
Loading shared build configuration...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
+53
-30
@@ -1,8 +1,9 @@
|
||||
import { useMutation, useQuery } from 'convex/react'
|
||||
import { pick } from 'convex-helpers'
|
||||
import { ArrowLeft, CheckCircle, Loader2, XCircle } from 'lucide-react'
|
||||
import { ArrowLeft, CheckCircle, Loader2, Share2, XCircle } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { humanizeStatus } from '@/lib/utils'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
@@ -25,6 +26,7 @@ export default function BuildProgress() {
|
||||
const [sourceDownloadError, setSourceDownloadError] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [shareUrlCopied, setShareUrlCopied] = useState(false)
|
||||
|
||||
if (!buildHash) {
|
||||
return (
|
||||
@@ -129,6 +131,21 @@ export default function BuildProgress() {
|
||||
? `https://github.com/MeshEnvy/configurable-web-flasher/actions/runs/${build.githubRunId}`
|
||||
: null
|
||||
|
||||
const shareUrl = `${window.location.origin}/builds/new/${build.buildHash}`
|
||||
|
||||
const handleShare = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl)
|
||||
setShareUrlCopied(true)
|
||||
toast.success('Share link copied to clipboard')
|
||||
setTimeout(() => setShareUrlCopied(false), 2000)
|
||||
} catch {
|
||||
toast.error('Failed to copy link', {
|
||||
description: 'Please copy the URL manually',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-white p-8">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
@@ -140,40 +157,46 @@ export default function BuildProgress() {
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back to Quick Build
|
||||
</Link>
|
||||
<p className="text-sm text-slate-500 font-mono break-all">
|
||||
Hash: {build.buildHash}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/60 rounded-lg border border-slate-800 p-6 space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{getStatusIcon()}
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-wide text-slate-500">
|
||||
Target
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold">{targetLabel}</h2>
|
||||
<div className="flex items-center gap-2 text-slate-400 mt-1 text-sm">
|
||||
<span className={getStatusColor()}>
|
||||
{humanizeStatus(status)}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(build.updatedAt).toLocaleString()}</span>
|
||||
{githubActionUrl && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<a
|
||||
href={githubActionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500 hover:text-slate-300"
|
||||
>
|
||||
View run
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{getStatusIcon()}
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-wide text-slate-500">
|
||||
Target
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold">{targetLabel}</h2>
|
||||
<div className="flex items-center gap-2 text-slate-400 mt-1 text-sm">
|
||||
<span className={getStatusColor()}>
|
||||
{humanizeStatus(status)}
|
||||
</span>
|
||||
<span>•</span>
|
||||
<span>{new Date(build.updatedAt).toLocaleString()}</span>
|
||||
{githubActionUrl && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<a
|
||||
href={githubActionUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-slate-500 hover:text-slate-300"
|
||||
>
|
||||
View run
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleShare}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Share2 className="w-4 h-4 mr-2" />
|
||||
{shareUrlCopied ? 'Copied!' : 'Share Build'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{status !== 'success' && status !== 'failure' && (
|
||||
|
||||
Reference in New Issue
Block a user