@tscircuit/fake-snippets 0.0.108 → 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 (203) 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 +389 -450
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1255 -625
  36. package/dist/index.d.ts +296 -4
  37. package/dist/index.js +325 -24
  38. package/dist/schema.d.ts +282 -1
  39. package/dist/schema.js +54 -2
  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 +62 -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 +27 -24
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +29 -10
  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 +133 -36
  71. package/src/components/FileSidebar.tsx +41 -50
  72. package/src/components/Footer.tsx +8 -10
  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 +44 -16
  77. package/src/components/HiddenFilesDropdown.tsx +0 -2
  78. package/src/components/NotFound.tsx +5 -5
  79. package/src/components/PackageBreadcrumb.tsx +6 -12
  80. package/src/components/PackageCard.tsx +0 -1
  81. package/src/components/PackageSearchResults.tsx +1 -1
  82. package/src/components/PrefetchPageLink.tsx +7 -1
  83. package/src/components/ProfileRouter.tsx +32 -0
  84. package/src/components/SearchComponent.tsx +12 -8
  85. package/src/components/UserCard.tsx +80 -0
  86. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  87. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  88. package/src/components/ViewPackagePage/components/important-files-view.tsx +174 -87
  89. package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -4
  90. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -2
  91. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -20
  92. package/src/components/ViewPackagePage/components/package-header.tsx +26 -37
  93. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -19
  94. package/src/components/ViewPackagePage/components/repo-page-content.tsx +33 -25
  95. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  96. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  97. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  98. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  99. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  100. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  101. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  102. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  103. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  104. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  105. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  106. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  107. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  108. package/src/components/organization/OrganizationCard.tsx +204 -0
  109. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  110. package/src/components/organization/OrganizationHeader.tsx +154 -0
  111. package/src/components/organization/OrganizationMembers.tsx +146 -0
  112. package/src/components/package-port/CodeAndPreview.tsx +32 -46
  113. package/src/components/package-port/CodeEditor.tsx +28 -31
  114. package/src/components/package-port/CodeEditorHeader.tsx +128 -63
  115. package/src/components/package-port/EditorNav.tsx +32 -49
  116. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  117. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  118. package/src/components/preview/PackageReleasesDashboard.tsx +53 -36
  119. package/src/components/ui/tree-view.tsx +6 -3
  120. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  121. package/src/hooks/use-create-org-mutation.ts +38 -0
  122. package/src/hooks/use-create-package-mutation.ts +3 -0
  123. package/src/hooks/use-current-package-id.ts +5 -30
  124. package/src/hooks/use-current-package-info.ts +29 -5
  125. package/src/hooks/use-current-package-release.ts +4 -3
  126. package/src/hooks/use-download-zip.ts +2 -2
  127. package/src/hooks/use-global-store.ts +6 -4
  128. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  129. package/src/hooks/use-list-org-members.ts +27 -0
  130. package/src/hooks/use-list-user-orgs.ts +25 -0
  131. package/src/hooks/use-org-by-github-handle.ts +26 -0
  132. package/src/hooks/use-org.ts +24 -0
  133. package/src/hooks/use-organization.ts +42 -0
  134. package/src/hooks/use-package-as-snippet.ts +4 -2
  135. package/src/hooks/use-package-builds.ts +6 -2
  136. package/src/hooks/use-package-files.ts +5 -3
  137. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  138. package/src/hooks/use-package-release-images.ts +105 -0
  139. package/src/hooks/use-package-release.ts +2 -2
  140. package/src/hooks/use-package-stars.ts +80 -4
  141. package/src/hooks/use-preview-images.ts +6 -3
  142. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  143. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  144. package/src/hooks/use-update-org-mutation.ts +41 -0
  145. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  146. package/src/hooks/useFileManagement.ts +183 -35
  147. package/src/hooks/useOptimizedPackageFilesLoader.ts +136 -0
  148. package/src/hooks/usePackageFilesLoader.ts +2 -2
  149. package/src/hooks/useUpdatePackageFilesMutation.ts +15 -1
  150. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  151. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  152. package/src/lib/download-fns/download-kicad-files.ts +12 -11
  153. package/src/lib/normalize-svg-for-tile.ts +50 -0
  154. package/src/lib/posthog.ts +11 -9
  155. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  156. package/src/lib/sentry.ts +14 -0
  157. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  158. package/src/lib/ts-lib-cache.ts +122 -7
  159. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  160. package/src/lib/utils/findTargetFile.ts +45 -10
  161. package/src/lib/utils/isComponentExported.ts +10 -0
  162. package/src/main.tsx +2 -1
  163. package/src/pages/authorize.tsx +0 -2
  164. package/src/pages/create-organization.tsx +168 -0
  165. package/src/pages/dashboard.tsx +38 -6
  166. package/src/pages/datasheet.tsx +1 -1
  167. package/src/pages/datasheets.tsx +3 -3
  168. package/src/pages/editor.tsx +4 -6
  169. package/src/pages/landing.tsx +6 -7
  170. package/src/pages/latest.tsx +3 -0
  171. package/src/pages/organization-profile.tsx +199 -0
  172. package/src/pages/organization-settings.tsx +566 -0
  173. package/src/pages/package-editor.tsx +21 -21
  174. package/src/pages/preview-release.tsx +76 -136
  175. package/src/pages/quickstart.tsx +159 -123
  176. package/src/pages/release-detail.tsx +119 -31
  177. package/src/pages/search.tsx +192 -57
  178. package/src/pages/settings-redirect.tsx +44 -0
  179. package/src/pages/trending.tsx +29 -20
  180. package/src/pages/user-profile.tsx +58 -7
  181. package/src/pages/view-package.tsx +21 -26
  182. package/vite.config.ts +9 -0
  183. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  184. package/src/components/Footer2.tsx +0 -100
  185. package/src/components/JLCPCBImportDialog.tsx +0 -280
  186. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  187. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -115
  188. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -27
  189. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  190. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  191. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  192. package/src/components/PageSearchComponent.tsx +0 -148
  193. package/src/components/ShippingInformationForm.tsx +0 -423
  194. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  195. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  196. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  197. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  198. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  199. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  200. package/src/components/ViewSnippetHeader.tsx +0 -181
  201. package/src/components/ui/input-otp.tsx +0 -69
  202. package/src/pages/package-builds.tsx +0 -33
  203. package/src/pages/settings.tsx +0 -25
@@ -0,0 +1,42 @@
1
+ import { useQuery } from "react-query"
2
+ import { useAxios } from "@/hooks/use-axios"
3
+ import type { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export const useOrganization = ({
6
+ orgId,
7
+ orgName,
8
+ github_handle,
9
+ }: { orgId?: string; orgName?: string; github_handle?: string }) => {
10
+ const axios = useAxios()
11
+
12
+ const orgQuery = useQuery<PublicOrgSchema, Error & { status: number }>(
13
+ ["orgs", "get", orgId || orgName || github_handle],
14
+ async () => {
15
+ if (!orgId && !orgName && !github_handle) {
16
+ throw new Error("Organization ID, name, or GitHub handle is required")
17
+ }
18
+ const params = orgId
19
+ ? { org_id: orgId }
20
+ : orgName
21
+ ? { org_name: orgName }
22
+ : { github_handle }
23
+ const { data } = await axios.get("/orgs/get", { params })
24
+ return data.org
25
+ },
26
+ {
27
+ enabled: Boolean(orgId || orgName),
28
+ retry: false,
29
+ refetchOnWindowFocus: false,
30
+ keepPreviousData: true,
31
+ },
32
+ )
33
+
34
+ return {
35
+ organization: orgQuery.data,
36
+ membersCount: orgQuery.data?.member_count || 0,
37
+ packagesCount: orgQuery.data?.package_count || 0,
38
+ isLoading: orgQuery.isLoading,
39
+ isError: orgQuery.isError,
40
+ error: orgQuery.error,
41
+ }
42
+ }
@@ -32,8 +32,10 @@ export const usePackageAsSnippet = (packageId: string | null) => {
32
32
  if (!packageQuery.data?.latest_package_release_id) {
33
33
  throw new Error("No latest release ID available")
34
34
  }
35
- const { data } = await axios.post("/package_files/list", {
36
- package_release_id: packageQuery.data.latest_package_release_id,
35
+ const { data } = await axios.get("/package_files/list", {
36
+ params: {
37
+ package_release_id: packageQuery.data.latest_package_release_id,
38
+ },
37
39
  })
38
40
  return data.package_files
39
41
  },
@@ -56,11 +56,14 @@ export const usePackageBuildsByReleaseId = (releaseId?: string | null) => {
56
56
  return usePackageBuilds(releaseId ? { package_release_id: releaseId } : null)
57
57
  }
58
58
 
59
- export const usePackageBuild = (packageBuildId: string | null) => {
59
+ export const usePackageBuild = (
60
+ packageBuildId: string | null,
61
+ options?: { include_logs?: boolean },
62
+ ) => {
60
63
  const axios = useAxios()
61
64
 
62
65
  return useQuery<PackageBuild, Error & { status: number }>(
63
- ["packageBuild", packageBuildId],
66
+ ["packageBuild", packageBuildId, options?.include_logs],
64
67
  async () => {
65
68
  if (!packageBuildId) {
66
69
  throw new Error("package_build_id is required")
@@ -69,6 +72,7 @@ export const usePackageBuild = (packageBuildId: string | null) => {
69
72
  const { data } = await axios.get("/package_builds/get", {
70
73
  params: {
71
74
  package_build_id: packageBuildId,
75
+ include_logs: options?.include_logs,
72
76
  },
73
77
  })
74
78
 
@@ -36,7 +36,9 @@ export const usePackageFile = (
36
36
  async () => {
37
37
  if (!query) return
38
38
 
39
- const { data } = await axios.post("/package_files/get", query)
39
+ const { data } = await axios.get("/package_files/get", {
40
+ params: query,
41
+ })
40
42
 
41
43
  if (!data.package_file) {
42
44
  throw new Error("Package file not found")
@@ -98,8 +100,8 @@ export const usePackageFiles = (packageReleaseId?: string | null) => {
98
100
  if (!packageReleaseId) return []
99
101
 
100
102
  try {
101
- const { data } = await axios.post("/package_files/list", {
102
- package_release_id: packageReleaseId,
103
+ const { data } = await axios.get("/package_files/list", {
104
+ params: { package_release_id: packageReleaseId },
103
105
  })
104
106
 
105
107
  if (!data.package_files) {
@@ -1,36 +1,45 @@
1
- import {
2
- usePackageReleaseById,
3
- usePackageReleaseByNameAndVersion,
4
- } from "./use-package-release"
1
+ import { usePackageRelease } from "./use-package-release"
5
2
  import { isUuid } from "@/lib/utils/isUuid"
6
3
 
7
4
  export const usePackageReleaseByIdOrVersion = (
8
5
  releaseIdOrVersion: string | null,
9
6
  packageName?: string | null,
7
+ options?: {
8
+ include_logs?: boolean
9
+ include_ai_review?: boolean
10
+ refetchInterval?:
11
+ | number
12
+ | false
13
+ | ((
14
+ data:
15
+ | import("fake-snippets-api/lib/db/schema").PackageRelease
16
+ | undefined,
17
+ ) => number | false)
18
+ },
10
19
  ) => {
11
20
  const isReleaseIdUuid = releaseIdOrVersion
12
21
  ? isUuid(releaseIdOrVersion)
13
22
  : false
14
23
 
15
- // If it's a UUID, use the ID-based hook
16
- const releaseByIdQuery = usePackageReleaseById(
17
- isReleaseIdUuid ? releaseIdOrVersion : null,
18
- )
24
+ let query: Parameters<typeof usePackageRelease>[0] = null
19
25
 
20
- // If it's not a UUID and we have a package name, construct the version-based query
21
- const packageNameWithVersion =
22
- !isReleaseIdUuid && packageName && releaseIdOrVersion
23
- ? `${packageName}@${releaseIdOrVersion}`
26
+ if (isReleaseIdUuid) {
27
+ query = releaseIdOrVersion
28
+ ? { package_release_id: releaseIdOrVersion }
24
29
  : null
30
+ } else if (packageName && releaseIdOrVersion) {
31
+ query = {
32
+ package_name_with_version: `${packageName}@${releaseIdOrVersion}`,
33
+ }
34
+ }
25
35
 
26
- const releaseByVersionQuery = usePackageReleaseByNameAndVersion(
27
- packageNameWithVersion,
28
- )
36
+ if (query && options?.include_logs !== undefined) {
37
+ query.include_logs = options.include_logs
38
+ }
29
39
 
30
- // Return the appropriate query result
31
- if (isReleaseIdUuid) {
32
- return releaseByIdQuery
33
- } else {
34
- return releaseByVersionQuery
40
+ if (query && options?.include_ai_review !== undefined) {
41
+ query.include_ai_review = options.include_ai_review
35
42
  }
43
+
44
+ return usePackageRelease(query, { refetchInterval: options?.refetchInterval })
36
45
  }
@@ -0,0 +1,105 @@
1
+ import { useMemo, useCallback } from "react"
2
+ import { useQueries } from "react-query"
3
+ import { useAxios } from "./use-axios"
4
+ import { useParams } from "wouter"
5
+ import { useApiBaseUrl } from "./use-packages-base-api-url"
6
+
7
+ interface UsePackageReleaseImagesProps {
8
+ packageReleaseId?: string | null
9
+ availableFilePaths?: string[] | undefined
10
+ }
11
+
12
+ interface ViewConfig {
13
+ id: string
14
+ label: string
15
+ backgroundClass: string
16
+ }
17
+
18
+ const VIEWS: ViewConfig[] = [
19
+ {
20
+ id: "3d",
21
+ label: "3D",
22
+ backgroundClass: "bg-gray-100",
23
+ },
24
+ {
25
+ id: "pcb",
26
+ label: "PCB",
27
+ backgroundClass: "bg-black",
28
+ },
29
+ {
30
+ id: "schematic",
31
+ label: "Schematic",
32
+ backgroundClass: "bg-[#F5F1ED]",
33
+ },
34
+ ]
35
+
36
+ export function usePackageReleaseImages({
37
+ packageReleaseId,
38
+ }: UsePackageReleaseImagesProps) {
39
+ const apiurl = useApiBaseUrl()
40
+ const axios = useAxios()
41
+ const { author, packageName } = useParams()
42
+
43
+ const createQueryFn = useCallback(
44
+ (viewId: string) => async () => {
45
+ if (!packageReleaseId || !author || !packageName) return null
46
+
47
+ const imageUrl = `${apiurl}/packages/images/${author}/${packageName}/${viewId}.png?package_release_id=${packageReleaseId}`
48
+
49
+ try {
50
+ const response = await axios.head(imageUrl)
51
+ return response.status === 200 ? imageUrl : null
52
+ } catch {
53
+ return null
54
+ }
55
+ },
56
+ [packageReleaseId, author, packageName, apiurl, axios],
57
+ )
58
+
59
+ const queryConfigs = useMemo(
60
+ () =>
61
+ VIEWS.map((view) => ({
62
+ queryKey: [
63
+ "packageReleaseImage",
64
+ packageReleaseId,
65
+ view.id,
66
+ author,
67
+ packageName,
68
+ ],
69
+ queryFn: createQueryFn(view.id),
70
+ enabled: Boolean(packageReleaseId && author && packageName),
71
+ retry: false,
72
+ refetchOnWindowFocus: false,
73
+ refetchOnMount: false,
74
+ refetchOnReconnect: false,
75
+ staleTime: Infinity,
76
+ cacheTime: Infinity,
77
+ })),
78
+ [packageReleaseId, author, packageName, createQueryFn],
79
+ )
80
+
81
+ const queries = useQueries(queryConfigs)
82
+
83
+ const availableViews = useMemo(() => {
84
+ const result = []
85
+
86
+ for (let i = 0; i < VIEWS.length; i++) {
87
+ const view = VIEWS[i]
88
+ const query = queries[i]
89
+
90
+ if (query.isLoading || query.data) {
91
+ result.push({
92
+ id: view.id,
93
+ label: view.label,
94
+ imageUrl: query.data || "",
95
+ isLoading: query.isLoading,
96
+ backgroundClass: view.backgroundClass,
97
+ })
98
+ }
99
+ }
100
+
101
+ return result
102
+ }, [queries])
103
+
104
+ return { availableViews }
105
+ }
@@ -40,8 +40,8 @@ export const usePackageRelease = (
40
40
 
41
41
  const { data } = await axios.post("/package_releases/get", query, {
42
42
  params: {
43
- include_logs: query.include_logs,
44
- include_ai_review: query.include_ai_review,
43
+ include_logs: Boolean(query.include_logs),
44
+ include_ai_review: Boolean(query.include_ai_review),
45
45
  },
46
46
  })
47
47
 
@@ -38,25 +38,77 @@ export const usePackageStarMutation = (query: PackageStarQuery) => {
38
38
  const axios = useAxios()
39
39
  const queryClient = useQueryClient()
40
40
 
41
- const addStar = useMutation(
41
+ const addStar = useMutation<
42
+ any,
43
+ Error,
44
+ void,
45
+ { previousStars?: PackageStarResponse }
46
+ >(
42
47
  async () => {
43
48
  const { data } = await axios.post("/packages/add_star", query)
44
49
  return data
45
50
  },
46
51
  {
47
- onSuccess: () => {
52
+ onMutate: async () => {
53
+ await queryClient.cancelQueries(["packageStars", query])
54
+ const previousStars = queryClient.getQueryData<PackageStarResponse>([
55
+ "packageStars",
56
+ query,
57
+ ])
58
+ const optimistic: PackageStarResponse = {
59
+ is_starred: true,
60
+ star_count: (previousStars?.star_count ?? 0) + 1,
61
+ }
62
+ queryClient.setQueryData(["packageStars", query], optimistic)
63
+ return { previousStars }
64
+ },
65
+ onError: (_error, _vars, context) => {
66
+ if (context?.previousStars) {
67
+ queryClient.setQueryData(
68
+ ["packageStars", query],
69
+ context.previousStars,
70
+ )
71
+ }
72
+ },
73
+ onSettled: () => {
48
74
  queryClient.invalidateQueries(["packageStars", query])
49
75
  },
50
76
  },
51
77
  )
52
78
 
53
- const removeStar = useMutation(
79
+ const removeStar = useMutation<
80
+ any,
81
+ Error,
82
+ void,
83
+ { previousStars?: PackageStarResponse }
84
+ >(
54
85
  async () => {
55
86
  const { data } = await axios.post("/packages/remove_star", query)
56
87
  return data
57
88
  },
58
89
  {
59
- onSuccess: () => {
90
+ onMutate: async () => {
91
+ await queryClient.cancelQueries(["packageStars", query])
92
+ const previousStars = queryClient.getQueryData<PackageStarResponse>([
93
+ "packageStars",
94
+ query,
95
+ ])
96
+ const optimistic: PackageStarResponse = {
97
+ is_starred: false,
98
+ star_count: Math.max(0, (previousStars?.star_count ?? 1) - 1),
99
+ }
100
+ queryClient.setQueryData(["packageStars", query], optimistic)
101
+ return { previousStars }
102
+ },
103
+ onError: (_error, _vars, context) => {
104
+ if (context?.previousStars) {
105
+ queryClient.setQueryData(
106
+ ["packageStars", query],
107
+ context.previousStars,
108
+ )
109
+ }
110
+ },
111
+ onSettled: () => {
60
112
  queryClient.invalidateQueries(["packageStars", query])
61
113
  },
62
114
  },
@@ -84,3 +136,27 @@ export const usePackageStarMutationById = (packageId: string) => {
84
136
  export const usePackageStarMutationByName = (packageName: string) => {
85
137
  return usePackageStarMutation({ name: packageName })
86
138
  }
139
+
140
+ // High-level hook that exposes current star state and a single toggle action
141
+ export const usePackageStarring = (query: PackageStarQuery | null) => {
142
+ const starsQuery = usePackageStars(query)
143
+ const mutations = usePackageStarMutation(query ?? { name: "" })
144
+
145
+ const toggleStar = async () => {
146
+ if (!query) return
147
+ const currentlyStarred = starsQuery.data?.is_starred ?? false
148
+ if (currentlyStarred) await mutations.removeStar.mutateAsync()
149
+ else await mutations.addStar.mutateAsync()
150
+ }
151
+
152
+ return {
153
+ isStarred: starsQuery.data?.is_starred ?? false,
154
+ starCount: starsQuery.data?.star_count ?? 0,
155
+ isPending: mutations.addStar.isLoading || mutations.removeStar.isLoading,
156
+ toggleStar,
157
+ }
158
+ }
159
+
160
+ export const usePackageStarringByName = (packageName: string | null) => {
161
+ return usePackageStarring(packageName ? { name: packageName } : null)
162
+ }
@@ -21,22 +21,25 @@ export function usePreviewImages({
21
21
  {
22
22
  id: "3d",
23
23
  label: "3D View",
24
+ backgroundClass: "bg-gray-100",
24
25
  imageUrl: packageName
25
- ? `https://registry-api.tscircuit.com/packages/images/${packageName}/3d.png?fs_sha=${fsMapHash}`
26
+ ? `https://api.tscircuit.com/packages/images/${packageName}/3d.png?fs_sha=${fsMapHash}`
26
27
  : undefined,
27
28
  },
28
29
  {
29
30
  id: "pcb",
30
31
  label: "PCB View",
32
+ backgroundClass: "bg-black",
31
33
  imageUrl: packageName
32
- ? `https://registry-api.tscircuit.com/packages/images/${packageName}/pcb.png?fs_sha=${fsMapHash}`
34
+ ? `https://api.tscircuit.com/packages/images/${packageName}/pcb.png?fs_sha=${fsMapHash}`
33
35
  : undefined,
34
36
  },
35
37
  {
36
38
  id: "schematic",
37
39
  label: "Schematic View",
40
+ backgroundClass: "bg-[#F5F1ED]",
38
41
  imageUrl: packageName
39
- ? `https://registry-api.tscircuit.com/packages/images/${packageName}/schematic.png?fs_sha=${fsMapHash}`
42
+ ? `https://api.tscircuit.com/packages/images/${packageName}/schematic.png?fs_sha=${fsMapHash}`
40
43
  : undefined,
41
44
  },
42
45
  ]
@@ -0,0 +1,32 @@
1
+ import { useMutation, useQueryClient } from "react-query"
2
+ import { useAxios } from "@/hooks/use-axios"
3
+ import { useGlobalStore } from "@/hooks/use-global-store"
4
+
5
+ export const useRemoveOrgMemberMutation = ({
6
+ onSuccess,
7
+ }: { onSuccess?: () => void } = {}) => {
8
+ const axios = useAxios()
9
+ const session = useGlobalStore((s) => s.session)
10
+ const queryClient = useQueryClient()
11
+
12
+ return useMutation(
13
+ ["removeOrgMember"],
14
+ async ({ orgId, accountId }: { orgId: string; accountId: string }) => {
15
+ if (!session) throw new Error("No session")
16
+
17
+ await axios.post("/orgs/remove_member", {
18
+ org_id: orgId,
19
+ account_id: accountId,
20
+ })
21
+ },
22
+ {
23
+ onSuccess: () => {
24
+ queryClient.invalidateQueries(["orgs", "members"])
25
+ onSuccess?.()
26
+ },
27
+ onError: (error: any) => {
28
+ console.error("Error removing organization member:", error)
29
+ },
30
+ },
31
+ )
32
+ }
@@ -0,0 +1,42 @@
1
+ import { useMutation, useQueryClient } from "react-query"
2
+ import { useAxios } from "./use-axios"
3
+ import { useToast } from "./use-toast"
4
+
5
+ export const useUpdateAiDescriptionMutation = ({
6
+ onSuccess,
7
+ }: {
8
+ onSuccess?: () => void
9
+ } = {}) => {
10
+ const axios = useAxios()
11
+ const { toast } = useToast()
12
+ const queryClient = useQueryClient()
13
+
14
+ return useMutation(
15
+ async ({ package_id }: { package_id: string }) => {
16
+ const { data } = await axios.post("/packages/update_ai_description", {
17
+ package_id,
18
+ })
19
+ return data
20
+ },
21
+ {
22
+ onSuccess: () => {
23
+ toast({
24
+ title: "AI description update requested",
25
+ description: "The AI description will be regenerated shortly.",
26
+ })
27
+ queryClient.invalidateQueries(["package"])
28
+ onSuccess?.()
29
+ },
30
+ onError: (error: any) => {
31
+ toast({
32
+ title: "Error",
33
+ description:
34
+ error?.response?.data?.message ||
35
+ error?.data?.message ||
36
+ "Failed to request AI description update.",
37
+ variant: "destructive",
38
+ })
39
+ },
40
+ },
41
+ )
42
+ }
@@ -0,0 +1,41 @@
1
+ import { useMutation, useQueryClient } from "react-query"
2
+ import { useAxios } from "@/hooks/use-axios"
3
+ import { useGlobalStore } from "@/hooks/use-global-store"
4
+ import type { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
5
+
6
+ export const useUpdateOrgMutation = ({
7
+ onSuccess,
8
+ }: { onSuccess?: (org: PublicOrgSchema) => void } = {}) => {
9
+ const axios = useAxios()
10
+ const session = useGlobalStore((s) => s.session)
11
+ const queryClient = useQueryClient()
12
+
13
+ return useMutation(
14
+ ["updateOrg"],
15
+ async ({ orgId, name }: { orgId: string; name?: string }) => {
16
+ if (!session) throw new Error("No session")
17
+
18
+ const {
19
+ data: { org: updatedOrg },
20
+ } = await axios.post("/orgs/update", {
21
+ org_id: orgId,
22
+ name,
23
+ })
24
+
25
+ if (!updatedOrg) {
26
+ throw new Error("Failed to update organization")
27
+ }
28
+
29
+ return updatedOrg
30
+ },
31
+ {
32
+ onSuccess: (org: PublicOrgSchema) => {
33
+ queryClient.invalidateQueries(["orgs"])
34
+ onSuccess?.(org)
35
+ },
36
+ onError: (error: any) => {
37
+ console.error("Error updating organization:", error)
38
+ },
39
+ },
40
+ )
41
+ }
@@ -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
  }