@tscircuit/fake-snippets 0.0.109 → 0.0.111

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 (185) 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 +32 -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 +151 -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 +361 -453
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1313 -639
  36. package/dist/index.d.ts +313 -6
  37. package/dist/index.js +328 -24
  38. package/dist/schema.d.ts +290 -1
  39. package/dist/schema.js +54 -1
  40. package/fake-snippets-api/lib/db/autoload-dev-packages.ts +31 -20
  41. package/fake-snippets-api/lib/db/db-client.ts +219 -4
  42. package/fake-snippets-api/lib/db/schema.ts +63 -1
  43. package/fake-snippets-api/lib/db/seed.ts +100 -0
  44. package/fake-snippets-api/lib/middleware/with-session-auth.ts +60 -8
  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 +33 -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 +48 -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 +60 -0
  54. package/fake-snippets-api/routes/api/orgs/remove_member.ts +46 -0
  55. package/fake-snippets-api/routes/api/orgs/update.ts +118 -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 +57 -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 +25 -19
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +27 -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 +17 -5
  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/SentryNotFoundReporter.tsx +44 -0
  84. package/src/components/UserCard.tsx +80 -0
  85. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  86. package/src/components/ViewPackagePage/components/important-files-view.tsx +105 -34
  87. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -6
  88. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -1
  89. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -19
  90. package/src/components/ViewPackagePage/components/package-header.tsx +25 -33
  91. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -18
  92. package/src/components/ViewPackagePage/components/repo-page-content.tsx +12 -5
  93. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  94. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  95. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  96. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  97. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  98. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  99. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  100. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  101. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  102. package/src/components/organization/OrganizationCard.tsx +206 -0
  103. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  104. package/src/components/organization/OrganizationHeader.tsx +154 -0
  105. package/src/components/organization/OrganizationMembers.tsx +146 -0
  106. package/src/components/package-port/CodeAndPreview.tsx +15 -12
  107. package/src/components/package-port/CodeEditor.tsx +4 -30
  108. package/src/components/package-port/CodeEditorHeader.tsx +123 -61
  109. package/src/components/package-port/EditorNav.tsx +32 -49
  110. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  111. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  112. package/src/components/preview/PackageReleasesDashboard.tsx +23 -11
  113. package/src/components/ui/tree-view.tsx +6 -3
  114. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  115. package/src/hooks/use-create-org-mutation.ts +38 -0
  116. package/src/hooks/use-create-package-mutation.ts +3 -0
  117. package/src/hooks/use-current-package-release.ts +4 -3
  118. package/src/hooks/use-download-zip.ts +2 -2
  119. package/src/hooks/use-global-store.ts +6 -4
  120. package/src/hooks/use-hydration.ts +30 -0
  121. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  122. package/src/hooks/use-list-org-members.ts +27 -0
  123. package/src/hooks/use-list-user-orgs.ts +25 -0
  124. package/src/hooks/use-org-by-github-handle.ts +26 -0
  125. package/src/hooks/use-org.ts +24 -0
  126. package/src/hooks/use-organization.ts +42 -0
  127. package/src/hooks/use-package-as-snippet.ts +4 -2
  128. package/src/hooks/use-package-builds.ts +6 -2
  129. package/src/hooks/use-package-files.ts +5 -3
  130. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  131. package/src/hooks/use-package-release-images.ts +105 -0
  132. package/src/hooks/use-package-release.ts +2 -2
  133. package/src/hooks/use-package-stars.ts +80 -4
  134. package/src/hooks/use-preview-images.ts +6 -3
  135. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  136. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  137. package/src/hooks/use-update-org-mutation.ts +41 -0
  138. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  139. package/src/hooks/useFileManagement.ts +51 -22
  140. package/src/hooks/useOptimizedPackageFilesLoader.ts +11 -24
  141. package/src/hooks/usePackageFilesLoader.ts +2 -2
  142. package/src/hooks/useUpdatePackageFilesMutation.ts +13 -1
  143. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +1 -1
  144. package/src/lib/download-fns/download-kicad-files.ts +22 -11
  145. package/src/lib/download-fns/download-step.ts +12 -0
  146. package/src/lib/normalize-svg-for-tile.ts +50 -0
  147. package/src/lib/posthog.ts +11 -9
  148. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  149. package/src/lib/sentry.ts +14 -0
  150. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  151. package/src/lib/ts-lib-cache.ts +122 -7
  152. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  153. package/src/lib/utils/findTargetFile.ts +45 -10
  154. package/src/lib/utils/isComponentExported.ts +2 -1
  155. package/src/main.tsx +2 -1
  156. package/src/pages/create-organization.tsx +169 -0
  157. package/src/pages/dashboard.tsx +38 -6
  158. package/src/pages/datasheet.tsx +1 -1
  159. package/src/pages/datasheets.tsx +3 -3
  160. package/src/pages/editor.tsx +4 -6
  161. package/src/pages/landing.tsx +6 -6
  162. package/src/pages/latest.tsx +3 -0
  163. package/src/pages/organization-profile.tsx +199 -0
  164. package/src/pages/organization-settings.tsx +569 -0
  165. package/src/pages/package-editor.tsx +21 -21
  166. package/src/pages/preview-release.tsx +75 -145
  167. package/src/pages/quickstart.tsx +159 -123
  168. package/src/pages/release-detail.tsx +119 -31
  169. package/src/pages/search.tsx +197 -57
  170. package/src/pages/settings-redirect.tsx +44 -0
  171. package/src/pages/trending.tsx +29 -20
  172. package/src/pages/user-profile.tsx +58 -7
  173. package/src/pages/user-settings.tsx +161 -0
  174. package/src/pages/view-package.tsx +30 -16
  175. package/vite.config.ts +9 -0
  176. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  177. package/src/components/JLCPCBImportDialog.tsx +0 -280
  178. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  179. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -113
  180. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -56
  181. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  182. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  183. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  184. package/src/components/PageSearchComponent.tsx +0 -148
  185. package/src/pages/package-builds.tsx +0 -33
@@ -0,0 +1,569 @@
1
+ import { useState, useEffect } from "react"
2
+ import { useForm } from "react-hook-form"
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import { z } from "zod"
5
+ import { useParams, useLocation, Link, Redirect } from "wouter"
6
+ import { Helmet } from "react-helmet-async"
7
+ import {
8
+ Form,
9
+ FormControl,
10
+ FormDescription,
11
+ FormField,
12
+ FormItem,
13
+ FormLabel,
14
+ FormMessage,
15
+ } from "@/components/ui/form"
16
+ import { Input } from "@/components/ui/input"
17
+ import { Button } from "@/components/ui/button"
18
+ import { Badge } from "@/components/ui/badge"
19
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
20
+ import {
21
+ AlertDialog,
22
+ AlertDialogAction,
23
+ AlertDialogCancel,
24
+ AlertDialogContent,
25
+ AlertDialogDescription,
26
+ AlertDialogFooter,
27
+ AlertDialogHeader,
28
+ AlertDialogTitle,
29
+ } from "@/components/ui/alert-dialog"
30
+ import { useToast } from "@/hooks/use-toast"
31
+ import { useUpdateOrgMutation } from "@/hooks/use-update-org-mutation"
32
+ import { useListOrgMembers } from "@/hooks/use-list-org-members"
33
+ import { useAddOrgMemberMutation } from "@/hooks/use-add-org-member-mutation"
34
+ import { useRemoveOrgMemberMutation } from "@/hooks/use-remove-org-member-mutation"
35
+ import { useOrgByGithubHandle } from "@/hooks/use-org-by-github-handle"
36
+ import { useGlobalStore } from "@/hooks/use-global-store"
37
+ import { Account } from "fake-snippets-api/lib/db/schema"
38
+ import {
39
+ Users,
40
+ Crown,
41
+ AlertTriangle,
42
+ Loader2,
43
+ PlusIcon,
44
+ ArrowLeft,
45
+ Building2,
46
+ } from "lucide-react"
47
+ import Header from "@/components/Header"
48
+ import Footer from "@/components/Footer"
49
+ import NotFoundPage from "@/pages/404"
50
+ import { FullPageLoader } from "@/App"
51
+ import { OrganizationHeader } from "@/components/organization/OrganizationHeader"
52
+
53
+ const organizationSettingsSchema = z.object({
54
+ name: z
55
+ .string()
56
+ .min(1, "Organization name is required")
57
+ .max(50, "Organization name must be 50 characters or less")
58
+ .regex(
59
+ /^[a-zA-Z0-9_-]+$/,
60
+ "Organization name can only contain letters, numbers, underscores, and hyphens",
61
+ ),
62
+ })
63
+
64
+ type OrganizationSettingsFormData = z.infer<typeof organizationSettingsSchema>
65
+
66
+ export default function OrganizationSettingsPage() {
67
+ const { orgname } = useParams()
68
+ const [, navigate] = useLocation()
69
+ const { toast } = useToast()
70
+ const session = useGlobalStore((s) => s.session)
71
+
72
+ const {
73
+ data: organization,
74
+ isLoading: isLoadingOrg,
75
+ error: orgError,
76
+ } = useOrgByGithubHandle(orgname || null)
77
+
78
+ const [showRemoveMemberDialog, setShowRemoveMemberDialog] = useState<{
79
+ member: Account
80
+ show: boolean
81
+ }>({ member: {} as Account, show: false })
82
+ const [newMemberInput, setNewMemberInput] = useState("")
83
+ const [addMemberError, setAddMemberError] = useState("")
84
+
85
+ const form = useForm<OrganizationSettingsFormData>({
86
+ resolver: zodResolver(organizationSettingsSchema),
87
+ defaultValues: {
88
+ name: "",
89
+ },
90
+ })
91
+
92
+ const { data: members = [], isLoading: isLoadingMembers } = useListOrgMembers(
93
+ {
94
+ orgId: organization?.org_id || "",
95
+ },
96
+ )
97
+
98
+ const updateOrgMutation = useUpdateOrgMutation({
99
+ onSuccess: (updatedOrg) => {
100
+ toast({
101
+ title: "Organization updated",
102
+ description: "Organization settings have been updated successfully.",
103
+ })
104
+ form.reset({ name: updatedOrg.name || "" })
105
+ if (updatedOrg.name !== orgname) {
106
+ navigate(`/${updatedOrg.name}/settings`)
107
+ }
108
+ },
109
+ })
110
+
111
+ const addMemberMutation = useAddOrgMemberMutation({
112
+ onSuccess: () => {
113
+ toast({
114
+ title: "Member added",
115
+ description: "Member has been added to the organization successfully.",
116
+ })
117
+ setNewMemberInput("")
118
+ setAddMemberError("")
119
+ },
120
+ onError: (error: any) => {
121
+ const errorMessage = error?.data?.error?.message
122
+ setAddMemberError(errorMessage)
123
+
124
+ toast({
125
+ title: "Failed to add member",
126
+ description:
127
+ errorMessage || "An error occurred while adding the member.",
128
+ variant: "destructive",
129
+ })
130
+ },
131
+ })
132
+
133
+ const removeMemberMutation = useRemoveOrgMemberMutation({
134
+ onSuccess: () => {
135
+ toast({
136
+ title: "Member removed",
137
+ description: "Member has been removed from the organization.",
138
+ })
139
+ setShowRemoveMemberDialog({ member: {} as Account, show: false })
140
+ },
141
+ })
142
+
143
+ useEffect(() => {
144
+ if (organization) {
145
+ form.reset({ name: organization.name || "" })
146
+ }
147
+ }, [organization, form])
148
+
149
+ if (!orgname) {
150
+ return <NotFoundPage />
151
+ }
152
+
153
+ if (isLoadingOrg) {
154
+ return <FullPageLoader />
155
+ }
156
+
157
+ if (orgError || !organization) {
158
+ return <NotFoundPage heading="Organization Not Found" />
159
+ }
160
+
161
+ const pageTitle = organization?.name
162
+ ? `${organization.name} Settings - tscircuit`
163
+ : orgname
164
+ ? `${orgname} Settings - tscircuit`
165
+ : "Organization Settings - tscircuit"
166
+
167
+ const canManageOrg =
168
+ organization.user_permissions?.can_manage_org ||
169
+ organization.owner_account_id === session?.account_id
170
+
171
+ if (organization.is_personal_org) {
172
+ return <Redirect to="/settings" />
173
+ }
174
+
175
+ if (!canManageOrg) {
176
+ return (
177
+ <div className="min-h-screen bg-white">
178
+ <Helmet>
179
+ <title>{pageTitle}</title>
180
+ </Helmet>
181
+ <Header />
182
+ <Redirect to="/dashboard" />
183
+ <main className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
184
+ <div className="text-center">
185
+ <div className="mx-auto h-24 w-24 rounded-full bg-red-100 flex items-center justify-center mb-6">
186
+ <AlertTriangle className="h-12 w-12 text-red-600" />
187
+ </div>
188
+ <h1 className="text-2xl font-semibold text-gray-900 mb-3">
189
+ Access denied
190
+ </h1>
191
+ <p className="text-gray-600 mb-8 max-w-md mx-auto">
192
+ You don't have permission to manage this organization's settings.
193
+ Only organization owners and members with admin access can view
194
+ this page.
195
+ </p>
196
+ <div className="flex flex-col sm:flex-row gap-3 justify-center">
197
+ <Button variant="outline" onClick={() => navigate(`/${orgname}`)}>
198
+ <ArrowLeft className="h-4 w-4 mr-2" />
199
+ Back to {orgname}
200
+ </Button>
201
+ <Button onClick={() => navigate("/dashboard")}>
202
+ Go to Dashboard
203
+ </Button>
204
+ </div>
205
+ </div>
206
+ </main>
207
+ <Footer />
208
+ </div>
209
+ )
210
+ }
211
+
212
+ const onSubmit = (data: OrganizationSettingsFormData) => {
213
+ if (!organization) return
214
+ updateOrgMutation.mutate({
215
+ orgId: organization.org_id,
216
+ name: data.name,
217
+ })
218
+ }
219
+
220
+ const handleAddMember = () => {
221
+ if (!newMemberInput.trim() || !organization) return
222
+
223
+ // Clear previous errors
224
+ setAddMemberError("")
225
+
226
+ const input = newMemberInput.trim()
227
+
228
+ // Basic validation
229
+ if (input.length < 1) {
230
+ setAddMemberError("Please enter a GitHub username.")
231
+ return
232
+ }
233
+
234
+ if (input.length > 39) {
235
+ setAddMemberError("GitHub usernames cannot be longer than 39 characters.")
236
+ return
237
+ }
238
+
239
+ // Check for invalid characters (GitHub usernames can only contain alphanumeric characters and hyphens)
240
+ if (!/^[a-zA-Z0-9-]+$/.test(input)) {
241
+ setAddMemberError(
242
+ "GitHub usernames can only contain letters, numbers, and hyphens.",
243
+ )
244
+ return
245
+ }
246
+
247
+ // Check if it starts or ends with hyphen
248
+ if (input.startsWith("-") || input.endsWith("-")) {
249
+ setAddMemberError("GitHub usernames cannot start or end with a hyphen.")
250
+ return
251
+ }
252
+
253
+ addMemberMutation.mutate({
254
+ orgId: organization.org_id,
255
+ githubUsername: input,
256
+ })
257
+ }
258
+
259
+ const handleRemoveMember = (member: Account) => {
260
+ if (!organization) return
261
+ if (member.account_id === organization.owner_account_id) {
262
+ toast({
263
+ title: "Cannot remove owner",
264
+ description: "The organization owner cannot be removed.",
265
+ variant: "destructive",
266
+ })
267
+ return
268
+ }
269
+ setShowRemoveMemberDialog({ member, show: true })
270
+ }
271
+
272
+ const confirmRemoveMember = () => {
273
+ if (!organization) return
274
+ removeMemberMutation.mutate({
275
+ orgId: organization.org_id,
276
+ accountId: showRemoveMemberDialog.member.account_id,
277
+ })
278
+ }
279
+
280
+ return (
281
+ <div className="min-h-screen bg-white">
282
+ <Helmet>
283
+ <title>{pageTitle}</title>
284
+ </Helmet>
285
+ <Header />
286
+
287
+ <main className="w-full px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16">
288
+ <OrganizationHeader organization={organization} showActions={false} />
289
+
290
+ <div className="py-8">
291
+ {/* Main Content */}
292
+ <div className="max-w-7xl mx-auto space-y-8">
293
+ {/* Organization Profile */}
294
+ <div className="bg-white border border-gray-200 rounded-xl shadow-sm">
295
+ <div className="px-6 py-5 border-b border-gray-200 bg-gray-50">
296
+ <h2 className="text-xl font-semibold text-gray-900">
297
+ Organization profile
298
+ </h2>
299
+ <p className="text-sm text-gray-600 mt-2">
300
+ Update your organization's basic information and settings.
301
+ </p>
302
+ </div>
303
+
304
+ <div className="p-6 lg:p-8">
305
+ <Form {...form}>
306
+ <form
307
+ onSubmit={form.handleSubmit(onSubmit)}
308
+ className="space-y-6"
309
+ >
310
+ <FormField
311
+ control={form.control}
312
+ name="name"
313
+ render={({ field }) => (
314
+ <FormItem className="grid grid-cols-1 lg:grid-cols-5 gap-6 items-start">
315
+ <div className="lg:col-span-2">
316
+ <FormLabel className="text-sm font-semibold text-gray-900">
317
+ Organization name
318
+ </FormLabel>
319
+ <FormDescription className="text-sm text-gray-500 mt-2 leading-relaxed">
320
+ This is your organization's display name and URL
321
+ identifier. Choose carefully as this affects your
322
+ organization's web address.
323
+ </FormDescription>
324
+ </div>
325
+ <div className="lg:col-span-3">
326
+ <FormControl>
327
+ <Input
328
+ placeholder="Enter organization name"
329
+ {...field}
330
+ disabled={updateOrgMutation.isLoading}
331
+ className="w-full max-w-lg h-11 text-base border-gray-300 focus:border-blue-500 focus:ring-blue-500"
332
+ />
333
+ </FormControl>
334
+ <FormMessage className="mt-2" />
335
+ </div>
336
+ </FormItem>
337
+ )}
338
+ />
339
+
340
+ <div className="pt-6 border-t border-gray-200 flex flex-col sm:flex-row gap-3">
341
+ <Button
342
+ type="submit"
343
+ disabled={
344
+ updateOrgMutation.isLoading || !form.formState.isDirty
345
+ }
346
+ className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2.5 text-sm font-medium shadow-sm"
347
+ >
348
+ {updateOrgMutation.isLoading && (
349
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
350
+ )}
351
+ Update organization
352
+ </Button>
353
+ <Button
354
+ type="button"
355
+ variant="outline"
356
+ onClick={() => form.reset()}
357
+ disabled={updateOrgMutation.isLoading}
358
+ className="px-6 py-2.5 text-sm font-medium border-gray-300 hover:bg-gray-50"
359
+ >
360
+ Reset changes
361
+ </Button>
362
+ </div>
363
+ </form>
364
+ </Form>
365
+ </div>
366
+ </div>
367
+
368
+ {/* Members Management */}
369
+ <div className="bg-white border border-gray-200 rounded-xl shadow-sm">
370
+ <div className="px-6 py-5 border-b border-gray-200 bg-gray-50">
371
+ <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
372
+ <div>
373
+ <h2 className="text-xl font-semibold text-gray-900">
374
+ Organization members
375
+ </h2>
376
+ <p className="text-sm text-gray-600 mt-2">
377
+ Manage who has access to this organization and their
378
+ permissions.
379
+ </p>
380
+ </div>
381
+ <Badge
382
+ variant="secondary"
383
+ className="text-sm px-3 py-1 bg-white border border-gray-200 self-start"
384
+ >
385
+ {members.length} member{members.length > 1 ? "s" : ""}
386
+ </Badge>
387
+ </div>
388
+ </div>
389
+
390
+ <div className="p-6 lg:p-8">
391
+ {/* Add Member Section */}
392
+ <div className="mb-8 p-6 bg-gray-50 rounded-xl">
393
+ <h3 className="font-semibold text-gray-900 mb-4 flex items-center gap-2">
394
+ Add a member
395
+ </h3>
396
+ <div className="grid grid-cols-1 lg:grid-cols-5 gap-4 items-end">
397
+ <div className="lg:col-span-4">
398
+ <Input
399
+ id="member-input"
400
+ placeholder="Enter GitHub username"
401
+ value={newMemberInput}
402
+ onChange={(e) => {
403
+ setNewMemberInput(e.target.value)
404
+ // Clear error when user starts typing
405
+ if (addMemberError) {
406
+ setAddMemberError("")
407
+ }
408
+ }}
409
+ disabled={addMemberMutation.isLoading}
410
+ className={`w-full h-11 text-base bg-white focus:border-blue-500 focus:ring-blue-500 ${
411
+ addMemberError
412
+ ? "border-red-300 focus:border-red-500 focus:ring-red-500"
413
+ : "border-gray-300"
414
+ }`}
415
+ onKeyDown={(e) => {
416
+ if (
417
+ e.key === "Enter" &&
418
+ !addMemberMutation.isLoading
419
+ ) {
420
+ handleAddMember()
421
+ }
422
+ }}
423
+ />
424
+ {addMemberError && (
425
+ <p className="mt-2 text-sm text-red-600">
426
+ {addMemberError}
427
+ </p>
428
+ )}
429
+ </div>
430
+ <div className="lg:col-span-1">
431
+ <Button
432
+ onClick={handleAddMember}
433
+ disabled={
434
+ !newMemberInput.trim() || addMemberMutation.isLoading
435
+ }
436
+ className="w-full h-11 bg-blue-600 hover:bg-blue-700 text-white px-6 text-sm font-medium shadow-sm"
437
+ >
438
+ {addMemberMutation.isLoading ? (
439
+ <Loader2 className="h-4 w-4 animate-spin mr-2" />
440
+ ) : (
441
+ <PlusIcon className="h-4 w-4 mr-2" />
442
+ )}
443
+ Add member
444
+ </Button>
445
+ </div>
446
+ </div>
447
+ </div>
448
+
449
+ {/* Members List */}
450
+ <div className="space-y-0 border border-gray-200 rounded-xl divide-y divide-gray-200 overflow-hidden">
451
+ {isLoadingMembers ? (
452
+ <div className="flex items-center justify-center py-16">
453
+ <Loader2 className="h-8 w-8 animate-spin text-blue-500" />
454
+ </div>
455
+ ) : members.length === 0 ? (
456
+ <div className="text-center py-16 text-gray-500">
457
+ <Users className="h-12 w-12 mx-auto mb-4 text-gray-300" />
458
+ <p className="text-lg font-medium">No members found</p>
459
+ <p className="text-sm mt-1">
460
+ Add your first team member to get started.
461
+ </p>
462
+ </div>
463
+ ) : (
464
+ members.map((member) => (
465
+ <div
466
+ key={member.account_id}
467
+ className="flex flex-col sm:flex-row sm:items-center sm:justify-between p-5 hover:bg-gray-50 transition-all duration-200 gap-4 sm:gap-0"
468
+ >
469
+ <Link
470
+ href={`/${member.github_username || member.account_id}`}
471
+ className="flex items-center gap-4 group cursor-pointer flex-1 min-w-0"
472
+ >
473
+ <Avatar className="h-12 w-12 border-2 border-gray-200 shadow-sm">
474
+ <AvatarImage
475
+ src={`https://github.com/${member.github_username}.png`}
476
+ alt={`${member.github_username} avatar`}
477
+ />
478
+ <AvatarFallback className="text-sm bg-gradient-to-br from-blue-100 to-indigo-100 text-blue-700 font-medium">
479
+ {(
480
+ member.github_username ||
481
+ member.account_id ||
482
+ ""
483
+ )
484
+ .slice(0, 2)
485
+ .toUpperCase()}
486
+ </AvatarFallback>
487
+ </Avatar>
488
+ <div className="min-w-0 flex-1">
489
+ <div className="flex items-center gap-2 mb-1">
490
+ <span className="font-semibold text-gray-900 text-base group-hover:text-blue-600 transition-colors truncate">
491
+ {member.github_username || member.account_id}
492
+ </span>
493
+ {member.account_id ===
494
+ organization.owner_account_id && (
495
+ <div className="flex items-center gap-1 px-2 py-1 bg-yellow-100 text-yellow-800 rounded-full text-xs font-medium flex-shrink-0">
496
+ <Crown className="h-3 w-3" />
497
+ Owner
498
+ </div>
499
+ )}
500
+ </div>
501
+ <p className="text-sm text-gray-500 truncate">
502
+ {member.account_id ===
503
+ organization.owner_account_id
504
+ ? "Full access to organization settings"
505
+ : "Standard member access"}
506
+ </p>
507
+ </div>
508
+ </Link>
509
+ {member.account_id !== organization.owner_account_id &&
510
+ member.account_id !== session?.account_id && (
511
+ <Button
512
+ variant="ghost"
513
+ size="sm"
514
+ onClick={() => handleRemoveMember(member)}
515
+ disabled={removeMemberMutation.isLoading}
516
+ className="text-red-600 hover:text-red-700 hover:bg-red-50 border border-red-200 hover:border-red-300 self-start sm:self-center px-4 py-2"
517
+ >
518
+ Remove
519
+ </Button>
520
+ )}
521
+ </div>
522
+ ))
523
+ )}
524
+ </div>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ </div>
529
+ </main>
530
+
531
+ <Footer />
532
+
533
+ {/* Remove Member Confirmation Dialog */}
534
+ <AlertDialog
535
+ open={showRemoveMemberDialog.show}
536
+ onOpenChange={(open) =>
537
+ setShowRemoveMemberDialog({ ...showRemoveMemberDialog, show: open })
538
+ }
539
+ >
540
+ <AlertDialogContent>
541
+ <AlertDialogHeader>
542
+ <AlertDialogTitle>Remove member</AlertDialogTitle>
543
+ <AlertDialogDescription>
544
+ Are you sure you want to remove{" "}
545
+ <strong>
546
+ {showRemoveMemberDialog.member.github_username ||
547
+ showRemoveMemberDialog.member.account_id}
548
+ </strong>{" "}
549
+ from this organization? This action cannot be undone.
550
+ </AlertDialogDescription>
551
+ </AlertDialogHeader>
552
+ <AlertDialogFooter>
553
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
554
+ <AlertDialogAction
555
+ onClick={confirmRemoveMember}
556
+ disabled={removeMemberMutation.isLoading}
557
+ className="bg-red-600 hover:bg-red-700"
558
+ >
559
+ {removeMemberMutation.isLoading && (
560
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
561
+ )}
562
+ Remove member
563
+ </AlertDialogAction>
564
+ </AlertDialogFooter>
565
+ </AlertDialogContent>
566
+ </AlertDialog>
567
+ </div>
568
+ )
569
+ }
@@ -1,21 +1,19 @@
1
1
  import { CodeAndPreview } from "@/components/package-port/CodeAndPreview"
2
- import Footer from "@/components/Footer"
3
2
  import Header from "@/components/Header"
4
- import { usePackage } from "@/hooks/use-package"
5
3
  import { Helmet } from "react-helmet-async"
6
- import { useCurrentPackageId } from "@/hooks/use-current-package-id"
7
4
  import { NotFound } from "@/components/NotFound"
8
5
  import { ErrorOutline } from "@/components/ErrorOutline"
6
+ import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
9
7
 
10
8
  export const EditorPage = () => {
11
- const { packageId } = useCurrentPackageId()
12
- const { data: pkg, error } = usePackage(packageId)
9
+ const { packageInfo: pkg, error } = useCurrentPackageInfo()
10
+
13
11
  const uuid4RegExp = new RegExp(
14
12
  /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
15
13
  )
16
14
 
17
15
  return (
18
- <div className="overflow-x-hidden">
16
+ <div className="flex flex-col h-screen overflow-x-hidden">
19
17
  <Helmet>
20
18
  <title>{pkg ? `${pkg.name} - tscircuit` : "tscircuit editor"}</title>
21
19
  {pkg && (
@@ -26,31 +24,33 @@ export const EditorPage = () => {
26
24
  />
27
25
  <meta
28
26
  property="og:image"
29
- 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}`}
27
+ content={`https://api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
30
28
  />
31
29
  <meta name="twitter:card" content="summary_large_image" />
32
30
  <meta
33
31
  name="twitter:image"
34
- 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}`}
32
+ content={`https://api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
35
33
  />
36
34
  </>
37
35
  )}
38
36
  </Helmet>
39
37
  <Header />
40
- {!error && <CodeAndPreview pkg={pkg} />}
41
- {error &&
42
- (error.status === 404 || !uuid4RegExp.test(packageId ?? "")) && (
43
- <NotFound heading="Package not found" />
38
+ <div className="flex-1 overflow-y-auto">
39
+ {!error && <CodeAndPreview pkg={pkg} />}
40
+ {error &&
41
+ (error.status === 404 ||
42
+ !uuid4RegExp.test(pkg?.package_id ?? "")) && (
43
+ <NotFound heading="Package not found" />
44
+ )}
45
+ {error && error.status !== 404 && (
46
+ <div className="min-h-screen grid place-items-center">
47
+ <ErrorOutline
48
+ error={error}
49
+ description={"There was an error loading the editor page"}
50
+ />
51
+ </div>
44
52
  )}
45
- {error && error.status !== 404 && (
46
- <div className="min-h-screen grid place-items-center">
47
- <ErrorOutline
48
- error={error}
49
- description={"There was an error loading the editor page"}
50
- />
51
- </div>
52
- )}
53
- <Footer />
53
+ </div>
54
54
  </div>
55
55
  )
56
56
  }