@tscircuit/fake-snippets 0.0.65 → 0.0.67

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 (87) hide show
  1. package/CONTRIBUTING.md +2 -2
  2. package/README.md +2 -2
  3. package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
  4. package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
  5. package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
  6. package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
  7. package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
  8. package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
  9. package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +1 -16
  10. package/bun.lock +42 -31
  11. package/dist/bundle.js +34 -41
  12. package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
  13. package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
  14. package/fake-snippets-api/routes/api/packages/images/[owner_github_username]/[unscoped_name]/[view_format].ts +3 -3
  15. package/package.json +7 -5
  16. package/src/App.tsx +0 -7
  17. package/src/ContextProviders.tsx +2 -0
  18. package/src/components/DownloadButtonAndMenu.tsx +1 -4
  19. package/src/components/ErrorTabContent.tsx +1 -1
  20. package/src/components/Footer.tsx +5 -2
  21. package/src/components/HeaderLogin.tsx +37 -54
  22. package/src/components/ImageWithFallback.tsx +37 -0
  23. package/src/components/JLCPCBImportDialog.tsx +43 -24
  24. package/src/components/PackageCard.tsx +12 -3
  25. package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
  26. package/src/components/PackageSearchResults.tsx +87 -0
  27. package/src/components/PackagesList.tsx +3 -3
  28. package/src/components/PageSearchComponent.tsx +9 -9
  29. package/src/components/ShippingInformationForm.tsx +1 -1
  30. package/src/components/TrendingPackagesCarousel.tsx +43 -23
  31. package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
  32. package/src/components/ViewPackagePage/components/main-content-header.tsx +10 -22
  33. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +25 -14
  34. package/src/components/ViewPackagePage/components/package-header.tsx +9 -4
  35. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +6 -1
  36. package/src/components/ViewPackagePage/components/repo-page-content.tsx +4 -4
  37. package/src/components/ViewPackagePage/components/sidebar.tsx +2 -14
  38. package/src/components/ViewSnippetSidebar.tsx +1 -1
  39. package/src/components/package-port/CodeEditor.tsx +13 -10
  40. package/src/components/package-port/CodeEditorHeader.tsx +1 -1
  41. package/src/components/package-port/EditorNav.tsx +2 -2
  42. package/src/hooks/use-get-fsmap-hash-for-package.ts +19 -0
  43. package/src/hooks/use-global-store.ts +1 -0
  44. package/src/hooks/use-preview-images.ts +20 -4
  45. package/src/hooks/use-shiki-highlighter.ts +13 -6
  46. package/src/hooks/use-toast.tsx +1 -1
  47. package/src/lib/download-fns/download-gltf.ts +3 -10
  48. package/src/lib/handleManualEditsImport.tsx +1 -1
  49. package/src/lib/types.ts +4 -2
  50. package/src/pages/dashboard.tsx +3 -4
  51. package/src/pages/editor.tsx +20 -14
  52. package/src/pages/latest.tsx +25 -26
  53. package/src/pages/package-editor.tsx +14 -2
  54. package/src/pages/search.tsx +120 -19
  55. package/src/pages/trending.tsx +14 -59
  56. package/src/pages/user-profile.tsx +13 -8
  57. package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
  58. package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
  59. package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
  60. package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
  61. package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
  62. package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
  63. package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
  64. package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
  65. package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
  66. package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
  67. package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
  68. package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
  69. package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
  70. package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
  71. package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
  72. package/src/components/AiChatInterface.tsx +0 -229
  73. package/src/components/CodeAndPreview.tsx +0 -289
  74. package/src/components/CodeEditor.tsx +0 -539
  75. package/src/components/CodeEditorHeader.tsx +0 -135
  76. package/src/components/EditorNav.tsx +0 -502
  77. package/src/components/PreviewContent.tsx +0 -372
  78. package/src/components/SnippetCard.tsx +0 -159
  79. package/src/components/SnippetList.tsx +0 -71
  80. package/src/hooks/use-compiled-tsx.ts +0 -37
  81. package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
  82. package/src/hooks/use-run-tsx/index.tsx +0 -256
  83. package/src/hooks/use-save-snippet.ts +0 -66
  84. package/src/hooks/use-typecheck.ts +0 -54
  85. package/src/lib/utils/getSyntaxError.ts +0 -13
  86. package/src/pages/ai.tsx +0 -92
  87. package/src/pages/view-snippet.tsx +0 -166
@@ -0,0 +1,19 @@
1
+ import { usePackageFiles } from "@/hooks/use-package-files"
2
+ import md5 from "md5"
3
+
4
+ export const useGetFsMapHashForPackage = (packageReleaseId: string) => {
5
+ const { data: pkgFilesList } = usePackageFiles(packageReleaseId)
6
+
7
+ if (!pkgFilesList) {
8
+ console.error(
9
+ `No package files found for package release ${packageReleaseId}`,
10
+ )
11
+ return null
12
+ }
13
+ const fsMap = new Map<string, string>()
14
+ for (const file of pkgFilesList) {
15
+ fsMap.set(file.file_path, file.content_text ?? "")
16
+ }
17
+ const fsMapHash = md5(JSON.stringify(fsMap))
18
+ return `md5-${fsMapHash}`
19
+ }
@@ -30,4 +30,5 @@ export const useGlobalStore = create<Store>()(
30
30
 
31
31
  useGlobalStore.subscribe((state, prevState) => {
32
32
  ;(window as any).globalStore = state
33
+ window.TSCIRCUIT_REGISTRY_TOKEN = state.session?.token ?? null
33
34
  })
@@ -2,9 +2,13 @@ import { useState } from "react"
2
2
 
3
3
  interface UsePreviewImagesProps {
4
4
  packageName?: string
5
+ fsMapHash?: string
5
6
  }
6
7
 
7
- export function usePreviewImages({ packageName }: UsePreviewImagesProps) {
8
+ export function usePreviewImages({
9
+ packageName,
10
+ fsMapHash,
11
+ }: UsePreviewImagesProps) {
8
12
  const [imageStatus, setImageStatus] = useState<
9
13
  Record<string, "loading" | "loaded" | "error">
10
14
  >({
@@ -18,21 +22,33 @@ export function usePreviewImages({ packageName }: UsePreviewImagesProps) {
18
22
  id: "3d",
19
23
  label: "3D View",
20
24
  imageUrl: packageName
21
- ? `https://registry-api.tscircuit.com/snippets/images/${packageName}/3d.png`
25
+ ? `https://registry-api.tscircuit.com/packages/images/${packageName}/3d.png?${new URLSearchParams(
26
+ {
27
+ fs_sha: fsMapHash ?? "",
28
+ },
29
+ ).toString()}`
22
30
  : undefined,
23
31
  },
24
32
  {
25
33
  id: "pcb",
26
34
  label: "PCB View",
27
35
  imageUrl: packageName
28
- ? `https://registry-api.tscircuit.com/snippets/images/${packageName}/pcb.png`
36
+ ? `https://registry-api.tscircuit.com/packages/images/${packageName}/pcb.png?${new URLSearchParams(
37
+ {
38
+ fs_sha: fsMapHash ?? "",
39
+ },
40
+ ).toString()}`
29
41
  : undefined,
30
42
  },
31
43
  {
32
44
  id: "schematic",
33
45
  label: "Schematic View",
34
46
  imageUrl: packageName
35
- ? `https://registry-api.tscircuit.com/snippets/images/${packageName}/schematic.png`
47
+ ? `https://registry-api.tscircuit.com/packages/images/${packageName}/schematic.png?${new URLSearchParams(
48
+ {
49
+ fs_sha: fsMapHash ?? "",
50
+ },
51
+ ).toString()}`
36
52
  : undefined,
37
53
  },
38
54
  ]
@@ -1,18 +1,25 @@
1
1
  import { useState, useEffect } from "react"
2
- import { getSingletonHighlighter, Highlighter } from "shiki"
2
+ import { createHighlighterCore, type HighlighterCore } from "shiki/core"
3
+ import { createOnigurumaEngine } from "shiki/engine/oniguruma"
3
4
 
4
- let cachedHighlighter: Highlighter | null = null
5
+ let cachedHighlighter: HighlighterCore | null = null
5
6
 
6
7
  export const useShikiHighlighter = () => {
7
- const [highlighter, setHighlighter] = useState<Highlighter | null>(null)
8
+ const [highlighter, setHighlighter] = useState<HighlighterCore | null>(null)
8
9
  const [isLoading, setIsLoading] = useState(true)
9
10
 
10
11
  useEffect(() => {
11
12
  const fetchHighlighter = async () => {
12
13
  if (!cachedHighlighter) {
13
- cachedHighlighter = await getSingletonHighlighter({
14
- themes: ["github-dark", "github-light"],
15
- langs: ["typescript", "tsx"],
14
+ cachedHighlighter = await createHighlighterCore({
15
+ themes: [
16
+ import("@shikijs/themes/github-dark"),
17
+ import("@shikijs/themes/github-light"),
18
+ import("@shikijs/themes/vitesse-light"),
19
+ ],
20
+ langs: [import("@shikijs/langs/tsx")],
21
+ // `shiki/wasm` contains the wasm binary inlined as base64 string.
22
+ engine: createOnigurumaEngine(import("shiki/wasm")),
16
23
  })
17
24
  }
18
25
  setHighlighter(cachedHighlighter)
@@ -197,7 +197,7 @@ export function useNotImplementedToast() {
197
197
  The {feature} feature is not implemented yet. Help us out!{" "}
198
198
  <a
199
199
  className="text-blue-500 hover:underline font-semibold"
200
- href="https://github.com/tscircuit/snippets"
200
+ href="https://github.com/tscircuit/tscircuit.com"
201
201
  >
202
202
  Check out our Github
203
203
  </a>
@@ -1,19 +1,12 @@
1
- import { AnyCircuitElement } from "circuit-json"
2
- import { convertCircuitJsonToAssemblySvg } from "circuit-to-svg"
3
1
  import { GLTFExporter, type GLTFExporterOptions } from "three-stdlib"
4
2
  import { saveAs } from "file-saver"
5
3
  import * as THREE from "three"
6
4
 
7
- export const downloadGltf = async (
8
- circuitJson: AnyCircuitElement[],
9
- fileName: string,
10
- ) => {
11
- const threeJsObject = window.TSCIRCUIT_3D_OBJECT_REF
12
- ?.current as THREE.Object3D
13
-
5
+ export const downloadGltf = async (fileName: string) => {
6
+ const threeJsObject = window.TSCIRCUIT_3D_OBJECT_REF as THREE.Object3D
14
7
  if (!threeJsObject) {
15
8
  throw new Error(
16
- "To download the 3D model, please open the 3D view first and run the snippet",
9
+ "To download the 3D model, please open the 3D view first and run the package",
17
10
  )
18
11
  }
19
12
 
@@ -1,4 +1,4 @@
1
- import { FileName } from "@/components/CodeEditorHeader"
1
+ import { FileName } from "@/components/package-port/CodeEditorHeader"
2
2
 
3
3
  export const handleManualEditsImport = (
4
4
  files: Record<string, string>,
package/src/lib/types.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import { Object3D, Object3DEventMap } from "three"
2
+
1
3
  declare global {
2
4
  interface Window {
3
5
  TSCIRCUIT_REGISTRY_API_BASE_URL: string
4
- TSCIRCUIT_REGISTRY_TOKEN: string
6
+ TSCIRCUIT_REGISTRY_TOKEN: string | null
5
7
  TSCIRCUIT_STRIPE_CHECKOUT_BASE_URL: string
6
- TSCIRCUIT_3D_OBJECT_REF: any
8
+ TSCIRCUIT_3D_OBJECT_REF: Object3D<Object3DEventMap> | undefined
7
9
  __DEBUG_CODE_EDITOR_FS_MAP: Map<string, string>
8
10
  prettier: {
9
11
  format: (code: string, options: any) => string
@@ -9,8 +9,7 @@ import { Edit2, KeyRound } from "lucide-react"
9
9
  import { Button } from "@/components/ui/button"
10
10
  import { useGlobalStore } from "@/hooks/use-global-store"
11
11
  import { PrefetchPageLink } from "@/components/PrefetchPageLink"
12
- import { PackageList } from "@/components/PackagesList"
13
- import { SnippetList } from "@/components/SnippetList"
12
+ import { PackagesList } from "@/components/PackagesList"
14
13
  import { Helmet } from "react-helmet-async"
15
14
  import { useSignIn } from "@/hooks/use-sign-in"
16
15
  import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
@@ -175,14 +174,14 @@ export const DashboardPage = () => {
175
174
  )}
176
175
  </div>
177
176
  <div className="md:w-1/4">
178
- <PackageList
177
+ <PackagesList
179
178
  title="Trending Packages"
180
179
  packages={trendingPackages}
181
180
  showAll={showAllTrending}
182
181
  onToggleShowAll={() => setShowAllTrending(!showAllTrending)}
183
182
  />
184
183
  <div className="mt-8">
185
- <PackageList
184
+ <PackagesList
186
185
  title="Latest Packages"
187
186
  packages={latestPackages}
188
187
  showAll={showAllLatest}
@@ -1,45 +1,51 @@
1
- import { CodeAndPreview } from "@/components/CodeAndPreview"
1
+ import { CodeAndPreview } from "@/components/package-port/CodeAndPreview"
2
2
  import Footer from "@/components/Footer"
3
3
  import Header from "@/components/Header"
4
- import { useCurrentSnippetId } from "@/hooks/use-current-snippet-id"
5
- import { useSnippet } from "@/hooks/use-snippet"
6
4
  import { Helmet } from "react-helmet-async"
5
+ import { useCurrentPackageId } from "@/hooks/use-current-package-id"
6
+ import { usePackage } from "@/hooks/use-package"
7
+ import { useGetFsMapHashForPackage } from "@/hooks/use-get-fsmap-hash-for-package"
7
8
 
8
9
  export const EditorPage = () => {
9
- const { snippetId } = useCurrentSnippetId()
10
- const { data: snippet, isLoading, error } = useSnippet(snippetId)
10
+ const { packageId } = useCurrentPackageId()
11
+ const { data: pkg, isLoading, error } = usePackage(packageId)
12
+ const fsMapHash = useGetFsMapHashForPackage(
13
+ pkg?.latest_package_release_id ?? "",
14
+ )
11
15
 
12
16
  return (
13
17
  <div className="overflow-x-hidden">
14
18
  <Helmet>
15
19
  <title>
16
- {snippet
17
- ? `${snippet.unscoped_name} - tscircuit`
18
- : "tscircuit editor"}
20
+ {pkg ? `${pkg.unscoped_name} - tscircuit` : "tscircuit editor"}
19
21
  </title>
20
- {snippet && (
22
+ {pkg && (
21
23
  <>
22
24
  <meta
23
25
  property="og:title"
24
- content={`${snippet.unscoped_name} - tscircuit`}
26
+ content={`${pkg.unscoped_name} - tscircuit`}
25
27
  />
26
28
  <meta
27
29
  property="og:image"
28
- content={`https://registry-api.tscircuit.com/snippets/images/${snippet.owner_name}/${snippet.unscoped_name}/pcb.png`}
30
+ content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?${new URLSearchParams(
31
+ {
32
+ fs_sha: fsMapHash ?? "",
33
+ },
34
+ ).toString()}`}
29
35
  />
30
36
  <meta name="twitter:card" content="summary_large_image" />
31
37
  <meta
32
38
  name="twitter:image"
33
- content={`https://registry-api.tscircuit.com/snippets/images/${snippet.owner_name}/${snippet.unscoped_name}/pcb.png`}
39
+ content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png`}
34
40
  />
35
41
  </>
36
42
  )}
37
43
  </Helmet>
38
44
  <Header />
39
- {!error && <CodeAndPreview snippet={snippet} />}
45
+ {!error && <CodeAndPreview pkg={pkg} />}
40
46
  {error && error.status === 404 && (
41
47
  <div className="w-full h-[calc(100vh-20rem)] text-xl text-center flex justify-center items-center">
42
- Snippet not found
48
+ Package not found
43
49
  </div>
44
50
  )}
45
51
  {error && error.status !== 404 && (
@@ -1,7 +1,7 @@
1
1
  import React, { useState } from "react"
2
2
  import { useQuery } from "react-query"
3
3
  import { useAxios } from "@/hooks/use-axios"
4
- import { Snippet } from "fake-snippets-api/lib/db/schema"
4
+ import { Package } from "fake-snippets-api/lib/db/schema"
5
5
  import Header from "@/components/Header"
6
6
  import Footer from "@/components/Footer"
7
7
  import {
@@ -23,8 +23,8 @@ import {
23
23
  SelectTrigger,
24
24
  SelectValue,
25
25
  } from "@/components/ui/select"
26
- import { SnippetCard } from "@/components/SnippetCard"
27
26
  import { PackageCardSkeleton } from "@/components/PackageCardSkeleton"
27
+ import { PackageCard } from "@/components/PackageCard"
28
28
 
29
29
  const LatestPage: React.FC = () => {
30
30
  const axios = useAxios()
@@ -33,30 +33,30 @@ const LatestPage: React.FC = () => {
33
33
  const [category, setCategory] = useState("all")
34
34
 
35
35
  const {
36
- data: snippets,
36
+ data: packages,
37
37
  isLoading,
38
38
  error,
39
- } = useQuery<Snippet[]>(
40
- ["latestSnippets", category],
39
+ } = useQuery<Package[]>(
40
+ ["latestPackages", category],
41
41
  async () => {
42
42
  const params = category !== "all" ? { tag: category } : {}
43
- const response = await axios.get("/snippets/list_latest", { params })
44
- return response.data.snippets
43
+ const response = await axios.get("/packages/list_latest", { params })
44
+ return response.data.packages
45
45
  },
46
46
  {
47
47
  keepPreviousData: true,
48
48
  },
49
49
  )
50
50
 
51
- const filteredSnippets = snippets?.filter((snippet) => {
51
+ const filteredPackages = packages?.filter((pkg) => {
52
52
  if (!searchQuery) return true
53
53
 
54
54
  const query = searchQuery.toLowerCase().trim()
55
55
 
56
56
  const searchableFields = [
57
- snippet.unscoped_name.toLowerCase(),
58
- snippet.owner_name.toLowerCase(),
59
- (snippet.description || "").toLowerCase(),
57
+ pkg.unscoped_name.toLowerCase(),
58
+ pkg.owner_github_username?.toLowerCase() ?? "",
59
+ (pkg.description || "").toLowerCase(),
60
60
  ]
61
61
 
62
62
  return searchableFields.some((field) => {
@@ -73,7 +73,7 @@ const LatestPage: React.FC = () => {
73
73
  <div className="flex items-center gap-2 mb-3">
74
74
  <Calendar className="w-6 h-6 text-blue-500" />
75
75
  <h1 className="text-4xl font-bold text-gray-900">
76
- Latest Snippets
76
+ Latest Packages
77
77
  </h1>
78
78
  </div>
79
79
  <p className="text-lg text-gray-600 mb-4">
@@ -99,7 +99,7 @@ const LatestPage: React.FC = () => {
99
99
  <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
100
100
  <Input
101
101
  type="text"
102
- placeholder="Search latest snippets..."
102
+ placeholder="Search latest packages..."
103
103
  value={searchQuery}
104
104
  onChange={(e) => setSearchQuery(e.target.value)}
105
105
  className="pl-10"
@@ -154,40 +154,39 @@ const LatestPage: React.FC = () => {
154
154
  </div>
155
155
  <div>
156
156
  <h3 className="text-lg font-semibold mb-2">
157
- Error Loading Snippets
157
+ Error Loading Packages
158
158
  </h3>
159
159
  <p className="text-red-600">
160
- We couldn't load the latest snippets. Please try again later.
160
+ We couldn't load the latest packages. Please try again later.
161
161
  </p>
162
162
  </div>
163
163
  </div>
164
164
  </div>
165
- ) : filteredSnippets?.length === 0 ? (
165
+ ) : filteredPackages?.length === 0 ? (
166
166
  <div className="text-center py-12 px-4">
167
167
  <div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
168
168
  <Search className="w-8 h-8 text-slate-400" />
169
169
  </div>
170
170
  <h3 className="text-xl font-medium text-slate-900 mb-2">
171
- No Matching Snippets
171
+ No Matching Packages
172
172
  </h3>
173
173
  <p className="text-slate-500 max-w-md mx-auto mb-6">
174
174
  {searchQuery
175
- ? `No snippets match your search for "${searchQuery}".`
175
+ ? `No packages match your search for "${searchQuery}".`
176
176
  : category !== "all"
177
- ? `No ${category} snippets found in the latest list.`
178
- : "There are no new snippets at the moment."}
177
+ ? `No ${category} packages found in the latest list.`
178
+ : "There are no new packages at the moment."}
179
179
  </p>
180
180
  </div>
181
181
  ) : (
182
182
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
183
- {filteredSnippets
183
+ {filteredPackages
184
184
  ?.sort((a, b) => b.created_at.localeCompare(a.created_at))
185
- ?.map((snippet) => (
186
- <SnippetCard
187
- key={snippet.snippet_id}
188
- snippet={snippet}
185
+ ?.map((pkg) => (
186
+ <PackageCard
187
+ key={pkg.package_id}
188
+ pkg={pkg}
189
189
  baseUrl={apiBaseUrl}
190
- showOwner={true}
191
190
  />
192
191
  ))}
193
192
  </div>
@@ -6,10 +6,14 @@ import { Helmet } from "react-helmet-async"
6
6
  import { useCurrentPackageId } from "@/hooks/use-current-package-id"
7
7
  import { NotFound } from "@/components/NotFound"
8
8
  import { ErrorOutline } from "@/components/ErrorOutline"
9
+ import { useGetFsMapHashForPackage } from "@/hooks/use-get-fsmap-hash-for-package"
9
10
 
10
11
  export const EditorPage = () => {
11
12
  const { packageId } = useCurrentPackageId()
12
13
  const { data: pkg, isLoading, error } = usePackage(packageId)
14
+ const fsMapHash = useGetFsMapHashForPackage(
15
+ pkg?.latest_package_release_id ?? "",
16
+ )
13
17
  const uuid4RegExp = new RegExp(
14
18
  /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/,
15
19
  )
@@ -25,12 +29,20 @@ export const EditorPage = () => {
25
29
  />
26
30
  <meta
27
31
  property="og:image"
28
- content={`https://registry-api.tscircuit.com/snippets/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png`}
32
+ content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?${new URLSearchParams(
33
+ {
34
+ fs_sha: fsMapHash ?? "",
35
+ },
36
+ ).toString()}`}
29
37
  />
30
38
  <meta name="twitter:card" content="summary_large_image" />
31
39
  <meta
32
40
  name="twitter:image"
33
- content={`https://registry-api.tscircuit.com/snippets/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png`}
41
+ content={`https://registry-api.tscircuit.com/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.png?${new URLSearchParams(
42
+ {
43
+ fs_sha: fsMapHash ?? "",
44
+ },
45
+ ).toString()}`}
34
46
  />
35
47
  </>
36
48
  )}
@@ -1,13 +1,92 @@
1
+ import React, { useState, useEffect } from "react"
2
+ import { useQuery } from "react-query"
3
+ import { useAxios } from "@/hooks/use-axios"
4
+ import { useSearchParams } from "wouter"
1
5
  import Header from "@/components/Header"
2
6
  import Footer from "@/components/Footer"
3
- import PageSearchComponent from "@/components/PageSearchComponent"
4
- import { useState } from "react"
5
- import { Search, Tag, Filter } from "lucide-react"
6
- import { Badge } from "@/components/ui/badge"
7
- import { PrefetchPageLink } from "@/components/PrefetchPageLink"
7
+ import { Input } from "@/components/ui/input"
8
+ import { Search } from "lucide-react"
9
+ import PackageSearchResults from "@/components/PackageSearchResults"
10
+ import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "@/components/ui/select"
18
+ import { Package } from "fake-snippets-api/lib/db/schema"
8
19
 
9
20
  export const SearchPage = () => {
10
- const [searchResults, setSearchResults] = useState<any[]>([])
21
+ const axios = useAxios()
22
+ const apiBaseUrl = useSnippetsBaseApiUrl()
23
+ const [searchParams, setSearchParams] = useSearchParams()
24
+
25
+ const [searchQuery, setSearchQuery] = useState(searchParams.get("q") || "")
26
+ const [category, setCategory] = useState(
27
+ searchParams.get("category") || "all",
28
+ )
29
+ const [sortBy, setSortBy] = useState(searchParams.get("sort") || "stars")
30
+
31
+ useEffect(() => {
32
+ const params = new URLSearchParams()
33
+ if (searchQuery) params.set("q", searchQuery)
34
+ if (category !== "all") params.set("category", category)
35
+ if (sortBy !== "stars") params.set("sort", sortBy)
36
+ setSearchParams(params)
37
+ }, [searchQuery, category, sortBy, setSearchParams])
38
+
39
+ const {
40
+ data: packages,
41
+ isLoading,
42
+ error,
43
+ } = useQuery(
44
+ ["packageSearch", searchQuery, category],
45
+ async () => {
46
+ const params = new URLSearchParams()
47
+ if (searchQuery) params.append("q", searchQuery)
48
+ if (category !== "all") params.append("category", category)
49
+
50
+ const response = await axios.post(`/packages/search`, {
51
+ query: searchQuery,
52
+ })
53
+ return response.data.packages
54
+ },
55
+ { enabled: Boolean(searchQuery), keepPreviousData: true },
56
+ )
57
+
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))
74
+ })
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()
86
+ )
87
+ }
88
+ return 0
89
+ })
11
90
 
12
91
  return (
13
92
  <div className="min-h-screen flex flex-col">
@@ -17,26 +96,46 @@ export const SearchPage = () => {
17
96
  <div className="max-w-8xl mx-auto">
18
97
  <div className="mb-6">
19
98
  <div className="flex items-center gap-2 mb-3">
20
- <Search className="w-6 h-6 text-blue-500" />
21
99
  <h1 className="text-3xl font-bold text-gray-900">
22
100
  Search tscircuit Packages
23
101
  </h1>
24
102
  </div>
25
- <div className="flex flex-wrap gap-3">
26
- <PrefetchPageLink href="/trending">
27
- <Badge
28
- variant="secondary"
29
- className="px-3 py-1 cursor-pointer hover:bg-gray-200"
30
- >
31
- <Tag className="w-3.5 h-3.5 mr-1" />
32
- <span>Browse Packages</span>
33
- </Badge>
34
- </PrefetchPageLink>
103
+ <div className="flex flex-col sm:flex-row gap-4 mb-4">
104
+ <div className="relative flex-grow">
105
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
106
+ <Input
107
+ type="search"
108
+ placeholder="Search packages..."
109
+ className="pl-10"
110
+ value={searchQuery}
111
+ onChange={(e) => setSearchQuery(e.target.value)}
112
+ aria-label="Search packages"
113
+ role="searchbox"
114
+ />
115
+ </div>
116
+ <Select value={sortBy} onValueChange={setSortBy}>
117
+ <SelectTrigger className="w-[140px]">
118
+ <SelectValue placeholder="Sort By" />
119
+ </SelectTrigger>
120
+ <SelectContent>
121
+ <SelectItem value="stars">Most Starred</SelectItem>
122
+ <SelectItem value="newest">Newest</SelectItem>
123
+ <SelectItem value="oldest">Oldest</SelectItem>
124
+ </SelectContent>
125
+ </Select>
35
126
  </div>
36
127
  </div>
37
128
 
38
- <PageSearchComponent
39
- onResultsFetched={(results) => setSearchResults(results)}
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
+ }
40
139
  />
41
140
  </div>
42
141
  </div>
@@ -45,3 +144,5 @@ export const SearchPage = () => {
45
144
  </div>
46
145
  )
47
146
  }
147
+
148
+ export default SearchPage