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