@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,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
|
+
}
|