@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,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,204 @@
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.name}/settings`
76
+ }
77
+
78
+ const cardContent = (
79
+ <div
80
+ className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
81
+ onClick={handleCardClick}
82
+ >
83
+ <div className="flex items-start gap-4">
84
+ {/* Organization Avatar */}
85
+ <div className="flex-shrink-0">
86
+ <Avatar className="h-16 w-16 border-2 border-gray-100">
87
+ <AvatarImage
88
+ src={`https://github.com/${organization.name}.png`}
89
+ alt={`${organization.name} avatar`}
90
+ className="object-cover"
91
+ />
92
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-semibold text-lg">
93
+ {organization.name
94
+ ?.split(" ")
95
+ .map((word) => word[0])
96
+ .join("")
97
+ .toUpperCase()
98
+ .slice(0, 2)}
99
+ </AvatarFallback>
100
+ </Avatar>
101
+ </div>
102
+
103
+ {/* Organization Info */}
104
+ <div className="flex-1 min-w-0">
105
+ <div className="flex justify-between items-start mb-1">
106
+ <div className="min-w-0 flex-1">
107
+ <h2 className="text-md font-semibold text-gray-900 truncate pr-8">
108
+ {organization.name}
109
+ </h2>
110
+ </div>
111
+
112
+ {/* Actions Dropdown */}
113
+ <div className="flex items-center gap-2">
114
+ <DropdownMenu>
115
+ <DropdownMenuTrigger asChild>
116
+ <Button
117
+ variant="ghost"
118
+ size="icon"
119
+ className="h-6 w-6"
120
+ onClick={(e) => e.stopPropagation()}
121
+ >
122
+ <MoreVertical className="h-4 w-4" />
123
+ </Button>
124
+ </DropdownMenuTrigger>
125
+ <DropdownMenuContent align="end">
126
+ <DropdownMenuItem
127
+ className="text-xs cursor-pointer"
128
+ onClick={handleShareClick}
129
+ >
130
+ <Share2 className="mr-2 h-3 w-3" />
131
+ Share Organization
132
+ </DropdownMenuItem>
133
+ {canManageOrg && (
134
+ <DropdownMenuItem
135
+ className="text-xs cursor-pointer"
136
+ onClick={handleSettingsClick}
137
+ >
138
+ <Settings className="mr-2 h-3 w-3" />
139
+ Organization Settings
140
+ </DropdownMenuItem>
141
+ )}
142
+ </DropdownMenuContent>
143
+ </DropdownMenu>
144
+ {renderActions && renderActions(organization)}
145
+ </div>
146
+ </div>
147
+
148
+ {/* Statistics and Metadata */}
149
+ <div className="flex items-center gap-4 text-xs text-gray-500 mb-1">
150
+ {/* Visibility */}
151
+ <div className="flex items-center gap-1">
152
+ {!organization.is_personal_org ? (
153
+ <>
154
+ <Globe className="h-3 w-3" />
155
+ <span>Public</span>
156
+ </>
157
+ ) : (
158
+ <>
159
+ <Lock className="h-3 w-3" />
160
+ <span>Personal</span>
161
+ </>
162
+ )}
163
+ </div>
164
+
165
+ {/* Statistics */}
166
+ {showStats && (
167
+ <>
168
+ {showMembers && !organization.is_personal_org && (
169
+ <div className="flex items-center gap-1">
170
+ <Users className="h-3 w-3" />
171
+ <span>{organization.member_count} members</span>
172
+ </div>
173
+ )}
174
+ <div className="flex items-center gap-1">
175
+ <Package className="h-3 w-3" />
176
+ <span>{organization.package_count} packages</span>
177
+ </div>
178
+ </>
179
+ )}
180
+ </div>
181
+
182
+ {/* Created time */}
183
+ <div className="text-xs text-gray-400">
184
+ <span>Created {timeAgo(new Date(organization.created_at))}</span>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </div>
189
+ )
190
+
191
+ if (withLink) {
192
+ return (
193
+ <Link
194
+ key={organization.org_id}
195
+ href={`/${organization.name}`}
196
+ className="block"
197
+ >
198
+ {cardContent}
199
+ </Link>
200
+ )
201
+ }
202
+
203
+ return cardContent
204
+ }
@@ -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
+ }
@@ -0,0 +1,154 @@
1
+ import React, { useState } from "react"
2
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
3
+ import { Button } from "@/components/ui/button"
4
+ import { Building2, Users, Package, Lock, Globe2, Settings } from "lucide-react"
5
+ import { cn } from "@/lib/utils"
6
+ import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
7
+ import { useGlobalStore } from "@/hooks/use-global-store"
8
+ import { useLocation } from "wouter"
9
+ import { useOrganization } from "@/hooks/use-organization"
10
+
11
+ interface OrganizationHeaderProps {
12
+ organization: PublicOrgSchema
13
+ isCurrentUserOrganization?: boolean
14
+ className?: string
15
+ showActions?: boolean
16
+ }
17
+
18
+ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
19
+ organization,
20
+ className,
21
+ showActions = true,
22
+ }) => {
23
+ const session = useGlobalStore((s) => s.session)
24
+ const [, navigate] = useLocation()
25
+ const canManageOrg =
26
+ organization.user_permissions?.can_manage_org ||
27
+ organization.owner_account_id === session?.account_id
28
+
29
+ const { membersCount, packagesCount, isLoading } = useOrganization({
30
+ orgId: organization.org_id,
31
+ orgName: organization.name!,
32
+ })
33
+
34
+ const handleSettingsClick = () => {
35
+ navigate(`/${organization.name}/settings`)
36
+ }
37
+ return (
38
+ <div className={cn("bg-white border-b border-gray-200", className)}>
39
+ <div className="container mx-auto px-6 py-6">
40
+ {/* Mobile Layout */}
41
+ <div className="block sm:hidden">
42
+ <div className="flex flex-col items-center text-center space-y-4">
43
+ <Avatar className="border-4 border-gray-100 shadow-sm size-16 md:size-20 lg:size-24">
44
+ <AvatarImage
45
+ src={`https://github.com/${organization.name}.png`}
46
+ alt={`${organization.name} avatar`}
47
+ className="object-cover"
48
+ />
49
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-bold text-xl md:text-2xl lg:text-3xl">
50
+ {(organization.name || "")
51
+ .split(" ")
52
+ .map((word) => word[0])
53
+ .join("")
54
+ .toUpperCase()
55
+ .slice(0, 2)}
56
+ </AvatarFallback>
57
+ </Avatar>
58
+
59
+ <div>
60
+ <div className="flex flex-col items-center gap-3 mb-3">
61
+ <h1 className="font-bold text-gray-900 text-xl">
62
+ {organization.name}
63
+ </h1>
64
+ {canManageOrg && showActions && (
65
+ <Button
66
+ variant="outline"
67
+ size="sm"
68
+ onClick={handleSettingsClick}
69
+ >
70
+ <Settings className="mr-2 h-4 w-4" />
71
+ Settings
72
+ </Button>
73
+ )}
74
+ </div>
75
+
76
+ <div className="grid grid-cols-2 md:flex flex-wrap justify-center gap-4 text-sm">
77
+ <div className="flex items-center gap-1.5 text-gray-600">
78
+ <Users className="h-3.5 w-3.5" />
79
+ <span className="font-medium">
80
+ {isLoading ? "..." : membersCount}
81
+ </span>
82
+ <span>members</span>
83
+ </div>
84
+ <div className="flex items-center gap-1.5 text-gray-600">
85
+ <Package className="h-3.5 w-3.5" />
86
+ <span className="font-medium">
87
+ {isLoading ? "..." : packagesCount}
88
+ </span>
89
+ <span>packages</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ {/* Desktop Layout */}
97
+ <div className="hidden sm:block">
98
+ <div className="flex items-center gap-6">
99
+ <Avatar className="border-4 border-gray-100 shadow-sm size-16 md:size-20 lg:size-24 flex-shrink-0">
100
+ <AvatarImage
101
+ src={`https://github.com/${organization.name}.png`}
102
+ alt={`${organization.name} avatar`}
103
+ className="object-cover"
104
+ />
105
+ <AvatarFallback className="bg-blue-100 text-blue-600 font-bold text-xl md:text-2xl lg:text-3xl">
106
+ {(organization.name || "")
107
+ .split(" ")
108
+ .map((word) => word[0])
109
+ .join("")
110
+ .toUpperCase()
111
+ .slice(0, 2)}
112
+ </AvatarFallback>
113
+ </Avatar>
114
+
115
+ <div className="flex-1 min-w-0">
116
+ <div className="flex items-center justify-between mb-3">
117
+ <h1 className="font-bold text-gray-900 text-2xl md:text-3xl truncate">
118
+ {organization.name}
119
+ </h1>
120
+ {canManageOrg && showActions && (
121
+ <Button variant="outline" onClick={handleSettingsClick}>
122
+ <Settings className="mr-2 h-4 w-4" />
123
+ Settings
124
+ </Button>
125
+ )}
126
+ </div>
127
+
128
+ <div className="flex flex-wrap items-center gap-6 text-sm text-gray-600">
129
+ <div className="flex items-center gap-2">
130
+ <Users className="h-4 w-4" />
131
+ <span className="font-medium text-gray-900">
132
+ {isLoading ? "..." : membersCount}
133
+ </span>
134
+ <span>members</span>
135
+ </div>
136
+ <div className="flex items-center gap-2">
137
+ <Package className="h-4 w-4" />
138
+ <span className="font-medium text-gray-900">
139
+ {isLoading ? "..." : packagesCount}
140
+ </span>
141
+ <span>packages</span>
142
+ </div>
143
+ <div className="flex items-center gap-2">
144
+ <Building2 className="h-4 w-4" />
145
+ <span>Organization</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ )
154
+ }