@tscircuit/fake-snippets 0.0.77 → 0.0.78
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/dist/bundle.js +257 -162
- package/fake-snippets-api/routes/api/packages/generate_from_jlcpcb.ts +111 -0
- package/package.json +1 -1
- package/src/App.tsx +0 -3
- package/src/components/CircuitJsonImportDialog.tsx +20 -3
- package/src/components/JLCPCBImportDialog.tsx +31 -27
- package/src/components/PackageBuildsPage/LogContent.tsx +25 -9
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +5 -5
- package/src/components/PackageBuildsPage/package-build-details-panel.tsx +2 -2
- package/src/components/dialogs/rename-package-dialog.tsx +5 -0
- package/src/components/package-port/CodeEditor.tsx +28 -16
- package/src/hooks/use-current-package-release.ts +6 -2
- package/src/hooks/use-package-release.ts +16 -3
- package/src/pages/latest.tsx +29 -87
- package/src/pages/beta.tsx +0 -367
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import { packageSchema } from "fake-snippets-api/lib/db/schema"
|
|
4
|
+
import {
|
|
5
|
+
fetchEasyEDAComponent,
|
|
6
|
+
convertRawEasyEdaToTs,
|
|
7
|
+
normalizeManufacturerPartNumber,
|
|
8
|
+
EasyEdaJsonSchema,
|
|
9
|
+
} from "easyeda"
|
|
10
|
+
|
|
11
|
+
export default withRouteSpec({
|
|
12
|
+
methods: ["POST"],
|
|
13
|
+
auth: "session",
|
|
14
|
+
jsonBody: z.object({
|
|
15
|
+
jlcpcb_part_number: z.string().min(1, "JLCPCB part number is required"),
|
|
16
|
+
}),
|
|
17
|
+
jsonResponse: z.object({
|
|
18
|
+
ok: z.boolean(),
|
|
19
|
+
package: packageSchema,
|
|
20
|
+
}),
|
|
21
|
+
})(async (req, ctx) => {
|
|
22
|
+
const { jlcpcb_part_number } = req.jsonBody
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const rawEasyJson = await fetchEasyEDAComponent(jlcpcb_part_number)
|
|
26
|
+
|
|
27
|
+
if (!rawEasyJson) {
|
|
28
|
+
return ctx.error(404, {
|
|
29
|
+
error_code: "component_not_found",
|
|
30
|
+
message: `Component with JLCPCB part number ${jlcpcb_part_number} not found`,
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const betterEasy = await EasyEdaJsonSchema.parse(rawEasyJson)
|
|
35
|
+
|
|
36
|
+
const tsxComponent = await convertRawEasyEdaToTs(rawEasyJson)
|
|
37
|
+
|
|
38
|
+
const componentName = normalizeManufacturerPartNumber(
|
|
39
|
+
betterEasy.dataStr.head.c_para["Manufacturer Part"] ?? "",
|
|
40
|
+
)
|
|
41
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-")
|
|
42
|
+
.replace(/--/g, "-")
|
|
43
|
+
|
|
44
|
+
const packageName = `${ctx.auth.github_username}/${componentName}`
|
|
45
|
+
|
|
46
|
+
const existingPackage = ctx.db.packages.find((p) => p.name === packageName)
|
|
47
|
+
|
|
48
|
+
if (existingPackage) {
|
|
49
|
+
return ctx.json({
|
|
50
|
+
ok: true,
|
|
51
|
+
package: existingPackage,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const newPackage = {
|
|
56
|
+
name: packageName,
|
|
57
|
+
unscoped_name: componentName,
|
|
58
|
+
owner_name: ctx.auth.github_username,
|
|
59
|
+
code: tsxComponent,
|
|
60
|
+
created_at: new Date().toISOString(),
|
|
61
|
+
updated_at: new Date().toISOString(),
|
|
62
|
+
description: `Generated from JLCPCB part number ${jlcpcb_part_number}`,
|
|
63
|
+
creator_account_id: ctx.auth.account_id,
|
|
64
|
+
owner_org_id: ctx.auth.personal_org_id,
|
|
65
|
+
owner_github_username: ctx.auth.github_username,
|
|
66
|
+
latest_package_release_id: null,
|
|
67
|
+
latest_package_release_fs_sha: null,
|
|
68
|
+
latest_version: null,
|
|
69
|
+
license: null,
|
|
70
|
+
ai_description: "placeholder ai description",
|
|
71
|
+
ai_usage_instructions: "placeholder ai usage instructions",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const createdPackage = ctx.db.addPackage(newPackage)
|
|
75
|
+
|
|
76
|
+
const createdPackageRelease = ctx.db.addPackageRelease({
|
|
77
|
+
package_id: createdPackage.package_id,
|
|
78
|
+
version: "1.0.0",
|
|
79
|
+
created_at: new Date().toISOString(),
|
|
80
|
+
is_latest: true,
|
|
81
|
+
is_locked: false,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
ctx.db.updatePackage(createdPackage.package_id, {
|
|
85
|
+
latest_package_release_id: createdPackageRelease.package_release_id,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
ctx.db.addPackageFile({
|
|
89
|
+
package_release_id: createdPackageRelease.package_release_id,
|
|
90
|
+
file_path: "index.tsx",
|
|
91
|
+
content_text: String(tsxComponent),
|
|
92
|
+
created_at: new Date().toISOString(),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return ctx.json({
|
|
96
|
+
ok: true,
|
|
97
|
+
package: createdPackage as any,
|
|
98
|
+
})
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
if (String(error).includes("Component not found")) {
|
|
101
|
+
return ctx.error(404, {
|
|
102
|
+
error_code: "component_not_found",
|
|
103
|
+
message: `Component with JLCPCB part number ${jlcpcb_part_number} not found`,
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
return ctx.error(500, {
|
|
107
|
+
error_code: "package_generation_failed",
|
|
108
|
+
message: `Failed to generate package from JLCPCB part: ${error.message || String(error)}`,
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
})
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -66,7 +66,6 @@ const SearchPage = lazyImport(() => import("@/pages/search"))
|
|
|
66
66
|
const SettingsPage = lazyImport(() => import("@/pages/settings"))
|
|
67
67
|
const UserProfilePage = lazyImport(() => import("@/pages/user-profile"))
|
|
68
68
|
const DevLoginPage = lazyImport(() => import("@/pages/dev-login"))
|
|
69
|
-
const BetaPage = lazyImport(() => import("@/pages/beta"))
|
|
70
69
|
const ViewPackagePage = lazyImport(() => import("@/pages/view-package"))
|
|
71
70
|
const PackageBuildsPage = lazyImport(() => import("@/pages/package-builds"))
|
|
72
71
|
const TrendingPage = lazyImport(() => import("@/pages/trending"))
|
|
@@ -106,8 +105,6 @@ function App() {
|
|
|
106
105
|
<Suspense fallback={<FullPageLoader />}>
|
|
107
106
|
<Switch>
|
|
108
107
|
<Route path="/" component={LandingPage} />
|
|
109
|
-
<Route path="/beta" component={BetaPage} />
|
|
110
|
-
<Route path="/beta/:author/:packageName" component={BetaPage} />
|
|
111
108
|
<Route
|
|
112
109
|
path="/view-package/:author/:packageName"
|
|
113
110
|
component={ViewPackagePage}
|
|
@@ -174,14 +174,31 @@ export function CircuitJsonImportDialog({
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
177
|
-
if (event.ctrlKey && event.key === "Enter") {
|
|
178
|
-
|
|
177
|
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
|
178
|
+
event.preventDefault()
|
|
179
|
+
if (!isLoading && isLoggedIn && (circuitJson.trim() || file)) {
|
|
180
|
+
handleImport()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const handleDialogKeyDown = (event: React.KeyboardEvent) => {
|
|
186
|
+
if (
|
|
187
|
+
event.key === "Enter" &&
|
|
188
|
+
!event.ctrlKey &&
|
|
189
|
+
!event.metaKey &&
|
|
190
|
+
event.target !== document.querySelector("textarea")
|
|
191
|
+
) {
|
|
192
|
+
event.preventDefault()
|
|
193
|
+
if (!isLoading && isLoggedIn && (circuitJson.trim() || file)) {
|
|
194
|
+
handleImport()
|
|
195
|
+
}
|
|
179
196
|
}
|
|
180
197
|
}
|
|
181
198
|
|
|
182
199
|
return (
|
|
183
200
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
184
|
-
<DialogContent>
|
|
201
|
+
<DialogContent onKeyDown={handleDialogKeyDown}>
|
|
185
202
|
<DialogHeader>
|
|
186
203
|
<DialogTitle>Import Circuit JSON</DialogTitle>
|
|
187
204
|
<DialogDescription>
|
|
@@ -50,31 +50,16 @@ export function JLCPCBImportDialog({
|
|
|
50
50
|
setHasBeenImportedToAccountAlready(false)
|
|
51
51
|
|
|
52
52
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
const existingPackageRes = await axios.get(apiUrl, {
|
|
56
|
-
validateStatus: (status) => true,
|
|
53
|
+
const response = await axios.post("/packages/generate_from_jlcpcb", {
|
|
54
|
+
jlcpcb_part_number: partNumber,
|
|
57
55
|
})
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
setHasBeenImportedToAccountAlready(true)
|
|
56
|
+
if (!response.data.ok) {
|
|
57
|
+
setError("Failed to generate package from JLCPCB part")
|
|
61
58
|
setIsLoading(false)
|
|
62
59
|
return
|
|
63
60
|
}
|
|
64
61
|
|
|
65
|
-
const
|
|
66
|
-
.post("/snippets/generate_from_jlcpcb", {
|
|
67
|
-
jlcpcb_part_number: partNumber,
|
|
68
|
-
})
|
|
69
|
-
.catch((e) => e)
|
|
70
|
-
|
|
71
|
-
const { snippet, error } = response.data
|
|
72
|
-
|
|
73
|
-
if (error) {
|
|
74
|
-
setError(error.message)
|
|
75
|
-
setIsLoading(false)
|
|
76
|
-
return
|
|
77
|
-
}
|
|
62
|
+
const { package: generatedPackage } = response.data
|
|
78
63
|
|
|
79
64
|
toast({
|
|
80
65
|
title: "Import Successful",
|
|
@@ -82,13 +67,21 @@ export function JLCPCBImportDialog({
|
|
|
82
67
|
})
|
|
83
68
|
|
|
84
69
|
onOpenChange(false)
|
|
85
|
-
navigate(`/editor?package_id=${
|
|
86
|
-
} catch (error) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
70
|
+
navigate(`/editor?package_id=${generatedPackage.package_id}`)
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
if (error.response?.status === 404) {
|
|
73
|
+
setError(`Component with JLCPCB part number ${partNumber} not found`)
|
|
74
|
+
} else if (error.response?.data?.message) {
|
|
75
|
+
setError(error.response.data.message)
|
|
76
|
+
} else {
|
|
77
|
+
setError("Failed to import the JLCPCB component. Please try again.")
|
|
78
|
+
toast({
|
|
79
|
+
title: "Import Failed",
|
|
80
|
+
description:
|
|
81
|
+
"Failed to import the JLCPCB component. Please try again.",
|
|
82
|
+
variant: "destructive",
|
|
83
|
+
})
|
|
84
|
+
}
|
|
92
85
|
} finally {
|
|
93
86
|
setIsLoading(false)
|
|
94
87
|
}
|
|
@@ -116,11 +109,22 @@ export function JLCPCBImportDialog({
|
|
|
116
109
|
className="mt-3"
|
|
117
110
|
placeholder="Enter JLCPCB part number (e.g., C46749)"
|
|
118
111
|
value={partNumber}
|
|
112
|
+
disabled={isLoading}
|
|
119
113
|
onChange={(e) => {
|
|
120
114
|
setPartNumber(e.target.value)
|
|
121
115
|
setError(null)
|
|
122
116
|
setHasBeenImportedToAccountAlready(false)
|
|
123
117
|
}}
|
|
118
|
+
onKeyDown={(e) => {
|
|
119
|
+
if (
|
|
120
|
+
e.key === "Enter" &&
|
|
121
|
+
!isLoading &&
|
|
122
|
+
isLoggedIn &&
|
|
123
|
+
partNumber.trim()
|
|
124
|
+
) {
|
|
125
|
+
handleImport()
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
124
128
|
/>
|
|
125
129
|
{error && !hasBeenImportedToAccountAlready && (
|
|
126
130
|
<p className="bg-red-100 p-2 mt-2 pre-wrap">{error}</p>
|
|
@@ -14,17 +14,33 @@ const getErrorText = (error: ErrorObject | string) => {
|
|
|
14
14
|
export const LogContent = ({
|
|
15
15
|
logs,
|
|
16
16
|
error,
|
|
17
|
-
}: {
|
|
17
|
+
}: {
|
|
18
|
+
logs: Array<{
|
|
19
|
+
type: "info" | "success" | "error"
|
|
20
|
+
message: string
|
|
21
|
+
timestamp: string
|
|
22
|
+
}>
|
|
23
|
+
error?: ErrorObject | string | null
|
|
24
|
+
}) => {
|
|
18
25
|
return (
|
|
19
26
|
<div className="whitespace-pre-wrap font-mono text-xs">
|
|
20
|
-
{logs.map(
|
|
21
|
-
log
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
{logs.map(
|
|
28
|
+
(log, i) =>
|
|
29
|
+
log.timestamp &&
|
|
30
|
+
log.message && (
|
|
31
|
+
<div
|
|
32
|
+
key={i}
|
|
33
|
+
className={
|
|
34
|
+
log.type === "error"
|
|
35
|
+
? "text-red-600"
|
|
36
|
+
: log.type === "success"
|
|
37
|
+
? "text-green-600"
|
|
38
|
+
: "text-gray-600"
|
|
39
|
+
}
|
|
40
|
+
>
|
|
41
|
+
{new Date(log.timestamp).toLocaleTimeString()} {log.message}
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
28
44
|
)}
|
|
29
45
|
{error && <div className="text-red-600">{getErrorText(error)}</div>}
|
|
30
46
|
</div>
|
|
@@ -18,7 +18,7 @@ function computeDuration(
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const PackageBuildDetailsPage = () => {
|
|
21
|
-
const { packageRelease } = useCurrentPackageRelease()
|
|
21
|
+
const { packageRelease } = useCurrentPackageRelease({ include_logs: true })
|
|
22
22
|
const [openSections, setOpenSections] = useState<Record<string, boolean>>({})
|
|
23
23
|
|
|
24
24
|
const {
|
|
@@ -79,8 +79,8 @@ export const PackageBuildDetailsPage = () => {
|
|
|
79
79
|
>
|
|
80
80
|
<LogContent
|
|
81
81
|
logs={
|
|
82
|
-
|
|
83
|
-
{
|
|
82
|
+
transpilation_logs ?? [
|
|
83
|
+
{ message: "No transpilation logs available" },
|
|
84
84
|
]
|
|
85
85
|
}
|
|
86
86
|
error={transpilation_error}
|
|
@@ -100,8 +100,8 @@ export const PackageBuildDetailsPage = () => {
|
|
|
100
100
|
>
|
|
101
101
|
<LogContent
|
|
102
102
|
logs={
|
|
103
|
-
|
|
104
|
-
{
|
|
103
|
+
circuit_json_build_logs ?? [
|
|
104
|
+
{ message: "No Circuit JSON logs available" },
|
|
105
105
|
]
|
|
106
106
|
}
|
|
107
107
|
error={circuit_json_build_error!}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Globe, Clock } from "lucide-react"
|
|
1
|
+
import { Globe, GitBranch, GitCommit, Clock } from "lucide-react"
|
|
2
2
|
import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
|
|
3
3
|
import { useParams } from "wouter"
|
|
4
4
|
import { timeAgo } from "@/lib/utils/timeAgo"
|
|
@@ -66,7 +66,7 @@ export function PackageBuildDetailsPanel() {
|
|
|
66
66
|
</div>
|
|
67
67
|
<span className="text-sm">{author}</span>
|
|
68
68
|
<span className="text-sm text-gray-500">
|
|
69
|
-
{timeAgo(
|
|
69
|
+
{timeAgo(created_at, "")}
|
|
70
70
|
</span>
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
@@ -66,6 +66,11 @@ export const RenamePackageDialog = ({
|
|
|
66
66
|
onChange={(e) => setNewName(e.target.value.replace(" ", "").trim())}
|
|
67
67
|
placeholder="Enter new name"
|
|
68
68
|
disabled={renamePackageMutation.isLoading}
|
|
69
|
+
onKeyDown={(e) => {
|
|
70
|
+
if (e.key === "Enter" && !renamePackageMutation.isLoading) {
|
|
71
|
+
renamePackageMutation.mutate()
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
69
74
|
/>
|
|
70
75
|
<Button
|
|
71
76
|
disabled={renamePackageMutation.isLoading}
|
|
@@ -9,18 +9,15 @@ import { Decoration, hoverTooltip, keymap } from "@codemirror/view"
|
|
|
9
9
|
import { getImportsFromCode } from "@tscircuit/prompt-benchmarks/code-runner-utils"
|
|
10
10
|
import type { ATABootstrapConfig } from "@typescript/ata"
|
|
11
11
|
import { setupTypeAcquisition } from "@typescript/ata"
|
|
12
|
+
import { linter } from "@codemirror/lint"
|
|
12
13
|
import { TSCI_PACKAGE_PATTERN } from "@/lib/constants"
|
|
13
14
|
import {
|
|
14
15
|
createDefaultMapFromCDN,
|
|
15
16
|
createSystem,
|
|
16
17
|
createVirtualTypeScriptEnvironment,
|
|
17
18
|
} from "@typescript/vfs"
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
tsFacet,
|
|
21
|
-
tsLinter,
|
|
22
|
-
tsSync,
|
|
23
|
-
} from "@valtown/codemirror-ts"
|
|
19
|
+
import { tsAutocomplete, tsFacet, tsSync } from "@valtown/codemirror-ts"
|
|
20
|
+
import { getLints } from "@valtown/codemirror-ts"
|
|
24
21
|
import { EditorView } from "codemirror"
|
|
25
22
|
import { useEffect, useMemo, useRef, useState } from "react"
|
|
26
23
|
import tsModule from "typescript"
|
|
@@ -75,6 +72,7 @@ export const CodeEditor = ({
|
|
|
75
72
|
const editorRef = useRef<HTMLDivElement>(null)
|
|
76
73
|
const viewRef = useRef<EditorView | null>(null)
|
|
77
74
|
const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
|
|
75
|
+
const lastReceivedTsFileTimeRef = useRef<number>(0)
|
|
78
76
|
const apiUrl = useSnippetsBaseApiUrl()
|
|
79
77
|
const codeCompletionApi = useCodeCompletionApi()
|
|
80
78
|
const [cursorPosition, setCursorPosition] = useState<number | null>(null)
|
|
@@ -218,16 +216,12 @@ export const CodeEditor = ({
|
|
|
218
216
|
receivedFile: (code: string, path: string) => {
|
|
219
217
|
fsMap.set(path, code)
|
|
220
218
|
env.createFile(path, code)
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
changes: {
|
|
224
|
-
from: 0,
|
|
225
|
-
to: viewRef.current.state.doc.length,
|
|
226
|
-
insert: viewRef.current.state.doc.toString(),
|
|
227
|
-
},
|
|
228
|
-
selection: viewRef.current.state.selection,
|
|
229
|
-
})
|
|
219
|
+
if (/\.tsx?$|\.d\.ts$/.test(path)) {
|
|
220
|
+
lastReceivedTsFileTimeRef.current = Date.now()
|
|
230
221
|
}
|
|
222
|
+
// Avoid dispatching a view update when ATA downloads files. Dispatching
|
|
223
|
+
// here caused the editor to reset the user's selection, which made text
|
|
224
|
+
// selection impossible while dependencies were loading.
|
|
231
225
|
},
|
|
232
226
|
},
|
|
233
227
|
}
|
|
@@ -292,7 +286,18 @@ export const CodeEditor = ({
|
|
|
292
286
|
: currentFile,
|
|
293
287
|
}),
|
|
294
288
|
tsSync(),
|
|
295
|
-
|
|
289
|
+
linter(async (view) => {
|
|
290
|
+
if (Date.now() - lastReceivedTsFileTimeRef.current < 3000) {
|
|
291
|
+
return []
|
|
292
|
+
}
|
|
293
|
+
const config = view.state.facet(tsFacet)
|
|
294
|
+
return config
|
|
295
|
+
? getLints({
|
|
296
|
+
...config,
|
|
297
|
+
diagnosticCodesToIgnore: [],
|
|
298
|
+
})
|
|
299
|
+
: []
|
|
300
|
+
}),
|
|
296
301
|
autocompletion({ override: [tsAutocomplete()] }),
|
|
297
302
|
hoverTooltip((view, pos) => {
|
|
298
303
|
const line = view.state.doc.lineAt(pos)
|
|
@@ -386,6 +391,13 @@ export const CodeEditor = ({
|
|
|
386
391
|
}
|
|
387
392
|
return false
|
|
388
393
|
},
|
|
394
|
+
keydown: (event) => {
|
|
395
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
|
|
396
|
+
event.preventDefault()
|
|
397
|
+
return true
|
|
398
|
+
}
|
|
399
|
+
return false
|
|
400
|
+
},
|
|
389
401
|
}),
|
|
390
402
|
EditorView.theme({
|
|
391
403
|
".cm-tooltip-hover": {
|
|
@@ -3,7 +3,9 @@ import { useCurrentPackageId } from "./use-current-package-id"
|
|
|
3
3
|
import { useUrlParams } from "./use-url-params"
|
|
4
4
|
import { usePackageRelease } from "./use-package-release"
|
|
5
5
|
|
|
6
|
-
export const useCurrentPackageRelease = (
|
|
6
|
+
export const useCurrentPackageRelease = (options?: {
|
|
7
|
+
include_logs: boolean
|
|
8
|
+
}) => {
|
|
7
9
|
const { packageId } = useCurrentPackageId()
|
|
8
10
|
const urlParams = useUrlParams()
|
|
9
11
|
const { author, packageName } = useParams()
|
|
@@ -23,6 +25,8 @@ export const useCurrentPackageRelease = () => {
|
|
|
23
25
|
query = { package_id: packageId, is_latest: true }
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const { data: packageRelease, ...rest } = usePackageRelease(query
|
|
28
|
+
const { data: packageRelease, ...rest } = usePackageRelease(query, {
|
|
29
|
+
include_logs: options?.include_logs ?? false,
|
|
30
|
+
})
|
|
27
31
|
return { packageRelease, ...rest }
|
|
28
32
|
}
|
|
@@ -18,15 +18,28 @@ type PackageReleaseQuery =
|
|
|
18
18
|
is_latest: boolean
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const usePackageRelease = (
|
|
21
|
+
export const usePackageRelease = (
|
|
22
|
+
query: PackageReleaseQuery | null,
|
|
23
|
+
options?: { include_logs: boolean },
|
|
24
|
+
) => {
|
|
22
25
|
const axios = useAxios()
|
|
23
26
|
|
|
24
27
|
return useQuery<PackageRelease, Error & { status: number }>(
|
|
25
|
-
["packageRelease", query],
|
|
28
|
+
["packageRelease", query, options?.include_logs],
|
|
26
29
|
async () => {
|
|
27
30
|
if (!query) return
|
|
28
31
|
|
|
29
|
-
const { data } = await axios.post(
|
|
32
|
+
const { data } = await axios.post(
|
|
33
|
+
"/package_releases/get",
|
|
34
|
+
query,
|
|
35
|
+
options?.include_logs
|
|
36
|
+
? {
|
|
37
|
+
params: {
|
|
38
|
+
include_logs: true,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
: undefined,
|
|
42
|
+
)
|
|
30
43
|
|
|
31
44
|
if (!data.package_release) {
|
|
32
45
|
throw new Error("Package release not found")
|
package/src/pages/latest.tsx
CHANGED
|
@@ -4,17 +4,8 @@ import { useAxios } from "@/hooks/use-axios"
|
|
|
4
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
|
-
import {
|
|
8
|
-
Search,
|
|
9
|
-
Tag,
|
|
10
|
-
Calendar,
|
|
11
|
-
Keyboard,
|
|
12
|
-
Cpu,
|
|
13
|
-
Layers,
|
|
14
|
-
LucideBellElectric,
|
|
15
|
-
} from "lucide-react"
|
|
7
|
+
import { Search, Keyboard, Cpu, Layers, LucideBellElectric } from "lucide-react"
|
|
16
8
|
import { Input } from "@/components/ui/input"
|
|
17
|
-
import { Badge } from "@/components/ui/badge"
|
|
18
9
|
import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
|
|
19
10
|
import {
|
|
20
11
|
Select,
|
|
@@ -23,8 +14,7 @@ import {
|
|
|
23
14
|
SelectTrigger,
|
|
24
15
|
SelectValue,
|
|
25
16
|
} from "@/components/ui/select"
|
|
26
|
-
import
|
|
27
|
-
import { PackageCard } from "@/components/PackageCard"
|
|
17
|
+
import PackageSearchResults from "@/components/PackageSearchResults"
|
|
28
18
|
|
|
29
19
|
const LatestPage: React.FC = () => {
|
|
30
20
|
const axios = useAxios()
|
|
@@ -48,22 +38,24 @@ const LatestPage: React.FC = () => {
|
|
|
48
38
|
},
|
|
49
39
|
)
|
|
50
40
|
|
|
51
|
-
const filteredPackages = packages
|
|
52
|
-
|
|
41
|
+
const filteredPackages = packages
|
|
42
|
+
?.filter((pkg) => {
|
|
43
|
+
if (!searchQuery) return true
|
|
53
44
|
|
|
54
|
-
|
|
45
|
+
const query = searchQuery.toLowerCase().trim()
|
|
55
46
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
47
|
+
const searchableFields = [
|
|
48
|
+
pkg.unscoped_name.toLowerCase(),
|
|
49
|
+
pkg.owner_github_username?.toLowerCase() ?? "",
|
|
50
|
+
(pkg.description || "").toLowerCase(),
|
|
51
|
+
]
|
|
61
52
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
53
|
+
return searchableFields.some((field) => {
|
|
54
|
+
const queryWords = query.split(/\s+/).filter((word) => word.length > 0)
|
|
55
|
+
return queryWords.every((word) => field.includes(word))
|
|
56
|
+
})
|
|
65
57
|
})
|
|
66
|
-
|
|
58
|
+
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
67
59
|
|
|
68
60
|
return (
|
|
69
61
|
<div className="min-h-screen flex flex-col bg-gray-50">
|
|
@@ -71,7 +63,6 @@ const LatestPage: React.FC = () => {
|
|
|
71
63
|
<main className="flex-grow container mx-auto px-4 py-8">
|
|
72
64
|
<div className="mb-8 max-w-3xl">
|
|
73
65
|
<div className="flex items-center gap-2 mb-3">
|
|
74
|
-
<Calendar className="w-6 h-6 text-blue-500" />
|
|
75
66
|
<h1 className="text-4xl font-bold text-gray-900">
|
|
76
67
|
Latest Packages
|
|
77
68
|
</h1>
|
|
@@ -81,16 +72,6 @@ const LatestPage: React.FC = () => {
|
|
|
81
72
|
additions showcase new ideas and innovative approaches to circuit
|
|
82
73
|
design.
|
|
83
74
|
</p>
|
|
84
|
-
<div className="flex flex-wrap gap-3">
|
|
85
|
-
<Badge variant="secondary" className="px-3 py-1">
|
|
86
|
-
<Tag className="w-3.5 h-3.5 mr-1" />
|
|
87
|
-
<span>Latest Uploads</span>
|
|
88
|
-
</Badge>
|
|
89
|
-
<Badge variant="secondary" className="px-3 py-1">
|
|
90
|
-
<Calendar className="w-3.5 h-3.5 mr-1" />
|
|
91
|
-
<span>Most Recent First</span>
|
|
92
|
-
</Badge>
|
|
93
|
-
</div>
|
|
94
75
|
</div>
|
|
95
76
|
|
|
96
77
|
<div className="mb-6">
|
|
@@ -139,58 +120,19 @@ const LatestPage: React.FC = () => {
|
|
|
139
120
|
</Select>
|
|
140
121
|
</div>
|
|
141
122
|
</div>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<div>
|
|
156
|
-
<h3 className="text-lg font-semibold mb-2">
|
|
157
|
-
Error Loading Packages
|
|
158
|
-
</h3>
|
|
159
|
-
<p className="text-red-600">
|
|
160
|
-
We couldn't load the latest packages. Please try again later.
|
|
161
|
-
</p>
|
|
162
|
-
</div>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
) : filteredPackages?.length === 0 ? (
|
|
166
|
-
<div className="text-center py-12 px-4">
|
|
167
|
-
<div className="bg-slate-50 inline-flex rounded-full p-4 mb-4">
|
|
168
|
-
<Search className="w-8 h-8 text-slate-400" />
|
|
169
|
-
</div>
|
|
170
|
-
<h3 className="text-xl font-medium text-slate-900 mb-2">
|
|
171
|
-
No Matching Packages
|
|
172
|
-
</h3>
|
|
173
|
-
<p className="text-slate-500 max-w-md mx-auto mb-6">
|
|
174
|
-
{searchQuery
|
|
175
|
-
? `No packages match your search for "${searchQuery}".`
|
|
176
|
-
: category !== "all"
|
|
177
|
-
? `No ${category} packages found in the latest list.`
|
|
178
|
-
: "There are no new packages at the moment."}
|
|
179
|
-
</p>
|
|
180
|
-
</div>
|
|
181
|
-
) : (
|
|
182
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
183
|
-
{filteredPackages
|
|
184
|
-
?.sort((a, b) => b.created_at.localeCompare(a.created_at))
|
|
185
|
-
?.map((pkg) => (
|
|
186
|
-
<PackageCard
|
|
187
|
-
key={pkg.package_id}
|
|
188
|
-
pkg={pkg}
|
|
189
|
-
baseUrl={apiBaseUrl}
|
|
190
|
-
/>
|
|
191
|
-
))}
|
|
192
|
-
</div>
|
|
193
|
-
)}
|
|
123
|
+
<PackageSearchResults
|
|
124
|
+
isLoading={isLoading}
|
|
125
|
+
error={error}
|
|
126
|
+
filteredPackages={filteredPackages}
|
|
127
|
+
apiBaseUrl={apiBaseUrl}
|
|
128
|
+
emptyStateMessage={
|
|
129
|
+
searchQuery
|
|
130
|
+
? `No packages match your search for "${searchQuery}".`
|
|
131
|
+
: category !== "all"
|
|
132
|
+
? `No ${category} packages found in the latest list.`
|
|
133
|
+
: "There are no new packages at the moment."
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
194
136
|
</main>
|
|
195
137
|
<Footer />
|
|
196
138
|
</div>
|