@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
@@ -6,6 +6,7 @@ import {
6
6
  CommandGroup,
7
7
  CommandInput,
8
8
  CommandItem,
9
+ CommandList,
9
10
  } from "@/components/ui/command"
10
11
  import {
11
12
  Popover,
@@ -203,63 +204,69 @@ export const GitHubRepositorySelector = ({
203
204
  <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
204
205
  </Button>
205
206
  </PopoverTrigger>
206
- <PopoverContent className="w-full p-0 z-[999]">
207
+ <PopoverContent
208
+ className="w-[var(--radix-popover-trigger-width)] p-0 z-[999]"
209
+ align="start"
210
+ >
207
211
  <Command shouldFilter={false}>
208
212
  <CommandInput
209
213
  value={searchValue}
210
214
  onValueChange={setSearchValue}
211
215
  placeholder="Search repositories..."
212
216
  />
213
- <CommandEmpty className="text-sm text-slate-500 py-6">
214
- No repositories found.
215
- </CommandEmpty>
216
- <CommandGroup className="max-h-[400px] overflow-y-auto">
217
- {filteredOptions.map((option) => (
218
- <CommandItem
219
- key={option.value}
220
- onSelect={() => handleComboboxSelect(option.value)}
221
- className="cursor-pointer"
222
- >
223
- <div className="flex items-center space-x-2 w-full">
224
- {option.type === "repo" ? (
225
- <>
226
- <Check
227
- className={cn(
228
- "mr-2 h-4 w-4",
229
- selectedRepository === option.value
230
- ? "opacity-100"
231
- : "opacity-0",
217
+ <CommandList className="max-h-[400px] overflow-y-auto">
218
+ <CommandEmpty className="text-sm text-slate-500 py-6 pl-4">
219
+ No repositories found.
220
+ </CommandEmpty>
221
+ <CommandGroup>
222
+ {filteredOptions.map((option) => (
223
+ <CommandItem
224
+ key={option.value}
225
+ value={option.value}
226
+ onSelect={() => handleComboboxSelect(option.value)}
227
+ className="cursor-pointer hover:bg-slate-100"
228
+ >
229
+ <div className="flex items-center space-x-2 w-full">
230
+ {option.type === "repo" ? (
231
+ <>
232
+ <Check
233
+ className={cn(
234
+ "mr-2 h-4 w-4",
235
+ selectedRepository === option.value
236
+ ? "opacity-100"
237
+ : "opacity-0",
238
+ )}
239
+ />
240
+ <span>{option.label}</span>
241
+ {option.isPrivate && (
242
+ <span className="text-xs text-muted-foreground">
243
+ (private)
244
+ </span>
245
+ )}
246
+ </>
247
+ ) : (
248
+ <>
249
+ {option.icon === "plus" ? (
250
+ <Plus className="w-3 h-3 text-blue-600" />
251
+ ) : (
252
+ <Minus className="w-3 h-3 text-red-600" />
232
253
  )}
233
- />
234
- <span>{option.label}</span>
235
- {option.isPrivate && (
236
- <span className="text-xs text-muted-foreground">
237
- (private)
254
+ <span
255
+ className={
256
+ option.icon === "plus"
257
+ ? "text-blue-600"
258
+ : "text-red-600"
259
+ }
260
+ >
261
+ {option.label}
238
262
  </span>
239
- )}
240
- </>
241
- ) : (
242
- <>
243
- {option.icon === "plus" ? (
244
- <Plus className="w-3 h-3 text-blue-600" />
245
- ) : (
246
- <Minus className="w-3 h-3 text-red-600" />
247
- )}
248
- <span
249
- className={
250
- option.icon === "plus"
251
- ? "text-blue-600"
252
- : "text-red-600"
253
- }
254
- >
255
- {option.label}
256
- </span>
257
- </>
258
- )}
259
- </div>
260
- </CommandItem>
261
- ))}
262
- </CommandGroup>
263
+ </>
264
+ )}
265
+ </div>
266
+ </CommandItem>
267
+ ))}
268
+ </CommandGroup>
269
+ </CommandList>
263
270
  </Command>
264
271
  </PopoverContent>
265
272
  </Popover>
@@ -133,8 +133,10 @@ export const EditPackageDetailsDialog = ({
133
133
  if (response.status !== 200)
134
134
  throw new Error("Failed to update package details")
135
135
 
136
- const filesRes = await axios.post("/package_files/list", {
137
- package_name_with_version: `${packageAuthor}/${formData.unscopedPackageName}`,
136
+ const filesRes = await axios.get("/package_files/list", {
137
+ params: {
138
+ package_name_with_version: `${packageAuthor}/${formData.unscopedPackageName}`,
139
+ },
138
140
  })
139
141
  const packageFiles: string[] =
140
142
  filesRes.status === 200
@@ -194,10 +196,7 @@ export const EditPackageDetailsDialog = ({
194
196
  onSuccess: (data) => {
195
197
  onUpdate?.(data.description, data.website, data.license, data.defaultView)
196
198
  onOpenChange(false)
197
- qc.invalidateQueries([
198
- "packageFile",
199
- { package_release_id: packageReleaseId },
200
- ])
199
+ qc.invalidateQueries("packageFile")
201
200
  qc.invalidateQueries(["packageFiles", packageReleaseId])
202
201
  toast({
203
202
  title: "Package details updated",
@@ -1,23 +1,30 @@
1
1
  import { createUseDialog } from "./create-use-dialog"
2
2
  import {
3
- ComponentSearchResult,
4
- ImportComponentDialog as RunframeImportComponentDialog,
3
+ ImportComponentDialog2 as RunframeImportComponentDialog,
4
+ type ImportComponentDialog2Props,
5
5
  } from "@tscircuit/runframe/runner"
6
6
 
7
+ export type ImportComponentDialogProps = {
8
+ open: boolean
9
+ onOpenChange: (open: boolean) => void
10
+ } & Pick<
11
+ ImportComponentDialog2Props,
12
+ | "onTscircuitPackageSelected"
13
+ | "onJlcpcbComponentTsxLoaded"
14
+ | "onKicadStringSelected"
15
+ | "jlcpcbProxyRequestHeaders"
16
+ >
17
+
7
18
  export const ImportComponentDialog = ({
8
19
  open,
9
20
  onOpenChange,
10
- onComponentSelected,
11
- }: {
12
- open: boolean
13
- onOpenChange: (open: boolean) => any
14
- onComponentSelected: (pkg: ComponentSearchResult) => any
15
- }) => {
21
+ ...rest
22
+ }: ImportComponentDialogProps) => {
16
23
  return (
17
24
  <RunframeImportComponentDialog
18
25
  isOpen={open}
19
26
  onClose={() => onOpenChange(false)}
20
- onImport={(data) => onComponentSelected(data)}
27
+ {...rest}
21
28
  />
22
29
  )
23
30
  }
@@ -7,6 +7,7 @@ import { Button } from "../ui/button"
7
7
  import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
8
8
  import { Input } from "../ui/input"
9
9
  import { createUseDialog } from "./create-use-dialog"
10
+ import { Link } from "wouter"
10
11
 
11
12
  export const ImportPackageDialog = ({
12
13
  open,
@@ -55,13 +56,13 @@ export const ImportPackageDialog = ({
55
56
  className="flex flex-col sm:flex-row items-start sm:items-center my-2 text-sm w-full"
56
57
  key={pkg.package_id}
57
58
  >
58
- <a
59
+ <Link
59
60
  href={`/${pkg.name}`}
60
61
  target="_blank"
61
62
  className="text-blue-500 hover:underline cursor-pointer flex-shrink-0 mb-1 sm:mb-0 sm:mr-2"
62
63
  >
63
64
  {pkg.name}
64
- </a>
65
+ </Link>
65
66
  <div className="text-gray-500 flex-grow overflow-hidden text-ellipsis whitespace-nowrap mb-1 sm:mb-0">
66
67
  {pkg.description}
67
68
  </div>
@@ -0,0 +1,190 @@
1
+ import { useState, useEffect, useMemo } from "react"
2
+ import { Button } from "../ui/button"
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogDescription,
9
+ } from "../ui/dialog"
10
+ import { Input } from "../ui/input"
11
+ import { Label } from "../ui/label"
12
+ import {
13
+ Select,
14
+ SelectContent,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "../ui/select"
19
+ import { createUseDialog } from "./create-use-dialog"
20
+ import { useListUserOrgs } from "@/hooks/use-list-user-orgs"
21
+ import { useGlobalStore } from "@/hooks/use-global-store"
22
+
23
+ export const NewPackageSavePromptDialog = ({
24
+ open,
25
+ onOpenChange,
26
+ initialIsPrivate = false,
27
+ initialName = "",
28
+ onSave,
29
+ }: {
30
+ open: boolean
31
+ onOpenChange: (open: boolean) => void
32
+ initialIsPrivate?: boolean
33
+ initialName?: string
34
+ onSave: ({
35
+ name,
36
+ isPrivate,
37
+ orgId,
38
+ }: {
39
+ name?: string
40
+ isPrivate: boolean
41
+ orgId: string
42
+ }) => void
43
+ }) => {
44
+ const [packageName, setPackageName] = useState(initialName)
45
+ const session = useGlobalStore((s) => s.session)
46
+ const [isPrivate, setIsPrivate] = useState(initialIsPrivate)
47
+ const [selectedOrgId, setSelectedOrgId] = useState<string>("")
48
+ const { data: organizations, isLoading: orgsLoading } = useListUserOrgs()
49
+ const fullPackageName = useMemo(() => {
50
+ if (selectedOrgId) {
51
+ return `${organizations?.find((x) => x.org_id === selectedOrgId)?.name}/${packageName}`
52
+ }
53
+ return `${session?.github_username}/${packageName}`
54
+ }, [selectedOrgId, packageName, organizations, session?.github_username])
55
+ useEffect(() => {
56
+ if (organizations && organizations.length > 0 && !selectedOrgId) {
57
+ setSelectedOrgId(
58
+ organizations.find((x) => x.is_personal_org)?.org_id ||
59
+ organizations[0].org_id,
60
+ )
61
+ }
62
+ }, [organizations, selectedOrgId])
63
+ return (
64
+ <Dialog open={open} onOpenChange={onOpenChange}>
65
+ <DialogContent>
66
+ <DialogHeader>
67
+ <DialogTitle>Creating new package</DialogTitle>
68
+ <DialogDescription>
69
+ Would you like to save this package?
70
+ </DialogDescription>
71
+ </DialogHeader>
72
+ <div className="space-y-4 py-1">
73
+ <div className="space-y-2">
74
+ <Label className="text-sm font-medium">Organization</Label>
75
+ <Select
76
+ value={selectedOrgId}
77
+ onValueChange={setSelectedOrgId}
78
+ disabled={orgsLoading}
79
+ >
80
+ <SelectTrigger className="w-full">
81
+ <div className="flex items-center gap-2 flex-1">
82
+ {selectedOrgId && organizations ? (
83
+ <span className="truncate">
84
+ {organizations.find((org) => org.org_id === selectedOrgId)
85
+ ?.display_name ||
86
+ organizations.find(
87
+ (org) => org.org_id === selectedOrgId,
88
+ )?.name ||
89
+ `Org ${selectedOrgId.slice(0, 8)}`}
90
+ </span>
91
+ ) : (
92
+ <span className="text-slate-500">
93
+ {orgsLoading
94
+ ? "Loading organizations..."
95
+ : "Select organization"}
96
+ </span>
97
+ )}
98
+ </div>
99
+ </SelectTrigger>
100
+ <SelectContent className="!z-[999]">
101
+ {organizations?.length === 0 ? (
102
+ <div className="px-2 py-1.5 text-sm text-slate-500">
103
+ No organizations found
104
+ </div>
105
+ ) : (
106
+ organizations?.map((org) => (
107
+ <SelectItem
108
+ key={org.org_id}
109
+ value={org.org_id}
110
+ className="cursor-pointer"
111
+ >
112
+ {org.display_name ||
113
+ org.name ||
114
+ `Org ${org.org_id.slice(0, 8)}`}
115
+ </SelectItem>
116
+ ))
117
+ )}
118
+ </SelectContent>
119
+ </Select>
120
+ </div>
121
+
122
+ <div className="space-y-2">
123
+ <Label className="text-sm font-medium">Package Name</Label>
124
+ <Input
125
+ value={packageName}
126
+ onChange={(e) => setPackageName(e.target.value)}
127
+ placeholder="Untitled Package"
128
+ className="w-full"
129
+ />
130
+ </div>
131
+
132
+ <div className="space-y-2">
133
+ <div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 sm:gap-4">
134
+ <div className="space-y-1 flex-1">
135
+ <Label className="text-sm font-medium">Visibility</Label>
136
+ <p className="text-xs text-slate-500">
137
+ {isPrivate
138
+ ? "Only you can view and use this package"
139
+ : "Anyone can view and use your package"}
140
+ </p>
141
+ </div>
142
+ <Select
143
+ value={isPrivate ? "private" : "public"}
144
+ onValueChange={(value) => setIsPrivate(value === "private")}
145
+ >
146
+ <SelectTrigger className="w-full sm:w-32 sm:mt-2">
147
+ <SelectValue />
148
+ </SelectTrigger>
149
+ <SelectContent className="!z-[999]">
150
+ <SelectItem value="public" className="cursor-pointer">
151
+ <div className="flex items-center gap-2">
152
+ <span>Public</span>
153
+ </div>
154
+ </SelectItem>
155
+ <SelectItem value="private" className="cursor-pointer">
156
+ <div className="flex items-center gap-2">
157
+ <span>Private</span>
158
+ </div>
159
+ </SelectItem>
160
+ </SelectContent>
161
+ </Select>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ <div className="flex justify-end gap-2">
166
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
167
+ Cancel
168
+ </Button>
169
+ <Button
170
+ onClick={() => {
171
+ onSave({
172
+ name: fullPackageName.trim(),
173
+ isPrivate,
174
+ orgId: selectedOrgId,
175
+ })
176
+ onOpenChange(false)
177
+ }}
178
+ disabled={!selectedOrgId || orgsLoading || !session}
179
+ >
180
+ Save
181
+ </Button>
182
+ </div>
183
+ </DialogContent>
184
+ </Dialog>
185
+ )
186
+ }
187
+
188
+ export const useNewPackageSavePromptDialog = createUseDialog(
189
+ NewPackageSavePromptDialog,
190
+ )
@@ -0,0 +1,206 @@
1
+ import React from "react"
2
+ import { Link } from "wouter"
3
+ import {
4
+ Users,
5
+ Package,
6
+ Globe,
7
+ Lock,
8
+ MoreVertical,
9
+ Share2,
10
+ Settings,
11
+ } from "lucide-react"
12
+ import { Button } from "@/components/ui/button"
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger,
18
+ } from "@/components/ui/dropdown-menu"
19
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
20
+ import { timeAgo } from "@/lib/utils/timeAgo"
21
+ import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
22
+ import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
23
+ import { useGlobalStore } from "@/hooks/use-global-store"
24
+
25
+ export interface OrganizationCardProps {
26
+ /** The organization data to display */
27
+ organization: PublicOrgSchema
28
+ /** Whether to show member count */
29
+ showMembers?: boolean
30
+ /** Whether to show statistics (packages, members) */
31
+ showStats?: boolean
32
+ /** Callback when the card is clicked */
33
+ onClick?: (org: PublicOrgSchema) => void
34
+ /** Custom class name for the card container */
35
+ className?: string
36
+ /** Whether to render the card with a link to the organization page */
37
+ withLink?: boolean
38
+ /** Custom render function for actions */
39
+ renderActions?: (org: PublicOrgSchema) => React.ReactNode
40
+ }
41
+
42
+ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
43
+ organization,
44
+ showMembers = true,
45
+ showStats = true,
46
+ onClick,
47
+ className = "",
48
+ withLink = true,
49
+ renderActions,
50
+ }) => {
51
+ const { copyToClipboard } = useCopyToClipboard()
52
+ const { session } = useGlobalStore()
53
+
54
+ const canManageOrg =
55
+ organization.owner_account_id === session?.account_id ||
56
+ organization.user_permissions?.can_manage_org
57
+
58
+ const handleCardClick = (e: React.MouseEvent) => {
59
+ if (onClick && !withLink) {
60
+ e.preventDefault()
61
+ onClick(organization)
62
+ }
63
+ }
64
+
65
+ const handleShareClick = (e: React.MouseEvent) => {
66
+ e.preventDefault()
67
+ e.stopPropagation()
68
+ const shareUrl = `${window.location.origin}/${organization.name}`
69
+ copyToClipboard(shareUrl)
70
+ }
71
+
72
+ const handleSettingsClick = (e: React.MouseEvent) => {
73
+ e.preventDefault()
74
+ e.stopPropagation()
75
+ window.location.href = organization.is_personal_org
76
+ ? `/settings`
77
+ : `/${organization.name}/settings`
78
+ }
79
+
80
+ const cardContent = (
81
+ <div
82
+ className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 h-full ${className}`}
83
+ onClick={handleCardClick}
84
+ >
85
+ <div className="flex items-start gap-4">
86
+ {/* Organization Avatar */}
87
+ <div className="flex-shrink-0">
88
+ <Avatar className="h-16 w-16 border-2 border-gray-100">
89
+ <AvatarImage
90
+ src={`https://github.com/${organization.name}.png`}
91
+ alt={`${organization.name} avatar`}
92
+ className="object-cover"
93
+ />
94
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-semibold text-lg">
95
+ {organization.name
96
+ ?.split(" ")
97
+ .map((word) => word[0])
98
+ .join("")
99
+ .toUpperCase()
100
+ .slice(0, 2)}
101
+ </AvatarFallback>
102
+ </Avatar>
103
+ </div>
104
+
105
+ {/* Organization Info */}
106
+ <div className="flex-1 min-w-0">
107
+ <div className="flex justify-between items-start mb-1">
108
+ <div className="min-w-0 flex-1">
109
+ <h2 className="text-md font-semibold text-gray-900 truncate pr-8">
110
+ {organization.name}
111
+ </h2>
112
+ </div>
113
+
114
+ {/* Actions Dropdown */}
115
+ <div className="flex items-center gap-2">
116
+ <DropdownMenu>
117
+ <DropdownMenuTrigger asChild>
118
+ <Button
119
+ variant="ghost"
120
+ size="icon"
121
+ className="h-6 w-6"
122
+ onClick={(e) => e.stopPropagation()}
123
+ >
124
+ <MoreVertical className="h-4 w-4" />
125
+ </Button>
126
+ </DropdownMenuTrigger>
127
+ <DropdownMenuContent align="end">
128
+ <DropdownMenuItem
129
+ className="text-xs cursor-pointer"
130
+ onClick={handleShareClick}
131
+ >
132
+ <Share2 className="mr-2 h-3 w-3" />
133
+ Share Organization
134
+ </DropdownMenuItem>
135
+ {canManageOrg && (
136
+ <DropdownMenuItem
137
+ className="text-xs cursor-pointer"
138
+ onClick={handleSettingsClick}
139
+ >
140
+ <Settings className="mr-2 h-3 w-3" />
141
+ Organization Settings
142
+ </DropdownMenuItem>
143
+ )}
144
+ </DropdownMenuContent>
145
+ </DropdownMenu>
146
+ {renderActions && renderActions(organization)}
147
+ </div>
148
+ </div>
149
+
150
+ {/* Statistics and Metadata */}
151
+ <div className="flex items-center gap-4 text-xs text-gray-500 mb-1">
152
+ {/* Visibility */}
153
+ <div className="flex items-center gap-1">
154
+ {!organization.is_personal_org ? (
155
+ <>
156
+ <Globe className="h-3 w-3" />
157
+ <span>Public</span>
158
+ </>
159
+ ) : (
160
+ <>
161
+ <Lock className="h-3 w-3" />
162
+ <span>Personal</span>
163
+ </>
164
+ )}
165
+ </div>
166
+
167
+ {/* Statistics */}
168
+ {showStats && (
169
+ <>
170
+ {showMembers && !organization.is_personal_org && (
171
+ <div className="flex items-center gap-1">
172
+ <Users className="h-3 w-3" />
173
+ <span>{organization.member_count} members</span>
174
+ </div>
175
+ )}
176
+ <div className="flex items-center gap-1">
177
+ <Package className="h-3 w-3" />
178
+ <span>{organization.package_count} packages</span>
179
+ </div>
180
+ </>
181
+ )}
182
+ </div>
183
+
184
+ {/* Created time */}
185
+ <div className="text-xs text-gray-400">
186
+ <span>Created {timeAgo(new Date(organization.created_at))}</span>
187
+ </div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ )
192
+
193
+ if (withLink) {
194
+ return (
195
+ <Link
196
+ key={organization.org_id}
197
+ href={`/${organization.name}`}
198
+ className="block h-full"
199
+ >
200
+ {cardContent}
201
+ </Link>
202
+ )
203
+ }
204
+
205
+ return cardContent
206
+ }
@@ -0,0 +1,55 @@
1
+ import React from "react"
2
+
3
+ export const OrganizationCardSkeleton: React.FC = () => {
4
+ return (
5
+ <div className="border p-4 rounded-md animate-pulse">
6
+ <div className="flex flex-col gap-4">
7
+ <div className="flex items-start gap-4">
8
+ {/* Organization Avatar skeleton */}
9
+ <div className="flex-shrink-0">
10
+ <div className="h-16 w-16 rounded-full bg-slate-200 border-2 border-gray-100"></div>
11
+ </div>
12
+
13
+ {/* Organization Info skeleton */}
14
+ <div className="flex-1 min-w-0">
15
+ <div className="flex justify-between items-start mb-1">
16
+ <div className="min-w-0 flex-1">
17
+ {/* Organization name */}
18
+ <div className="h-5 bg-slate-200 rounded w-3/4 sm:w-1/2 mb-1"></div>
19
+ </div>
20
+
21
+ {/* Actions dropdown skeleton */}
22
+ <div className="flex items-center gap-2">
23
+ <div className="h-6 w-6 bg-slate-200 rounded"></div>
24
+ </div>
25
+ </div>
26
+
27
+ {/* Statistics and Metadata skeleton */}
28
+ <div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs mb-1">
29
+ {/* Visibility */}
30
+ <div className="flex items-center gap-1">
31
+ <div className="h-3 w-3 bg-slate-200 rounded"></div>
32
+ <div className="h-3 bg-slate-200 rounded w-8 sm:w-12"></div>
33
+ </div>
34
+
35
+ {/* Members */}
36
+ <div className="flex items-center gap-1">
37
+ <div className="h-3 w-3 bg-slate-200 rounded"></div>
38
+ <div className="h-3 bg-slate-200 rounded w-12 sm:w-16"></div>
39
+ </div>
40
+
41
+ {/* Packages */}
42
+ <div className="flex items-center gap-1">
43
+ <div className="h-3 w-3 bg-slate-200 rounded"></div>
44
+ <div className="h-3 bg-slate-200 rounded w-14 sm:w-16"></div>
45
+ </div>
46
+ </div>
47
+
48
+ {/* Created time skeleton */}
49
+ <div className="h-3 bg-slate-200 rounded w-20 sm:w-24"></div>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ )
55
+ }