@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.
- package/.github/workflows/bun-formatcheck.yml +2 -2
- package/.github/workflows/bun-pver-release.yml +3 -3
- package/.github/workflows/bun-test.yml +1 -1
- package/.github/workflows/bun-typecheck.yml +2 -2
- package/.github/workflows/update-snapshots.yml +1 -1
- package/README.md +4 -0
- package/api/generated-index.js +37 -3
- package/biome.json +2 -1
- package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +31 -3
- package/bun-tests/fake-snippets-api/fixtures/preload.ts +18 -0
- package/bun-tests/fake-snippets-api/routes/orgs/add_member.test.ts +26 -0
- package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +37 -0
- package/bun-tests/fake-snippets-api/routes/orgs/get.test.ts +52 -0
- package/bun-tests/fake-snippets-api/routes/orgs/list.test.ts +17 -0
- package/bun-tests/fake-snippets-api/routes/orgs/list_members.test.ts +23 -0
- package/bun-tests/fake-snippets-api/routes/orgs/remove_member.test.ts +81 -0
- package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +99 -0
- package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +1 -1
- package/bun-tests/fake-snippets-api/routes/package_files/create.test.ts +15 -13
- package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +26 -24
- package/bun-tests/fake-snippets-api/routes/package_files/delete.test.ts +9 -9
- package/bun-tests/fake-snippets-api/routes/package_files/download.test.ts +4 -4
- package/bun-tests/fake-snippets-api/routes/package_files/get.test.ts +38 -28
- package/bun-tests/fake-snippets-api/routes/package_files/list.test.ts +23 -15
- package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +33 -0
- package/bun-tests/fake-snippets-api/routes/package_releases/get.test.ts +4 -4
- package/bun-tests/fake-snippets-api/routes/package_releases/get_image_generation_fields.test.ts +38 -0
- package/bun-tests/fake-snippets-api/routes/packages/create.test.ts +19 -0
- package/bun-tests/fake-snippets-api/routes/packages/fork.test.ts +3 -4
- package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +30 -0
- package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +4 -2
- package/bun-tests/fake-snippets-api/routes/packages/list-1.test.ts +34 -0
- package/bun.lock +349 -453
- package/bunfig.toml +2 -1
- package/dist/bundle.js +1253 -624
- package/dist/index.d.ts +291 -4
- package/dist/index.js +323 -23
- package/dist/schema.d.ts +274 -1
- package/dist/schema.js +52 -1
- package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
- package/fake-snippets-api/lib/db/db-client.ts +214 -3
- package/fake-snippets-api/lib/db/schema.ts +61 -0
- package/fake-snippets-api/lib/db/seed.ts +100 -0
- package/fake-snippets-api/lib/middleware/with-session-auth.ts +1 -1
- package/fake-snippets-api/lib/package_file/get-package-file-id-from-file-descriptor.ts +2 -2
- package/fake-snippets-api/lib/public-mapping/public-map-org.ts +32 -0
- package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +10 -0
- package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +17 -0
- package/fake-snippets-api/routes/api/orgs/add_member.ts +52 -0
- package/fake-snippets-api/routes/api/orgs/create.ts +46 -0
- package/fake-snippets-api/routes/api/orgs/get.ts +39 -0
- package/fake-snippets-api/routes/api/orgs/list.ts +31 -0
- package/fake-snippets-api/routes/api/orgs/list_members.ts +67 -0
- package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
- package/fake-snippets-api/routes/api/orgs/update.ts +93 -0
- package/fake-snippets-api/routes/api/package_files/get.ts +3 -6
- package/fake-snippets-api/routes/api/package_files/list.ts +7 -4
- package/fake-snippets-api/routes/api/packages/create.ts +54 -10
- package/fake-snippets-api/routes/api/packages/get.ts +23 -0
- package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +13 -11
- package/fake-snippets-api/routes/api/packages/list.ts +29 -2
- package/fake-snippets-api/routes/api/packages/update_ai_description.ts +37 -0
- package/package.json +24 -20
- package/renovate.json +1 -1
- package/scripts/generate-sitemap.ts +1 -1
- package/src/App.tsx +29 -8
- package/src/ContextProviders.tsx +25 -2
- package/src/components/CircuitJsonImportDialog.tsx +1 -1
- package/src/components/CmdKMenu.tsx +281 -247
- package/src/components/DownloadButtonAndMenu.tsx +3 -4
- package/src/components/FileSidebar.tsx +11 -17
- package/src/components/Footer.tsx +8 -9
- package/src/components/Header.tsx +19 -32
- package/src/components/Header2.tsx +16 -32
- package/src/components/HeaderDropdown.tsx +13 -8
- package/src/components/HeaderLogin.tsx +43 -15
- package/src/components/NotFound.tsx +5 -5
- package/src/components/PackageBreadcrumb.tsx +6 -12
- package/src/components/PackageSearchResults.tsx +1 -1
- package/src/components/PrefetchPageLink.tsx +7 -1
- package/src/components/ProfileRouter.tsx +32 -0
- package/src/components/SearchComponent.tsx +12 -8
- package/src/components/UserCard.tsx +80 -0
- package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
- package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
- package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
- package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
- package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
- package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
- package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
- package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
- package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
- package/src/components/dialogs/import-component-dialog.tsx +16 -9
- package/src/components/dialogs/import-package-dialog.tsx +3 -2
- package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
- package/src/components/organization/OrganizationCard.tsx +204 -0
- package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
- package/src/components/organization/OrganizationHeader.tsx +154 -0
- package/src/components/organization/OrganizationMembers.tsx +146 -0
- package/src/components/package-port/CodeAndPreview.tsx +15 -12
- package/src/components/package-port/CodeEditor.tsx +4 -30
- package/src/components/package-port/CodeEditorHeader.tsx +123 -61
- package/src/components/package-port/EditorNav.tsx +32 -49
- package/src/components/preview/ConnectedPackagesList.tsx +8 -8
- package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
- package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
- package/src/components/ui/tree-view.tsx +6 -3
- package/src/hooks/use-add-org-member-mutation.ts +51 -0
- package/src/hooks/use-create-org-mutation.ts +38 -0
- package/src/hooks/use-create-package-mutation.ts +3 -0
- package/src/hooks/use-current-package-release.ts +4 -3
- package/src/hooks/use-download-zip.ts +2 -2
- package/src/hooks/use-global-store.ts +6 -4
- package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
- package/src/hooks/use-list-org-members.ts +27 -0
- package/src/hooks/use-list-user-orgs.ts +25 -0
- package/src/hooks/use-org-by-github-handle.ts +26 -0
- package/src/hooks/use-org.ts +24 -0
- package/src/hooks/use-organization.ts +42 -0
- package/src/hooks/use-package-as-snippet.ts +4 -2
- package/src/hooks/use-package-builds.ts +6 -2
- package/src/hooks/use-package-files.ts +5 -3
- package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
- package/src/hooks/use-package-release-images.ts +105 -0
- package/src/hooks/use-package-release.ts +2 -2
- package/src/hooks/use-package-stars.ts +80 -4
- package/src/hooks/use-preview-images.ts +6 -3
- package/src/hooks/use-remove-org-member-mutation.ts +32 -0
- package/src/hooks/use-update-ai-description-mutation.ts +42 -0
- package/src/hooks/use-update-org-mutation.ts +41 -0
- package/src/hooks/use-warn-user-on-page-change.ts +71 -4
- package/src/hooks/useFileManagement.ts +51 -22
- package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
- package/src/hooks/usePackageFilesLoader.ts +2 -2
- package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
- package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
- package/src/lib/download-fns/download-kicad-files.ts +12 -11
- package/src/lib/normalize-svg-for-tile.ts +50 -0
- package/src/lib/posthog.ts +11 -9
- package/src/lib/react-query-api-failure-tracking.ts +148 -0
- package/src/lib/sentry.ts +14 -0
- package/src/lib/templates/blank-circuit-board-template.ts +0 -4
- package/src/lib/ts-lib-cache.ts +122 -7
- package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
- package/src/lib/utils/findTargetFile.ts +45 -10
- package/src/lib/utils/isComponentExported.ts +2 -1
- package/src/main.tsx +2 -1
- package/src/pages/create-organization.tsx +168 -0
- package/src/pages/dashboard.tsx +38 -6
- package/src/pages/datasheet.tsx +1 -1
- package/src/pages/datasheets.tsx +3 -3
- package/src/pages/editor.tsx +4 -6
- package/src/pages/landing.tsx +6 -6
- package/src/pages/latest.tsx +3 -0
- package/src/pages/organization-profile.tsx +199 -0
- package/src/pages/organization-settings.tsx +566 -0
- package/src/pages/package-editor.tsx +21 -21
- package/src/pages/preview-release.tsx +75 -145
- package/src/pages/quickstart.tsx +159 -123
- package/src/pages/release-detail.tsx +119 -31
- package/src/pages/search.tsx +192 -57
- package/src/pages/settings-redirect.tsx +44 -0
- package/src/pages/trending.tsx +29 -20
- package/src/pages/user-profile.tsx +58 -7
- package/src/pages/view-package.tsx +7 -13
- package/vite.config.ts +9 -0
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
- package/src/components/JLCPCBImportDialog.tsx +0 -280
- package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
- package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
- package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
- package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
- package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
- package/src/components/PageSearchComponent.tsx +0 -148
- 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 =
|
|
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(
|
|
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(
|
|
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
|
-
|
|
254
|
-
|
|
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 (
|
|
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
|
-
:
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
96
|
-
staleTime:
|
|
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.
|
|
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:
|
|
122
|
-
staleTime:
|
|
107
|
+
refetchOnMount: true,
|
|
108
|
+
staleTime: 0,
|
|
123
109
|
cacheTime: Infinity,
|
|
124
110
|
})) ?? [],
|
|
125
111
|
)
|
|
126
112
|
|
|
127
113
|
const allFiles = useMemo(() => {
|
|
128
|
-
|
|
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.
|
|
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
|
|
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 {
|
|
3
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
? JSON.stringify(kicadPcbString)
|
|
15
|
-
: kicadPcbString
|
|
13
|
+
const pcbConverter = new CircuitJsonToKicadPcbConverter(circuitJson)
|
|
14
|
+
pcbConverter.runUntilFinished()
|
|
15
|
+
const kicadPcbContent = pcbConverter.getOutputString()
|
|
16
16
|
|
|
17
|
-
const
|
|
18
|
-
|
|
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`,
|
|
22
|
-
zip.file(`${fileName}.
|
|
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
|
+
}
|
package/src/lib/posthog.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import posthog from "posthog-js"
|
|
2
2
|
|
|
3
|
-
if (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
posthog.
|
|
9
|
-
|
|
10
|
-
|
|
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>
|