Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import circuit json #599

Merged
merged 20 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"circuit-json-to-bom-csv": "^0.0.6",
"circuit-json-to-gerber": "^0.0.16",
"circuit-json-to-pnp-csv": "^0.0.6",
"circuit-json-to-tscircuit": "^0.0.4",
"circuit-json-to-readable-netlist": "^0.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down Expand Up @@ -1041,6 +1042,8 @@

"circuit-json-to-readable-netlist": ["[email protected]", "", { "dependencies": { "@tscircuit/core": "^0.0.286", "@tscircuit/soup-util": "^0.0.41", "circuit-json-to-connectivity-map": "^0.0.17" }, "peerDependencies": { "typescript": "^5.7.2" } }, "sha512-GvlVMzEzLpB9WTsLkN4p5aHITjKhfEOQKFjZaUNQrd3FbyFbUXnx1e8vR1cB2M0fQXwaUQ0cxGTjK3W2AihKng=="],

"circuit-json-to-tscircuit": ["[email protected]", "", { "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-LpHbOwdPE4+CooWPAPoKXWs4vxTrjJgu/avnxE3AqGwCD9r0ZnT73mEAB9oQi6T1i7T53zdkSR6y+zpsyCSE7g=="],

"circuit-to-svg": ["[email protected]", "", { "dependencies": { "@tscircuit/footprinter": "^0.0.91", "@tscircuit/routing": "^1.3.5", "@tscircuit/soup-util": "^0.0.41", "@types/node": "^22.5.5", "bun-types": "^1.1.40", "svgson": "^5.3.1", "transformation-matrix": "^2.16.1" }, "peerDependencies": { "circuit-json": "*", "schematic-symbols": "*" } }, "sha512-AwTD5Ww5ujzK5pEkrVDFtFx5nfGqVbtbIHgXNEeji5RKfDpb0WzeXtaw75kkHl715JB1WBwUupewKO7mTaI06A=="],

"class-variance-authority": ["[email protected]", "", { "dependencies": { "clsx": "2.0.0" } }, "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A=="],
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"circuit-json-to-bom-csv": "^0.0.6",
"circuit-json-to-gerber": "^0.0.16",
"circuit-json-to-pnp-csv": "^0.0.6",
"circuit-json-to-tscircuit": "^0.0.4",
"circuit-json-to-readable-netlist": "^0.0.7",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
Expand Down
186 changes: 186 additions & 0 deletions src/components/CircuitJsonImportDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, { useState } from "react"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { useAxios } from "@/hooks/use-axios"
import { useToast } from "@/hooks/use-toast"
import { useLocation } from "wouter"
import { useGlobalStore } from "@/hooks/use-global-store"
import { convertCircuitJsonToTscircuit } from "circuit-json-to-tscircuit"

interface CircuitJsonImportDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}

const isValidJSON = (code: string) => {
try {
JSON.parse(code)
return true
} catch {
return false
}
}

export function CircuitJsonImportDialog({
open,
onOpenChange,
}: CircuitJsonImportDialogProps) {
const [circuitJson, setcircuitJson] = useState("")
const [file, setFile] = useState<File | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const axios = useAxios()
const { toast } = useToast()
const [, navigate] = useLocation()
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
const session = useGlobalStore((s) => s.session)

const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const selectedFile = e.target.files?.[0]
if (selectedFile && selectedFile.type === "application/json") {
setFile(selectedFile)
} else {
setError("Please select a valid JSON file.")
}
}

const handleImport = async () => {
let importedCircuitJson

if (file) {
try {
const fileText = await file.text()
importedCircuitJson = JSON.parse(fileText)
} catch (err) {
setError("Error reading JSON file. Please ensure it is valid.")
return
}
} else if (isValidJSON(circuitJson)) {
setIsLoading(true)
setError(null)
importedCircuitJson = JSON.parse(circuitJson)
} else {
toast({
title: "Invalid Input",
description: "Please provide a valid JSON content or file.",
variant: "destructive",
})
return
}
let tscircuit
try {
tscircuit = convertCircuitJsonToTscircuit(importedCircuitJson as any, {
componentName: "circuit",
})
console.info(tscircuit)
} catch {
toast({
title: "Import Failed",
description: "Invalid Circuit JSON was provided.",
variant: "destructive",
})
setIsLoading(false)
return
}

try {
const newSnippetData = {
snippet_type: importedCircuitJson.type ?? "board",
circuit_json: importedCircuitJson,
code: tscircuit,
}
const response = await axios
.post("/snippets/create", newSnippetData)
.catch((e) => e)
const { snippet, message } = response.data
if (message) {
setError(message)
setIsLoading(false)
return
}
toast({
title: "Import Successful",
description: "Circuit Json has been imported successfully.",
})
onOpenChange(false)
navigate(`/editor?snippet_id=${snippet.snippet_id}`)
} catch (error) {
console.error("Error importing Circuit Json:", error)
toast({
title: "Import Failed",
description: "Failed to import the Circuit Json. Please try again.",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
}

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Import Circuit JSON</DialogTitle>
</DialogHeader>
<div className="pb-4">
<Textarea
className="mt-3"
placeholder="Paste the Circuit JSON."
value={circuitJson}
onChange={(e) => setcircuitJson(e.target.value)}
disabled={!!file}
/>
<div className="mt-4 flex flex-col gap-2">
<label
htmlFor="file-input"
className="block text-sm font-medium text-gray-700"
>
Upload JSON File
</label>
<div className="flex items-center gap-4">
<input
id="file-input"
type="file"
accept="application/json"
onChange={handleFileChange}
className="hidden" // Hide the default file input
/>
<label
htmlFor="file-input"
className="px-4 py-2 bg-slate-900 text-slate-50 rounded-lg shadow cursor-pointer hover:bg-slate-900/90 dark:bg-slate-50 dark:text-slate-900 dark:hover:bg-slate-50/90 transition-all duration-200"
>
Choose File
</label>
</div>
{file && (
<p className="text-sm text-gray-600">
<span className="font-medium text-gray-900">
Selected file:
</span>{" "}
{file.name}
</p>
)}
</div>

{error && <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>}
</div>
<DialogFooter>
<Button onClick={handleImport} disabled={isLoading || !isLoggedIn}>
{!isLoggedIn
? "Must be logged in for JSON import"
: isLoading
? "Importing..."
: "Import"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
24 changes: 24 additions & 0 deletions src/pages/quickstart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { TypeBadge } from "@/components/TypeBadge"
import { JLCPCBImportDialog } from "@/components/JLCPCBImportDialog"
import { CircuitJsonImportDialog } from "@/components/CircuitJsonImportDialog"
import { useNotImplementedToast } from "@/hooks/use-toast"
import { useGlobalStore } from "@/hooks/use-global-store"
import { cn } from "@/lib/utils"
Expand All @@ -16,6 +17,8 @@ import { PrefetchPageLink } from "@/components/PrefetchPageLink"
export const QuickstartPage = () => {
const axios = useAxios()
const [isJLCPCBDialogOpen, setIsJLCPCBDialogOpen] = useState(false)
const [isCircuitJsonImportDialogOpen, setIsCircuitJsonImportDialogOpen] =
useState(false)
const toastNotImplemented = useNotImplementedToast()
const currentUser = useGlobalStore((s) => s.session?.github_username)
const { data: mySnippets, isLoading } = useQuery<Snippet[]>(
Expand Down Expand Up @@ -159,6 +162,22 @@ export const QuickstartPage = () => {
</Button>
</CardContent>
</Card>
<Card className="hover:shadow-md transition-shadow rounded-md">
<CardHeader className="p-4 pb-0">
<CardTitle className="text-lg flex items-center justify-between">
Circuit Json
<TypeBadge type="module" className="ml-2" />
</CardTitle>
</CardHeader>
<CardContent className="p-4">
<Button
className="w-full"
onClick={() => setIsCircuitJsonImportDialogOpen(true)}
>
Import Circuit JSON
</Button>
</CardContent>
</Card>
</div>
</div>

Expand All @@ -167,6 +186,11 @@ export const QuickstartPage = () => {
onOpenChange={setIsJLCPCBDialogOpen}
/>

<CircuitJsonImportDialog
open={isCircuitJsonImportDialogOpen}
onOpenChange={setIsCircuitJsonImportDialogOpen}
/>

<div>
<h2 className="text-xl font-semibold mb-4 mt-12">
Start from a Template
Expand Down
Loading