mirror of
https://github.com/MeshEnvy/mesh-forge.git
synced 2026-08-05 16:33:50 +02:00
feat: add BuildDownloadButton component for firmware and source downloads
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
import { useMutation } from 'convex/react'
|
||||
import { pick } from 'convex-helpers'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { type BuildFields, buildFields } from '../../convex/schema'
|
||||
|
||||
interface BuildDownloadButtonProps {
|
||||
build: BuildFields
|
||||
type: 'firmware' | 'source'
|
||||
variant?: 'default' | 'outline'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function BuildDownloadButton({
|
||||
build,
|
||||
type,
|
||||
variant,
|
||||
className,
|
||||
}: BuildDownloadButtonProps) {
|
||||
const generateDownloadUrl = useMutation(
|
||||
api.builds.generateAnonymousDownloadUrl
|
||||
)
|
||||
const generateSourceDownloadUrl = useMutation(
|
||||
api.builds.generateAnonymousSourceDownloadUrl
|
||||
)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// Default styling based on type
|
||||
const defaultVariant =
|
||||
variant ?? (type === 'firmware' ? 'default' : 'outline')
|
||||
const defaultClassName =
|
||||
className ??
|
||||
(type === 'firmware'
|
||||
? 'bg-cyan-600 hover:bg-cyan-700'
|
||||
: 'bg-slate-700 hover:bg-slate-600')
|
||||
|
||||
const handleDownload = async () => {
|
||||
setError(null)
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const url =
|
||||
type === 'firmware'
|
||||
? await generateDownloadUrl({
|
||||
build: pick(
|
||||
build,
|
||||
Object.keys(buildFields) as (keyof BuildFields)[]
|
||||
),
|
||||
slug: 'download',
|
||||
})
|
||||
: await generateSourceDownloadUrl({
|
||||
build: pick(
|
||||
build,
|
||||
Object.keys(buildFields) as (keyof BuildFields)[]
|
||||
),
|
||||
slug: 'download',
|
||||
})
|
||||
window.location.href = url
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
const errorMsg =
|
||||
type === 'firmware'
|
||||
? 'Failed to generate download link.'
|
||||
: 'Failed to generate source download link.'
|
||||
setError(errorMsg)
|
||||
toast.error(errorMsg, {
|
||||
description: message,
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'firmware' && !build.artifactPath) return null
|
||||
if (type === 'source' && !build.sourceUrl && !build.buildHash) return null
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
disabled={isLoading}
|
||||
variant={defaultVariant}
|
||||
className={defaultClassName}
|
||||
>
|
||||
Download {type === 'firmware' ? 'firmware' : 'source'}
|
||||
</Button>
|
||||
{error && <p className="text-sm text-red-400">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useMutation, useQuery } from 'convex/react'
|
||||
import { useState } from 'react'
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { BuildDownloadButton } from '@/components/BuildDownloadButton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import type { Id } from '../../convex/_generated/dataModel'
|
||||
@@ -201,6 +202,18 @@ export default function Admin() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{(build.artifactPath ||
|
||||
build.sourceUrl ||
|
||||
build.buildHash) && (
|
||||
<div className="mt-3 flex gap-3">
|
||||
{build.artifactPath && (
|
||||
<BuildDownloadButton build={build} type="firmware" />
|
||||
)}
|
||||
{(build.sourceUrl || build.buildHash) && (
|
||||
<BuildDownloadButton build={build} type="source" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<Button
|
||||
|
||||
+13
-66
@@ -1,5 +1,4 @@
|
||||
import { useMutation, useQuery } from 'convex/react'
|
||||
import { pick } from 'convex-helpers'
|
||||
import { useQuery } from 'convex/react'
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
@@ -11,10 +10,10 @@ import {
|
||||
import { useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { toast } from 'sonner'
|
||||
import { BuildDownloadButton } from '@/components/BuildDownloadButton'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { humanizeStatus } from '@/lib/utils'
|
||||
import { api } from '../../convex/_generated/api'
|
||||
import { type BuildFields, buildFields } from '../../convex/schema'
|
||||
import { TARGETS } from '../constants/targets'
|
||||
|
||||
export default function BuildProgress() {
|
||||
@@ -23,16 +22,6 @@ export default function BuildProgress() {
|
||||
api.builds.getByHash,
|
||||
buildHash ? { buildHash } : 'skip'
|
||||
)
|
||||
const generateDownloadUrl = useMutation(
|
||||
api.builds.generateAnonymousDownloadUrl
|
||||
)
|
||||
const generateSourceDownloadUrl = useMutation(
|
||||
api.builds.generateAnonymousSourceDownloadUrl
|
||||
)
|
||||
const [downloadError, setDownloadError] = useState<string | null>(null)
|
||||
const [sourceDownloadError, setSourceDownloadError] = useState<string | null>(
|
||||
null
|
||||
)
|
||||
const [shareUrlCopied, setShareUrlCopied] = useState(false)
|
||||
|
||||
if (!buildHash) {
|
||||
@@ -87,36 +76,6 @@ export default function BuildProgress() {
|
||||
const targetLabel = targetMeta?.name ?? build.config.target
|
||||
const status = build.status || 'queued'
|
||||
|
||||
const handleDownload = async () => {
|
||||
setDownloadError(null)
|
||||
try {
|
||||
const url = await generateDownloadUrl({
|
||||
build: pick(build, Object.keys(buildFields) as (keyof BuildFields)[]),
|
||||
slug: `quick-build`,
|
||||
})
|
||||
window.location.href = url
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setDownloadError('Failed to generate download link.')
|
||||
console.error('Download error', message)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSourceDownload = async () => {
|
||||
setSourceDownloadError(null)
|
||||
try {
|
||||
const url = await generateSourceDownloadUrl({
|
||||
build: pick(build, Object.keys(buildFields) as (keyof BuildFields)[]),
|
||||
slug: `quick-build`,
|
||||
})
|
||||
window.location.href = url
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
setSourceDownloadError('Failed to generate source download link.')
|
||||
console.error('Source download error', message)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (status === 'success') {
|
||||
return <CheckCircle className="w-6 h-6 text-green-500" />
|
||||
@@ -274,32 +233,20 @@ export default function BuildProgress() {
|
||||
)}
|
||||
|
||||
{status === 'success' && build.artifactPath && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={handleDownload}
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-700"
|
||||
>
|
||||
Download firmware
|
||||
</Button>
|
||||
{downloadError && (
|
||||
<p className="text-sm text-red-400">{downloadError}</p>
|
||||
)}
|
||||
</div>
|
||||
<BuildDownloadButton
|
||||
build={build}
|
||||
type="firmware"
|
||||
className="w-full bg-cyan-600 hover:bg-cyan-700"
|
||||
/>
|
||||
)}
|
||||
|
||||
{build.sourceUrl && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
onClick={handleSourceDownload}
|
||||
className="w-full bg-slate-700 hover:bg-slate-600"
|
||||
variant="outline"
|
||||
>
|
||||
Download source
|
||||
</Button>
|
||||
{sourceDownloadError && (
|
||||
<p className="text-sm text-red-400">{sourceDownloadError}</p>
|
||||
)}
|
||||
</div>
|
||||
<BuildDownloadButton
|
||||
build={build}
|
||||
type="source"
|
||||
variant="outline"
|
||||
className="w-full bg-slate-700 hover:bg-slate-600"
|
||||
/>
|
||||
)}
|
||||
|
||||
{status === 'failure' && (
|
||||
|
||||
Reference in New Issue
Block a user