@tscircuit/fake-snippets 0.0.66 → 0.0.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun-tests/fake-snippets-api/fixtures/get-circuit-json.ts +5 -143
- package/bun-tests/fake-snippets-api/fixtures/get-test-server.ts +1 -4
- package/bun-tests/fake-snippets-api/fixtures/start-server.ts +7 -3
- package/bun-tests/fake-snippets-api/routes/order_quotes/create.test.ts +20 -56
- package/bun-tests/fake-snippets-api/routes/package_files/create_or_update.test.ts +2 -2
- package/bun-tests/fake-snippets-api/routes/package_releases/update.test.ts +1 -1
- package/bun-tests/fake-snippets-api/routes/packages/images.test.ts +0 -11
- package/bun.lock +26 -75
- package/dist/bundle.js +32 -39
- package/fake-snippets-api/routes/api/order_quotes/create.ts +30 -37
- package/fake-snippets-api/routes/api/order_quotes/get.ts +5 -8
- package/package.json +4 -4
- package/src/App.tsx +0 -11
- package/src/ContextProviders.tsx +2 -0
- package/src/components/CmdKMenu.tsx +19 -19
- package/src/components/DownloadButtonAndMenu.tsx +1 -4
- package/src/components/FAQ.tsx +3 -1
- package/src/components/FileSidebar.tsx +50 -1
- package/src/components/Footer.tsx +5 -2
- package/src/components/Header2.tsx +20 -9
- package/src/components/HeaderLogin.tsx +37 -54
- package/src/components/ImageWithFallback.tsx +37 -0
- package/src/components/JLCPCBImportDialog.tsx +45 -29
- package/src/components/PackageCard.tsx +2 -2
- package/src/components/{SnippetLink.tsx → PackageLink.tsx} +8 -16
- package/src/components/PackageSearchResults.tsx +87 -0
- package/src/components/PackagesList.tsx +3 -3
- package/src/components/PageSearchComponent.tsx +9 -9
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +5 -28
- package/src/components/ViewPackagePage/components/important-files-view.tsx +1 -1
- package/src/components/ViewPackagePage/components/main-content-header.tsx +8 -8
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +24 -14
- package/src/components/ViewPackagePage/components/package-header.tsx +7 -2
- package/src/components/dialogs/confirm-delete-package-dialog.tsx +8 -0
- package/src/components/dialogs/edit-package-details-dialog.tsx +145 -138
- package/src/components/package-port/CodeAndPreview.tsx +40 -19
- package/src/components/package-port/CodeEditor.tsx +21 -37
- package/src/components/package-port/CodeEditorHeader.tsx +1 -1
- package/src/components/package-port/EditorNav.tsx +3 -13
- package/src/hooks/use-global-store.ts +1 -0
- package/src/hooks/use-shiki-highlighter.ts +13 -6
- package/src/hooks/useFileManagement.ts +59 -0
- package/src/lib/download-fns/download-gltf.ts +3 -10
- package/src/lib/handleManualEditsImport.tsx +1 -1
- package/src/lib/types.ts +4 -2
- package/src/lib/utils/isValidFileName.ts +5 -0
- package/src/pages/dashboard.tsx +4 -4
- package/src/pages/editor.tsx +20 -14
- package/src/pages/latest.tsx +25 -26
- package/src/pages/quickstart.tsx +5 -5
- package/src/pages/search.tsx +121 -20
- package/src/pages/trending.tsx +14 -58
- package/src/pages/user-profile.tsx +14 -8
- package/bun-tests/fake-snippets-api/routes/snippets/add_star.test.ts +0 -84
- package/bun-tests/fake-snippets-api/routes/snippets/create.test.ts +0 -53
- package/bun-tests/fake-snippets-api/routes/snippets/delete.test.ts +0 -82
- package/bun-tests/fake-snippets-api/routes/snippets/download.test.ts +0 -90
- package/bun-tests/fake-snippets-api/routes/snippets/generate_from_jlcpcb.test.ts +0 -16
- package/bun-tests/fake-snippets-api/routes/snippets/get.test.ts +0 -163
- package/bun-tests/fake-snippets-api/routes/snippets/get_image.test.ts +0 -117
- package/bun-tests/fake-snippets-api/routes/snippets/images.test.ts +0 -114
- package/bun-tests/fake-snippets-api/routes/snippets/list.test.ts +0 -169
- package/bun-tests/fake-snippets-api/routes/snippets/list_newest.test.ts +0 -50
- package/bun-tests/fake-snippets-api/routes/snippets/list_trending.test.ts +0 -72
- package/bun-tests/fake-snippets-api/routes/snippets/remove_star.test.ts +0 -80
- package/bun-tests/fake-snippets-api/routes/snippets/search.test.ts +0 -75
- package/bun-tests/fake-snippets-api/routes/snippets/star-count.test.ts +0 -51
- package/bun-tests/fake-snippets-api/routes/snippets/update.test.ts +0 -175
- package/src/components/AiChatInterface.tsx +0 -229
- package/src/components/CodeAndPreview.tsx +0 -289
- package/src/components/CodeEditor.tsx +0 -539
- package/src/components/CodeEditorHeader.tsx +0 -135
- package/src/components/EditorNav.tsx +0 -502
- package/src/components/OrderPreviewContent.tsx +0 -61
- package/src/components/PreviewContent.tsx +0 -372
- package/src/components/SnippetCard.tsx +0 -159
- package/src/components/SnippetList.tsx +0 -71
- package/src/components/ViewSnippetSidebar.tsx +0 -162
- package/src/components/dialogs/create-order-dialog.tsx +0 -146
- package/src/hooks/use-compiled-tsx.ts +0 -37
- package/src/hooks/use-run-tsx/construct-circuit.tsx +0 -62
- package/src/hooks/use-run-tsx/index.tsx +0 -256
- package/src/hooks/use-save-snippet.ts +0 -66
- package/src/hooks/use-typecheck.ts +0 -54
- package/src/lib/utils/getSyntaxError.ts +0 -13
- package/src/pages/ai.tsx +0 -92
- package/src/pages/preview.tsx +0 -44
- package/src/pages/view-order.tsx +0 -111
- package/src/pages/view-snippet.tsx +0 -166
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
import { useState, useEffect } from "react"
|
|
2
|
-
import {
|
|
2
|
+
import { createHighlighterCore, type HighlighterCore } from "shiki/core"
|
|
3
|
+
import { createOnigurumaEngine } from "shiki/engine/oniguruma"
|
|
3
4
|
|
|
4
|
-
let cachedHighlighter:
|
|
5
|
+
let cachedHighlighter: HighlighterCore | null = null
|
|
5
6
|
|
|
6
7
|
export const useShikiHighlighter = () => {
|
|
7
|
-
const [highlighter, setHighlighter] = useState<
|
|
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
|
|
14
|
-
themes: [
|
|
15
|
-
|
|
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)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Dispatch, SetStateAction } from "react"
|
|
2
|
+
import { isValidFileName } from "@/lib/utils/isValidFileName"
|
|
3
|
+
import {
|
|
4
|
+
CodeAndPreviewState,
|
|
5
|
+
CreateFileProps,
|
|
6
|
+
} from "../components/package-port/CodeAndPreview"
|
|
7
|
+
|
|
8
|
+
export function useFileManagement(
|
|
9
|
+
state: CodeAndPreviewState,
|
|
10
|
+
setState: Dispatch<SetStateAction<CodeAndPreviewState>>,
|
|
11
|
+
) {
|
|
12
|
+
const handleCreateFile = async ({
|
|
13
|
+
newFileName,
|
|
14
|
+
setErrorMessage,
|
|
15
|
+
onFileSelect,
|
|
16
|
+
setNewFileName,
|
|
17
|
+
setIsCreatingFile,
|
|
18
|
+
}: CreateFileProps) => {
|
|
19
|
+
newFileName = newFileName.trim()
|
|
20
|
+
if (!newFileName) {
|
|
21
|
+
setErrorMessage("File name cannot be empty")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (!isValidFileName(newFileName)) {
|
|
25
|
+
setErrorMessage(
|
|
26
|
+
'Invalid file name. Avoid using special characters like <>:"/\\|?*',
|
|
27
|
+
)
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
setErrorMessage("")
|
|
31
|
+
|
|
32
|
+
const fileExists = state.pkgFilesWithContent.some(
|
|
33
|
+
(file) => file.path === newFileName,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if (fileExists) {
|
|
37
|
+
setErrorMessage("A file with this name already exists")
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setState((prev) => {
|
|
42
|
+
const updatedFiles = [
|
|
43
|
+
...prev.pkgFilesWithContent,
|
|
44
|
+
{ path: newFileName, content: "" },
|
|
45
|
+
]
|
|
46
|
+
return {
|
|
47
|
+
...prev,
|
|
48
|
+
pkgFilesWithContent: updatedFiles,
|
|
49
|
+
} as CodeAndPreviewState
|
|
50
|
+
})
|
|
51
|
+
onFileSelect(newFileName)
|
|
52
|
+
setIsCreatingFile(false)
|
|
53
|
+
setNewFileName("")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
handleCreateFile,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
9
|
+
"To download the 3D model, please open the 3D view first and run the package",
|
|
17
10
|
)
|
|
18
11
|
}
|
|
19
12
|
|
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:
|
|
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
|
package/src/pages/dashboard.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
<
|
|
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
|
-
<
|
|
184
|
+
<PackagesList
|
|
186
185
|
title="Latest Packages"
|
|
187
186
|
packages={latestPackages}
|
|
188
187
|
showAll={showAllLatest}
|
|
@@ -195,6 +194,7 @@ export const DashboardPage = () => {
|
|
|
195
194
|
<DeleteDialog
|
|
196
195
|
packageId={packageToDelete.package_id}
|
|
197
196
|
packageName={packageToDelete.unscoped_name}
|
|
197
|
+
packageOwner={packageToDelete.owner_github_username ?? ""}
|
|
198
198
|
/>
|
|
199
199
|
)}
|
|
200
200
|
</div>
|
package/src/pages/editor.tsx
CHANGED
|
@@ -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 {
|
|
10
|
-
const { data:
|
|
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
|
-
{
|
|
17
|
-
? `${snippet.unscoped_name} - tscircuit`
|
|
18
|
-
: "tscircuit editor"}
|
|
20
|
+
{pkg ? `${pkg.unscoped_name} - tscircuit` : "tscircuit editor"}
|
|
19
21
|
</title>
|
|
20
|
-
{
|
|
22
|
+
{pkg && (
|
|
21
23
|
<>
|
|
22
24
|
<meta
|
|
23
25
|
property="og:title"
|
|
24
|
-
content={`${
|
|
26
|
+
content={`${pkg.unscoped_name} - tscircuit`}
|
|
25
27
|
/>
|
|
26
28
|
<meta
|
|
27
29
|
property="og:image"
|
|
28
|
-
content={`https://registry-api.tscircuit.com/
|
|
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/
|
|
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
|
|
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
|
-
|
|
48
|
+
Package not found
|
|
43
49
|
</div>
|
|
44
50
|
)}
|
|
45
51
|
{error && error.status !== 404 && (
|
package/src/pages/latest.tsx
CHANGED
|
@@ -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 {
|
|
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:
|
|
36
|
+
data: packages,
|
|
37
37
|
isLoading,
|
|
38
38
|
error,
|
|
39
|
-
} = useQuery<
|
|
40
|
-
["
|
|
39
|
+
} = useQuery<Package[]>(
|
|
40
|
+
["latestPackages", category],
|
|
41
41
|
async () => {
|
|
42
42
|
const params = category !== "all" ? { tag: category } : {}
|
|
43
|
-
const response = await axios.get("/
|
|
44
|
-
return response.data.
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
(
|
|
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
|
|
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
|
|
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
|
|
157
|
+
Error Loading Packages
|
|
158
158
|
</h3>
|
|
159
159
|
<p className="text-red-600">
|
|
160
|
-
We couldn't load the latest
|
|
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
|
-
) :
|
|
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
|
|
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
|
|
175
|
+
? `No packages match your search for "${searchQuery}".`
|
|
176
176
|
: category !== "all"
|
|
177
|
-
? `No ${category}
|
|
178
|
-
: "There are no new
|
|
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
|
-
{
|
|
183
|
+
{filteredPackages
|
|
184
184
|
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
185
|
-
?.map((
|
|
186
|
-
<
|
|
187
|
-
key={
|
|
188
|
-
|
|
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>
|
package/src/pages/quickstart.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { useQuery } from "react-query"
|
|
|
3
3
|
import { useAxios } from "@/hooks/use-axios"
|
|
4
4
|
import Header from "@/components/Header"
|
|
5
5
|
import Footer from "@/components/Footer"
|
|
6
|
-
import { Package
|
|
6
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
7
7
|
import { Button } from "@/components/ui/button"
|
|
8
8
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
9
9
|
import { TypeBadge } from "@/components/TypeBadge"
|
|
@@ -48,7 +48,7 @@ export const QuickstartPage = () => {
|
|
|
48
48
|
<Header />
|
|
49
49
|
<div className="container mx-auto px-4 py-8">
|
|
50
50
|
<div className="mb-8 hidden md:block">
|
|
51
|
-
<h2 className="text-xl font-semibold mb-4">Recent
|
|
51
|
+
<h2 className="text-xl font-semibold mb-4">Recent Packages</h2>
|
|
52
52
|
{isLoading ? (
|
|
53
53
|
<div>Loading...</div>
|
|
54
54
|
) : (
|
|
@@ -63,7 +63,7 @@ export const QuickstartPage = () => {
|
|
|
63
63
|
.map((pkg) => (
|
|
64
64
|
<PrefetchPageLink
|
|
65
65
|
key={pkg.package_id}
|
|
66
|
-
href={`/editor?
|
|
66
|
+
href={`/editor?package_id=${pkg.package_id}`}
|
|
67
67
|
>
|
|
68
68
|
<Card className="hover:shadow-md transition-shadow rounded-md flex flex-col h-full">
|
|
69
69
|
<CardHeader className="pb-0 p-4">
|
|
@@ -85,7 +85,7 @@ export const QuickstartPage = () => {
|
|
|
85
85
|
</div>
|
|
86
86
|
|
|
87
87
|
<div className="mb-8">
|
|
88
|
-
<h2 className="text-xl font-semibold mb-4">Start Blank
|
|
88
|
+
<h2 className="text-xl font-semibold mb-4">Start Blank Packages</h2>
|
|
89
89
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
90
90
|
{blankTemplates.map((template, index) => (
|
|
91
91
|
<PrefetchPageLink
|
|
@@ -117,7 +117,7 @@ export const QuickstartPage = () => {
|
|
|
117
117
|
</div>
|
|
118
118
|
|
|
119
119
|
<div className="mt-12">
|
|
120
|
-
<h2 className="text-xl font-semibold mb-4">Import as
|
|
120
|
+
<h2 className="text-xl font-semibold mb-4">Import as Package</h2>
|
|
121
121
|
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
122
122
|
{[
|
|
123
123
|
{ name: "KiCad Footprint", type: "footprint" },
|
package/src/pages/search.tsx
CHANGED
|
@@ -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
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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
|
|
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
|
-
Search
|
|
100
|
+
Search Packages
|
|
23
101
|
</h1>
|
|
24
102
|
</div>
|
|
25
|
-
<div className="flex flex-
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
<
|
|
39
|
-
|
|
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
|