@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
@@ -1,13 +1,18 @@
1
- import React, { useState, useEffect } from "react"
1
+ import { useState, useEffect, useMemo } from "react"
2
2
  import { useQuery } from "react-query"
3
3
  import { useAxios } from "@/hooks/use-axios"
4
4
  import { useSearchParams } from "wouter"
5
5
  import Header from "@/components/Header"
6
6
  import Footer from "@/components/Footer"
7
7
  import { Input } from "@/components/ui/input"
8
- import { Search } from "lucide-react"
9
- import PackageSearchResults from "@/components/PackageSearchResults"
8
+ import { Search, User } from "lucide-react"
9
+ import PackageSearchResults, {
10
+ LoadingState,
11
+ } from "@/components/PackageSearchResults"
12
+ import { UserCard } from "@/components/UserCard"
10
13
  import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
14
+ import { useGlobalStore } from "@/hooks/use-global-store"
15
+ import { fuzzyMatch } from "@/components/ViewPackagePage/utils/fuzz-search"
11
16
  import {
12
17
  Select,
13
18
  SelectContent,
@@ -15,31 +20,48 @@ import {
15
20
  SelectTrigger,
16
21
  SelectValue,
17
22
  } from "@/components/ui/select"
18
- import { Package } from "fake-snippets-api/lib/db/schema"
23
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
24
+ import { Package, Account } from "fake-snippets-api/lib/db/schema"
25
+
26
+ interface ScoredPackage extends Package {
27
+ score: number
28
+ matches: number[]
29
+ }
30
+
31
+ interface ScoredAccount
32
+ extends Omit<Account, "account_id" | "is_tscircuit_staff"> {
33
+ score: number
34
+ matches: number[]
35
+ }
19
36
 
20
37
  export const SearchPage = () => {
21
38
  const axios = useAxios()
22
39
  const apiBaseUrl = useApiBaseUrl()
23
40
  const [searchParams, setSearchParams] = useSearchParams()
41
+ const currentUser = useGlobalStore((s) => s.session?.github_username)
24
42
 
25
43
  const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "")
26
44
  const [category, setCategory] = useState(
27
45
  searchParams.get("category") || "all",
28
46
  )
29
47
  const [sortBy, setSortBy] = useState(searchParams.get("sort") || "stars")
48
+ const [activeTab, setActiveTab] = useState(
49
+ searchParams.get("tab") || "packages",
50
+ )
30
51
 
31
52
  useEffect(() => {
32
53
  const params = new URLSearchParams()
33
54
  if (searchQuery) params.set("q", searchQuery)
34
55
  if (category !== "all") params.set("category", category)
35
56
  if (sortBy !== "stars") params.set("sort", sortBy)
57
+ if (activeTab !== "packages") params.set("tab", activeTab)
36
58
  setSearchParams(params)
37
- }, [searchQuery, category, sortBy, setSearchParams])
59
+ }, [searchQuery, category, sortBy, activeTab, setSearchParams])
38
60
 
39
61
  const {
40
62
  data: packages,
41
- isLoading,
42
- error,
63
+ isLoading: isLoadingPackages,
64
+ error: packagesError,
43
65
  } = useQuery(
44
66
  ["packageSearch", searchQuery, category],
45
67
  async () => {
@@ -52,69 +74,130 @@ export const SearchPage = () => {
52
74
  })
53
75
  return response.data.packages
54
76
  },
55
- { enabled: Boolean(searchQuery), keepPreviousData: true },
77
+ {
78
+ enabled: Boolean(searchQuery),
79
+ keepPreviousData: true,
80
+ refetchOnWindowFocus: false,
81
+ },
82
+ )
83
+
84
+ const { data: allAccounts = [], isLoading: isLoadingAccounts } = useQuery(
85
+ ["accountSearch", searchQuery],
86
+ async () => {
87
+ if (!searchQuery) return []
88
+ try {
89
+ const { data } = await axios.post("/accounts/search", {
90
+ query: searchQuery,
91
+ limit: 20,
92
+ })
93
+ return data.accounts || []
94
+ } catch (error) {
95
+ console.warn("Failed to fetch accounts:", error)
96
+ return []
97
+ }
98
+ },
99
+ {
100
+ enabled: Boolean(searchQuery) && Boolean(currentUser),
101
+ retry: false,
102
+ refetchOnWindowFocus: false,
103
+ },
56
104
  )
57
105
 
58
- const filteredPackages = packages
59
- ?.filter((pkg: Package) => {
60
- if (!searchQuery) return true
61
-
62
- const query = searchQuery.toLowerCase().trim()
63
- const searchableFields = [
64
- pkg.unscoped_name.toLowerCase(),
65
- (pkg.owner_github_username || "").toLowerCase(),
66
- (pkg.description || "").toLowerCase(),
67
- pkg.description?.toLowerCase(),
68
- ]
69
-
70
- return searchableFields.some((field) => {
71
- const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
72
- if (!field) return false
73
- return queryWords.every((word) => field.includes(word))
106
+ const searchResults = useMemo((): ScoredPackage[] => {
107
+ if (!searchQuery || !packages?.length) return []
108
+
109
+ return packages
110
+ .map((pkg: Package) => {
111
+ const { score, matches } = fuzzyMatch(searchQuery, pkg.name)
112
+ return { ...pkg, score, matches }
74
113
  })
75
- })
76
- ?.sort((a: Package, b: Package) => {
77
- if (sortBy === "stars") {
78
- return (b.star_count || 0) - (a.star_count || 0)
79
- } else if (sortBy === "newest") {
80
- return (
81
- new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
82
- )
83
- } else if (sortBy === "oldest") {
84
- return (
85
- new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime()
114
+ .filter((pkg: ScoredPackage) => pkg.score >= 0)
115
+ .sort((a: ScoredPackage, b: ScoredPackage) => b.score - a.score)
116
+ }, [packages, searchQuery])
117
+
118
+ const filteredPackages = searchResults?.sort((a: Package, b: Package) => {
119
+ if (sortBy === "stars") {
120
+ return (b.star_count || 0) - (a.star_count || 0)
121
+ } else if (sortBy === "newest") {
122
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
123
+ } else if (sortBy === "oldest") {
124
+ return new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime()
125
+ }
126
+ return 0
127
+ })
128
+
129
+ const accountSearchResults = useMemo((): ScoredAccount[] => {
130
+ if (!searchQuery) return []
131
+
132
+ // First, get scored accounts from API
133
+ const apiAccounts = allAccounts
134
+ .map((account: Account) => {
135
+ const { score, matches } = fuzzyMatch(
136
+ searchQuery,
137
+ account.github_username,
86
138
  )
139
+ return { ...account, score, matches }
140
+ })
141
+ .filter((account: ScoredAccount) => account.score >= 0)
142
+
143
+ // Then, extract unique package owners not already in API accounts
144
+ const packageOwners: ScoredAccount[] = []
145
+ const existingUsernames = new Set(
146
+ apiAccounts.map((acc: Account) => acc.github_username),
147
+ )
148
+
149
+ filteredPackages.forEach((pkg) => {
150
+ if (
151
+ pkg.owner_github_username &&
152
+ !existingUsernames.has(pkg.owner_github_username)
153
+ ) {
154
+ packageOwners.push({
155
+ github_username: pkg.owner_github_username,
156
+ score: 1,
157
+ matches: [],
158
+ })
159
+ existingUsernames.add(pkg.owner_github_username)
87
160
  }
88
- return 0
89
161
  })
162
+ return [...apiAccounts, ...packageOwners].sort(
163
+ (a: ScoredAccount, b: ScoredAccount) => b.score - a.score,
164
+ )
165
+ }, [allAccounts, searchQuery, filteredPackages])
90
166
 
167
+ useEffect(() => {
168
+ if (accountSearchResults.length == 0 && !isLoadingAccounts) {
169
+ setActiveTab("packages")
170
+ }
171
+ }, [accountSearchResults, isLoadingAccounts])
91
172
  return (
92
173
  <div className="min-h-screen flex flex-col">
93
174
  <Header />
94
- <main className="flex-grow pb-12 min-h-[80vh]">
95
- <div className="container mx-auto px-4 py-8">
96
- <div className="max-w-8xl mx-auto">
97
- <div className="mb-6">
175
+ <main className="flex-grow pb-12 min-h-[80vh] w-full min-w-full">
176
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
177
+ <div className="max-w-7xl mx-auto">
178
+ <div className="mb-4 sm:mb-6">
98
179
  <div className="flex items-center gap-2 mb-3">
99
- <h1 className="text-3xl font-bold text-gray-900">
100
- Search Packages
180
+ <h1 className="text-2xl sm:text-3xl font-bold text-gray-900">
181
+ Search
101
182
  </h1>
102
183
  </div>
103
- <div className="flex flex-col sm:flex-row gap-4 mb-4">
184
+ <div className="flex flex-col sm:flex-row gap-3 sm:gap-4 mb-4">
104
185
  <div className="relative flex-grow">
105
186
  <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
106
187
  <Input
107
188
  type="search"
108
- placeholder="Search packages..."
189
+ placeholder="Search packages and users..."
109
190
  className="pl-10"
110
191
  value={searchQuery}
192
+ spellCheck={false}
193
+ autoComplete="off"
111
194
  onChange={(e) => setSearchQuery(e.target.value)}
112
- aria-label="Search packages"
195
+ aria-label="Search packages and users"
113
196
  role="searchbox"
114
197
  />
115
198
  </div>
116
199
  <Select value={sortBy} onValueChange={setSortBy}>
117
- <SelectTrigger className="w-[140px]">
200
+ <SelectTrigger className="w-full sm:w-[140px]">
118
201
  <SelectValue placeholder="Sort By" />
119
202
  </SelectTrigger>
120
203
  <SelectContent>
@@ -126,17 +209,74 @@ export const SearchPage = () => {
126
209
  </div>
127
210
  </div>
128
211
 
129
- <PackageSearchResults
130
- isLoading={isLoading}
131
- error={error}
132
- filteredPackages={filteredPackages}
133
- apiBaseUrl={apiBaseUrl}
134
- emptyStateMessage={
135
- searchQuery
136
- ? `No packages match your search for "${searchQuery}".`
137
- : "Please enter a search query to find packages."
138
- }
139
- />
212
+ <Tabs
213
+ value={activeTab}
214
+ onValueChange={setActiveTab}
215
+ className="w-full"
216
+ >
217
+ {currentUser && accountSearchResults.length > 0 && (
218
+ <TabsList className="grid grid-cols-2 mb-6 select-none w-full max-w-md mx-auto">
219
+ <TabsTrigger
220
+ value="packages"
221
+ className="flex items-center gap-2"
222
+ >
223
+ Packages
224
+ </TabsTrigger>
225
+ <TabsTrigger
226
+ value="users"
227
+ className="flex items-center gap-2"
228
+ >
229
+ Users
230
+ </TabsTrigger>
231
+ </TabsList>
232
+ )}
233
+
234
+ <TabsContent value="packages" className="w-full">
235
+ <PackageSearchResults
236
+ isLoading={isLoadingPackages}
237
+ error={packagesError}
238
+ filteredPackages={filteredPackages}
239
+ apiBaseUrl={apiBaseUrl}
240
+ emptyStateMessage={
241
+ searchQuery
242
+ ? `No packages match your search for "${searchQuery}".`
243
+ : "Please enter a search query to find packages."
244
+ }
245
+ />
246
+ </TabsContent>
247
+
248
+ <TabsContent value="users" className="w-full">
249
+ {isLoadingAccounts ? (
250
+ <div>
251
+ <LoadingState />
252
+ </div>
253
+ ) : accountSearchResults.length > 0 ? (
254
+ <div className="grid grid-cols-1 w-full sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-3 sm:gap-4">
255
+ {accountSearchResults.map((account, i) => (
256
+ <UserCard
257
+ key={i}
258
+ account={account as unknown as Account}
259
+ className="w-full"
260
+ />
261
+ ))}
262
+ </div>
263
+ ) : (
264
+ <div className="text-center py-12 px-4">
265
+ <div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
266
+ <User className="w-8 h-8 text-slate-400" />
267
+ </div>
268
+ <h3 className="text-xl font-medium text-slate-900 mb-2">
269
+ No Matching Users
270
+ </h3>
271
+ <p className="text-slate-500 max-w-md mx-auto mb-6">
272
+ {searchQuery
273
+ ? `No users match your search for "${searchQuery}".`
274
+ : "Please enter a search query to find users."}
275
+ </p>
276
+ </div>
277
+ )}
278
+ </TabsContent>
279
+ </Tabs>
140
280
  </div>
141
281
  </div>
142
282
  </main>
@@ -0,0 +1,44 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Redirect } from "wouter"
3
+
4
+ import { FullPageLoader } from "@/App"
5
+ import { useGlobalStore } from "@/hooks/use-global-store"
6
+
7
+ const SettingsRedirectPage = () => {
8
+ const session = useGlobalStore((state) => state.session)
9
+ const [hasHydrated, setHasHydrated] = useState(() => {
10
+ if (typeof window === "undefined") return false
11
+ return useGlobalStore.persist?.hasHydrated?.() ?? false
12
+ })
13
+
14
+ useEffect(() => {
15
+ if (typeof window === "undefined") return
16
+
17
+ if (useGlobalStore.persist?.hasHydrated?.()) {
18
+ setHasHydrated(true)
19
+ return
20
+ }
21
+
22
+ const unsubFinishHydration = useGlobalStore.persist?.onFinishHydration?.(
23
+ () => {
24
+ setHasHydrated(true)
25
+ },
26
+ )
27
+
28
+ return () => {
29
+ unsubFinishHydration?.()
30
+ }
31
+ }, [])
32
+
33
+ if (!hasHydrated) {
34
+ return <FullPageLoader />
35
+ }
36
+
37
+ if (!session?.github_username) {
38
+ return <Redirect to="/" />
39
+ }
40
+
41
+ return <Redirect to={`/${session.github_username}/settings`} />
42
+ }
43
+
44
+ export default SettingsRedirectPage
@@ -49,7 +49,7 @@ const TrendingPage: React.FC = () => {
49
49
  async () => {
50
50
  const params = new URLSearchParams()
51
51
  if (category !== "all") params.append("tag", category)
52
- params.append("time_period", time_period)
52
+ if (time_period !== "all") params.append("time_period", time_period)
53
53
 
54
54
  const response = await axios.get(
55
55
  `/packages/list_trending?${params.toString()}`,
@@ -57,34 +57,47 @@ const TrendingPage: React.FC = () => {
57
57
  return response.data.packages
58
58
  },
59
59
  {
60
- keepPreviousData: true,
60
+ keepPreviousData: false,
61
+ refetchOnWindowFocus: false,
62
+ refetchOnMount: true,
63
+ staleTime: 5 * 60 * 1000,
61
64
  },
62
65
  )
63
66
 
64
- const filteredPackages = packages
65
- ?.filter((pkg) => {
67
+ const filteredAndSortedPackages = React.useMemo(() => {
68
+ if (!packages) return []
69
+
70
+ let filtered = packages.filter((pkg) => {
66
71
  if (!searchQuery) return true
67
72
 
68
73
  const query = searchQuery.toLowerCase().trim()
74
+ if (!query) return true
75
+
69
76
  const searchableFields = [
70
- pkg.unscoped_name.toLowerCase(),
71
- (pkg.owner_github_username || "").toLowerCase(),
72
- (pkg.description || "").toLowerCase(),
73
- pkg.description?.toLowerCase(),
74
- ]
77
+ pkg.unscoped_name?.toLowerCase() || "",
78
+ pkg.owner_github_username?.toLowerCase() || "",
79
+ pkg.description?.toLowerCase() || "",
80
+ ].filter(Boolean)
81
+
82
+ const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
75
83
 
76
84
  return searchableFields.some((field) => {
77
- const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
78
85
  if (!field) return false
79
86
  return queryWords.every((word) => field.includes(word))
80
87
  })
81
88
  })
82
- ?.sort((a, b) => {
83
- if (sortBy === "stars") {
84
- return (b.star_count || 0) - (a.star_count || 0)
89
+
90
+ return filtered.sort((a, b) => {
91
+ if (sortBy === "recent") {
92
+ const dateA = new Date(a.updated_at).getTime()
93
+ const dateB = new Date(b.updated_at).getTime()
94
+ return dateB - dateA
85
95
  }
86
- return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
96
+ const starsA = a.star_count || 0
97
+ const starsB = b.star_count || 0
98
+ return starsB - starsA
87
99
  })
100
+ }, [packages, searchQuery, sortBy])
88
101
 
89
102
  return (
90
103
  <div className="min-h-screen flex flex-col">
@@ -107,11 +120,7 @@ const TrendingPage: React.FC = () => {
107
120
  <SelectItem value="recent">Most Recent</SelectItem>
108
121
  </SelectContent>
109
122
  </Select>
110
- <Select
111
- value={time_period}
112
- onValueChange={setTimePeriod}
113
- disabled={sortBy === "recent"}
114
- >
123
+ <Select value={time_period} onValueChange={setTimePeriod}>
115
124
  <SelectTrigger className="w-[140px]">
116
125
  <SelectValue placeholder="Time Period" />
117
126
  </SelectTrigger>
@@ -173,7 +182,7 @@ const TrendingPage: React.FC = () => {
173
182
  <PackageSearchResults
174
183
  isLoading={isLoading}
175
184
  error={error}
176
- filteredPackages={filteredPackages}
185
+ filteredPackages={filteredAndSortedPackages}
177
186
  apiBaseUrl={apiBaseUrl}
178
187
  emptyStateMessage={
179
188
  searchQuery
@@ -10,7 +10,7 @@ import { useAxios } from "@/hooks/use-axios"
10
10
  import { useGlobalStore } from "@/hooks/use-global-store"
11
11
  import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
12
12
  import { GitHubLogoIcon } from "@radix-ui/react-icons"
13
- import type { Package } from "fake-snippets-api/lib/db/schema"
13
+ import type { Package, PublicOrgSchema } from "fake-snippets-api/lib/db/schema"
14
14
  import type React from "react"
15
15
  import { useState } from "react"
16
16
  import { useQuery } from "react-query"
@@ -23,21 +23,25 @@ import {
23
23
  SelectTrigger,
24
24
  SelectValue,
25
25
  } from "@/components/ui/select"
26
- import { Box, Star } from "lucide-react"
26
+ import { Box, Star, Building2 } from "lucide-react"
27
27
  import { PackageCardSkeleton } from "@/components/PackageCardSkeleton"
28
28
  import { ConnectedPackagesList } from "@/components/preview/ConnectedPackagesList"
29
+ import { useListUserOrgs } from "@/hooks/use-list-user-orgs"
30
+ import { OrganizationCard } from "@/components/organization/OrganizationCard"
29
31
 
30
32
  export const UserProfilePage = () => {
31
33
  const { username } = useParams()
32
34
  const axios = useAxios()
35
+ const { data: organizations } = useListUserOrgs(username)
33
36
  const [searchQuery, setSearchQuery] = useState("")
34
37
  const [activeTab, setActiveTab] = useState("all")
35
- const [filter, setFilter] = useState("most-recent") // Changed default from "newest" to "most-recent"
38
+ const [filter, setFilter] = useState("most-recent")
36
39
  const session = useGlobalStore((s) => s.session)
37
40
  const {
38
41
  data: account,
39
42
  error: accountError,
40
43
  isLoading: isLoadingAccount,
44
+ isFetched: isFetchedAccount,
41
45
  } = useQuery<
42
46
  { account: { github_username: string } },
43
47
  Error & { status: number }
@@ -98,6 +102,10 @@ export const UserProfilePage = () => {
98
102
 
99
103
  const baseUrl = useApiBaseUrl()
100
104
 
105
+ if (!isFetchedAccount) {
106
+ return null
107
+ }
108
+
101
109
  if (accountError) {
102
110
  return <NotFoundPage heading="User Not Found" />
103
111
  }
@@ -145,8 +153,11 @@ export const UserProfilePage = () => {
145
153
  <div className="container mx-auto px-4 py-8">
146
154
  <div className="flex items-center gap-4 mb-6">
147
155
  <Avatar className="h-16 w-16">
148
- <AvatarImage src={`https://github.com/${githubUsername}.png`} />
149
- <AvatarFallback>
156
+ <AvatarImage
157
+ src={`https://github.com/${githubUsername}.png`}
158
+ draggable={false}
159
+ />
160
+ <AvatarFallback className="select-none">
150
161
  {githubUsername?.[0]?.toUpperCase()}
151
162
  </AvatarFallback>
152
163
  </Avatar>
@@ -174,10 +185,17 @@ export const UserProfilePage = () => {
174
185
  </Button>
175
186
  </a>
176
187
  </div>
177
- <Tabs defaultValue="all" onValueChange={setActiveTab} className="mb-4">
188
+ <Tabs
189
+ defaultValue="all"
190
+ onValueChange={setActiveTab}
191
+ className="mb-4 select-none overflow-x-auto no-scrollbar"
192
+ >
178
193
  <TabsList>
179
194
  <TabsTrigger value="all">Packages</TabsTrigger>
180
195
  <TabsTrigger value="starred">Starred Packages</TabsTrigger>
196
+ {organizations && organizations.length > 0 && (
197
+ <TabsTrigger value="organizations">Organizations</TabsTrigger>
198
+ )}
181
199
  {isCurrentUserProfile &&
182
200
  (
183
201
  userPackages?.filter((x) => Boolean(x.github_repo_full_name)) ??
@@ -217,6 +235,39 @@ export const UserProfilePage = () => {
217
235
  []
218
236
  }
219
237
  />
238
+ ) : activeTab === "organizations" ? (
239
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
240
+ {organizations && organizations.length > 0 ? (
241
+ organizations
242
+ ?.filter((o) => {
243
+ if (!isCurrentUserProfile && o.is_personal_org) {
244
+ return false
245
+ } else {
246
+ return true
247
+ }
248
+ })
249
+ .map((org: PublicOrgSchema) => (
250
+ <OrganizationCard
251
+ key={org.org_id}
252
+ organization={org}
253
+ withLink={true}
254
+ showStats={true}
255
+ showMembers={true}
256
+ className="p-3"
257
+ />
258
+ ))
259
+ ) : (
260
+ <div className="col-span-full flex justify-center">
261
+ <div className="flex flex-col items-center py-12 text-gray-500">
262
+ <Building2 className="mb-2" size={24} />
263
+ <span className="text-lg font-medium">No organizations</span>
264
+ <span className="text-sm">
265
+ You're not a member of any organizations yet.
266
+ </span>
267
+ </div>
268
+ </div>
269
+ )}
270
+ </div>
220
271
  ) : isLoading ? (
221
272
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
222
273
  {[...Array(6)].map((_, i) => (
@@ -240,7 +291,7 @@ export const UserProfilePage = () => {
240
291
  ))
241
292
  ) : (
242
293
  <div className="col-span-full flex justify-center">
243
- <div className="flex flex-col items-center py-12 text-gray-500">
294
+ <div className="flex flex-col items-center py-20 text-gray-500">
244
295
  {activeTab === "starred" ? (
245
296
  <>
246
297
  <Star className="mb-2" size={24} />