@tscircuit/fake-snippets 0.0.110 → 0.0.112

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 (48) hide show
  1. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -0
  2. package/bun-tests/fake-snippets-api/routes/orgs/create.test.ts +2 -2
  3. package/bun-tests/fake-snippets-api/routes/orgs/get.test.ts +3 -2
  4. package/bun-tests/fake-snippets-api/routes/orgs/update.test.ts +52 -0
  5. package/bun-tests/fake-snippets-api/routes/packages/list_latest.test.ts +4 -4
  6. package/bun.lock +19 -7
  7. package/dist/bundle.js +85 -36
  8. package/dist/index.d.ts +33 -13
  9. package/dist/index.js +13 -6
  10. package/dist/schema.d.ts +27 -8
  11. package/dist/schema.js +6 -3
  12. package/fake-snippets-api/lib/db/db-client.ts +7 -3
  13. package/fake-snippets-api/lib/db/schema.ts +4 -2
  14. package/fake-snippets-api/lib/db/seed.ts +2 -0
  15. package/fake-snippets-api/lib/middleware/with-session-auth.ts +59 -7
  16. package/fake-snippets-api/lib/public-mapping/public-map-org.ts +4 -2
  17. package/fake-snippets-api/routes/api/orgs/create.ts +4 -2
  18. package/fake-snippets-api/routes/api/orgs/get.ts +1 -1
  19. package/fake-snippets-api/routes/api/orgs/list_members.ts +1 -8
  20. package/fake-snippets-api/routes/api/orgs/update.ts +31 -6
  21. package/fake-snippets-api/routes/api/packages/create.ts +5 -2
  22. package/package.json +7 -5
  23. package/src/App.tsx +3 -5
  24. package/src/components/CmdKMenu.tsx +1 -1
  25. package/src/components/DownloadButtonAndMenu.tsx +14 -1
  26. package/src/components/GithubAvatarWithFallback.tsx +33 -0
  27. package/src/components/PackageCard.tsx +2 -5
  28. package/src/components/PackagesList.tsx +2 -2
  29. package/src/components/ProfileRouter.tsx +2 -2
  30. package/src/components/SentryNotFoundReporter.tsx +44 -0
  31. package/src/components/TrendingPackagesCarousel.tsx +2 -2
  32. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +2 -2
  33. package/src/components/organization/OrganizationCard.tsx +15 -18
  34. package/src/components/organization/OrganizationHeader.tsx +14 -32
  35. package/src/components/organization/OrganizationMembers.tsx +1 -3
  36. package/src/components/preview/ConnectedPackagesList.tsx +1 -1
  37. package/src/hooks/use-hydration.ts +30 -0
  38. package/src/hooks/use-org-by-github-handle.ts +1 -3
  39. package/src/hooks/use-org-by-org-name.ts +24 -0
  40. package/src/lib/download-fns/download-kicad-files.ts +10 -0
  41. package/src/lib/download-fns/download-step.ts +12 -0
  42. package/src/pages/create-organization.tsx +1 -0
  43. package/src/pages/editor.tsx +1 -3
  44. package/src/pages/organization-settings.tsx +4 -1
  45. package/src/pages/search.tsx +7 -2
  46. package/src/pages/user-profile.tsx +1 -1
  47. package/src/pages/user-settings.tsx +161 -0
  48. package/src/pages/view-package.tsx +23 -3
@@ -7,15 +7,16 @@ export default withRouteSpec({
7
7
  methods: ["GET", "POST"],
8
8
  commonParams: z.object({
9
9
  name: z.string(),
10
+ github_handle: z.string().optional(),
10
11
  }),
11
12
  auth: "session",
12
13
  jsonResponse: z.object({
13
14
  org: publicOrgSchema,
14
15
  }),
15
16
  })(async (req, ctx) => {
16
- const { name } = req.commonParams
17
+ const { name, github_handle } = req.commonParams
17
18
 
18
- const existing = ctx.db.getOrg({ github_handle: name })
19
+ const existing = ctx.db.getOrg({ org_name: name })
19
20
 
20
21
  if (existing) {
21
22
  return ctx.error(400, {
@@ -28,6 +29,7 @@ export default withRouteSpec({
28
29
  name: name,
29
30
  created_at: new Date(),
30
31
  can_manage_org: true,
32
+ ...(github_handle ? { github_handle } : {}),
31
33
  }
32
34
 
33
35
  const org = ctx.db.addOrganization(newOrg)
@@ -9,7 +9,7 @@ export default withRouteSpec({
9
9
  .object({ org_id: z.string() })
10
10
  .or(z.object({ org_name: z.string() }))
11
11
  .or(z.object({ github_handle: z.string() })),
12
- auth: "session",
12
+ auth: "optional_session",
13
13
  jsonResponse: z.object({
14
14
  org: publicOrgSchema,
15
15
  }),
@@ -13,7 +13,7 @@ export default withRouteSpec({
13
13
  name: z.string(),
14
14
  }),
15
15
  ),
16
- auth: "session",
16
+ auth: "optional_session",
17
17
  jsonResponse: z.object({
18
18
  members: z.array(accountSchema),
19
19
  }),
@@ -35,13 +35,6 @@ export default withRouteSpec({
35
35
  })
36
36
  }
37
37
 
38
- if (!org.can_manage_org) {
39
- return ctx.error(403, {
40
- error_code: "not_authorized",
41
- message: "You do not have permission to manage this organization",
42
- })
43
- }
44
-
45
38
  const members = ctx.db.orgAccounts
46
39
  .map((m) => {
47
40
  if (m.org_id == org.org_id) return ctx.db.getAccount(m.account_id)
@@ -13,6 +13,7 @@ export default withRouteSpec({
13
13
  z.object({
14
14
  name: z.string().optional(),
15
15
  display_name: z.string().optional(),
16
+ github_handle: z.string().trim().min(1).nullable().optional(),
16
17
  }),
17
18
  ),
18
19
  auth: "session",
@@ -20,10 +21,11 @@ export default withRouteSpec({
20
21
  org: publicOrgSchema,
21
22
  }),
22
23
  })(async (req, ctx) => {
23
- const { org_id, name, display_name } = req.commonParams as {
24
+ const { org_id, name, display_name, github_handle } = req.commonParams as {
24
25
  org_id: string
25
26
  name?: string
26
27
  display_name?: string
28
+ github_handle?: string
27
29
  }
28
30
 
29
31
  const org = ctx.db.getOrg({ org_id }, ctx.auth)
@@ -43,13 +45,13 @@ export default withRouteSpec({
43
45
  }
44
46
 
45
47
  // No changes provided
46
- if (!name && display_name === undefined) {
48
+ if (!name && display_name === undefined && github_handle === null) {
47
49
  return ctx.json({ org: publicMapOrg(org) })
48
50
  }
49
51
 
50
- if (name && name !== org.github_handle) {
52
+ if (name && name !== org.org_name) {
51
53
  // Validate duplicate name
52
- const duplicate = ctx.db.getOrg({ github_handle: name })
54
+ const duplicate = ctx.db.getOrg({ org_name: name })
53
55
 
54
56
  if (duplicate && duplicate.org_id !== org_id) {
55
57
  return ctx.error(400, {
@@ -58,20 +60,43 @@ export default withRouteSpec({
58
60
  })
59
61
  }
60
62
  }
63
+ if (
64
+ github_handle !== undefined &&
65
+ github_handle !== org.github_handle &&
66
+ github_handle !== null
67
+ ) {
68
+ const duplicateHandle = ctx.db.getOrg({ github_handle })
69
+ ? ctx.db.getOrg({ github_handle })?.org_id != org_id
70
+ : false
61
71
 
72
+ if (duplicateHandle) {
73
+ return ctx.error(400, {
74
+ error_code: "org_github_handle_already_exists",
75
+ message: "An organization with this GitHub handle already exists",
76
+ })
77
+ }
78
+ }
62
79
  const updates: {
63
- github_handle?: string
80
+ org_name?: string
64
81
  org_display_name?: string
82
+ github_handle?: string
65
83
  } = {}
66
84
 
67
85
  if (name) {
68
86
  updates.github_handle = name
87
+ updates.org_name = name
88
+ }
89
+
90
+ if (github_handle !== undefined) {
91
+ updates.github_handle = github_handle
69
92
  }
70
93
 
71
94
  if (display_name !== undefined) {
72
95
  const trimmedDisplayName = display_name.trim()
96
+ const handleForFallback =
97
+ github_handle !== undefined ? github_handle : org.github_handle
73
98
  const fallbackDisplayName =
74
- name ?? org.org_display_name ?? org.github_handle ?? ""
99
+ name ?? org.org_display_name ?? org.org_name ?? handleForFallback ?? ""
75
100
  updates.org_display_name =
76
101
  trimmedDisplayName.length > 0 ? trimmedDisplayName : fallbackDisplayName
77
102
  }
@@ -66,6 +66,7 @@ export default withRouteSpec({
66
66
  .find(
67
67
  (o) =>
68
68
  o.org_display_name?.toLowerCase() === requested_owner_lower ||
69
+ o.org_name?.toLowerCase() === requested_owner_lower ||
69
70
  o.github_handle?.toLowerCase() === requested_owner_lower,
70
71
  )
71
72
 
@@ -78,10 +79,12 @@ export default withRouteSpec({
78
79
  }
79
80
 
80
81
  owner_org_id = memberOrg.org_id
81
- owner_github_username = memberOrg.github_handle
82
+ owner_github_username = memberOrg.github_handle || memberOrg.org_name
82
83
  }
83
84
 
84
- const existingPackage = ctx.db.packages.find((pkg) => pkg.name === final_name)
85
+ const existingPackage = ctx.db
86
+ .getState()
87
+ .packages.find((pkg) => pkg.name === final_name)
85
88
 
86
89
  if (existingPackage) {
87
90
  throw ctx.error(400, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.110",
3
+ "version": "0.0.112",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -78,7 +78,7 @@
78
78
  "@resvg/resvg-wasm": "^2.6.2",
79
79
  "@sentry/react": "^10.10.0",
80
80
  "@tailwindcss/typography": "^0.5.16",
81
- "@tscircuit/3d-viewer": "^0.0.402",
81
+ "@tscircuit/3d-viewer": "^0.0.407",
82
82
  "@tscircuit/assembly-viewer": "^0.0.5",
83
83
  "@tscircuit/create-snippet-url": "^0.0.8",
84
84
  "@tscircuit/eval": "^0.0.325",
@@ -110,10 +110,12 @@
110
110
  "circuit-json-to-bom-csv": "^0.0.7",
111
111
  "circuit-json-to-gerber": "^0.0.29",
112
112
  "circuit-json-to-gltf": "^0.0.14",
113
+ "circuit-json-to-kicad": "^0.0.22",
113
114
  "circuit-json-to-pnp-csv": "^0.0.7",
114
115
  "circuit-json-to-readable-netlist": "^0.0.13",
115
116
  "circuit-json-to-simple-3d": "^0.0.7",
116
117
  "circuit-json-to-spice": "^0.0.6",
118
+ "circuit-json-to-step": "^0.0.2",
117
119
  "circuit-json-to-tscircuit": "^0.0.9",
118
120
  "circuit-to-svg": "^0.0.202",
119
121
  "class-variance-authority": "^0.7.1",
@@ -139,6 +141,7 @@
139
141
  "jscad-electronics": "^0.0.25",
140
142
  "jszip": "^3.10.1",
141
143
  "kicad-converter": "^0.0.17",
144
+ "kicadts": "^0.0.10",
142
145
  "ky": "^1.7.5",
143
146
  "lucide-react": "^0.488.0",
144
147
  "lz-string": "^1.5.0",
@@ -172,6 +175,7 @@
172
175
  "sitemap": "^8.0.0",
173
176
  "sonner": "^1.5.0",
174
177
  "states-us": "^1.1.1",
178
+ "stepts": "^0.0.1",
175
179
  "svgo": "^4.0.0",
176
180
  "tailwind-merge": "^2.5.2",
177
181
  "tailwindcss": "^3.4.13",
@@ -192,8 +196,6 @@
192
196
  "wouter": "^3.3.5",
193
197
  "zod": "^3.23.8",
194
198
  "zustand": "^4.5.5",
195
- "zustand-hoist": "^2.0.1",
196
- "circuit-json-to-kicad": "^0.0.3",
197
- "kicadts": "^0.0.9"
199
+ "zustand-hoist": "^2.0.1"
198
200
  }
199
201
  }
package/src/App.tsx CHANGED
@@ -87,9 +87,7 @@ const ReleasePreviewPage = lazyImport(() => import("@/pages/preview-release"))
87
87
  const OrganizationSettingsPage = lazyImport(
88
88
  () => import("@/pages/organization-settings"),
89
89
  )
90
- const SettingsRedirectPage = lazyImport(
91
- () => import("@/pages/settings-redirect"),
92
- )
90
+ const UserSettingsPage = lazyImport(() => import("@/pages/user-settings"))
93
91
 
94
92
  class ErrorBoundary extends React.Component<
95
93
  { children: React.ReactNode },
@@ -270,8 +268,8 @@ function App() {
270
268
  {/* Organization creation route */}
271
269
  <Route path="/orgs/new" component={CreateOrganizationPage} />
272
270
 
273
- {/* User settings redirect */}
274
- <Route path="/settings" component={SettingsRedirectPage} />
271
+ {/* User settings */}
272
+ <Route path="/settings" component={UserSettingsPage} />
275
273
 
276
274
  {/* Organization settings route */}
277
275
  <Route
@@ -372,7 +372,7 @@ const CmdKMenu = () => {
372
372
  switch (type) {
373
373
  case "package":
374
374
  case "recent":
375
- window.location.href = `/${item.owner_github_username}/${item.unscoped_name}`
375
+ window.location.href = `/${item.name}`
376
376
  setOpen(false)
377
377
  break
378
378
  case "account":
@@ -25,6 +25,7 @@ import { CubeIcon } from "@radix-ui/react-icons"
25
25
  import { useState } from "react"
26
26
  import { useAxios } from "@/hooks/use-axios"
27
27
  import { useCurrentPackageId } from "@/hooks/use-current-package-id"
28
+ import { downloadStepFile } from "@/lib/download-fns/download-step"
28
29
 
29
30
  interface DownloadButtonAndMenuProps {
30
31
  className?: string
@@ -225,7 +226,19 @@ export function DownloadButtonAndMenu({
225
226
  kicad_*.zip
226
227
  </span>
227
228
  </DropdownMenuItem>
228
-
229
+ <DropdownMenuItem
230
+ className="text-xs"
231
+ onSelect={async () => {
232
+ const cj = await getCircuitJson()
233
+ downloadStepFile(cj, unscopedName || "step_file")
234
+ }}
235
+ >
236
+ <Download className="mr-1 h-3 w-3" />
237
+ <span className="flex-grow mr-6">Step Format</span>
238
+ <span className="text-[0.6rem] bg-emerald-500 opacity-80 text-white font-mono rounded-md px-1 text-center py-0.5 mr-1">
239
+ STEP
240
+ </span>
241
+ </DropdownMenuItem>
229
242
  <DropdownMenuItem
230
243
  className="text-xs"
231
244
  onSelect={async () => {
@@ -0,0 +1,33 @@
1
+ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
2
+
3
+ export const GithubAvatarWithFallback = ({
4
+ username,
5
+ fallback,
6
+ className,
7
+ fallbackClassName,
8
+ }: {
9
+ username?: string | null
10
+ fallback?: string | null
11
+ className?: string
12
+ fallbackClassName?: string
13
+ }) => {
14
+ return (
15
+ <Avatar className={`border-4 border-gray-100 ${className}`}>
16
+ <AvatarImage
17
+ src={`https://github.com/${username}.png`}
18
+ alt={`${username || fallback} avatar`}
19
+ className="object-cover"
20
+ />
21
+ <AvatarFallback
22
+ className={`bg-blue-100 text-blue-600 ${fallbackClassName}`}
23
+ >
24
+ {(username || fallback || "")
25
+ .split(" ")
26
+ .map((word) => word[0])
27
+ .join("")
28
+ .toUpperCase()
29
+ .slice(0, 2)}
30
+ </AvatarFallback>
31
+ </Avatar>
32
+ )
33
+ }
@@ -67,7 +67,7 @@ export const PackageCard: React.FC<PackageCardProps> = ({
67
67
 
68
68
  const handleShareClick = (e: React.MouseEvent) => {
69
69
  e.preventDefault()
70
- const shareUrl = `${window.location.origin}/${pkg.owner_github_username}/${pkg.unscoped_name}`
70
+ const shareUrl = `${window.location.origin}/${pkg.name}`
71
71
  copyToClipboard(shareUrl)
72
72
  }
73
73
 
@@ -178,10 +178,7 @@ export const PackageCard: React.FC<PackageCardProps> = ({
178
178
 
179
179
  if (withLink) {
180
180
  return (
181
- <Link
182
- key={pkg.package_id}
183
- href={`/${pkg.owner_github_username}/${pkg.unscoped_name}`}
184
- >
181
+ <Link key={pkg.package_id} href={`/${pkg.name}`}>
185
182
  {cardContent}
186
183
  </Link>
187
184
  )
@@ -50,10 +50,10 @@ export const PackagesList = ({
50
50
  <li key={pkg.package_id}>
51
51
  <div className="flex items-center">
52
52
  <Link
53
- href={`/${pkg.owner_github_username}/${pkg.unscoped_name}`}
53
+ href={`/${pkg.name}`}
54
54
  className="text-blue-600 hover:underline text-sm"
55
55
  >
56
- {pkg.owner_github_username}/{pkg.unscoped_name}
56
+ {pkg.name}
57
57
  </Link>
58
58
  {pkg.star_count > 0 && (
59
59
  <span className="ml-2 text-gray-500 text-xs flex items-center">
@@ -1,10 +1,10 @@
1
1
  import React from "react"
2
2
  import { useParams } from "wouter"
3
- import { useOrgByGithubHandle } from "@/hooks/use-org-by-github-handle"
4
3
  import { OrganizationProfilePageContent } from "@/pages/organization-profile"
5
4
  import { UserProfilePage } from "@/pages/user-profile"
6
5
  import NotFoundPage from "@/pages/404"
7
6
  import { FullPageLoader } from "@/App"
7
+ import { useOrgByName } from "@/hooks/use-org-by-org-name"
8
8
 
9
9
  const ProfileRouter: React.FC = () => {
10
10
  const { username } = useParams()
@@ -12,7 +12,7 @@ const ProfileRouter: React.FC = () => {
12
12
  data: organization,
13
13
  isLoading,
14
14
  error,
15
- } = useOrgByGithubHandle(username || null)
15
+ } = useOrgByName(username || null)
16
16
 
17
17
  if (!username) {
18
18
  return <NotFoundPage heading="Username Not Provided" />
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from "react"
2
+ import { Sentry } from "@/lib/sentry"
3
+
4
+ type NotFoundContext = "package" | "package_release"
5
+
6
+ type SentryNotFoundReporterProps = {
7
+ context: NotFoundContext
8
+ slug: string
9
+ status?: number
10
+ message?: string
11
+ }
12
+
13
+ export const SentryNotFoundReporter = ({
14
+ context,
15
+ slug,
16
+ status,
17
+ message,
18
+ }: SentryNotFoundReporterProps) => {
19
+ const hasLoggedRef = useRef(false)
20
+
21
+ useEffect(() => {
22
+ if (typeof window === "undefined") return
23
+ if (typeof slug !== "string" || slug.length === 0) return
24
+ if (typeof status === "number" && status !== 404) return
25
+ if (hasLoggedRef.current) return
26
+
27
+ hasLoggedRef.current = true
28
+
29
+ Sentry.captureMessage(`package:view:${context}-not-found`, {
30
+ level: "warning",
31
+ tags: {
32
+ slug,
33
+ },
34
+ extra: {
35
+ status,
36
+ message,
37
+ },
38
+ })
39
+ }, [context, slug, status, message])
40
+
41
+ return null
42
+ }
43
+
44
+ export default SentryNotFoundReporter
@@ -11,10 +11,10 @@ const CarouselItem = ({
11
11
  apiBaseUrl,
12
12
  }: { pkg: Package; apiBaseUrl: string }) => {
13
13
  return (
14
- <Link href={`/${pkg.owner_github_username}/${pkg.unscoped_name}`}>
14
+ <Link href={`/${pkg.name}`}>
15
15
  <div className="flex-shrink-0 w-[200px] bg-white p-3 py-2 rounded-lg shadow-sm border border-gray-200 hover:border-gray-300 transition-colors">
16
16
  <div className="font-medium text-blue-600 mb-1 truncate text-sm">
17
- {pkg.owner_github_username}/{pkg.unscoped_name}
17
+ {pkg.name}
18
18
  </div>
19
19
  <div className="mb-2 h-24 w-full bg-black rounded overflow-hidden">
20
20
  <img
@@ -81,7 +81,7 @@ export default function SidebarReleasesSection() {
81
81
  <div className="mb-6">
82
82
  <h2 className="text-lg font-semibold mb-2">
83
83
  <Link
84
- href={`/${packageInfo?.owner_github_username}/${packageInfo?.unscoped_name}/releases`}
84
+ href={`/${packageInfo?.name}/releases`}
85
85
  className="hover:underline"
86
86
  >
87
87
  Releases
@@ -89,7 +89,7 @@ export default function SidebarReleasesSection() {
89
89
  </h2>
90
90
  <div className="flex flex-col space-y-2">
91
91
  <Link
92
- href={`/${packageInfo?.owner_github_username}/${packageInfo?.unscoped_name}/releases`}
92
+ href={`/${packageInfo?.name}/releases`}
93
93
  className="flex items-center hover:underline"
94
94
  >
95
95
  <Tag className="h-4 w-4 mr-2 text-gray-500 dark:text-[#8b949e]" />
@@ -21,6 +21,7 @@ import { timeAgo } from "@/lib/utils/timeAgo"
21
21
  import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
22
22
  import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
23
23
  import { useGlobalStore } from "@/hooks/use-global-store"
24
+ import { GithubAvatarWithFallback } from "../GithubAvatarWithFallback"
24
25
 
25
26
  export interface OrganizationCardProps {
26
27
  /** The organization data to display */
@@ -72,32 +73,28 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
72
73
  const handleSettingsClick = (e: React.MouseEvent) => {
73
74
  e.preventDefault()
74
75
  e.stopPropagation()
75
- window.location.href = `/${organization.name}/settings`
76
+ window.location.href = organization.is_personal_org
77
+ ? `/settings`
78
+ : `/${organization.name}/settings`
76
79
  }
77
80
 
78
81
  const cardContent = (
79
82
  <div
80
- className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
83
+ className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 h-full ${className}`}
81
84
  onClick={handleCardClick}
82
85
  >
83
86
  <div className="flex items-start gap-4">
84
87
  {/* Organization Avatar */}
85
88
  <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>
89
+ <GithubAvatarWithFallback
90
+ username={
91
+ organization.is_personal_org
92
+ ? organization.github_handle || organization.name
93
+ : organization.github_handle
94
+ }
95
+ fallback={organization.name}
96
+ fallbackClassName=" font-semibold text-lg"
97
+ />
101
98
  </div>
102
99
 
103
100
  {/* Organization Info */}
@@ -193,7 +190,7 @@ export const OrganizationCard: React.FC<OrganizationCardProps> = ({
193
190
  <Link
194
191
  key={organization.org_id}
195
192
  href={`/${organization.name}`}
196
- className="block"
193
+ className="block h-full"
197
194
  >
198
195
  {cardContent}
199
196
  </Link>
@@ -1,5 +1,4 @@
1
- import React, { useState } from "react"
2
- import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
1
+ import React from "react"
3
2
  import { Button } from "@/components/ui/button"
4
3
  import { Building2, Users, Package, Lock, Globe2, Settings } from "lucide-react"
5
4
  import { cn } from "@/lib/utils"
@@ -7,6 +6,7 @@ import { PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
7
6
  import { useGlobalStore } from "@/hooks/use-global-store"
8
7
  import { useLocation } from "wouter"
9
8
  import { useOrganization } from "@/hooks/use-organization"
9
+ import { GithubAvatarWithFallback } from "../GithubAvatarWithFallback"
10
10
 
11
11
  interface OrganizationHeaderProps {
12
12
  organization: PublicOrgSchema
@@ -40,21 +40,12 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
40
40
  {/* Mobile Layout */}
41
41
  <div className="block sm:hidden">
42
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>
43
+ <GithubAvatarWithFallback
44
+ username={organization.github_handle}
45
+ fallback={organization.name}
46
+ className="shadow-sm size-16 md:size-20 lg:size-24"
47
+ fallbackClassName="font-bold text-xl md:text-2xl lg:text-3xl"
48
+ />
58
49
 
59
50
  <div>
60
51
  <div className="flex flex-col items-center gap-3 mb-3">
@@ -96,21 +87,12 @@ export const OrganizationHeader: React.FC<OrganizationHeaderProps> = ({
96
87
  {/* Desktop Layout */}
97
88
  <div className="hidden sm:block">
98
89
  <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>
90
+ <GithubAvatarWithFallback
91
+ username={organization.github_handle}
92
+ fallback={organization.name}
93
+ className="flex-shrink-0 shadow-sm size-16 md:size-20 lg:size-24"
94
+ fallbackClassName="font-bold text-xl md:text-2xl lg:text-3xl"
95
+ />
114
96
 
115
97
  <div className="flex-1 min-w-0">
116
98
  <div className="flex items-center justify-between mb-3">
@@ -2,11 +2,9 @@ import React from "react"
2
2
  import { Link } from "wouter"
3
3
  import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
4
4
  import { Badge } from "@/components/ui/badge"
5
- import { Button } from "@/components/ui/button"
6
5
  import { Users, Crown, Shield, User, Loader2 } from "lucide-react"
7
6
  import { timeAgo } from "@/lib/utils/timeAgo"
8
7
  import { cn } from "@/lib/utils"
9
- import { Account } from "fake-snippets-api/lib/db/schema"
10
8
  import { useListOrgMembers } from "@/hooks/use-list-org-members"
11
9
 
12
10
  interface OrganizationMembersProps {
@@ -69,7 +67,7 @@ export const OrganizationMembers: React.FC<OrganizationMembersProps> = ({
69
67
  return (
70
68
  <div
71
69
  className={cn(
72
- "bg-white rounded-lg border border-gray-200 p-4 py-20 sm:p-6",
70
+ "bg-white rounded-lg border border-gray-200 p-4 sm:p-6",
73
71
  className,
74
72
  )}
75
73
  >
@@ -117,7 +117,7 @@ export const ConnectedPackageCard = ({
117
117
  <div className="flex items-start justify-between mb-4">
118
118
  <div className="flex items-center gap-3">
119
119
  <Link
120
- href={`/${pkg.owner_github_username}/${pkg.unscoped_name}`}
120
+ href={`/${pkg.name}`}
121
121
  className="text-lg font-semibold text-gray-900 hover:text-blue-600 transition-colors"
122
122
  >
123
123
  {pkg.unscoped_name}
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from "react"
2
+ import { useGlobalStore } from "@/hooks/use-global-store"
3
+
4
+ export const useHydration = () => {
5
+ const [hasHydrated, setHasHydrated] = useState(() => {
6
+ if (typeof window === "undefined") return false
7
+ return useGlobalStore.persist?.hasHydrated?.() ?? false
8
+ })
9
+
10
+ useEffect(() => {
11
+ if (typeof window === "undefined") return
12
+
13
+ if (useGlobalStore.persist?.hasHydrated?.()) {
14
+ setHasHydrated(true)
15
+ return
16
+ }
17
+
18
+ const unsubFinishHydration = useGlobalStore.persist?.onFinishHydration?.(
19
+ () => {
20
+ setHasHydrated(true)
21
+ },
22
+ )
23
+
24
+ return () => {
25
+ unsubFinishHydration?.()
26
+ }
27
+ }, [])
28
+
29
+ return hasHydrated
30
+ }