@tscircuit/fake-snippets 0.0.65 → 0.0.67

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.
Files changed (87) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/README.md +2 -2
  3. package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
  4. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
  5. package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
  6. package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
  7. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
  8. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
  9. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +1 -16
  10. package/bun.lock +42 -31
  11. package/dist/bundle.js +34 -41
  12. package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
  13. package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
  14. package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +3 -3
  15. package/package.json +7 -5
  16. package/src/App.tsx +0 -7
  17. package/src/ContextProviders.tsx +2 -0
  18. package/src/components/DownloadButtonAndMenu.tsx +1 -4
  19. package/src/components/ErrorTabContent.tsx +1 -1
  20. package/src/components/Footer.tsx +5 -2
  21. package/src/components/HeaderLogin.tsx +37 -54
  22. package/src/components/ImageWithFallback.tsx +37 -0
  23. package/src/components/JLCPCBImportDialog.tsx +43 -24
  24. package/src/components/PackageCard.tsx +12 -3
  25. package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
  26. package/src/components/PackageSearchResults.tsx +87 -0
  27. package/src/components/PackagesList.tsx +3 -3
  28. package/src/components/PageSearchComponent.tsx +9 -9
  29. package/src/components/ShippingInformationForm.tsx +1 -1
  30. package/src/components/TrendingPackagesCarousel.tsx +43 -23
  31. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
  32. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -22
  33. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +25 -14
  34. package/src/components/ViewPackagePage/components/package-header.tsx +9 -4
  35. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +6 -1
  36. package/src/components/ViewPackagePage/components/repo-page-content.tsx +4 -4
  37. package/src/components/ViewPackagePage/components/sidebar.tsx +2 -14
  38. package/src/components/ViewSnippetSidebar.tsx +1 -1
  39. package/src/components/package-port/CodeEditor.tsx +13 -10
  40. package/src/components/package-port/CodeEditorHeader.tsx +1 -1
  41. package/src/components/package-port/EditorNav.tsx +2 -2
  42. package/src/hooks/use-get-fsmap-hash-for-package.ts +19 -0
  43. package/src/hooks/use-global-store.ts +1 -0
  44. package/src/hooks/use-preview-images.ts +20 -4
  45. package/src/hooks/use-shiki-highlighter.ts +13 -6
  46. package/src/hooks/use-toast.tsx +1 -1
  47. package/src/lib/download-fns/download-gltf.ts +3 -10
  48. package/src/lib/handleManualEditsImport.tsx +1 -1
  49. package/src/lib/types.ts +4 -2
  50. package/src/pages/dashboard.tsx +3 -4
  51. package/src/pages/editor.tsx +20 -14
  52. package/src/pages/latest.tsx +25 -26
  53. package/src/pages/package-editor.tsx +14 -2
  54. package/src/pages/search.tsx +120 -19
  55. package/src/pages/trending.tsx +14 -59
  56. package/src/pages/user-profile.tsx +13 -8
  57. package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
  58. package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
  59. package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
  60. package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
  61. package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
  62. package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
  63. package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
  64. package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
  65. package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
  66. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
  67. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
  68. package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
  69. package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
  70. package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
  71. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
  72. package/src/components/AiChatInterface.tsx +0 -229
  73. package/src/components/CodeAndPreview.tsx +0 -289
  74. package/src/components/CodeEditor.tsx +0 -539
  75. package/src/components/CodeEditorHeader.tsx +0 -135
  76. package/src/components/EditorNav.tsx +0 -502
  77. package/src/components/PreviewContent.tsx +0 -372
  78. package/src/components/SnippetCard.tsx +0 -159
  79. package/src/components/SnippetList.tsx +0 -71
  80. package/src/hooks/use-compiled-tsx.ts +0 -37
  81. package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
  82. package/src/hooks/use-run-tsx/index.tsx +0 -256
  83. package/src/hooks/use-save-snippet.ts +0 -66
  84. package/src/hooks/use-typecheck.ts +0 -54
  85. package/src/lib/utils/getSyntaxError.ts +0 -13
  86. package/src/pages/ai.tsx +0 -92
  87. package/src/pages/view-snippet.tsx +0 -166
@@ -4,29 +4,10 @@ import { z } from "zod"
4
4
  export default withRouteSpec({
5
5
  methods: ["POST"],
6
6
  auth: "session",
7
- jsonBody: z
8
- .object({
9
- package_release_id: z.string().optional(),
10
- circuit_json: z.any().optional(),
11
- vendor_name: z.string(),
12
- })
13
- .superRefine((data, ctx) => {
14
- if (data.circuit_json && data.package_release_id) {
15
- ctx.addIssue({
16
- code: z.ZodIssueCode.custom,
17
- message:
18
- "You must provide either circuit_json or package_release_id, but not both.",
19
- })
20
- }
21
- if (!data.circuit_json && !data.package_release_id) {
22
- ctx.addIssue({
23
- code: z.ZodIssueCode.custom,
24
- message:
25
- "You must provide either circuit_json or package_release_id.",
26
- })
27
- }
28
- }),
29
-
7
+ jsonBody: z.object({
8
+ package_release_id: z.string(),
9
+ vendor_name: z.literal("jlcpcb"),
10
+ }),
30
11
  jsonResponse: z.object({
31
12
  order_quote_id: z.string().optional(),
32
13
  error: z
@@ -37,22 +18,34 @@ export default withRouteSpec({
37
18
  .optional(),
38
19
  }),
39
20
  })(async (req, ctx) => {
40
- const { package_release_id, vendor_name, circuit_json } = req.jsonBody
21
+ const { package_release_id, vendor_name } = req.jsonBody
22
+
23
+ // check package release exists
24
+ const packageRelease = ctx.db.getPackageReleaseById(package_release_id)
25
+ if (!packageRelease) {
26
+ return ctx.json(
27
+ {
28
+ error: {
29
+ error_code: "package_release_not_found",
30
+ message: "Package release not found",
31
+ },
32
+ },
33
+ { status: 404 },
34
+ )
35
+ }
41
36
 
42
- if (package_release_id) {
43
- // check package release exists
44
- const packageRelease = ctx.db.getPackageReleaseById(package_release_id)
45
- if (!packageRelease) {
46
- return ctx.json(
47
- {
48
- error: {
49
- error_code: "package_release_not_found",
50
- message: "Package release not found",
51
- },
37
+ const packageReleaseFiles =
38
+ ctx.db.getPackageFilesByReleaseId(package_release_id)
39
+ if (packageReleaseFiles.length === 0) {
40
+ return ctx.json(
41
+ {
42
+ error: {
43
+ error_code: "package_release_files_not_found",
44
+ message: "Package release files not found",
52
45
  },
53
- { status: 404 },
54
- )
55
- }
46
+ },
47
+ { status: 404 },
48
+ )
56
49
  }
57
50
 
58
51
  const orderQuoteId = ctx.db.addOrderQuote({
@@ -9,8 +9,7 @@ export default withRouteSpec({
9
9
  order_quote_id: z.string(),
10
10
  }),
11
11
  jsonResponse: z.object({
12
- order_quote: orderQuoteSchema.optional(),
13
- error: z.string().optional(),
12
+ order_quote: orderQuoteSchema,
14
13
  }),
15
14
  })(async (req, ctx) => {
16
15
  const { order_quote_id } = req.commonParams
@@ -18,12 +17,10 @@ export default withRouteSpec({
18
17
  const orderQuote = ctx.db.getOrderQuoteById(order_quote_id)
19
18
 
20
19
  if (!orderQuote) {
21
- return ctx.json(
22
- {
23
- error: "Order quote not found",
24
- },
25
- { status: 404 },
26
- )
20
+ return ctx.error(404, {
21
+ error_code: "order_quote_not_found",
22
+ message: "Order quote not found",
23
+ })
27
24
  }
28
25
 
29
26
  return ctx.json({
@@ -11,9 +11,9 @@ import { z } from "zod"
11
11
  const VIEW_TYPES = ["schematic", "pcb", "assembly", "3d"] as const
12
12
  const EXTENSIONS = ["svg", "png"] as const
13
13
 
14
- // Create a regex pattern for the view format
14
+ // Create a regex pattern for the view format that includes optional width suffix
15
15
  const viewFormatPattern = new RegExp(
16
- `^(${VIEW_TYPES.join("|")})\\.(${EXTENSIONS.join("|")})$`,
16
+ `^(${VIEW_TYPES.join("|")})-?(\\d+w)?\\.(${EXTENSIONS.join("|")})$`,
17
17
  )
18
18
 
19
19
  export default withRouteSpec({
@@ -23,7 +23,7 @@ export default withRouteSpec({
23
23
  owner_github_username: z.string(),
24
24
  unscoped_name: z.string(),
25
25
  view_format: z.string().regex(viewFormatPattern, {
26
- message: `Invalid view format. Must be one of: ${VIEW_TYPES.join(", ")}.${EXTENSIONS.join(" or ")}`,
26
+ message: `Invalid view format. Must be one of: ${VIEW_TYPES.join(", ")} with optional width suffix (e.g. -800w).${EXTENSIONS.join(" or ")}`,
27
27
  }),
28
28
  }),
29
29
  queryParams: z.object({
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.65",
3
+ "version": "0.0.67",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
7
- "url": "https://github.com/tscircuit/snippets"
7
+ "url": "https://github.com/tscircuit/tscircuit.com"
8
8
  },
9
9
  "main": "dist/index.js",
10
10
  "exports": {
@@ -24,6 +24,7 @@
24
24
  "preview": "vite preview",
25
25
  "format": "biome format --write .",
26
26
  "lint": "biome format .",
27
+ "build:vite:analyze": "VITE_BUNDLE_ANALYZE=true vite build",
27
28
  "build:fake-api:tsup": "tsup-node ./fake-snippets-api/lib/index.ts --format esm --dts",
28
29
  "build:fake-api:bundle": "winterspec bundle -o dist/bundle.js",
29
30
  "build:fake-api": "bun run build:fake-api:tsup && bun run build:fake-api:bundle && bun run build:fake-api:schema",
@@ -70,7 +71,7 @@
70
71
  "@radix-ui/react-toggle-group": "^1.1.0",
71
72
  "@radix-ui/react-tooltip": "^1.1.2",
72
73
  "@tscircuit/3d-viewer": "^0.0.142",
73
- "@tscircuit/eval": "^0.0.170",
74
+ "@tscircuit/eval": "^0.0.198",
74
75
  "@tscircuit/footprinter": "^0.0.124",
75
76
  "@tscircuit/layout": "^0.0.29",
76
77
  "@tscircuit/math-utils": "^0.0.10",
@@ -85,7 +86,7 @@
85
86
  "change-case": "^5.4.4",
86
87
  "circuit-json": "^0.0.164",
87
88
  "circuit-json-to-bom-csv": "^0.0.6",
88
- "circuit-json-to-gerber": "^0.0.19",
89
+ "circuit-json-to-gerber": "^0.0.21",
89
90
  "circuit-json-to-pnp-csv": "^0.0.6",
90
91
  "circuit-json-to-readable-netlist": "^0.0.8",
91
92
  "circuit-json-to-tscircuit": "^0.0.4",
@@ -127,6 +128,7 @@
127
128
  "recharts": "^2.12.7",
128
129
  "remark-gfm": "^4.0.1",
129
130
  "rollup-plugin-visualizer": "^5.12.0",
131
+ "schematic-symbols": "^0.0.132",
130
132
  "sitemap": "^8.0.0",
131
133
  "sonner": "^1.5.0",
132
134
  "states-us": "^1.1.1",
@@ -146,7 +148,7 @@
146
148
  "@tailwindcss/typography": "^0.5.16",
147
149
  "@tscircuit/core": "^0.0.384",
148
150
  "@tscircuit/prompt-benchmarks": "^0.0.28",
149
- "@tscircuit/runframe": "^0.0.427",
151
+ "@tscircuit/runframe": "^0.0.461",
150
152
  "@types/babel__standalone": "^7.1.7",
151
153
  "@types/bun": "^1.1.10",
152
154
  "@types/country-list": "^2.1.4",
package/src/App.tsx CHANGED
@@ -49,7 +49,6 @@ const lazyImport = (importFn: () => Promise<any>) =>
49
49
  }
50
50
  })
51
51
 
52
- const AiPage = lazyImport(() => import("@/pages/ai"))
53
52
  const AuthenticatePage = lazyImport(() => import("@/pages/authorize"))
54
53
  const DashboardPage = lazyImport(() => import("@/pages/dashboard"))
55
54
  const EditorPage = lazyImport(async () => {
@@ -68,7 +67,6 @@ const SearchPage = lazyImport(() => import("@/pages/search"))
68
67
  const SettingsPage = lazyImport(() => import("@/pages/settings"))
69
68
  const UserProfilePage = lazyImport(() => import("@/pages/user-profile"))
70
69
  const ViewOrderPage = lazyImport(() => import("@/pages/view-order"))
71
- const ViewSnippetPage = lazyImport(() => import("@/pages/view-snippet"))
72
70
  const DevLoginPage = lazyImport(() => import("@/pages/dev-login"))
73
71
  const BetaPage = lazyImport(() => import("@/pages/beta"))
74
72
  const ViewPackagePage = lazyImport(() => import("@/pages/view-package"))
@@ -119,7 +117,6 @@ function App() {
119
117
  <Route path="/legacy-editor" component={EditorPage} />
120
118
  <Route path="/quickstart" component={QuickstartPage} />
121
119
  <Route path="/dashboard" component={DashboardPage} />
122
- <Route path="/ai" component={AiPage} />
123
120
  <Route path="/latest" component={LatestPage} />
124
121
  <Route path="/settings" component={SettingsPage} />
125
122
  <Route path="/search" component={SearchPage} />
@@ -131,10 +128,6 @@ function App() {
131
128
  <Route path="/dev-login" component={DevLoginPage} />
132
129
  <Route path="/:username" component={UserProfilePage} />
133
130
  <Route path="/:author/:packageName" component={ViewPackagePage} />
134
- <Route
135
- path="/snippets/:author/:snippetName"
136
- component={ViewSnippetPage}
137
- />
138
131
  <Route component={lazyImport(() => import("@/pages/404"))} />
139
132
  </Switch>
140
133
  </Suspense>
@@ -3,6 +3,7 @@ import { HelmetProvider } from "react-helmet-async"
3
3
  import { useEffect } from "react"
4
4
  import { useGlobalStore } from "./hooks/use-global-store"
5
5
  import { posthog } from "./lib/posthog"
6
+ import { Toaster } from "react-hot-toast"
6
7
 
7
8
  const staffGithubUsernames = [
8
9
  "imrishabh18",
@@ -61,6 +62,7 @@ export const ContextProviders = ({ children }: any) => {
61
62
  <HelmetProvider>
62
63
  <PostHogIdentifier />
63
64
  {children}
65
+ <Toaster />
64
66
  </HelmetProvider>
65
67
  </QueryClientProvider>
66
68
  )
@@ -83,10 +83,7 @@ export function DownloadButtonAndMenu({
83
83
  className="text-xs"
84
84
  onClick={async () => {
85
85
  try {
86
- await downloadGltf(
87
- circuitJson,
88
- snippetUnscopedName || "circuit",
89
- )
86
+ await downloadGltf(snippetUnscopedName || "circuit")
90
87
  } catch (error: any) {
91
88
  toast({
92
89
  title: "Error Downloading 3D Model",
@@ -104,7 +104,7 @@ ${errorMessage}
104
104
  body = `\`\`\`tsx\n// Please paste the code here\`\`\`\n\n### Error\n\`\`\`\n${errorMessage.slice(0, 2000)}\n\`\`\``
105
105
  }
106
106
  window.open(
107
- `https://github.com/tscircuit/snippets/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`,
107
+ `https://github.com/tscircuit/tscircuit.com/issues/new?title=${encodeURIComponent(title)}&body=${encodeURIComponent(body)}`,
108
108
  "_blank",
109
109
  )
110
110
  }}
@@ -57,10 +57,13 @@ export default function Footer() {
57
57
  <h3 className="font-semibold uppercase">Explore</h3>
58
58
  <footer className="flex flex-col space-y-2">
59
59
  <PrefetchPageLink href="/latest" className="hover:underline">
60
- Latest Snippets
60
+ Latest Packages
61
61
  </PrefetchPageLink>
62
62
  <PrefetchPageLink href="/trending" className="hover:underline">
63
- Trending Snippets
63
+ Trending Packages
64
+ </PrefetchPageLink>
65
+ <PrefetchPageLink href="/search" className="hover:underline">
66
+ Search Packages
64
67
  </PrefetchPageLink>
65
68
  <a href="https://docs.tscircuit.com" className="hover:underline">
66
69
  Docs
@@ -1,4 +1,4 @@
1
- import React, { useState } from "react"
1
+ import React from "react"
2
2
  import { Button } from "@/components/ui/button"
3
3
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
4
4
  import {
@@ -7,39 +7,19 @@ import {
7
7
  DropdownMenuItem,
8
8
  DropdownMenuTrigger,
9
9
  } from "@/components/ui/dropdown-menu"
10
- import { Link, useLocation, useRouter } from "wouter"
11
10
  import { User } from "lucide-react"
12
- import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
13
11
  import { useGlobalStore } from "@/hooks/use-global-store"
14
12
  import { useAccountBalance } from "@/hooks/use-account-balance"
15
- import { useIsUsingFakeApi } from "@/hooks/use-is-using-fake-api"
16
13
  import { useSignIn } from "@/hooks/use-sign-in"
17
14
 
18
- interface HeaderLoginProps {}
19
-
20
- export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
21
- const [, setLocation] = useLocation()
15
+ export const HeaderLogin = () => {
22
16
  const session = useGlobalStore((s) => s.session)
23
17
  const isLoggedIn = Boolean(session)
24
18
  const setSession = useGlobalStore((s) => s.setSession)
25
- const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
26
- const isUsingFakeApi = useIsUsingFakeApi()
27
19
  const signIn = useSignIn()
28
20
  const { data: accountBalance } = useAccountBalance()
29
21
 
30
22
  if (!isLoggedIn) {
31
- const handleLogin = () => {
32
- if (isUsingFakeApi) {
33
- setSession({
34
- account_id: "account-1234",
35
- github_username: "testuser",
36
- token: "1234",
37
- session_id: "session-1234",
38
- })
39
- } else {
40
- signIn()
41
- }
42
- }
43
23
  return (
44
24
  <div className="flex items-center md:space-x-2 justify-end">
45
25
  <Button onClick={() => signIn()} variant="ghost">
@@ -51,44 +31,47 @@ export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
51
31
  }
52
32
 
53
33
  return (
54
- <div className="flex justify-end items-center">
55
- <DropdownMenu>
56
- <DropdownMenuTrigger>
57
- <Avatar className="w-8 h-8 login-avatar">
58
- <AvatarImage
59
- src={`https://github.com/${session?.github_username}.png`}
60
- alt={`${session?.github_username}'s profile picture`}
61
- />
62
- <AvatarFallback aria-label="User avatar fallback">
63
- <User size={16} aria-hidden="true" />
64
- </AvatarFallback>
65
- </Avatar>
66
- </DropdownMenuTrigger>
67
- <DropdownMenuContent>
68
- <DropdownMenuItem className="text-gray-500 text-xs" disabled>
34
+ <DropdownMenu>
35
+ <DropdownMenuTrigger asChild>
36
+ <Avatar className="w-8 h-8 login-avatar">
37
+ <AvatarImage
38
+ src={`https://github.com/${session?.github_username}.png`}
39
+ alt={`${session?.github_username}'s profile picture`}
40
+ />
41
+ <AvatarFallback aria-label="User avatar fallback">
42
+ <User size={16} aria-hidden="true" />
43
+ </AvatarFallback>
44
+ </Avatar>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent>
47
+ <DropdownMenuItem asChild className="text-gray-500 text-xs" disabled>
48
+ <div>
69
49
  AI Usage $
70
50
  {accountBalance?.monthly_ai_budget_used_usd.toFixed(2) ?? "0.00"} /
71
51
  $5.00
72
- </DropdownMenuItem>
73
- <DropdownMenuItem
74
- onClick={() => setLocation(`/${session?.github_username}`)}
75
- >
52
+ </div>
53
+ </DropdownMenuItem>
54
+ <DropdownMenuItem asChild>
55
+ <a href={`/${session?.github_username}`} className="cursor-pointer">
76
56
  My Profile
77
- </DropdownMenuItem>
78
- <DropdownMenuItem onClick={() => setLocation("/dashboard")}>
57
+ </a>
58
+ </DropdownMenuItem>
59
+ <DropdownMenuItem asChild>
60
+ <a href="/dashboard" className="cursor-pointer">
79
61
  Dashboard
80
- </DropdownMenuItem>
81
- <DropdownMenuItem onClick={() => setLocation("/my-orders")}>
82
- My Orders
83
- </DropdownMenuItem>
84
- <DropdownMenuItem onClick={() => setLocation("/settings")}>
62
+ </a>
63
+ </DropdownMenuItem>
64
+ <DropdownMenuItem asChild>
65
+ <a href="/settings" className="cursor-pointer">
85
66
  Settings
86
- </DropdownMenuItem>
87
- <DropdownMenuItem onClick={() => setSession(null)}>
67
+ </a>
68
+ </DropdownMenuItem>
69
+ <DropdownMenuItem asChild onClick={() => setSession(null)}>
70
+ <a href="/sign-out" className="cursor-pointer">
88
71
  Sign out
89
- </DropdownMenuItem>
90
- </DropdownMenuContent>
91
- </DropdownMenu>
92
- </div>
72
+ </a>
73
+ </DropdownMenuItem>
74
+ </DropdownMenuContent>
75
+ </DropdownMenu>
93
76
  )
94
77
  }
@@ -0,0 +1,37 @@
1
+ import { useState } from "react"
2
+
3
+ interface ImageWithFallbackProps
4
+ extends React.ImgHTMLAttributes<HTMLImageElement> {
5
+ src: string
6
+ alt: string
7
+ className?: string
8
+ fallbackSrc?: string
9
+ }
10
+
11
+ export function ImageWithFallback({
12
+ src,
13
+ alt,
14
+ className = "",
15
+ fallbackSrc = "/assets/fallback-image.svg",
16
+ ...props
17
+ }: ImageWithFallbackProps) {
18
+ const [loading, setLoading] = useState(true)
19
+ const [currentSrc, setCurrentSrc] = useState(src)
20
+
21
+ return (
22
+ <img
23
+ src={currentSrc}
24
+ alt={alt}
25
+ className={`object-contain h-full w-full ${
26
+ loading ? "animate-pulse bg-gray-200" : ""
27
+ } ${className}`}
28
+ onLoad={() => setLoading(false)}
29
+ onError={() => {
30
+ console.error("PCB image failed to load:", src)
31
+ setCurrentSrc(fallbackSrc)
32
+ setLoading(false)
33
+ }}
34
+ {...props}
35
+ />
36
+ )
37
+ }
@@ -27,6 +27,9 @@ export function JLCPCBImportDialog({
27
27
  const [partNumber, setPartNumber] = useState("")
28
28
  const [isLoading, setIsLoading] = useState(false)
29
29
  const [error, setError] = useState<string | null>(null)
30
+ const [alreadyImportedPackageId, setAlreadyImportedPackageId] = useState<
31
+ string | null
32
+ >(null)
30
33
  const axios = useAxios()
31
34
  const { toast } = useToast()
32
35
  const [, navigate] = useLocation()
@@ -45,29 +48,20 @@ export function JLCPCBImportDialog({
45
48
 
46
49
  setIsLoading(true)
47
50
  setError(null)
51
+ setAlreadyImportedPackageId(null)
52
+
48
53
  try {
49
- // Check that module doesn't already exist
50
- const existingSnippetRes = await axios.get(
54
+ const existingPackageRes = await axios.get(
51
55
  `/snippets/get?owner_name=${session?.github_username}&unscoped_name=${partNumber}`,
52
56
  {
53
57
  validateStatus: (status) => true,
54
58
  },
55
59
  )
56
60
 
57
- if (existingSnippetRes.status !== 404) {
58
- toast({
59
- title: "JLCPCB Part Already Imported",
60
- description: (
61
- <div>
62
- <PrefetchPageLink
63
- className="text-blue-500 hover:underline"
64
- href={`/editor?package_id=${existingSnippetRes.data.snippet.snippet_id}`}
65
- >
66
- View {partNumber}
67
- </PrefetchPageLink>
68
- </div>
69
- ),
70
- })
61
+ if (existingPackageRes.status !== 404) {
62
+ const packageId = existingPackageRes.data.snippet.snippet_id
63
+ setAlreadyImportedPackageId(packageId)
64
+ setIsLoading(false)
71
65
  return
72
66
  }
73
67
 
@@ -76,16 +70,20 @@ export function JLCPCBImportDialog({
76
70
  jlcpcb_part_number: partNumber,
77
71
  })
78
72
  .catch((e) => e)
73
+
79
74
  const { snippet, error } = response.data
75
+
80
76
  if (error) {
81
77
  setError(error.message)
82
78
  setIsLoading(false)
83
79
  return
84
80
  }
81
+
85
82
  toast({
86
83
  title: "Import Successful",
87
84
  description: "JLCPCB component has been imported successfully.",
88
85
  })
86
+
89
87
  onOpenChange(false)
90
88
  navigate(`/editor?package_id=${snippet.snippet_id}`)
91
89
  } catch (error) {
@@ -121,10 +119,17 @@ export function JLCPCBImportDialog({
121
119
  className="mt-3"
122
120
  placeholder="Enter JLCPCB part number (e.g., C46749)"
123
121
  value={partNumber}
124
- onChange={(e) => setPartNumber(e.target.value)}
122
+ onChange={(e) => {
123
+ setPartNumber(e.target.value)
124
+ setError(null)
125
+ setAlreadyImportedPackageId(null)
126
+ }}
125
127
  />
126
- {error && <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>}
127
- {error && (
128
+ {error && !alreadyImportedPackageId && (
129
+ <p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>
130
+ )}
131
+
132
+ {error && !alreadyImportedPackageId && (
128
133
  <div className="flex justify-end mt-2">
129
134
  <Button
130
135
  variant="default"
@@ -132,21 +137,35 @@ export function JLCPCBImportDialog({
132
137
  const issueTitle = `[${partNumber}] Failed to import from JLCPCB`
133
138
  const issueBody = `I tried to import the part number ${partNumber} from JLCPCB, but it failed. Here's the error I got:\n\`\`\`\n${error}\n\`\`\`\n\nCould be an issue in \`fetchEasyEDAComponent\` or \`convertRawEasyEdaToTs\``
134
139
  const issueLabels = "snippets,good first issue"
135
- const url = `https://github.com/tscircuit/easyeda-converter/issues/new?title=${encodeURIComponent(issueTitle)}&body=${encodeURIComponent(issueBody)}&labels=${encodeURIComponent(issueLabels)}`
136
-
137
- // Open the issue in a new tab
140
+ const url = `https://github.com/tscircuit/easyeda-converter/issues/new?title=${encodeURIComponent(
141
+ issueTitle,
142
+ )}&body=${encodeURIComponent(
143
+ issueBody,
144
+ )}&labels=${encodeURIComponent(issueLabels)}`
138
145
  window.open(url, "_blank")
139
146
  }}
140
147
  >
141
- File Issue on Github (prefilled)
148
+ File Issue on GitHub (prefilled)
142
149
  </Button>
143
150
  </div>
144
151
  )}
152
+
153
+ {alreadyImportedPackageId && (
154
+ <p className="p-2 mt-2 pre-wrap text-md text-green-600">
155
+ This part number has already been imported to your profile.{" "}
156
+ <PrefetchPageLink
157
+ className="text-blue-500 hover:underline"
158
+ href={`/${session?.github_username}/${partNumber}`}
159
+ >
160
+ View it here
161
+ </PrefetchPageLink>
162
+ </p>
163
+ )}
145
164
  </div>
146
165
  <DialogFooter>
147
166
  <Button onClick={handleImport} disabled={isLoading || !isLoggedIn}>
148
167
  {!isLoggedIn
149
- ? "Must be logged in for jlcpcb import"
168
+ ? "You must be logged in to import from JLCPCB"
150
169
  : isLoading
151
170
  ? "Importing..."
152
171
  : "Import"}
@@ -10,9 +10,10 @@ import {
10
10
  DropdownMenuItem,
11
11
  DropdownMenuTrigger,
12
12
  } from "@/components/ui/dropdown-menu"
13
- import { OptimizedImage } from "./OptimizedImage"
14
13
  import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
15
14
  import { timeAgo } from "@/lib/utils/timeAgo"
15
+ import { useGetFsMapHashForPackage } from "@/hooks/use-get-fsmap-hash-for-package"
16
+ import { ImageWithFallback } from "./ImageWithFallback"
16
17
 
17
18
  export interface PackageCardProps {
18
19
  /** The package data to display */
@@ -56,6 +57,10 @@ export const PackageCard: React.FC<PackageCardProps> = ({
56
57
  }
57
58
  }
58
59
 
60
+ const fsMapHash = useGetFsMapHashForPackage(
61
+ pkg.latest_package_release_id ?? "",
62
+ )
63
+
59
64
  const cardContent = (
60
65
  <div
61
66
  className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
@@ -64,8 +69,12 @@ export const PackageCard: React.FC<PackageCardProps> = ({
64
69
  <div
65
70
  className={`${imageSize} flex-shrink-0 rounded-md overflow-hidden`}
66
71
  >
67
- <OptimizedImage
68
- src={`${baseUrl}/snippets/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.svg`}
72
+ <ImageWithFallback
73
+ src={`${baseUrl}/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.svg?${new URLSearchParams(
74
+ {
75
+ fs_sha: fsMapHash ?? "",
76
+ },
77
+ ).toString()}`}
69
78
  alt={`${pkg.unscoped_name} PCB image`}
70
79
  className={`object-cover h-full w-full ${imageTransform}`}
71
80
  />
@@ -1,35 +1,27 @@
1
1
  import { Link } from "wouter"
2
2
  import { Star } from "lucide-react"
3
+ import { Package } from "fake-snippets-api/lib/db/schema"
3
4
 
4
- export const SnippetLink = ({
5
- snippet,
6
- }: {
7
- snippet: {
8
- owner_name: string
9
- name: string
10
- unscoped_name: string
11
- star_count?: number
12
- }
13
- }) => {
5
+ export const PackageLink = (pkg: Package) => {
14
6
  return (
15
7
  <>
16
8
  <Link
17
9
  className="text-blue-500 font-semibold hover:underline"
18
- href={`/${snippet.owner_name}`}
10
+ href={`/${pkg.owner_github_username}`}
19
11
  >
20
- {snippet.owner_name}
12
+ {pkg.owner_github_username}
21
13
  </Link>
22
14
  <span className="px-0.5 text-gray-500">/</span>
23
15
  <Link
24
16
  className="text-blue-500 font-semibold hover:underline"
25
- href={`/${snippet.name}`}
17
+ href={`/${pkg.unscoped_name}`}
26
18
  >
27
- {snippet.unscoped_name}
19
+ {pkg.unscoped_name}
28
20
  </Link>
29
- {snippet.star_count !== undefined && (
21
+ {pkg.star_count !== undefined && (
30
22
  <span className="ml-2 text-gray-500 text-xs flex items-center">
31
23
  <Star className="w-3 h-3 mr-1" />
32
- {snippet.star_count}
24
+ {pkg.star_count}
33
25
  </span>
34
26
  )}
35
27
  </>