import * as React from "react"; import { Link, useNavigate, useParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; import { EXTENSION_TOOL, FILELESS_TOOLS, TOOL_LABELS, type Tool, projectPath, projectToolPath, } from "@pcbjam/shared"; import { ArrowLeft, Download, ExternalLink, Loader2, Trash2, } from "lucide-react"; import { fetchFileBytes, useProject, useSourceDescriptor } from "@/lib/api"; import { downloadBytes } from "@/lib/download"; import { localProjectStore } from "@/lib/project-source"; import { zipFiles } from "@/lib/zip"; import { Button } from "@/components/ui/button"; import { SourceChip } from "@/components/SourceChip"; import { ToolGrid } from "@/components/ToolGrid"; import { FileTree } from "@/components/FileTree"; import { NewFileDialog } from "@/components/NewFileDialog"; function toolForPath(path: string): Tool | null { const dot = path.lastIndexOf("."); if (dot < 0) return null; return EXTENSION_TOOL[path.slice(dot).toLowerCase()] ?? null; } /** * Project-centric view (mirrors the closed app): an iconed tool launcher (same * cards as the home page) + the project's files as a navigable folder tree. * Opening a read-only GALLERY project auto-"moves it to local" — it's copied * into a writable browser-local project at the same slug, so the composite source * shadows the gallery and this view re-resolves as a local, editable project. */ export function ProjectView() { const { scope = "", name: slug = "" } = useParams(); const navigate = useNavigate(); const qc = useQueryClient(); const { data, isLoading, error } = useProject(slug); const { data: descriptor } = useSourceDescriptor(slug); const isLocal = descriptor?.kind === "local"; const isRemoteRo = descriptor?.kind === "remote-ro"; const store = localProjectStore(); const [busy, setBusy] = React.useState(false); const [newFileTool, setNewFileTool] = React.useState(null); const [forkError, setForkError] = React.useState(null); const forkStartedRef = React.useRef(false); // Auto "move to local": a read-only gallery project is copied into a writable // browser-local project at the SAME slug. The composite source then routes this // /p/:slug to the local copy (local wins), and invalidating the queries re- // resolves this view as local — no button, no URL change. Runs once. React.useEffect(() => { if (!isRemoteRo || !store || !data || forkStartedRef.current) return; forkStartedRef.current = true; void (async () => { try { if (!(await store.hasProject(slug))) { const files = await Promise.all( data.files.map(async (f) => ({ path: f.path, bytes: await fetchFileBytes(slug, f.path), })), ); await store.createProject(data.project.name, files, { slug }); } await qc.invalidateQueries({ queryKey: ["local-projects"] }); await qc.invalidateQueries({ queryKey: ["source-descriptor", slug] }); await qc.invalidateQueries({ queryKey: ["project", slug] }); } catch (e) { setForkError(e instanceof Error ? e.message : String(e)); } })(); }, [isRemoteRo, store, data, slug, qc]); const exportZip = async () => { if (!store) return; setBusy(true); try { downloadBytes(`${slug}.zip`, zipFiles(await store.readFiles(slug))); } finally { setBusy(false); } }; const downloadOne = async (path: string) => { downloadBytes(path, await fetchFileBytes(slug, path)); }; const remove = async () => { if (!store || !data) return; if (!window.confirm(`Delete "${data.project.name}" from this browser? This can't be undone.`)) return; await store.deleteProject(slug); await qc.invalidateQueries({ queryKey: ["local-projects"] }); navigate("/"); }; // A tool launched from the grid: file-less tools (gerber/calculator + the // symbol/footprint editors, which load the full project lib set) open project- // scoped with a full reload; document tools create a templated file first. const launchTool = (tool: Tool) => { if (FILELESS_TOOLS.has(tool)) { window.location.assign(projectToolPath(scope, slug, tool)); } else { setNewFileTool(tool); } }; const renderFileActions = (path: string) => { const tool = toolForPath(path); return ( <> {isLocal && ( )} {tool && ( Open in {TOOL_LABELS[tool]} )} ); }; const movingToLocal = isRemoteRo && !!store && !forkError; return (
{isLoading && (

loading…

)} {error &&

{(error as Error).message}

} {data && ( <>

{data.project.name} {descriptor && }

{data.project.scope}/projects/{data.project.slug}

{movingToLocal ? (

Preparing your editable copy…

) : ( <> {forkError && (

Couldn't prepare a local copy: {forkError}

)} {/* Tools — same iconed launcher as the home page. */}

Tools

{/* Project-level actions (local projects). */} {isLocal && (
)}

Files ({data.files.length})

)} setNewFileTool(null)} project={{ slug, existingPaths: data.files.map((f) => f.path) }} /> )}
); }