@tscircuit/fake-snippets 0.0.109 → 0.0.110

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 (181) hide show
  1. package/.github/workflows/bun-formatcheck.yml +2 -2
  2. package/.github/workflows/bun-pver-release.yml +3 -3
  3. package/.github/workflows/bun-test.yml +1 -1
  4. package/.github/workflows/bun-typecheck.yml +2 -2
  5. package/.github/workflows/update-snapshots.yml +1 -1
  6. package/README.md +4 -0
  7. package/api/generated-index.js +37 -3
  8. package/biome.json +2 -1
  9. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +31 -3
  10. package/bun-tests/fake-snippets-api/fixtures/preload.ts +18 -0
  11. package/bun-tests/fake-snippets-api/routes/orgs/add_member.test.ts +26 -0
  12. package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +37 -0
  13. package/bun-tests/fake-snippets-api/routes/orgs/get.test.ts +52 -0
  14. package/bun-tests/fake-snippets-api/routes/orgs/list.test.ts +17 -0
  15. package/bun-tests/fake-snippets-api/routes/orgs/list_members.test.ts +23 -0
  16. package/bun-tests/fake-snippets-api/routes/orgs/remove_member.test.ts +81 -0
  17. package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +99 -0
  18. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +1 -1
  19. package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +15 -13
  20. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -24
  21. package/bun-tests/fake-snippets-api/routes/package_files/delete.test.ts +9 -9
  22. package/bun-tests/fake-snippets-api/routes/package_files/download.test.ts +4 -4
  23. package/bun-tests/fake-snippets-api/routes/package_files/get.test.ts +38 -28
  24. package/bun-tests/fake-snippets-api/routes/package_files/list.test.ts +23 -15
  25. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +33 -0
  26. package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +4 -4
  27. package/bun-tests/fake-snippets-api/routes/package_releases/get_image_generation_fields.test.ts +38 -0
  28. package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +19 -0
  29. package/bun-tests/fake-snippets-api/routes/packages/fork.test.ts +3 -4
  30. package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +30 -0
  31. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +4 -2
  32. package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +34 -0
  33. package/bun.lock +349 -453
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1253 -624
  36. package/dist/index.d.ts +291 -4
  37. package/dist/index.js +323 -23
  38. package/dist/schema.d.ts +274 -1
  39. package/dist/schema.js +52 -1
  40. package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
  41. package/fake-snippets-api/lib/db/db-client.ts +214 -3
  42. package/fake-snippets-api/lib/db/schema.ts +61 -0
  43. package/fake-snippets-api/lib/db/seed.ts +100 -0
  44. package/fake-snippets-api/lib/middleware/with-session-auth.ts +1 -1
  45. package/fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor.ts +2 -2
  46. package/fake-snippets-api/lib/public-mapping/public-map-org.ts +32 -0
  47. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +10 -0
  48. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +17 -0
  49. package/fake-snippets-api/routes/api/orgs/add_member.ts +52 -0
  50. package/fake-snippets-api/routes/api/orgs/create.ts +46 -0
  51. package/fake-snippets-api/routes/api/orgs/get.ts +39 -0
  52. package/fake-snippets-api/routes/api/orgs/list.ts +31 -0
  53. package/fake-snippets-api/routes/api/orgs/list_members.ts +67 -0
  54. package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
  55. package/fake-snippets-api/routes/api/orgs/update.ts +93 -0
  56. package/fake-snippets-api/routes/api/package_files/get.ts +3 -6
  57. package/fake-snippets-api/routes/api/package_files/list.ts +7 -4
  58. package/fake-snippets-api/routes/api/packages/create.ts +54 -10
  59. package/fake-snippets-api/routes/api/packages/get.ts +23 -0
  60. package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +13 -11
  61. package/fake-snippets-api/routes/api/packages/list.ts +29 -2
  62. package/fake-snippets-api/routes/api/packages/update_ai_description.ts +37 -0
  63. package/package.json +24 -20
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +29 -8
  67. package/src/ContextProviders.tsx +25 -2
  68. package/src/components/CircuitJsonImportDialog.tsx +1 -1
  69. package/src/components/CmdKMenu.tsx +281 -247
  70. package/src/components/DownloadButtonAndMenu.tsx +3 -4
  71. package/src/components/FileSidebar.tsx +11 -17
  72. package/src/components/Footer.tsx +8 -9
  73. package/src/components/Header.tsx +19 -32
  74. package/src/components/Header2.tsx +16 -32
  75. package/src/components/HeaderDropdown.tsx +13 -8
  76. package/src/components/HeaderLogin.tsx +43 -15
  77. package/src/components/NotFound.tsx +5 -5
  78. package/src/components/PackageBreadcrumb.tsx +6 -12
  79. package/src/components/PackageSearchResults.tsx +1 -1
  80. package/src/components/PrefetchPageLink.tsx +7 -1
  81. package/src/components/ProfileRouter.tsx +32 -0
  82. package/src/components/SearchComponent.tsx +12 -8
  83. package/src/components/UserCard.tsx +80 -0
  84. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  85. package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
  86. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
  87. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
  88. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
  89. package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
  90. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
  91. package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
  92. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  93. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  94. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  95. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  96. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  97. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  98. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  99. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  100. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  101. package/src/components/organization/OrganizationCard.tsx +204 -0
  102. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  103. package/src/components/organization/OrganizationHeader.tsx +154 -0
  104. package/src/components/organization/OrganizationMembers.tsx +146 -0
  105. package/src/components/package-port/CodeAndPreview.tsx +15 -12
  106. package/src/components/package-port/CodeEditor.tsx +4 -30
  107. package/src/components/package-port/CodeEditorHeader.tsx +123 -61
  108. package/src/components/package-port/EditorNav.tsx +32 -49
  109. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  110. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  111. package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
  112. package/src/components/ui/tree-view.tsx +6 -3
  113. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  114. package/src/hooks/use-create-org-mutation.ts +38 -0
  115. package/src/hooks/use-create-package-mutation.ts +3 -0
  116. package/src/hooks/use-current-package-release.ts +4 -3
  117. package/src/hooks/use-download-zip.ts +2 -2
  118. package/src/hooks/use-global-store.ts +6 -4
  119. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  120. package/src/hooks/use-list-org-members.ts +27 -0
  121. package/src/hooks/use-list-user-orgs.ts +25 -0
  122. package/src/hooks/use-org-by-github-handle.ts +26 -0
  123. package/src/hooks/use-org.ts +24 -0
  124. package/src/hooks/use-organization.ts +42 -0
  125. package/src/hooks/use-package-as-snippet.ts +4 -2
  126. package/src/hooks/use-package-builds.ts +6 -2
  127. package/src/hooks/use-package-files.ts +5 -3
  128. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  129. package/src/hooks/use-package-release-images.ts +105 -0
  130. package/src/hooks/use-package-release.ts +2 -2
  131. package/src/hooks/use-package-stars.ts +80 -4
  132. package/src/hooks/use-preview-images.ts +6 -3
  133. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  134. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  135. package/src/hooks/use-update-org-mutation.ts +41 -0
  136. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  137. package/src/hooks/useFileManagement.ts +51 -22
  138. package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
  139. package/src/hooks/usePackageFilesLoader.ts +2 -2
  140. package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
  141. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
  142. package/src/lib/download-fns/download-kicad-files.ts +12 -11
  143. package/src/lib/normalize-svg-for-tile.ts +50 -0
  144. package/src/lib/posthog.ts +11 -9
  145. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  146. package/src/lib/sentry.ts +14 -0
  147. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  148. package/src/lib/ts-lib-cache.ts +122 -7
  149. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  150. package/src/lib/utils/findTargetFile.ts +45 -10
  151. package/src/lib/utils/isComponentExported.ts +2 -1
  152. package/src/main.tsx +2 -1
  153. package/src/pages/create-organization.tsx +168 -0
  154. package/src/pages/dashboard.tsx +38 -6
  155. package/src/pages/datasheet.tsx +1 -1
  156. package/src/pages/datasheets.tsx +3 -3
  157. package/src/pages/editor.tsx +4 -6
  158. package/src/pages/landing.tsx +6 -6
  159. package/src/pages/latest.tsx +3 -0
  160. package/src/pages/organization-profile.tsx +199 -0
  161. package/src/pages/organization-settings.tsx +566 -0
  162. package/src/pages/package-editor.tsx +21 -21
  163. package/src/pages/preview-release.tsx +75 -145
  164. package/src/pages/quickstart.tsx +159 -123
  165. package/src/pages/release-detail.tsx +119 -31
  166. package/src/pages/search.tsx +192 -57
  167. package/src/pages/settings-redirect.tsx +44 -0
  168. package/src/pages/trending.tsx +29 -20
  169. package/src/pages/user-profile.tsx +58 -7
  170. package/src/pages/view-package.tsx +7 -13
  171. package/vite.config.ts +9 -0
  172. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  173. package/src/components/JLCPCBImportDialog.tsx +0 -280
  174. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  175. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
  176. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
  177. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  178. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  179. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  180. package/src/components/PageSearchComponent.tsx +0 -148
  181. package/src/pages/package-builds.tsx +0 -33
@@ -1,23 +1,90 @@
1
- import { useEffect } from "react"
1
+ import { useEffect, useRef } from "react"
2
+
2
3
  export default function useWarnUserOnPageChange({
3
4
  hasUnsavedChanges,
5
+ isPackageThere,
4
6
  }: {
5
7
  hasUnsavedChanges: boolean
8
+ isPackageThere: Boolean
6
9
  }) {
10
+ const originalTitleRef = useRef<string>("")
11
+
7
12
  useEffect(() => {
13
+ if (!hasUnsavedChanges || !originalTitleRef.current) {
14
+ originalTitleRef.current = document.title.replace(/^⚠️\s*/, "")
15
+ }
16
+
8
17
  const handleBeforeUnload = (event: BeforeUnloadEvent) => {
9
18
  if (hasUnsavedChanges) {
19
+ const message =
20
+ "You have unsaved changes. Are you sure you want to leave?"
10
21
  event.preventDefault()
11
- event.returnValue = "" // Shows the confirmation dialog on reload or close if there are unsaved changes
22
+ event.returnValue = message
23
+ return message
24
+ }
25
+ }
26
+
27
+ const handleVisibilityChange = () => {
28
+ if (document.hidden && hasUnsavedChanges) {
29
+ if (!document.title.startsWith("⚠️")) {
30
+ document.title = "⚠️ " + originalTitleRef.current
31
+ }
32
+ } else if (!document.hidden) {
33
+ document.title = originalTitleRef.current
34
+ }
35
+ }
36
+
37
+ const handlePopState = (event: PopStateEvent) => {
38
+ if (hasUnsavedChanges && isPackageThere) {
39
+ const shouldLeave = window.confirm(
40
+ "You have unsaved changes. Are you sure you want to leave this page?",
41
+ )
42
+ if (!shouldLeave) {
43
+ window.history.pushState(null, "", window.location.href)
44
+ }
45
+ }
46
+ }
47
+
48
+ const handleLinkClick = (event: MouseEvent) => {
49
+ if (!hasUnsavedChanges) return
50
+
51
+ const target = event.target as HTMLElement
52
+ const link = target.closest("a[href]") as HTMLAnchorElement
53
+
54
+ if (link && link.href) {
55
+ try {
56
+ const linkUrl = new URL(link.href)
57
+ const currentUrl = new URL(window.location.href)
58
+
59
+ if (linkUrl.origin === currentUrl.origin) {
60
+ event.preventDefault()
61
+ event.stopPropagation()
62
+
63
+ const shouldLeave = window.confirm(
64
+ "You have unsaved changes. Are you sure you want to leave this page?",
65
+ )
66
+
67
+ if (shouldLeave) {
68
+ window.location.href = link.href
69
+ }
70
+ }
71
+ } catch (error) {
72
+ console.warn("Failed to parse link URL:", error)
73
+ }
12
74
  }
13
75
  }
14
76
 
15
- // Attach event listeners
16
77
  window.addEventListener("beforeunload", handleBeforeUnload)
78
+ document.addEventListener("visibilitychange", handleVisibilityChange)
79
+ window.addEventListener("popstate", handlePopState)
80
+ document.addEventListener("click", handleLinkClick, true)
17
81
 
18
- // Cleanup event listeners on component unmount
19
82
  return () => {
20
83
  window.removeEventListener("beforeunload", handleBeforeUnload)
84
+ document.removeEventListener("visibilitychange", handleVisibilityChange)
85
+ window.removeEventListener("popstate", handlePopState)
86
+ document.removeEventListener("click", handleLinkClick, true)
87
+ document.title = originalTitleRef.current
21
88
  }
22
89
  }, [hasUnsavedChanges])
23
90
  }
@@ -73,11 +73,7 @@ export function useFileManagement({
73
73
  totalFilesCount,
74
74
  loadedFilesCount,
75
75
  isPriorityFileFetched,
76
- } = useOptimizedPackageFilesLoader(
77
- currentPackage,
78
- fileChosen,
79
- urlParams.package_id,
80
- )
76
+ } = useOptimizedPackageFilesLoader(currentPackage, fileChosen)
81
77
 
82
78
  const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
83
79
  usePackageFiles(currentPackage?.latest_package_release_id)
@@ -148,7 +144,10 @@ export function useFileManagement({
148
144
  content: String(content),
149
145
  }),
150
146
  )
151
- const targetFile = findTargetFile(filesFromUrl, fileChosen)
147
+ const targetFile = findTargetFile({
148
+ files: filesFromUrl,
149
+ filePathFromUrl: fileChosen,
150
+ })
152
151
  setLocalFiles(filesFromUrl)
153
152
  setInitialFiles([])
154
153
  setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
@@ -208,7 +207,10 @@ export function useFileManagement({
208
207
  if (fileChosen) {
209
208
  const targetFile =
210
209
  packageFilesWithContent.find((f) => f.path === fileChosen) ||
211
- findTargetFile(packageFilesWithContent, fileChosen)
210
+ findTargetFile({
211
+ files: packageFilesWithContent,
212
+ filePathFromUrl: fileChosen,
213
+ })
212
214
  if (targetFile) {
213
215
  setCurrentFile((prevCurrentFile) => {
214
216
  return targetFile.path !== prevCurrentFile
@@ -221,10 +223,10 @@ export function useFileManagement({
221
223
  if (!prevCurrentFile) {
222
224
  // Wait for priority file to load before making selection to avoid flicker
223
225
  // Only select if we have a good candidate (tsx/ts file) or priority file isn't loading
224
- const targetFile = findTargetFile(
225
- packageFilesWithContent,
226
- fileChosen,
227
- )
226
+ const targetFile = findTargetFile({
227
+ files: packageFilesWithContent,
228
+ filePathFromUrl: fileChosen,
229
+ })
228
230
  // Only consider it a "good enough" candidate if it's index.tsx
229
231
  // Otherwise, wait for the actual priority file (index.tsx) to load
230
232
  const isTopPriorityFile =
@@ -250,8 +252,22 @@ export function useFileManagement({
250
252
  ])
251
253
 
252
254
  const isLoading = useMemo(() => {
253
- return isPriorityLoading || isLoadingPackageFiles
254
- }, [isPriorityLoading, isLoadingPackageFiles])
255
+ const waitingForPriorityFile =
256
+ Boolean(urlParams.package_id) && !isPriorityFileFetched
257
+
258
+ const hasPackageWithFilesButNoneLoaded =
259
+ Boolean(urlParams.package_id) &&
260
+ isPriorityFileFetched &&
261
+ totalFilesCount > 0 &&
262
+ localFiles.length === 0
263
+
264
+ return waitingForPriorityFile || hasPackageWithFilesButNoneLoaded
265
+ }, [
266
+ isPriorityFileFetched,
267
+ urlParams.package_id,
268
+ totalFilesCount,
269
+ localFiles.length,
270
+ ])
255
271
 
256
272
  const isFullyLoaded = useMemo(() => {
257
273
  return !isLoadingPackageFilesWithContent && !isLoadingPackageFiles
@@ -409,7 +425,15 @@ export function useFileManagement({
409
425
  }
410
426
  }
411
427
 
412
- const savePackage = async (isPrivate: boolean) => {
428
+ const savePackage = async ({
429
+ name,
430
+ isPrivate,
431
+ orgId,
432
+ }: {
433
+ name?: string
434
+ isPrivate: boolean
435
+ orgId: string
436
+ }) => {
413
437
  if (!isLoggedIn) {
414
438
  toast({
415
439
  title: "Not Logged In",
@@ -420,6 +444,8 @@ export function useFileManagement({
420
444
 
421
445
  await createPackageMutation.mutateAsync({
422
446
  is_private: isPrivate,
447
+ org_id: orgId,
448
+ ...(name ? { name } : {}),
423
449
  })
424
450
  }
425
451
 
@@ -523,6 +549,15 @@ export function useFileManagement({
523
549
  [localFiles, currentFile],
524
550
  )
525
551
  const mainComponentPath = useMemo(() => {
552
+ const targetFile = findTargetFile({
553
+ files: localFiles,
554
+ filePathFromUrl: fileChosen,
555
+ fallbackToAnyFile: false,
556
+ })?.path
557
+ if (targetFile && !fileChosen && !currentFile) {
558
+ return targetFile
559
+ }
560
+
526
561
  const isComponentExportedInCurrentFile =
527
562
  isComponentExported(currentFileCode)
528
563
 
@@ -531,16 +566,10 @@ export function useFileManagement({
531
566
  !!localFiles.some((x) => x.path === currentFile) &&
532
567
  isComponentExportedInCurrentFile
533
568
  ? currentFile
534
- : localFiles.find((x) => {
535
- const isComponentExportedInFile = isComponentExported(x.content)
536
- return (
537
- x.path.endsWith(".tsx") ||
538
- (x.path.endsWith(".ts") && isComponentExportedInFile)
539
- )
540
- })?.path || localFiles[0]?.path
569
+ : targetFile
541
570
 
542
571
  return selectedComponent
543
- }, [currentFile, localFiles, currentFileCode, packageFilesWithContent])
572
+ }, [currentFile, localFiles, currentFileCode])
544
573
 
545
574
  const priorityFileFetched = useMemo(() => {
546
575
  return urlParams.package_id && isPriorityFileFetched
@@ -1,10 +1,8 @@
1
1
  import { useQuery, useQueries } from "react-query"
2
2
  import { useAxios } from "@/hooks/use-axios"
3
3
  import { usePackageFiles } from "@/hooks/use-package-files"
4
- import { usePackageFileById } from "@/hooks/use-package-files"
5
4
  import type { Package } from "fake-snippets-api/lib/db/schema"
6
5
  import { useState, useMemo } from "react"
7
- import { findTargetFile } from "@/lib/utils/findTargetFile"
8
6
 
9
7
  export interface PackageFile {
10
8
  path: string
@@ -22,25 +20,13 @@ export interface OptimizedLoadingState {
22
20
  export function useOptimizedPackageFilesLoader(
23
21
  pkg?: Package,
24
22
  priorityFilePath?: string | null,
25
- packageId?: string | null,
26
23
  ) {
27
24
  const axios = useAxios()
28
25
  const [loadedFiles, setLoadedFiles] = useState<Map<string, PackageFile>>(
29
26
  new Map(),
30
27
  )
31
28
 
32
- const pkgFilesRaw = usePackageFiles(pkg?.latest_package_release_id)
33
-
34
- // Filter out dist/ files to avoid downloading build artifacts
35
- const pkgFiles = useMemo(
36
- () => ({
37
- ...pkgFilesRaw,
38
- data: pkgFilesRaw.data?.filter(
39
- (file) => !file.file_path.startsWith("dist/"),
40
- ),
41
- }),
42
- [pkgFilesRaw.data, pkgFilesRaw.isLoading, pkgFilesRaw.error],
43
- )
29
+ const pkgFiles = usePackageFiles(pkg?.latest_package_release_id)
44
30
 
45
31
  const targetFilePath = useMemo(() => {
46
32
  if (!pkgFiles.data) return priorityFilePath
@@ -76,8 +62,8 @@ export function useOptimizedPackageFilesLoader(
76
62
  queryFn: async () => {
77
63
  if (!priorityFileData) return null
78
64
 
79
- const response = await axios.post(`/package_files/get`, {
80
- package_file_id: priorityFileData.package_file_id,
65
+ const response = await axios.get(`/package_files/get`, {
66
+ params: { package_file_id: priorityFileData.package_file_id },
81
67
  })
82
68
  const content = response.data.package_file?.content_text
83
69
  const file = { path: priorityFileData.file_path, content: content ?? "" }
@@ -92,8 +78,8 @@ export function useOptimizedPackageFilesLoader(
92
78
  },
93
79
  enabled: !!priorityFileData,
94
80
  refetchOnWindowFocus: false,
95
- refetchOnMount: false,
96
- staleTime: Infinity,
81
+ refetchOnMount: true,
82
+ staleTime: 0,
97
83
  cacheTime: Infinity,
98
84
  })
99
85
 
@@ -103,8 +89,8 @@ export function useOptimizedPackageFilesLoader(
103
89
  ?.map((file) => ({
104
90
  queryKey: ["packageFile", file.package_file_id],
105
91
  queryFn: async () => {
106
- const response = await axios.post(`/package_files/get`, {
107
- package_file_id: file.package_file_id,
92
+ const response = await axios.get(`/package_files/get`, {
93
+ params: { package_file_id: file.package_file_id },
108
94
  })
109
95
  const content = response.data.package_file?.content_text
110
96
  const fileData = { path: file.file_path, content: content ?? "" }
@@ -118,14 +104,15 @@ export function useOptimizedPackageFilesLoader(
118
104
  return fileData
119
105
  },
120
106
  refetchOnWindowFocus: false,
121
- refetchOnMount: false,
122
- staleTime: Infinity,
107
+ refetchOnMount: true,
108
+ staleTime: 0,
123
109
  cacheTime: Infinity,
124
110
  })) ?? [],
125
111
  )
126
112
 
127
113
  const allFiles = useMemo(() => {
128
- return Array.from(loadedFiles.values())
114
+ const files = Array.from(loadedFiles.values())
115
+ return files
129
116
  }, [loadedFiles])
130
117
 
131
118
  const areAllFilesLoading =
@@ -30,8 +30,8 @@ export function usePackageFilesLoader(pkg?: Package) {
30
30
  }
31
31
  }
32
32
 
33
- const response = await axios.post(`/package_files/get`, {
34
- package_file_id: file.package_file_id,
33
+ const response = await axios.get(`/package_files/get`, {
34
+ params: { package_file_id: file.package_file_id },
35
35
  })
36
36
  const content = response.data.package_file?.content_text
37
37
  return { path: file.file_path, content: content ?? "" }
@@ -1,4 +1,4 @@
1
- import { useMutation } from "react-query"
1
+ import { useMutation, useQueryClient } from "react-query"
2
2
  import type { Package } from "fake-snippets-api/lib/db/schema"
3
3
  import { useAxios } from "./use-axios"
4
4
  import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
@@ -29,6 +29,7 @@ export function useUpdatePackageFilesMutation({
29
29
  }: UseUpdatePackageFilesMutationProps) {
30
30
  const axios = useAxios()
31
31
  const { toast } = useToast()
32
+ const queryClient = useQueryClient()
32
33
  return useMutation({
33
34
  mutationFn: async (
34
35
  newPackage: Pick<Package, "package_id" | "name"> & {
@@ -95,6 +96,17 @@ export function useUpdatePackageFilesMutation({
95
96
  title: `Package's ${updatedFilesCount} files saved`,
96
97
  description: "Your changes have been saved successfully.",
97
98
  })
99
+ queryClient.invalidateQueries({
100
+ predicate: (q) => {
101
+ const key = q.queryKey as any
102
+ return (
103
+ Array.isArray(key) &&
104
+ (key[0] === "packageFiles" ||
105
+ key[0] === "packageFile" ||
106
+ key[0] === "priorityPackageFile")
107
+ )
108
+ },
109
+ })
98
110
  }
99
111
  },
100
112
  onError: (error: any) => {
@@ -27,7 +27,7 @@ export const downloadGltfFromCircuitJson = async (
27
27
  typeof (result as any).buffer === "object" &&
28
28
  (result as any).byteLength !== undefined
29
29
  ) {
30
- const view = result as ArrayBufferView
30
+ const view = result as ArrayBuffer
31
31
  blob = new Blob([view], { type: "model/gltf-binary" })
32
32
  extension = options?.format
33
33
  ? options.format === "glb"
@@ -1,6 +1,8 @@
1
1
  import { saveAs } from "file-saver"
2
- import { convertCircuitJsonToKiCadPcb } from "kicad-converter"
3
- import { convertCircuitJsonToKicadPro } from "kicad-converter"
2
+ import {
3
+ CircuitJsonToKicadPcbConverter,
4
+ CircuitJsonToKicadSchConverter,
5
+ } from "circuit-json-to-kicad"
4
6
  import { AnyCircuitElement } from "circuit-json"
5
7
  import JSZip from "jszip"
6
8
 
@@ -8,18 +10,17 @@ export const downloadKicadFiles = (
8
10
  circuitJson: AnyCircuitElement[],
9
11
  fileName: string,
10
12
  ) => {
11
- const kicadPcbString = convertCircuitJsonToKiCadPcb(circuitJson as any)
12
- const pcbContent =
13
- typeof kicadPcbString === "object"
14
- ? JSON.stringify(kicadPcbString)
15
- : kicadPcbString
13
+ const pcbConverter = new CircuitJsonToKicadPcbConverter(circuitJson)
14
+ pcbConverter.runUntilFinished()
15
+ const kicadPcbContent = pcbConverter.getOutputString()
16
16
 
17
- const kicadProContent = convertCircuitJsonToKicadPro(circuitJson as any)
18
- const proContent = JSON.stringify(kicadProContent)
17
+ const schConverter = new CircuitJsonToKicadSchConverter(circuitJson)
18
+ schConverter.runUntilFinished()
19
+ const kicadSchContent = schConverter.getOutputString()
19
20
 
20
21
  const zip = new JSZip()
21
- zip.file(`${fileName}.kicad_pcb`, pcbContent)
22
- zip.file(`${fileName}.kicad_pro`, proContent)
22
+ zip.file(`${fileName}.kicad_pcb`, kicadPcbContent)
23
+ zip.file(`${fileName}.kicad_sch`, kicadSchContent)
23
24
 
24
25
  zip.generateAsync({ type: "blob" }).then((content) => {
25
26
  saveAs(content, `${fileName}_kicad.zip`)
@@ -0,0 +1,50 @@
1
+ export function normalizeSvgForSquareTile(svg: string): string {
2
+ try {
3
+ const openTagMatch = svg.match(/<svg[^>]*>/i)
4
+ if (!openTagMatch) return svg
5
+
6
+ const openTag = openTagMatch[0]
7
+
8
+ const hasViewBox = /viewBox="[^"]+"/i.test(openTag)
9
+ const widthMatch = openTag.match(/\swidth="([0-9.]+)"/i)
10
+ const heightMatch = openTag.match(/\sheight="([0-9.]+)"/i)
11
+
12
+ let newOpenTag = openTag
13
+
14
+ // Remove explicit width/height so CSS can control sizing
15
+ newOpenTag = newOpenTag.replace(/\swidth="[^"]*"/i, "")
16
+ newOpenTag = newOpenTag.replace(/\sheight="[^"]*"/i, "")
17
+
18
+ // Ensure viewBox is present for proper scaling
19
+ if (!hasViewBox && widthMatch && heightMatch) {
20
+ const w = widthMatch[1]
21
+ const h = heightMatch[1]
22
+ newOpenTag = newOpenTag.replace(/<svg/i, `<svg viewBox="0 0 ${w} ${h}"`)
23
+ }
24
+
25
+ // Force preserveAspectRatio to fit within square without distortion
26
+ if (/preserveAspectRatio="[^"]+"/i.test(newOpenTag)) {
27
+ newOpenTag = newOpenTag.replace(
28
+ /preserveAspectRatio="[^"]+"/i,
29
+ 'preserveAspectRatio="xMidYMid meet"',
30
+ )
31
+ } else {
32
+ newOpenTag = newOpenTag.replace(
33
+ /<svg(\s|>)/i,
34
+ (_m, p1) => `<svg preserveAspectRatio="xMidYMid meet"${p1}`,
35
+ )
36
+ }
37
+
38
+ return svg.replace(openTag, newOpenTag)
39
+ } catch {
40
+ return svg
41
+ }
42
+ }
43
+
44
+ export function svgToDataUrl(svg: string): string {
45
+ try {
46
+ return `data:image/svg+xml,${encodeURIComponent(svg)}`
47
+ } catch {
48
+ return `data:image/svg+xml,${svg}`
49
+ }
50
+ }
@@ -1,14 +1,16 @@
1
1
  import posthog from "posthog-js"
2
2
 
3
- if (
4
- !window.location.hostname.includes("localhost") &&
5
- !window.location.hostname.includes("127.0.0.1")
6
- ) {
7
- if (!posthog.__loaded) {
8
- posthog.init("phc_htd8AQjSfVEsFCLQMAiUooG4Q0DKBCjqYuQglc9V3Wo", {
9
- api_host: "https://postpig.tscircuit.com",
10
- person_profiles: "always",
11
- })
3
+ if (typeof window !== "undefined") {
4
+ if (
5
+ !window.location.hostname.includes("localhost") &&
6
+ !window.location.hostname.includes("127.0.0.1")
7
+ ) {
8
+ if (!posthog.__loaded) {
9
+ posthog.init("phc_htd8AQjSfVEsFCLQMAiUooG4Q0DKBCjqYuQglc9V3Wo", {
10
+ api_host: "https://postpig.tscircuit.com",
11
+ person_profiles: "always",
12
+ })
13
+ }
12
14
  }
13
15
  }
14
16
 
@@ -0,0 +1,148 @@
1
+ import type { QueryKey } from "react-query"
2
+ import { posthog } from "./posthog"
3
+
4
+ const TARGET_HOSTNAMES = new Set(["api.tscircuit.com"])
5
+
6
+ type FailureContext = {
7
+ operationType: "query" | "mutation"
8
+ queryKey?: QueryKey
9
+ mutationKey?: unknown
10
+ }
11
+
12
+ type RedaxiosConfig = {
13
+ url?: string
14
+ baseURL?: string
15
+ method?: string
16
+ }
17
+
18
+ type ResponseLike = {
19
+ url?: string
20
+ status?: number
21
+ statusText?: string
22
+ config?: RedaxiosConfig
23
+ }
24
+
25
+ const isPosthogLoaded = () => Boolean((posthog as any)?.__loaded)
26
+
27
+ const toUpperCaseMethod = (method?: string) => method?.toUpperCase() ?? "GET"
28
+
29
+ const resolveAbsoluteUrl = (url?: string, baseURL?: string) => {
30
+ if (!url) return undefined
31
+
32
+ try {
33
+ if (baseURL) {
34
+ return new URL(url, baseURL).toString()
35
+ }
36
+
37
+ if (typeof window !== "undefined") {
38
+ return new URL(url, window.location.origin).toString()
39
+ }
40
+
41
+ return new URL(url).toString()
42
+ } catch (error) {
43
+ console.warn("Failed to resolve API failure URL", error)
44
+ return undefined
45
+ }
46
+ }
47
+
48
+ const shouldTrackUrl = (resolvedUrl?: string) => {
49
+ if (!resolvedUrl) return false
50
+
51
+ try {
52
+ const { hostname } = new URL(resolvedUrl)
53
+ return TARGET_HOSTNAMES.has(hostname)
54
+ } catch (error) {
55
+ console.warn("Failed to parse URL for API failure tracking", error)
56
+ return false
57
+ }
58
+ }
59
+
60
+ const serializeKey = (key?: unknown) => {
61
+ if (!key) return undefined
62
+
63
+ try {
64
+ return JSON.stringify(key)
65
+ } catch {
66
+ return String(key)
67
+ }
68
+ }
69
+
70
+ const extractFromResponse = (error: unknown): ResponseLike | null => {
71
+ if (!error || typeof error !== "object") return null
72
+
73
+ const maybeResponse = error as Partial<ResponseLike>
74
+ if (typeof maybeResponse.status !== "number" || !("url" in maybeResponse)) {
75
+ return null
76
+ }
77
+
78
+ return {
79
+ url: maybeResponse.url,
80
+ status: maybeResponse.status,
81
+ statusText: maybeResponse.statusText,
82
+ config: maybeResponse.config,
83
+ }
84
+ }
85
+
86
+ const extractFromAxiosError = (error: unknown): ResponseLike | null => {
87
+ if (!error || typeof error !== "object") return null
88
+
89
+ const maybeAxiosError = error as {
90
+ response?: ResponseLike
91
+ config?: RedaxiosConfig
92
+ message?: string
93
+ }
94
+
95
+ if (!maybeAxiosError.response && !maybeAxiosError.config) {
96
+ return null
97
+ }
98
+
99
+ return {
100
+ url: maybeAxiosError.response?.url,
101
+ status: maybeAxiosError.response?.status,
102
+ statusText: maybeAxiosError.response?.statusText,
103
+ config: maybeAxiosError.response?.config ?? maybeAxiosError.config,
104
+ }
105
+ }
106
+
107
+ const captureApiFailure = (
108
+ response: ResponseLike,
109
+ error: unknown,
110
+ context: FailureContext,
111
+ ) => {
112
+ if (!isPosthogLoaded()) return
113
+
114
+ const resolvedUrl =
115
+ resolveAbsoluteUrl(response.url, response.config?.baseURL) ??
116
+ resolveAbsoluteUrl(response.config?.url, response.config?.baseURL)
117
+ if (!shouldTrackUrl(resolvedUrl)) return
118
+
119
+ const errorMessage =
120
+ error instanceof Error
121
+ ? error.message
122
+ : typeof error === "string"
123
+ ? error
124
+ : undefined
125
+
126
+ posthog.capture("api_request_failed", {
127
+ url: resolvedUrl,
128
+ method: toUpperCaseMethod(response.config?.method),
129
+ status: response.status,
130
+ statusText: response.statusText,
131
+ errorMessage,
132
+ operationType: context.operationType,
133
+ queryKey: serializeKey(context.queryKey),
134
+ mutationKey: serializeKey(context.mutationKey),
135
+ environment:
136
+ typeof window !== "undefined" ? window.location.hostname : undefined,
137
+ })
138
+ }
139
+
140
+ export const trackReactQueryApiFailure = (
141
+ error: unknown,
142
+ context: FailureContext,
143
+ ) => {
144
+ const response = extractFromResponse(error) ?? extractFromAxiosError(error)
145
+ if (!response) return
146
+
147
+ captureApiFailure(response, error, context)
148
+ }
@@ -0,0 +1,14 @@
1
+ import * as Sentry from "@sentry/react"
2
+
3
+ if (
4
+ typeof window !== "undefined" &&
5
+ import.meta.env.VITE_SENTRY_DSN &&
6
+ !window.location.hostname.includes("localhost") &&
7
+ !window.location.hostname.includes("127.0.0.1")
8
+ ) {
9
+ Sentry.init({
10
+ dsn: import.meta.env.VITE_SENTRY_DSN,
11
+ })
12
+ }
13
+
14
+ export { Sentry }
@@ -7,15 +7,11 @@ export default () => (
7
7
  resistance="1k"
8
8
  footprint="0402"
9
9
  name="R1"
10
- schX={3}
11
- pcbX={3}
12
10
  />
13
11
  <capacitor
14
12
  capacitance="1000pF"
15
13
  footprint="0402"
16
14
  name="C1"
17
- schX={-3}
18
- pcbX={-3}
19
15
  />
20
16
  <trace from=".R1 > .pin1" to=".C1 > .pin1" />
21
17
  </board>