@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
@@ -8,6 +8,7 @@ import ts from "typescript"
8
8
 
9
9
  const TS_LIB_VERSION = "5.6.3"
10
10
  const CACHE_PREFIX = `ts-lib-${TS_LIB_VERSION}-`
11
+ const CACHE_TTL = 7 * 24 * 60 * 60 * 1000 // 7 days
11
12
 
12
13
  export async function loadDefaultLibMap(): Promise<Map<string, string>> {
13
14
  const fsMap = new Map<string, string>()
@@ -18,9 +19,23 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
18
19
  const missing: string[] = []
19
20
 
20
21
  for (const lib of libs) {
21
- const cached = await get(CACHE_PREFIX + lib)
22
- if (cached) {
23
- fsMap.set("/" + lib, decompressFromUTF16(cached as string))
22
+ const cacheKey = CACHE_PREFIX + lib
23
+ const cached = await get(cacheKey)
24
+ if (
25
+ cached &&
26
+ typeof cached === "object" &&
27
+ "compressedFileContent" in cached &&
28
+ "timestamp" in cached
29
+ ) {
30
+ const { compressedFileContent, timestamp } = cached as {
31
+ compressedFileContent: string
32
+ timestamp: number
33
+ }
34
+ if (Date.now() - timestamp < CACHE_TTL) {
35
+ fsMap.set("/" + lib, decompressFromUTF16(compressedFileContent))
36
+ } else {
37
+ missing.push(lib)
38
+ }
24
39
  } else {
25
40
  missing.push(lib)
26
41
  }
@@ -36,12 +51,112 @@ export async function loadDefaultLibMap(): Promise<Map<string, string>> {
36
51
  )
37
52
  for (const [filename, content] of fetched) {
38
53
  fsMap.set(filename, content)
39
- await set(
40
- CACHE_PREFIX + filename.replace(/^\//, ""),
41
- compressToUTF16(content),
42
- )
54
+ const cacheKey = CACHE_PREFIX + filename.replace(/^\//, "")
55
+ const compressed = compressToUTF16(content)
56
+ await set(cacheKey, {
57
+ compressedFileContent: compressed,
58
+ timestamp: Date.now(),
59
+ }).catch(() => {})
43
60
  }
44
61
  }
45
62
 
46
63
  return fsMap
47
64
  }
65
+
66
+ export async function fetchWithPackageCaching(
67
+ input: RequestInfo | URL,
68
+ init?: RequestInit,
69
+ ): Promise<Response> {
70
+ const url = typeof input === "string" ? input : input.toString()
71
+
72
+ // Only cache GET requests for packages
73
+ if (init?.method && init.method !== "GET") {
74
+ return fetch(input, init)
75
+ }
76
+
77
+ // Check if this should be cached
78
+ const shouldCache =
79
+ url.includes("jsdelivr.net") ||
80
+ url.includes("unpkg.com") ||
81
+ url.includes("@types/") ||
82
+ url.includes("@tsci/")
83
+
84
+ if (!shouldCache) {
85
+ return fetch(input, init)
86
+ }
87
+
88
+ const cacheKey = `package-cache-${url}`
89
+
90
+ // Check cache
91
+ const cached = await get(cacheKey).catch(() => null)
92
+ if (
93
+ cached &&
94
+ typeof cached === "object" &&
95
+ "compressedFileContent" in cached &&
96
+ "timestamp" in cached
97
+ ) {
98
+ const { compressedFileContent, timestamp } = cached as {
99
+ compressedFileContent: string
100
+ timestamp: number
101
+ }
102
+ if (Date.now() - timestamp < CACHE_TTL) {
103
+ return new Response(decompressFromUTF16(compressedFileContent), {
104
+ status: 200,
105
+ statusText: "OK",
106
+ })
107
+ }
108
+ }
109
+
110
+ // Handle @tsci packages
111
+ let fetchUrl = url
112
+ if (
113
+ url.includes("@tsci/") &&
114
+ (url.includes("jsdelivr.net") || url.includes("data.jsdelivr.com"))
115
+ ) {
116
+ let packagePath = ""
117
+ if (url.includes("jsdelivr.net")) {
118
+ packagePath = url.replace("https://cdn.jsdelivr.net/npm/@tsci/", "")
119
+ } else if (url.includes("/v1/package/resolve/npm/@tsci/")) {
120
+ const resolveIndex = url.indexOf("/v1/package/resolve/npm/@tsci/")
121
+ packagePath = url.substring(
122
+ resolveIndex + "/v1/package/resolve/npm/@tsci/".length,
123
+ )
124
+ } else if (url.includes("/v1/package/npm/@tsci/")) {
125
+ const npmIndex = url.indexOf("/v1/package/npm/@tsci/")
126
+ packagePath = url.substring(npmIndex + "/v1/package/npm/@tsci/".length)
127
+ }
128
+
129
+ if (packagePath) {
130
+ // Convert dots to slashes in the package name part (like original logic)
131
+ const parts = packagePath.split("/")
132
+ if (parts.length > 0) {
133
+ parts[0] = parts[0].replace(/\./, "/")
134
+ }
135
+ const transformedPackagePath = parts.join("/")
136
+
137
+ const apiUrl = import.meta.env.VITE_SNIPPETS_API_URL ?? "/api"
138
+ const isResolve = url.includes("/resolve/")
139
+ fetchUrl = `${apiUrl}/snippets/download?jsdelivr_resolve=${isResolve}&jsdelivr_path=${encodeURIComponent(
140
+ transformedPackagePath,
141
+ )}`
142
+ }
143
+ }
144
+
145
+ // Fetch and cache
146
+ const response = await fetch(fetchUrl, init)
147
+ if (response.ok) {
148
+ const text = await response.text()
149
+ const compressed = compressToUTF16(text)
150
+ await set(cacheKey, {
151
+ compressedFileContent: compressed,
152
+ timestamp: Date.now(),
153
+ }).catch(() => {})
154
+ return new Response(text, {
155
+ status: response.status,
156
+ statusText: response.statusText,
157
+ headers: response.headers,
158
+ })
159
+ }
160
+
161
+ return response
162
+ }
@@ -5,10 +5,10 @@ export const checkIfManualEditsImported = (
5
5
  file: string = "index.tsx",
6
6
  ) => {
7
7
  if (!files[file]) return false
8
- const targetFile = findTargetFile(
9
- Object.keys(files).map((f) => ({ path: f, content: files[f] })),
10
- null,
11
- )
8
+ const targetFile = findTargetFile({
9
+ files: Object.keys(files).map((f) => ({ path: f, content: files[f] })),
10
+ filePathFromUrl: null,
11
+ })
12
12
  if (targetFile && file !== targetFile.path) {
13
13
  return false
14
14
  }
@@ -1,4 +1,6 @@
1
+ import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
1
2
  import { PackageFile } from "@/types/package"
3
+ import { isComponentExported } from "./isComponentExported"
2
4
 
3
5
  export const findMainEntrypointFileFromTscircuitConfig = (
4
6
  files: PackageFile[],
@@ -24,37 +26,70 @@ export const findMainEntrypointFileFromTscircuitConfig = (
24
26
  return null
25
27
  }
26
28
 
27
- export const findTargetFile = (
28
- files: PackageFile[],
29
- filePathFromUrl: string | null,
30
- ): PackageFile | null => {
29
+ export const findTargetFile = ({
30
+ files,
31
+ filePathFromUrl,
32
+ fallbackToAnyFile = true,
33
+ }: {
34
+ files: PackageFile[]
35
+ filePathFromUrl: string | null
36
+ fallbackToAnyFile?: boolean
37
+ }): PackageFile | null => {
31
38
  if (files.length === 0) {
32
39
  return null
33
40
  }
34
41
 
35
- let targetFile: PackageFile | null = null
42
+ let targetFile: PackageFile | undefined | null = null
43
+ if (!filePathFromUrl) {
44
+ files = files.filter((x) => !isHiddenFile(x.path))
45
+ }
36
46
 
37
47
  if (filePathFromUrl) {
38
- targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
48
+ const file = files.find((file) => file.path === filePathFromUrl)?.path
49
+ if (
50
+ file &&
51
+ !file.endsWith(".ts") &&
52
+ !file.endsWith(".tsx") &&
53
+ fallbackToAnyFile
54
+ ) {
55
+ targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
56
+ } else {
57
+ const _isComponentExported = isComponentExported(
58
+ files.find((file) => file.path === filePathFromUrl)?.content || "",
59
+ )
60
+ if (_isComponentExported) {
61
+ targetFile = files.find((file) => file.path === filePathFromUrl) ?? null
62
+ }
63
+ }
39
64
  }
40
65
 
41
66
  if (!targetFile) {
42
67
  targetFile = findMainEntrypointFileFromTscircuitConfig(files)
43
68
  }
69
+ if (!targetFile) {
70
+ targetFile =
71
+ files.find(
72
+ (file) => file.path === "index.tsx" || file.path === "index.ts",
73
+ ) ?? null
74
+ }
44
75
 
45
76
  if (!targetFile) {
46
- targetFile = files.find((file) => file.path === "index.tsx") ?? null
77
+ targetFile =
78
+ files.find((file) => file.path.endsWith(".circuit.tsx")) ?? null
47
79
  }
48
80
 
49
81
  if (!targetFile) {
50
- targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
82
+ targetFile =
83
+ files.find(
84
+ (file) => file.path === "main.tsx" || file.path === "main.ts",
85
+ ) ?? null
51
86
  }
52
87
 
53
88
  if (!targetFile) {
54
- targetFile = files.find((file) => file.path === "index.ts") ?? null
89
+ targetFile = files.find((file) => file.path.endsWith(".tsx")) ?? null
55
90
  }
56
91
 
57
- if (!targetFile && files[0]) {
92
+ if (!targetFile && files[0] && fallbackToAnyFile) {
58
93
  targetFile = files[0]
59
94
  }
60
95
 
@@ -4,6 +4,7 @@ export const isComponentExported = (code: string) => {
4
4
  /export const\s+\w+\s*=/.test(code) ||
5
5
  /export default\s+\w+/.test(code) ||
6
6
  /export default\s+function\s*(\w*)\s*\(/.test(code) ||
7
- /export default\s*\(\s*\)\s*=>/.test(code)
7
+ /export default\s*\(\s*\)\s*=>/.test(code) ||
8
+ /export default\s*\(.*?\)\s*=>/.test(code)
8
9
  )
9
10
  }
package/src/main.tsx CHANGED
@@ -1,11 +1,12 @@
1
1
  import { StrictMode } from "react"
2
2
  import { createRoot } from "react-dom/client"
3
3
  import App from "./App.tsx"
4
+ import "./lib/sentry"
5
+ import "./index.css"
4
6
 
5
7
  if (typeof window !== "undefined" && !window.__APP_LOADED_AT) {
6
8
  window.__APP_LOADED_AT = Date.now()
7
9
  }
8
- import "./index.css"
9
10
 
10
11
  createRoot(document.getElementById("root")!).render(
11
12
  <StrictMode>
@@ -0,0 +1,168 @@
1
+ import React, { useState } from "react"
2
+ import { useLocation } from "wouter"
3
+ import { Helmet } from "react-helmet-async"
4
+ import Header from "@/components/Header"
5
+ import { Button } from "@/components/ui/button"
6
+ import { Input } from "@/components/ui/input"
7
+ import { Label } from "@/components/ui/label"
8
+ import toast from "react-hot-toast"
9
+ import { useCreateOrgMutation } from "@/hooks/use-create-org-mutation"
10
+
11
+ interface FormErrors {
12
+ name?: string
13
+ }
14
+
15
+ export const CreateOrganizationPage = () => {
16
+ const [, setLocation] = useLocation()
17
+
18
+ const [formData, setFormData] = useState({
19
+ name: "",
20
+ })
21
+
22
+ const [errors, setErrors] = useState<FormErrors>({})
23
+ const [isLoading, setIsLoading] = useState(false)
24
+
25
+ const { mutate: createOrganization, isLoading: isMutating } =
26
+ useCreateOrgMutation({
27
+ onSuccess: (newOrganization) => {
28
+ toast.success(
29
+ `Organization "${newOrganization.name || formData.name}" created successfully!`,
30
+ )
31
+ setLocation(`/${newOrganization.name || formData.name}`)
32
+ setIsLoading(false)
33
+ },
34
+ })
35
+
36
+ const validateForm = (): boolean => {
37
+ const newErrors: FormErrors = {}
38
+
39
+ if (!formData.name) {
40
+ newErrors.name = "Organization name is required"
41
+ } else if (formData.name.length > 30) {
42
+ newErrors.name = "Organization name must be less than 30 characters"
43
+ } else if (!/^[a-zA-Z0-9-_]+$/.test(formData.name)) {
44
+ newErrors.name =
45
+ "Organization name can only contain letters, numbers, hyphens, and underscores"
46
+ }
47
+ setErrors(newErrors)
48
+ return Object.keys(newErrors).length === 0
49
+ }
50
+
51
+ const handleSubmit = async (e: React.FormEvent) => {
52
+ e.preventDefault()
53
+
54
+ if (!validateForm()) {
55
+ return
56
+ }
57
+
58
+ setIsLoading(true)
59
+ createOrganization(
60
+ { name: formData.name },
61
+ {
62
+ onError: (error: any) => {
63
+ console.error("Failed to create organization:", error)
64
+ toast.error(
65
+ error?.response?.data?.error?.message ||
66
+ "Failed to create organization. Please try again.",
67
+ )
68
+ setIsLoading(false)
69
+ },
70
+ },
71
+ )
72
+ }
73
+
74
+ const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
75
+ const name = e.target.value
76
+ setFormData((prev) => ({ ...prev, name }))
77
+ if (errors.name) {
78
+ setErrors({})
79
+ }
80
+ }
81
+
82
+ const handleCancel = () => {
83
+ setLocation("/dashboard")
84
+ }
85
+
86
+ return (
87
+ <div className="min-h-screen bg-white">
88
+ <Helmet>
89
+ <title>Create Organization - tscircuit</title>
90
+ <meta
91
+ name="description"
92
+ content="Create a new organization to collaborate with others and manage shared projects."
93
+ />
94
+ </Helmet>
95
+
96
+ <Header />
97
+
98
+ <div className="flex items-center justify-center min-h-[calc(100vh-80px)] px-4 sm:px-6 lg:px-8">
99
+ <div className="w-full max-w-lg mx-auto">
100
+ <div className="text-center mb-8">
101
+ <h1 className="text-xl sm:text-2xl font-semibold text-gray-900 mb-2">
102
+ Set up your organization
103
+ </h1>
104
+ <p className="text-gray-600 text-sm">
105
+ Tell us about your organization
106
+ </p>
107
+ </div>
108
+
109
+ <form onSubmit={handleSubmit} className="space-y-6">
110
+ <div className="space-y-2">
111
+ <Label
112
+ htmlFor="org-name"
113
+ className="text-sm font-semibold text-gray-900"
114
+ >
115
+ Organization Name
116
+ <span className="text-red-500">*</span>
117
+ </Label>
118
+ <Input
119
+ spellCheck={false}
120
+ id="org-handle"
121
+ type="text"
122
+ placeholder="tscircuit"
123
+ value={formData.name}
124
+ onChange={handleNameChange}
125
+ className={`h-10 sm:h-11 ${
126
+ errors.name
127
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
128
+ : "border-gray-300 focus:border-blue-500 focus:ring-blue-500"
129
+ }`}
130
+ disabled={isLoading || isMutating}
131
+ />
132
+ {errors.name && (
133
+ <p className="text-sm text-red-600">{errors.name}</p>
134
+ )}
135
+ <p className="text-xs text-gray-500">
136
+ This will be the name of your organization on tscircuit.
137
+ <br />
138
+ Your URL will be:{" "}
139
+ <span className="font-mono text-gray-700">
140
+ tscircuit.com/{formData.name || "orgname"}
141
+ </span>
142
+ </p>
143
+ </div>
144
+
145
+ <div>
146
+ <Button
147
+ type="submit"
148
+ disabled={isLoading || isMutating || !formData.name}
149
+ className="w-full h-10 sm:h-11 bg-blue-600 hover:bg-blue-700 text-white font-medium text-sm sm:text-base"
150
+ >
151
+ {isLoading || isMutating ? (
152
+ <>
153
+ <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
154
+ Creating organization...
155
+ </>
156
+ ) : (
157
+ "Create organization"
158
+ )}
159
+ </Button>
160
+ </div>
161
+ </form>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ )
166
+ }
167
+
168
+ export default CreateOrganizationPage
@@ -3,12 +3,11 @@ import { useQuery } from "react-query"
3
3
  import { useAxios } from "@/hooks/use-axios"
4
4
  import Header from "@/components/Header"
5
5
  import Footer from "@/components/Footer"
6
- import { Package, Snippet } from "fake-snippets-api/lib/db/schema"
7
- import { Link } from "wouter"
6
+ import { Package } from "fake-snippets-api/lib/db/schema"
8
7
  import { Edit2, KeyRound } from "lucide-react"
9
8
  import { Button } from "@/components/ui/button"
10
9
  import { useGlobalStore } from "@/hooks/use-global-store"
11
- import { PrefetchPageLink } from "@/components/PrefetchPageLink"
10
+ import { Link } from "wouter"
12
11
  import { PackagesList } from "@/components/PackagesList"
13
12
  import { Helmet } from "react-helmet-async"
14
13
  import { useSignIn } from "@/hooks/use-sign-in"
@@ -16,9 +15,12 @@ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
16
15
  import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
17
16
  import { PackageCardSkeleton } from "@/components/PackageCardSkeleton"
18
17
  import { PackageCard } from "@/components/PackageCard"
18
+ import { useListUserOrgs } from "@/hooks/use-list-user-orgs"
19
+ import { OrganizationCard } from "@/components/organization/OrganizationCard"
19
20
 
20
21
  export const DashboardPage = () => {
21
22
  const axios = useAxios()
23
+ const { data: organizations } = useListUserOrgs()
22
24
 
23
25
  const currentUser = useGlobalStore((s) => s.session?.github_username)
24
26
  const isLoggedIn = Boolean(currentUser)
@@ -99,7 +101,9 @@ export const DashboardPage = () => {
99
101
  </Helmet>
100
102
  <Header />
101
103
  <div className="container mx-auto px-4 py-8 min-h-[80vh]">
102
- <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
104
+ <div className="flex items-center justify-between mb-6">
105
+ <h1 className="text-3xl font-bold">Dashboard</h1>
106
+ </div>
103
107
  <div className="flex md:flex-row flex-col">
104
108
  <div className="md:w-3/4 p-0 md:pr-6">
105
109
  {!isLoggedIn ? (
@@ -129,7 +133,7 @@ export const DashboardPage = () => {
129
133
  {myPackages &&
130
134
  myPackages.slice(0, 3).map((pkg) => (
131
135
  <div key={pkg.package_id}>
132
- <PrefetchPageLink
136
+ <Link
133
137
  href={`/editor?package_id=${pkg.package_id}`}
134
138
  className="text-blue-600 hover:underline"
135
139
  >
@@ -141,7 +145,7 @@ export const DashboardPage = () => {
141
145
  {pkg.unscoped_name}
142
146
  <Edit2 className="w-3 h-3 ml-2" />
143
147
  </Button>
144
- </PrefetchPageLink>
148
+ </Link>
145
149
  </div>
146
150
  ))}
147
151
  </div>
@@ -188,6 +192,34 @@ export const DashboardPage = () => {
188
192
  View all packages
189
193
  </Link>
190
194
  )}
195
+
196
+ {/* Organizations Section */}
197
+ {organizations && organizations.length > 0 && (
198
+ <div className="mt-8">
199
+ <h2 className="text-sm font-bold mb-2 text-gray-700 border-b border-gray-200">
200
+ Your Organizations
201
+ </h2>
202
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
203
+ {organizations?.slice(0, 4).map((org: any, i: number) => (
204
+ <OrganizationCard
205
+ key={i}
206
+ organization={org}
207
+ withLink={true}
208
+ showStats={true}
209
+ showMembers={true}
210
+ />
211
+ ))}
212
+ </div>
213
+ {organizations && organizations.length > 4 && (
214
+ <Link
215
+ href="/organizations"
216
+ className="text-sm text-blue-600 hover:underline mt-2 inline-block"
217
+ >
218
+ View all organizations
219
+ </Link>
220
+ )}
221
+ </div>
222
+ )}
191
223
  </>
192
224
  )}
193
225
  </div>
@@ -55,7 +55,7 @@ export const DatasheetPage = () => {
55
55
  </div>
56
56
 
57
57
  {datasheetQuery.isLoading ? (
58
- <div className="flex flex-col items-center justify-center py-16">
58
+ <div className="flex flex-col items-center justify-center py-20">
59
59
  <Loader2 className="w-10 h-10 animate-spin text-blue-500 mb-4" />
60
60
  <h3 className="text-xl font-semibold mb-2">Loading Datasheet...</h3>
61
61
  <p className="text-gray-500 max-w-md text-center">
@@ -5,7 +5,7 @@ import { useCreateDatasheet } from "@/hooks/use-create-datasheet"
5
5
  import Header from "@/components/Header"
6
6
  import Footer from "@/components/Footer"
7
7
  import { Input } from "@/components/ui/input"
8
- import { Search } from "lucide-react"
8
+ import { Loader2, Search } from "lucide-react"
9
9
  import { Link, useLocation } from "wouter"
10
10
 
11
11
  interface DatasheetSummary {
@@ -72,9 +72,9 @@ export const DatasheetsPage: React.FC = () => {
72
72
  </div>
73
73
 
74
74
  {isLoading ? (
75
- <div className="text-center py-12 px-4">
75
+ <div className="text-center py-20 px-4">
76
76
  <div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
77
- <Search className="w-8 h-8 text-slate-400" />
77
+ <Loader2 className="size-8 animate-spin text-slate-400" />
78
78
  </div>
79
79
  <h3 className="text-xl font-medium text-slate-900 mb-2">
80
80
  Loading Datasheets
@@ -2,12 +2,10 @@ import { CodeAndPreview } from "@/components/package-port/CodeAndPreview"
2
2
  import Footer from "@/components/Footer"
3
3
  import Header from "@/components/Header"
4
4
  import { Helmet } from "react-helmet-async"
5
- import { useCurrentPackageId } from "@/hooks/use-current-package-id"
6
- import { usePackage } from "@/hooks/use-package"
5
+ import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
7
6
 
8
7
  export const EditorPage = () => {
9
- const { packageId } = useCurrentPackageId()
10
- const { data: pkg, isLoading, error } = usePackage(packageId)
8
+ const { packageInfo: pkg, error } = useCurrentPackageInfo()
11
9
 
12
10
  const projectUrl = pkg
13
11
  ? `https://tscircuit.com/${pkg.owner_github_username}/${pkg.unscoped_name}`
@@ -27,12 +25,12 @@ export const EditorPage = () => {
27
25
  />
28
26
  <meta
29
27
  property="og:image"
30
- content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
28
+ content={`https://api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
31
29
  />
32
30
  <meta name="twitter:card" content="summary_large_image" />
33
31
  <meta
34
32
  name="twitter:image"
35
- content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
33
+ content={`https://api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
36
34
  />
37
35
  </>
38
36
  )}
@@ -18,7 +18,7 @@ import { useGlobalStore } from "@/hooks/use-global-store"
18
18
  import { navigate } from "wouter/use-browser-location"
19
19
  import { FAQ } from "@/components/FAQ"
20
20
  import { TrendingPackagesCarousel } from "@/components/TrendingPackagesCarousel"
21
- import { PrefetchPageLink } from "@/components/PrefetchPageLink"
21
+ import { Link } from "wouter"
22
22
 
23
23
  export function LandingPage() {
24
24
  const signIn = useSignIn()
@@ -35,8 +35,8 @@ export function LandingPage() {
35
35
  <link rel="preconnect" href="https://tscircuit.com" />
36
36
  <link rel="dns-prefetch" href="https://tscircuit.com" />
37
37
 
38
- <link rel="preconnect" href="https://registry-api.tscircuit.com" />
39
- <link rel="dns-prefetch" href="https://registry-api.tscircuit.com" />
38
+ <link rel="preconnect" href="https://api.tscircuit.com" />
39
+ <link rel="dns-prefetch" href="https://api.tscircuit.com" />
40
40
  </Helmet>
41
41
  <Header2 />
42
42
  <main className="flex-1">
@@ -53,7 +53,7 @@ export function LandingPage() {
53
53
  AI codes electronics with tscircuit
54
54
  </h1>
55
55
  <p className="max-w-[600px] text-muted-foreground md:text-xl">
56
- Build electronics with code, AI, and drag'n'drop tools.
56
+ Build electronics with code and AI tools.
57
57
  <br />
58
58
  Render code into schematics, PCBs, 3D, fabrication files,
59
59
  and more.
@@ -72,7 +72,7 @@ export function LandingPage() {
72
72
  Get Started
73
73
  </Button>
74
74
  </a>
75
- <PrefetchPageLink
75
+ <Link
76
76
  href="/seveibar/led-water-accelerometer#3d"
77
77
  className="w-[70vw] min-[500px]:w-auto"
78
78
  >
@@ -84,7 +84,7 @@ export function LandingPage() {
84
84
  >
85
85
  Open Online Example
86
86
  </Button>
87
- </PrefetchPageLink>
87
+ </Link>
88
88
  <a
89
89
  href="https://github.com/tscircuit/tscircuit"
90
90
  target="_blank"
@@ -35,6 +35,9 @@ const LatestPage: React.FC = () => {
35
35
  },
36
36
  {
37
37
  keepPreviousData: true,
38
+ refetchOnWindowFocus: false,
39
+ refetchOnMount: false,
40
+ refetchOnReconnect: false,
38
41
  },
39
42
  )
40
43