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