@tscircuit/fake-snippets 0.0.77 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,36 +1,8 @@
1
1
  {
2
2
  "packages": [
3
- {
4
- "owner": "Abse2001",
5
- "name": "Arduino-Nano-Servo-Breakout"
6
- },
7
- {
8
- "owner": "ShiboSoftwareDev",
9
- "name": "Wifi-Camera-Module"
10
- },
11
- {
12
- "owner": "imrishabh18",
13
- "name": "Arduino-nano"
14
- },
15
3
  {
16
4
  "owner": "seveibar",
17
5
  "name": "usb-c-flashlight"
18
- },
19
- {
20
- "owner": "seveibar",
21
- "name": "nine-key-keyboard"
22
- },
23
- {
24
- "owner": "AnasSarkiz",
25
- "name": "grid-of-LEDs-with-an-ESP32"
26
- },
27
- {
28
- "owner": "seveibar",
29
- "name": "keyboard-default60"
30
- },
31
- {
32
- "owner": "imrishabh18",
33
- "name": "motor-driver-breakout"
34
6
  }
35
7
  ]
36
8
  }
@@ -17,7 +17,8 @@ export default withRouteSpec({
17
17
  if (req.method === "POST") {
18
18
  const { github_username } = req.jsonBody
19
19
  account = ctx.db.accounts.find(
20
- (acc: Account) => acc.github_username === github_username,
20
+ (acc: Account) =>
21
+ acc.github_username.toLowerCase() === github_username.toLowerCase(),
21
22
  )
22
23
  } else {
23
24
  account = ctx.db.getAccount(ctx.auth.account_id)
@@ -0,0 +1,111 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { packageSchema } from "fake-snippets-api/lib/db/schema"
4
+ import {
5
+ fetchEasyEDAComponent,
6
+ convertRawEasyEdaToTs,
7
+ normalizeManufacturerPartNumber,
8
+ EasyEdaJsonSchema,
9
+ } from "easyeda"
10
+
11
+ export default withRouteSpec({
12
+ methods: ["POST"],
13
+ auth: "session",
14
+ jsonBody: z.object({
15
+ jlcpcb_part_number: z.string().min(1, "JLCPCB part number is required"),
16
+ }),
17
+ jsonResponse: z.object({
18
+ ok: z.boolean(),
19
+ package: packageSchema,
20
+ }),
21
+ })(async (req, ctx) => {
22
+ const { jlcpcb_part_number } = req.jsonBody
23
+
24
+ try {
25
+ const rawEasyJson = await fetchEasyEDAComponent(jlcpcb_part_number)
26
+
27
+ if (!rawEasyJson) {
28
+ return ctx.error(404, {
29
+ error_code: "component_not_found",
30
+ message: `Component with JLCPCB part number ${jlcpcb_part_number} not found`,
31
+ })
32
+ }
33
+
34
+ const betterEasy = await EasyEdaJsonSchema.parse(rawEasyJson)
35
+
36
+ const tsxComponent = await convertRawEasyEdaToTs(rawEasyJson)
37
+
38
+ const componentName = normalizeManufacturerPartNumber(
39
+ betterEasy.dataStr.head.c_para["Manufacturer Part"] ?? "",
40
+ )
41
+ .replace(/[^a-zA-Z0-9-_]/g, "-")
42
+ .replace(/--/g, "-")
43
+
44
+ const packageName = `${ctx.auth.github_username}/${componentName}`
45
+
46
+ const existingPackage = ctx.db.packages.find((p) => p.name === packageName)
47
+
48
+ if (existingPackage) {
49
+ return ctx.json({
50
+ ok: true,
51
+ package: existingPackage,
52
+ })
53
+ }
54
+
55
+ const newPackage = {
56
+ name: packageName,
57
+ unscoped_name: componentName,
58
+ owner_name: ctx.auth.github_username,
59
+ code: tsxComponent,
60
+ created_at: new Date().toISOString(),
61
+ updated_at: new Date().toISOString(),
62
+ description: `Generated from JLCPCB part number ${jlcpcb_part_number}`,
63
+ creator_account_id: ctx.auth.account_id,
64
+ owner_org_id: ctx.auth.personal_org_id,
65
+ owner_github_username: ctx.auth.github_username,
66
+ latest_package_release_id: null,
67
+ latest_package_release_fs_sha: null,
68
+ latest_version: null,
69
+ license: null,
70
+ ai_description: "placeholder ai description",
71
+ ai_usage_instructions: "placeholder ai usage instructions",
72
+ }
73
+
74
+ const createdPackage = ctx.db.addPackage(newPackage)
75
+
76
+ const createdPackageRelease = ctx.db.addPackageRelease({
77
+ package_id: createdPackage.package_id,
78
+ version: "1.0.0",
79
+ created_at: new Date().toISOString(),
80
+ is_latest: true,
81
+ is_locked: false,
82
+ })
83
+
84
+ ctx.db.updatePackage(createdPackage.package_id, {
85
+ latest_package_release_id: createdPackageRelease.package_release_id,
86
+ })
87
+
88
+ ctx.db.addPackageFile({
89
+ package_release_id: createdPackageRelease.package_release_id,
90
+ file_path: "index.tsx",
91
+ content_text: String(tsxComponent),
92
+ created_at: new Date().toISOString(),
93
+ })
94
+
95
+ return ctx.json({
96
+ ok: true,
97
+ package: createdPackage as any,
98
+ })
99
+ } catch (error: any) {
100
+ if (String(error).includes("Component not found")) {
101
+ return ctx.error(404, {
102
+ error_code: "component_not_found",
103
+ message: `Component with JLCPCB part number ${jlcpcb_part_number} not found`,
104
+ })
105
+ }
106
+ return ctx.error(500, {
107
+ error_code: "package_generation_failed",
108
+ message: `Failed to generate package from JLCPCB part: ${error.message || String(error)}`,
109
+ })
110
+ }
111
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.77",
3
+ "version": "0.0.79",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/src/App.tsx CHANGED
@@ -66,7 +66,6 @@ const SearchPage = lazyImport(() => import("@/pages/search"))
66
66
  const SettingsPage = lazyImport(() => import("@/pages/settings"))
67
67
  const UserProfilePage = lazyImport(() => import("@/pages/user-profile"))
68
68
  const DevLoginPage = lazyImport(() => import("@/pages/dev-login"))
69
- const BetaPage = lazyImport(() => import("@/pages/beta"))
70
69
  const ViewPackagePage = lazyImport(() => import("@/pages/view-package"))
71
70
  const PackageBuildsPage = lazyImport(() => import("@/pages/package-builds"))
72
71
  const TrendingPage = lazyImport(() => import("@/pages/trending"))
@@ -106,8 +105,6 @@ function App() {
106
105
  <Suspense fallback={<FullPageLoader />}>
107
106
  <Switch>
108
107
  <Route path="/" component={LandingPage} />
109
- <Route path="/beta" component={BetaPage} />
110
- <Route path="/beta/:author/:packageName" component={BetaPage} />
111
108
  <Route
112
109
  path="/view-package/:author/:packageName"
113
110
  component={ViewPackagePage}
@@ -174,14 +174,31 @@ export function CircuitJsonImportDialog({
174
174
  }
175
175
 
176
176
  const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
177
- if (event.ctrlKey && event.key === "Enter") {
178
- handleImport()
177
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
178
+ event.preventDefault()
179
+ if (!isLoading && isLoggedIn && (circuitJson.trim() || file)) {
180
+ handleImport()
181
+ }
182
+ }
183
+ }
184
+
185
+ const handleDialogKeyDown = (event: React.KeyboardEvent) => {
186
+ if (
187
+ event.key === "Enter" &&
188
+ !event.ctrlKey &&
189
+ !event.metaKey &&
190
+ event.target !== document.querySelector("textarea")
191
+ ) {
192
+ event.preventDefault()
193
+ if (!isLoading && isLoggedIn && (circuitJson.trim() || file)) {
194
+ handleImport()
195
+ }
179
196
  }
180
197
  }
181
198
 
182
199
  return (
183
200
  <Dialog open={open} onOpenChange={onOpenChange}>
184
- <DialogContent>
201
+ <DialogContent onKeyDown={handleDialogKeyDown}>
185
202
  <DialogHeader>
186
203
  <DialogTitle>Import Circuit JSON</DialogTitle>
187
204
  <DialogDescription>
@@ -50,31 +50,16 @@ export function JLCPCBImportDialog({
50
50
  setHasBeenImportedToAccountAlready(false)
51
51
 
52
52
  try {
53
- const apiUrl = `/packages/get?name=${session?.github_username}/${partNumber}`
54
-
55
- const existingPackageRes = await axios.get(apiUrl, {
56
- validateStatus: (status) => true,
53
+ const response = await axios.post("/packages/generate_from_jlcpcb", {
54
+ jlcpcb_part_number: partNumber,
57
55
  })
58
-
59
- if (existingPackageRes.status !== 404) {
60
- setHasBeenImportedToAccountAlready(true)
56
+ if (!response.data.ok) {
57
+ setError("Failed to generate package from JLCPCB part")
61
58
  setIsLoading(false)
62
59
  return
63
60
  }
64
61
 
65
- const response = await axios
66
- .post("/snippets/generate_from_jlcpcb", {
67
- jlcpcb_part_number: partNumber,
68
- })
69
- .catch((e) => e)
70
-
71
- const { snippet, error } = response.data
72
-
73
- if (error) {
74
- setError(error.message)
75
- setIsLoading(false)
76
- return
77
- }
62
+ const { package: generatedPackage } = response.data
78
63
 
79
64
  toast({
80
65
  title: "Import Successful",
@@ -82,13 +67,21 @@ export function JLCPCBImportDialog({
82
67
  })
83
68
 
84
69
  onOpenChange(false)
85
- navigate(`/editor?package_id=${snippet.snippet_id}`)
86
- } catch (error) {
87
- toast({
88
- title: "Import Failed",
89
- description: "Failed to import the JLCPCB component. Please try again.",
90
- variant: "destructive",
91
- })
70
+ navigate(`/editor?package_id=${generatedPackage.package_id}`)
71
+ } catch (error: any) {
72
+ if (error.response?.status === 404) {
73
+ setError(`Component with JLCPCB part number ${partNumber} not found`)
74
+ } else if (error.response?.data?.message) {
75
+ setError(error.response.data.message)
76
+ } else {
77
+ setError("Failed to import the JLCPCB component. Please try again.")
78
+ toast({
79
+ title: "Import Failed",
80
+ description:
81
+ "Failed to import the JLCPCB component. Please try again.",
82
+ variant: "destructive",
83
+ })
84
+ }
92
85
  } finally {
93
86
  setIsLoading(false)
94
87
  }
@@ -116,11 +109,22 @@ export function JLCPCBImportDialog({
116
109
  className="mt-3"
117
110
  placeholder="Enter JLCPCB part number (e.g., C46749)"
118
111
  value={partNumber}
112
+ disabled={isLoading}
119
113
  onChange={(e) => {
120
114
  setPartNumber(e.target.value)
121
115
  setError(null)
122
116
  setHasBeenImportedToAccountAlready(false)
123
117
  }}
118
+ onKeyDown={(e) => {
119
+ if (
120
+ e.key === "Enter" &&
121
+ !isLoading &&
122
+ isLoggedIn &&
123
+ partNumber.trim()
124
+ ) {
125
+ handleImport()
126
+ }
127
+ }}
124
128
  />
125
129
  {error && !hasBeenImportedToAccountAlready && (
126
130
  <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>
@@ -14,17 +14,33 @@ const getErrorText = (error: ErrorObject | string) => {
14
14
  export const LogContent = ({
15
15
  logs,
16
16
  error,
17
- }: { logs: any[]; error?: ErrorObject | string | null }) => {
17
+ }: {
18
+ logs: Array<{
19
+ type: "info" | "success" | "error"
20
+ message: string
21
+ timestamp: string
22
+ }>
23
+ error?: ErrorObject | string | null
24
+ }) => {
18
25
  return (
19
26
  <div className="whitespace-pre-wrap font-mono text-xs">
20
- {logs.map((log) =>
21
- log.msg || log.message ? (
22
- <div>{log.msg ?? log.message}</div>
23
- ) : (
24
- <div>
25
- <pre>{log.message}</pre>
26
- </div>
27
- ),
27
+ {logs.map(
28
+ (log, i) =>
29
+ log.timestamp &&
30
+ log.message && (
31
+ <div
32
+ key={i}
33
+ className={
34
+ log.type === "error"
35
+ ? "text-red-600"
36
+ : log.type === "success"
37
+ ? "text-green-600"
38
+ : "text-gray-600"
39
+ }
40
+ >
41
+ {new Date(log.timestamp).toLocaleTimeString()} {log.message}
42
+ </div>
43
+ ),
28
44
  )}
29
45
  {error && <div className="text-red-600">{getErrorText(error)}</div>}
30
46
  </div>
@@ -18,7 +18,7 @@ function computeDuration(
18
18
  }
19
19
 
20
20
  export const PackageBuildDetailsPage = () => {
21
- const { packageRelease } = useCurrentPackageRelease()
21
+ const { packageRelease } = useCurrentPackageRelease({ include_logs: true })
22
22
  const [openSections, setOpenSections] = useState<Record<string, boolean>>({})
23
23
 
24
24
  const {
@@ -79,8 +79,8 @@ export const PackageBuildDetailsPage = () => {
79
79
  >
80
80
  <LogContent
81
81
  logs={
82
- packageRelease?.transpilation_logs ?? [
83
- { msg: "No transpilation logs available" },
82
+ transpilation_logs ?? [
83
+ { message: "No transpilation logs available" },
84
84
  ]
85
85
  }
86
86
  error={transpilation_error}
@@ -100,8 +100,8 @@ export const PackageBuildDetailsPage = () => {
100
100
  >
101
101
  <LogContent
102
102
  logs={
103
- packageRelease?.circuit_json_build_logs ?? [
104
- { msg: "No Circuit JSON logs available" },
103
+ circuit_json_build_logs ?? [
104
+ { message: "No Circuit JSON logs available" },
105
105
  ]
106
106
  }
107
107
  error={circuit_json_build_error!}
@@ -1,4 +1,4 @@
1
- import { Globe, Clock } from "lucide-react"
1
+ import { Globe, GitBranch, GitCommit, Clock } from "lucide-react"
2
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
3
  import { useParams } from "wouter"
4
4
  import { timeAgo } from "@/lib/utils/timeAgo"
@@ -66,7 +66,7 @@ export function PackageBuildDetailsPanel() {
66
66
  </div>
67
67
  <span className="text-sm">{author}</span>
68
68
  <span className="text-sm text-gray-500">
69
- {timeAgo(packageRelease?.created_at, "")}
69
+ {timeAgo(created_at, "")}
70
70
  </span>
71
71
  </div>
72
72
  </div>
@@ -1,18 +1,10 @@
1
- import { ChevronDown, Download, Github, Link, RefreshCw } from "lucide-react"
1
+ import { Github, RefreshCw } from "lucide-react"
2
2
  import { Button } from "@/components/ui/button"
3
- import {
4
- DropdownMenu,
5
- DropdownMenuContent,
6
- DropdownMenuItem,
7
- DropdownMenuTrigger,
8
- } from "@/components/ui/dropdown-menu"
9
3
  import { useParams } from "wouter"
10
- import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
11
4
  import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
12
5
 
13
6
  export function PackageBuildHeader() {
14
7
  const { author, packageName } = useParams()
15
- const { packageRelease } = useCurrentPackageRelease()
16
8
 
17
9
  return (
18
10
  <div className="border-b border-gray-200 bg-white px-6 py-4">
@@ -211,6 +211,7 @@ const MobileSidebar = ({
211
211
  packageAuthor={packageInfo.owner_github_username}
212
212
  onUpdate={handlePackageUpdate}
213
213
  packageName={packageInfo.name}
214
+ unscopedPackageName={packageInfo.unscoped_name}
214
215
  currentDefaultView={packageInfo.default_view}
215
216
  />
216
217
  )}
@@ -161,6 +161,7 @@ export default function SidebarAboutSection() {
161
161
 
162
162
  {packageInfo && (
163
163
  <EditPackageDetailsDialog
164
+ unscopedPackageName={packageInfo.unscoped_name}
164
165
  packageReleaseId={packageInfo.latest_package_release_id}
165
166
  packageId={packageInfo.package_id}
166
167
  currentDescription={
@@ -38,6 +38,7 @@ interface EditPackageDetailsDialogProps {
38
38
  currentDefaultView?: string
39
39
  isPrivate?: boolean
40
40
  packageName: string
41
+ unscopedPackageName: string
41
42
  packageReleaseId: string | null
42
43
  packageAuthor?: string | null
43
44
  onUpdate?: (
@@ -57,7 +58,7 @@ export const EditPackageDetailsDialog = ({
57
58
  currentLicense,
58
59
  currentDefaultView = "files",
59
60
  isPrivate = false,
60
- packageName,
61
+ unscopedPackageName,
61
62
  packageReleaseId,
62
63
  packageAuthor,
63
64
  onUpdate,
@@ -65,13 +66,11 @@ export const EditPackageDetailsDialog = ({
65
66
  const axios = useAxios()
66
67
  const { toast } = useToast()
67
68
  const qc = useQueryClient()
68
-
69
69
  const {
70
70
  formData,
71
71
  setFormData,
72
72
  websiteError,
73
73
  hasLicenseChanged,
74
- hasDefaultViewChanged,
75
74
  hasChanges,
76
75
  isFormValid,
77
76
  } = usePackageDetailsForm({
@@ -79,6 +78,7 @@ export const EditPackageDetailsDialog = ({
79
78
  initialWebsite: currentWebsite,
80
79
  initialLicense: currentLicense || null,
81
80
  initialDefaultView: currentDefaultView,
81
+ initialUnscopedPackageName: unscopedPackageName,
82
82
  isDialogOpen: open,
83
83
  initialVisibility: isPrivate ? "private" : "public",
84
84
  })
@@ -114,16 +114,15 @@ export const EditPackageDetailsDialog = ({
114
114
  website: formData.website,
115
115
  is_private: formData.visibility == "private",
116
116
  default_view: formData.defaultView,
117
- })
118
- const privacyUpdateResponse = await axios.post("/snippets/update", {
119
- snippet_id: packageId,
120
- is_private: formData.visibility === "private",
117
+ ...(formData.unscopedPackageName !== unscopedPackageName && {
118
+ name: formData.unscopedPackageName,
119
+ }),
121
120
  })
122
121
  if (response.status !== 200)
123
122
  throw new Error("Failed to update package details")
124
123
 
125
124
  const filesRes = await axios.post("/package_files/list", {
126
- package_name_with_version: packageName,
125
+ package_name_with_version: `${packageAuthor}/${formData.unscopedPackageName}`,
127
126
  })
128
127
  const packageFiles: string[] =
129
128
  filesRes.status === 200
@@ -137,18 +136,32 @@ export const EditPackageDetailsDialog = ({
137
136
  if (hasLicenseChanged) {
138
137
  if (packageFiles.includes("LICENSE") && !licenseContent) {
139
138
  await axios.post("/package_files/delete", {
140
- package_name_with_version: packageName,
139
+ package_name_with_version: `${packageAuthor}/${formData.unscopedPackageName}`,
141
140
  file_path: "LICENSE",
142
141
  })
143
142
  }
144
143
  if (licenseContent) {
145
144
  await axios.post("/package_files/create_or_update", {
146
- package_name_with_version: packageName,
145
+ package_name_with_version: `${packageAuthor}/${formData.unscopedPackageName}`,
147
146
  file_path: "LICENSE",
148
147
  content_text: licenseContent,
149
148
  })
150
149
  }
151
150
  }
151
+ console.log(
152
+ "formData.unscopedPackageName",
153
+ formData.unscopedPackageName,
154
+ "unscopedPackageName",
155
+ unscopedPackageName,
156
+ )
157
+ if (formData.unscopedPackageName !== unscopedPackageName) {
158
+ // Use router for client-side navigation
159
+ window.history.replaceState(
160
+ {},
161
+ "",
162
+ `/${packageAuthor}/${formData.unscopedPackageName}`,
163
+ )
164
+ }
152
165
 
153
166
  return {
154
167
  description: formData.description,
@@ -187,7 +200,9 @@ export const EditPackageDetailsDialog = ({
187
200
  qc.setQueryData(["packages", packageId], context?.previous)
188
201
  toast({
189
202
  title: "Error",
190
- description: "Failed to update package details. Please try again.",
203
+ description:
204
+ (error as any)?.data?.error?.message ||
205
+ "Failed to update package details. Please try again.",
191
206
  variant: "destructive",
192
207
  })
193
208
  },
@@ -237,11 +252,29 @@ export const EditPackageDetailsDialog = ({
237
252
  </DialogHeader>
238
253
  <div className="">
239
254
  <div className="grid gap-2">
255
+ <div className="space-y-1">
256
+ <Label htmlFor="packageName">Package Name</Label>
257
+ <Input
258
+ id="packageName"
259
+ value={formData.unscopedPackageName}
260
+ onChange={(e) =>
261
+ setFormData((prev) => ({
262
+ ...prev,
263
+ unscopedPackageName: e.target.value,
264
+ }))
265
+ }
266
+ placeholder="Enter package name"
267
+ disabled={updatePackageDetailsMutation.isLoading}
268
+ className="w-full"
269
+ autoComplete="off"
270
+ />
271
+ </div>
240
272
  <div className="space-y-1">
241
273
  <Label htmlFor="website">Website</Label>
242
274
  <Input
243
275
  id="website"
244
276
  value={formData.website}
277
+ autoComplete="off"
245
278
  onChange={(e) =>
246
279
  setFormData((prev) => ({
247
280
  ...prev,
@@ -282,6 +315,7 @@ export const EditPackageDetailsDialog = ({
282
315
  <Label htmlFor="description">Description</Label>
283
316
  <Textarea
284
317
  id="description"
318
+ spellCheck={false}
285
319
  value={formData.description}
286
320
  onChange={(e) =>
287
321
  setFormData((prev) => ({
@@ -66,6 +66,11 @@ export const RenamePackageDialog = ({
66
66
  onChange={(e) => setNewName(e.target.value.replace(" ", "").trim())}
67
67
  placeholder="Enter new name"
68
68
  disabled={renamePackageMutation.isLoading}
69
+ onKeyDown={(e) => {
70
+ if (e.key === "Enter" && !renamePackageMutation.isLoading) {
71
+ renamePackageMutation.mutate()
72
+ }
73
+ }}
69
74
  />
70
75
  <Button
71
76
  disabled={renamePackageMutation.isLoading}