feat: add route for shared builds and enhance BuildNew and BuildProgress components for loading and sharing functionality

This commit is contained in:
Ben Allfree
2025-11-29 22:17:24 -08:00
parent 8904593f76
commit e6eb9a977f
3 changed files with 136 additions and 31 deletions
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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' && (