@tscircuit/fake-snippets 0.0.108 → 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 (203) 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 +389 -450
  34. package/bunfig.toml +2 -1
  35. package/dist/bundle.js +1255 -625
  36. package/dist/index.d.ts +296 -4
  37. package/dist/index.js +325 -24
  38. package/dist/schema.d.ts +282 -1
  39. package/dist/schema.js +54 -2
  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 +62 -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 +27 -24
  64. package/renovate.json +1 -1
  65. package/scripts/generate-sitemap.ts +1 -1
  66. package/src/App.tsx +29 -10
  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 +133 -36
  71. package/src/components/FileSidebar.tsx +41 -50
  72. package/src/components/Footer.tsx +8 -10
  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 +44 -16
  77. package/src/components/HiddenFilesDropdown.tsx +0 -2
  78. package/src/components/NotFound.tsx +5 -5
  79. package/src/components/PackageBreadcrumb.tsx +6 -12
  80. package/src/components/PackageCard.tsx +0 -1
  81. package/src/components/PackageSearchResults.tsx +1 -1
  82. package/src/components/PrefetchPageLink.tsx +7 -1
  83. package/src/components/ProfileRouter.tsx +32 -0
  84. package/src/components/SearchComponent.tsx +12 -8
  85. package/src/components/UserCard.tsx +80 -0
  86. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
  87. package/src/components/ViewPackagePage/components/build-status.tsx +1 -1
  88. package/src/components/ViewPackagePage/components/important-files-view.tsx +174 -87
  89. package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -4
  90. package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +1 -2
  91. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +54 -20
  92. package/src/components/ViewPackagePage/components/package-header.tsx +26 -37
  93. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +11 -19
  94. package/src/components/ViewPackagePage/components/repo-page-content.tsx +33 -25
  95. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +16 -10
  96. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +11 -11
  97. package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
  98. package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
  99. package/src/components/ViewPackagePage/components/tab-views/pcb-view.tsx +1 -2
  100. package/src/components/ViewPackagePage/components/tab-views/schematic-view.tsx +2 -1
  101. package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
  102. package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
  103. package/src/components/dialogs/GitHubRepositorySelector.tsx +56 -49
  104. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -6
  105. package/src/components/dialogs/import-component-dialog.tsx +16 -9
  106. package/src/components/dialogs/import-package-dialog.tsx +3 -2
  107. package/src/components/dialogs/new-package-save-prompt-dialog.tsx +190 -0
  108. package/src/components/organization/OrganizationCard.tsx +204 -0
  109. package/src/components/organization/OrganizationCardSkeleton.tsx +55 -0
  110. package/src/components/organization/OrganizationHeader.tsx +154 -0
  111. package/src/components/organization/OrganizationMembers.tsx +146 -0
  112. package/src/components/package-port/CodeAndPreview.tsx +32 -46
  113. package/src/components/package-port/CodeEditor.tsx +28 -31
  114. package/src/components/package-port/CodeEditorHeader.tsx +128 -63
  115. package/src/components/package-port/EditorNav.tsx +32 -49
  116. package/src/components/preview/ConnectedPackagesList.tsx +8 -8
  117. package/src/components/preview/ConnectedRepoOverview.tsx +102 -2
  118. package/src/components/preview/PackageReleasesDashboard.tsx +53 -36
  119. package/src/components/ui/tree-view.tsx +6 -3
  120. package/src/hooks/use-add-org-member-mutation.ts +51 -0
  121. package/src/hooks/use-create-org-mutation.ts +38 -0
  122. package/src/hooks/use-create-package-mutation.ts +3 -0
  123. package/src/hooks/use-current-package-id.ts +5 -30
  124. package/src/hooks/use-current-package-info.ts +29 -5
  125. package/src/hooks/use-current-package-release.ts +4 -3
  126. package/src/hooks/use-download-zip.ts +2 -2
  127. package/src/hooks/use-global-store.ts +6 -4
  128. package/src/hooks/use-jlcpcb-component-import.tsx +164 -0
  129. package/src/hooks/use-list-org-members.ts +27 -0
  130. package/src/hooks/use-list-user-orgs.ts +25 -0
  131. package/src/hooks/use-org-by-github-handle.ts +26 -0
  132. package/src/hooks/use-org.ts +24 -0
  133. package/src/hooks/use-organization.ts +42 -0
  134. package/src/hooks/use-package-as-snippet.ts +4 -2
  135. package/src/hooks/use-package-builds.ts +6 -2
  136. package/src/hooks/use-package-files.ts +5 -3
  137. package/src/hooks/use-package-release-by-id-or-version.ts +29 -20
  138. package/src/hooks/use-package-release-images.ts +105 -0
  139. package/src/hooks/use-package-release.ts +2 -2
  140. package/src/hooks/use-package-stars.ts +80 -4
  141. package/src/hooks/use-preview-images.ts +6 -3
  142. package/src/hooks/use-remove-org-member-mutation.ts +32 -0
  143. package/src/hooks/use-update-ai-description-mutation.ts +42 -0
  144. package/src/hooks/use-update-org-mutation.ts +41 -0
  145. package/src/hooks/use-warn-user-on-page-change.ts +71 -4
  146. package/src/hooks/useFileManagement.ts +183 -35
  147. package/src/hooks/useOptimizedPackageFilesLoader.ts +136 -0
  148. package/src/hooks/usePackageFilesLoader.ts +2 -2
  149. package/src/hooks/useUpdatePackageFilesMutation.ts +15 -1
  150. package/src/lib/download-fns/download-circuit-png.ts +11 -3
  151. package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
  152. package/src/lib/download-fns/download-kicad-files.ts +12 -11
  153. package/src/lib/normalize-svg-for-tile.ts +50 -0
  154. package/src/lib/posthog.ts +11 -9
  155. package/src/lib/react-query-api-failure-tracking.ts +148 -0
  156. package/src/lib/sentry.ts +14 -0
  157. package/src/lib/templates/blank-circuit-board-template.ts +0 -4
  158. package/src/lib/ts-lib-cache.ts +122 -7
  159. package/src/lib/utils/checkIfManualEditsImported.ts +4 -4
  160. package/src/lib/utils/findTargetFile.ts +45 -10
  161. package/src/lib/utils/isComponentExported.ts +10 -0
  162. package/src/main.tsx +2 -1
  163. package/src/pages/authorize.tsx +0 -2
  164. package/src/pages/create-organization.tsx +168 -0
  165. package/src/pages/dashboard.tsx +38 -6
  166. package/src/pages/datasheet.tsx +1 -1
  167. package/src/pages/datasheets.tsx +3 -3
  168. package/src/pages/editor.tsx +4 -6
  169. package/src/pages/landing.tsx +6 -7
  170. package/src/pages/latest.tsx +3 -0
  171. package/src/pages/organization-profile.tsx +199 -0
  172. package/src/pages/organization-settings.tsx +566 -0
  173. package/src/pages/package-editor.tsx +21 -21
  174. package/src/pages/preview-release.tsx +76 -136
  175. package/src/pages/quickstart.tsx +159 -123
  176. package/src/pages/release-detail.tsx +119 -31
  177. package/src/pages/search.tsx +192 -57
  178. package/src/pages/settings-redirect.tsx +44 -0
  179. package/src/pages/trending.tsx +29 -20
  180. package/src/pages/user-profile.tsx +58 -7
  181. package/src/pages/view-package.tsx +21 -26
  182. package/vite.config.ts +9 -0
  183. package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +0 -133
  184. package/src/components/Footer2.tsx +0 -100
  185. package/src/components/JLCPCBImportDialog.tsx +0 -280
  186. package/src/components/PackageBuildsPage/LogContent.tsx +0 -72
  187. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -115
  188. package/src/components/PackageBuildsPage/build-preview-content.tsx +0 -27
  189. package/src/components/PackageBuildsPage/collapsible-section.tsx +0 -63
  190. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +0 -166
  191. package/src/components/PackageBuildsPage/package-build-header.tsx +0 -79
  192. package/src/components/PageSearchComponent.tsx +0 -148
  193. package/src/components/ShippingInformationForm.tsx +0 -423
  194. package/src/components/StaticViewSnippetHeader.tsx +0 -70
  195. package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
  196. package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
  197. package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
  198. package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
  199. package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
  200. package/src/components/ViewSnippetHeader.tsx +0 -181
  201. package/src/components/ui/input-otp.tsx +0 -69
  202. package/src/pages/package-builds.tsx +0 -33
  203. package/src/pages/settings.tsx +0 -25
@@ -6,10 +6,14 @@ import { usePackageBuild } from "@/hooks/use-package-builds"
6
6
  import { ConnectedRepoOverview } from "@/components/preview/ConnectedRepoOverview"
7
7
  import Header from "@/components/Header"
8
8
  import { Badge } from "@/components/ui/badge"
9
- import { Calendar, GitBranch } from "lucide-react"
10
- import { useState } from "react"
9
+ import { Button } from "@/components/ui/button"
10
+ import { Calendar, GitBranch, RefreshCw } from "lucide-react"
11
11
  import { formatTimeAgo } from "@/lib/utils/formatTimeAgo"
12
12
  import { PackageBreadcrumb } from "@/components/PackageBreadcrumb"
13
+ import { usePackageReleaseImages } from "@/hooks/use-package-release-images"
14
+ import { Skeleton } from "@/components/ui/skeleton"
15
+ import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-release-mutation"
16
+ import { useGlobalStore } from "@/hooks/use-global-store"
13
17
 
14
18
  export default function ReleaseDetailPage() {
15
19
  const params = useParams<{
@@ -24,8 +28,6 @@ export default function ReleaseDetailPage() {
24
28
  ? `${params.author}/${params.packageName}`
25
29
  : null
26
30
 
27
- const [copied, setCopied] = useState(false)
28
-
29
31
  const {
30
32
  data: pkg,
31
33
  isLoading: isLoadingPackage,
@@ -39,16 +41,58 @@ export default function ReleaseDetailPage() {
39
41
  data: packageRelease,
40
42
  isLoading: isLoadingRelease,
41
43
  error: releaseError,
42
- } = usePackageReleaseByIdOrVersion(releaseIdOrVersion, packageName)
44
+ } = usePackageReleaseByIdOrVersion(releaseIdOrVersion, packageName, {
45
+ include_logs: true,
46
+ })
43
47
 
44
48
  const {
45
49
  data: latestBuild,
46
50
  isLoading: isLoadingBuild,
47
51
  error: buildError,
48
- } = usePackageBuild(packageRelease?.latest_package_build_id ?? null)
52
+ } = usePackageBuild(packageRelease?.latest_package_build_id ?? null, {
53
+ include_logs: true,
54
+ })
55
+
56
+ const { availableViews } = usePackageReleaseImages({
57
+ packageReleaseId: packageRelease?.package_release_id,
58
+ })
59
+
60
+ const session = useGlobalStore((s) => s.session)
61
+ const { mutate: rebuildPackage, isLoading: isRebuildLoading } =
62
+ useRebuildPackageReleaseMutation()
49
63
 
50
64
  if (isLoadingPackage || isLoadingRelease) {
51
- return null
65
+ return (
66
+ <>
67
+ <Header />
68
+ <div className="min-h-screen bg-white">
69
+ {/* Page Header Skeleton */}
70
+ <div className="bg-gray-50 border-b py-6">
71
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
72
+ <Skeleton className="h-6 w-64 mb-4" />
73
+ <div className="flex items-center gap-4">
74
+ <Skeleton className="h-4 w-20" />
75
+ <Skeleton className="h-4 w-32" />
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ {/* Images Skeleton */}
81
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
82
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
83
+ {[1, 2, 3].map((i) => (
84
+ <Skeleton key={i} className="h-48 rounded-lg" />
85
+ ))}
86
+ </div>
87
+ </div>
88
+
89
+ {/* Main Content Skeleton */}
90
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
91
+ <Skeleton className="h-64 w-full" />
92
+ </div>
93
+ </div>
94
+ </>
95
+ )
52
96
  }
53
97
 
54
98
  if (packageError?.status === 404 || !pkg) {
@@ -65,7 +109,7 @@ export default function ReleaseDetailPage() {
65
109
  <div className="min-h-screen bg-white">
66
110
  {/* Page Header */}
67
111
  <div className="bg-gray-50 border-b py-6">
68
- <div className="max-w-7xl lg:flex lg:justify-between mx-auto px-4 sm:px-6 lg:px-8">
112
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
69
113
  {/* Breadcrumb */}
70
114
  <PackageBreadcrumb
71
115
  author={pkg.owner_github_username || ""}
@@ -78,35 +122,79 @@ export default function ReleaseDetailPage() {
78
122
  />
79
123
 
80
124
  {/* Header Content */}
81
- <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-2">
82
- <div className="flex-1">
83
- <div className="flex items-center gap-4 text-sm text-gray-600">
84
- {packageRelease.is_pr_preview && (
85
- <a
86
- href={`https://github.com/${pkg.github_repo_full_name}/pull/${packageRelease.github_pr_number}`}
87
- target="_blank"
88
- rel="noopener noreferrer"
89
- >
90
- <div className="flex items-center gap-1">
91
- <GitBranch className="w-4 h-4" />
92
- <Badge variant="outline" className="text-xs">
93
- PR #{packageRelease.github_pr_number}
94
- </Badge>
95
- </div>
96
- </a>
97
- )}
98
- <div className="flex items-center gap-1">
99
- <Calendar className="w-4 h-4" />
100
- <span>
101
- Created {formatTimeAgo(packageRelease.created_at)}
102
- </span>
103
- </div>
125
+ <div className="flex flex-wrap items-center justify-between gap-3 mt-4">
126
+ <div className="flex flex-wrap items-center gap-4 text-sm text-gray-600">
127
+ {packageRelease.is_pr_preview && (
128
+ <a
129
+ href={`https://github.com/${pkg.github_repo_full_name}/pull/${packageRelease.github_pr_number}`}
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ className="flex items-center gap-1 hover:text-gray-800 transition-colors"
133
+ >
134
+ <GitBranch className="w-4 h-4" />
135
+ <Badge variant="outline" className="text-xs">
136
+ PR #{packageRelease.github_pr_number}
137
+ </Badge>
138
+ </a>
139
+ )}
140
+ <div className="flex items-center gap-1">
141
+ <Calendar className="w-4 h-4" />
142
+ <span>
143
+ Created {formatTimeAgo(packageRelease.created_at)}
144
+ </span>
104
145
  </div>
105
146
  </div>
147
+
148
+ {/* Rebuild Button */}
149
+ {session?.github_username === pkg.owner_github_username && (
150
+ <Button
151
+ variant="outline"
152
+ size="sm"
153
+ className="border-gray-300 bg-white hover:bg-gray-50 flex-shrink-0"
154
+ disabled={isRebuildLoading || !packageRelease}
155
+ onClick={() =>
156
+ packageRelease &&
157
+ rebuildPackage({
158
+ package_release_id: packageRelease.package_release_id,
159
+ })
160
+ }
161
+ >
162
+ <RefreshCw
163
+ className={`w-4 h-4 mr-2 ${isRebuildLoading ? "animate-spin" : ""}`}
164
+ />
165
+ {isRebuildLoading ? "Rebuilding..." : "Rebuild"}
166
+ </Button>
167
+ )}
106
168
  </div>
107
169
  </div>
108
170
  </div>
109
171
 
172
+ {/* Images Section - Always show with skeletons while loading */}
173
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
174
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
175
+ {availableViews.length > 0
176
+ ? availableViews.map((view) => (
177
+ <div
178
+ key={view.id}
179
+ className="flex items-center justify-center border rounded-lg bg-gray-50 overflow-hidden h-48"
180
+ >
181
+ {view.isLoading ? (
182
+ <Skeleton className="w-full h-full" />
183
+ ) : (
184
+ <img
185
+ src={view.imageUrl}
186
+ alt={`${view.label} preview`}
187
+ className={`w-full h-full object-contain ${view.label.toLowerCase() == "pcb" ? "bg-black" : view.label.toLowerCase() == "schematic" ? "bg-[#F5F1ED]" : "bg-gray-100"}`}
188
+ />
189
+ )}
190
+ </div>
191
+ ))
192
+ : [1, 2, 3].map((i) => (
193
+ <Skeleton key={i} className="h-48 rounded-lg" />
194
+ ))}
195
+ </div>
196
+ </div>
197
+
110
198
  {/* Main Content */}
111
199
  <ConnectedRepoOverview
112
200
  packageBuild={latestBuild ?? null}
@@ -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,47 @@ 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 extends Omit<Account, "account_id"> {
32
+ score: number
33
+ matches: number[]
34
+ }
19
35
 
20
36
  export const SearchPage = () => {
21
37
  const axios = useAxios()
22
38
  const apiBaseUrl = useApiBaseUrl()
23
39
  const [searchParams, setSearchParams] = useSearchParams()
40
+ const currentUser = useGlobalStore((s) => s.session?.github_username)
24
41
 
25
42
  const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "")
26
43
  const [category, setCategory] = useState(
27
44
  searchParams.get("category") || "all",
28
45
  )
29
46
  const [sortBy, setSortBy] = useState(searchParams.get("sort") || "stars")
47
+ const [activeTab, setActiveTab] = useState(
48
+ searchParams.get("tab") || "packages",
49
+ )
30
50
 
31
51
  useEffect(() => {
32
52
  const params = new URLSearchParams()
33
53
  if (searchQuery) params.set("q", searchQuery)
34
54
  if (category !== "all") params.set("category", category)
35
55
  if (sortBy !== "stars") params.set("sort", sortBy)
56
+ if (activeTab !== "packages") params.set("tab", activeTab)
36
57
  setSearchParams(params)
37
- }, [searchQuery, category, sortBy, setSearchParams])
58
+ }, [searchQuery, category, sortBy, activeTab, setSearchParams])
38
59
 
39
60
  const {
40
61
  data: packages,
41
- isLoading,
42
- error,
62
+ isLoading: isLoadingPackages,
63
+ error: packagesError,
43
64
  } = useQuery(
44
65
  ["packageSearch", searchQuery, category],
45
66
  async () => {
@@ -52,69 +73,130 @@ export const SearchPage = () => {
52
73
  })
53
74
  return response.data.packages
54
75
  },
55
- { enabled: Boolean(searchQuery), keepPreviousData: true },
76
+ {
77
+ enabled: Boolean(searchQuery),
78
+ keepPreviousData: true,
79
+ refetchOnWindowFocus: false,
80
+ },
81
+ )
82
+
83
+ const { data: allAccounts = [], isLoading: isLoadingAccounts } = useQuery(
84
+ ["accountSearch", searchQuery],
85
+ async () => {
86
+ if (!searchQuery) return []
87
+ try {
88
+ const { data } = await axios.post("/accounts/search", {
89
+ query: searchQuery,
90
+ limit: 20,
91
+ })
92
+ return data.accounts || []
93
+ } catch (error) {
94
+ console.warn("Failed to fetch accounts:", error)
95
+ return []
96
+ }
97
+ },
98
+ {
99
+ enabled: Boolean(searchQuery) && Boolean(currentUser),
100
+ retry: false,
101
+ refetchOnWindowFocus: false,
102
+ },
56
103
  )
57
104
 
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))
105
+ const searchResults = useMemo((): ScoredPackage[] => {
106
+ if (!searchQuery || !packages?.length) return []
107
+
108
+ return packages
109
+ .map((pkg: Package) => {
110
+ const { score, matches } = fuzzyMatch(searchQuery, pkg.name)
111
+ return { ...pkg, score, matches }
74
112
  })
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()
113
+ .filter((pkg: ScoredPackage) => pkg.score >= 0)
114
+ .sort((a: ScoredPackage, b: ScoredPackage) => b.score - a.score)
115
+ }, [packages, searchQuery])
116
+
117
+ const filteredPackages = searchResults?.sort((a: Package, b: Package) => {
118
+ if (sortBy === "stars") {
119
+ return (b.star_count || 0) - (a.star_count || 0)
120
+ } else if (sortBy === "newest") {
121
+ return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
122
+ } else if (sortBy === "oldest") {
123
+ return new Date(a.updated_at).getTime() - new Date(b.updated_at).getTime()
124
+ }
125
+ return 0
126
+ })
127
+
128
+ const accountSearchResults = useMemo((): ScoredAccount[] => {
129
+ if (!searchQuery) return []
130
+
131
+ // First, get scored accounts from API
132
+ const apiAccounts = allAccounts
133
+ .map((account: Account) => {
134
+ const { score, matches } = fuzzyMatch(
135
+ searchQuery,
136
+ account.github_username,
86
137
  )
138
+ return { ...account, score, matches }
139
+ })
140
+ .filter((account: ScoredAccount) => account.score >= 0)
141
+
142
+ // Then, extract unique package owners not already in API accounts
143
+ const packageOwners: ScoredAccount[] = []
144
+ const existingUsernames = new Set(
145
+ apiAccounts.map((acc: Account) => acc.github_username),
146
+ )
147
+
148
+ filteredPackages.forEach((pkg) => {
149
+ if (
150
+ pkg.owner_github_username &&
151
+ !existingUsernames.has(pkg.owner_github_username)
152
+ ) {
153
+ packageOwners.push({
154
+ github_username: pkg.owner_github_username,
155
+ score: 1,
156
+ matches: [],
157
+ })
158
+ existingUsernames.add(pkg.owner_github_username)
87
159
  }
88
- return 0
89
160
  })
161
+ return [...apiAccounts, ...packageOwners].sort(
162
+ (a: ScoredAccount, b: ScoredAccount) => b.score - a.score,
163
+ )
164
+ }, [allAccounts, searchQuery, filteredPackages])
90
165
 
166
+ useEffect(() => {
167
+ if (accountSearchResults.length == 0 && !isLoadingAccounts) {
168
+ setActiveTab("packages")
169
+ }
170
+ }, [accountSearchResults, isLoadingAccounts])
91
171
  return (
92
172
  <div className="min-h-screen flex flex-col">
93
173
  <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">
174
+ <main className="flex-grow pb-12 min-h-[80vh] w-full min-w-full">
175
+ <div className="container mx-auto px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
176
+ <div className="max-w-7xl mx-auto">
177
+ <div className="mb-4 sm:mb-6">
98
178
  <div className="flex items-center gap-2 mb-3">
99
- <h1 className="text-3xl font-bold text-gray-900">
100
- Search Packages
179
+ <h1 className="text-2xl sm:text-3xl font-bold text-gray-900">
180
+ Search
101
181
  </h1>
102
182
  </div>
103
- <div className="flex flex-col sm:flex-row gap-4 mb-4">
183
+ <div className="flex flex-col sm:flex-row gap-3 sm:gap-4 mb-4">
104
184
  <div className="relative flex-grow">
105
185
  <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
106
186
  <Input
107
187
  type="search"
108
- placeholder="Search packages..."
188
+ placeholder="Search packages and users..."
109
189
  className="pl-10"
110
190
  value={searchQuery}
191
+ spellCheck={false}
192
+ autoComplete="off"
111
193
  onChange={(e) => setSearchQuery(e.target.value)}
112
- aria-label="Search packages"
194
+ aria-label="Search packages and users"
113
195
  role="searchbox"
114
196
  />
115
197
  </div>
116
198
  <Select value={sortBy} onValueChange={setSortBy}>
117
- <SelectTrigger className="w-[140px]">
199
+ <SelectTrigger className="w-full sm:w-[140px]">
118
200
  <SelectValue placeholder="Sort By" />
119
201
  </SelectTrigger>
120
202
  <SelectContent>
@@ -126,17 +208,70 @@ export const SearchPage = () => {
126
208
  </div>
127
209
  </div>
128
210
 
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
- />
211
+ <Tabs
212
+ value={activeTab}
213
+ onValueChange={setActiveTab}
214
+ className="w-full"
215
+ >
216
+ {currentUser && accountSearchResults.length > 0 && (
217
+ <TabsList className="grid grid-cols-2 mb-6 select-none w-full max-w-md mx-auto">
218
+ <TabsTrigger
219
+ value="packages"
220
+ className="flex items-center gap-2"
221
+ >
222
+ Packages
223
+ </TabsTrigger>
224
+ <TabsTrigger
225
+ value="users"
226
+ className="flex items-center gap-2"
227
+ >
228
+ Users
229
+ </TabsTrigger>
230
+ </TabsList>
231
+ )}
232
+
233
+ <TabsContent value="packages" className="w-full">
234
+ <PackageSearchResults
235
+ isLoading={isLoadingPackages}
236
+ error={packagesError}
237
+ filteredPackages={filteredPackages}
238
+ apiBaseUrl={apiBaseUrl}
239
+ emptyStateMessage={
240
+ searchQuery
241
+ ? `No packages match your search for "${searchQuery}".`
242
+ : "Please enter a search query to find packages."
243
+ }
244
+ />
245
+ </TabsContent>
246
+
247
+ <TabsContent value="users" className="w-full">
248
+ {isLoadingAccounts ? (
249
+ <div>
250
+ <LoadingState />
251
+ </div>
252
+ ) : accountSearchResults.length > 0 ? (
253
+ <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">
254
+ {accountSearchResults.map((account, i) => (
255
+ <UserCard key={i} account={account} className="w-full" />
256
+ ))}
257
+ </div>
258
+ ) : (
259
+ <div className="text-center py-12 px-4">
260
+ <div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
261
+ <User className="w-8 h-8 text-slate-400" />
262
+ </div>
263
+ <h3 className="text-xl font-medium text-slate-900 mb-2">
264
+ No Matching Users
265
+ </h3>
266
+ <p className="text-slate-500 max-w-md mx-auto mb-6">
267
+ {searchQuery
268
+ ? `No users match your search for "${searchQuery}".`
269
+ : "Please enter a search query to find users."}
270
+ </p>
271
+ </div>
272
+ )}
273
+ </TabsContent>
274
+ </Tabs>
140
275
  </div>
141
276
  </div>
142
277
  </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