@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
|
@@ -4,29 +4,10 @@ import { z } from "zod"
|
|
|
4
4
|
export default withRouteSpec({
|
|
5
5
|
methods: ["POST"],
|
|
6
6
|
auth: "session",
|
|
7
|
-
jsonBody: z
|
|
8
|
-
.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
vendor_name: z.string(),
|
|
12
|
-
})
|
|
13
|
-
.superRefine((data, ctx) => {
|
|
14
|
-
if (data.circuit_json && data.package_release_id) {
|
|
15
|
-
ctx.addIssue({
|
|
16
|
-
code: z.ZodIssueCode.custom,
|
|
17
|
-
message:
|
|
18
|
-
"You must provide either circuit_json or package_release_id, but not both.",
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
if (!data.circuit_json && !data.package_release_id) {
|
|
22
|
-
ctx.addIssue({
|
|
23
|
-
code: z.ZodIssueCode.custom,
|
|
24
|
-
message:
|
|
25
|
-
"You must provide either circuit_json or package_release_id.",
|
|
26
|
-
})
|
|
27
|
-
}
|
|
28
|
-
}),
|
|
29
|
-
|
|
7
|
+
jsonBody: z.object({
|
|
8
|
+
package_release_id: z.string(),
|
|
9
|
+
vendor_name: z.literal("jlcpcb"),
|
|
10
|
+
}),
|
|
30
11
|
jsonResponse: z.object({
|
|
31
12
|
order_quote_id: z.string().optional(),
|
|
32
13
|
error: z
|
|
@@ -37,22 +18,34 @@ export default withRouteSpec({
|
|
|
37
18
|
.optional(),
|
|
38
19
|
}),
|
|
39
20
|
})(async (req, ctx) => {
|
|
40
|
-
const { package_release_id, vendor_name
|
|
21
|
+
const { package_release_id, vendor_name } = req.jsonBody
|
|
22
|
+
|
|
23
|
+
// check package release exists
|
|
24
|
+
const packageRelease = ctx.db.getPackageReleaseById(package_release_id)
|
|
25
|
+
if (!packageRelease) {
|
|
26
|
+
return ctx.json(
|
|
27
|
+
{
|
|
28
|
+
error: {
|
|
29
|
+
error_code: "package_release_not_found",
|
|
30
|
+
message: "Package release not found",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{ status: 404 },
|
|
34
|
+
)
|
|
35
|
+
}
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
message: "Package release not found",
|
|
51
|
-
},
|
|
37
|
+
const packageReleaseFiles =
|
|
38
|
+
ctx.db.getPackageFilesByReleaseId(package_release_id)
|
|
39
|
+
if (packageReleaseFiles.length === 0) {
|
|
40
|
+
return ctx.json(
|
|
41
|
+
{
|
|
42
|
+
error: {
|
|
43
|
+
error_code: "package_release_files_not_found",
|
|
44
|
+
message: "Package release files not found",
|
|
52
45
|
},
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
},
|
|
47
|
+
{ status: 404 },
|
|
48
|
+
)
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
const orderQuoteId = ctx.db.addOrderQuote({
|
|
@@ -9,8 +9,7 @@ export default withRouteSpec({
|
|
|
9
9
|
order_quote_id: z.string(),
|
|
10
10
|
}),
|
|
11
11
|
jsonResponse: z.object({
|
|
12
|
-
order_quote: orderQuoteSchema
|
|
13
|
-
error: z.string().optional(),
|
|
12
|
+
order_quote: orderQuoteSchema,
|
|
14
13
|
}),
|
|
15
14
|
})(async (req, ctx) => {
|
|
16
15
|
const { order_quote_id } = req.commonParams
|
|
@@ -18,12 +17,10 @@ export default withRouteSpec({
|
|
|
18
17
|
const orderQuote = ctx.db.getOrderQuoteById(order_quote_id)
|
|
19
18
|
|
|
20
19
|
if (!orderQuote) {
|
|
21
|
-
return ctx.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ status: 404 },
|
|
26
|
-
)
|
|
20
|
+
return ctx.error(404, {
|
|
21
|
+
error_code: "order_quote_not_found",
|
|
22
|
+
message: "Order quote not found",
|
|
23
|
+
})
|
|
27
24
|
}
|
|
28
25
|
|
|
29
26
|
return ctx.json({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/fake-snippets",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.68",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"preview": "vite preview",
|
|
25
25
|
"format": "biome format --write .",
|
|
26
26
|
"lint": "biome format .",
|
|
27
|
+
"build:vite:analyze": "VITE_BUNDLE_ANALYZE=true vite build",
|
|
27
28
|
"build:fake-api:tsup": "tsup-node ./fake-snippets-api/lib/index.ts --format esm --dts",
|
|
28
29
|
"build:fake-api:bundle": "winterspec bundle -o dist/bundle.js",
|
|
29
30
|
"build:fake-api": "bun run build:fake-api:tsup && bun run build:fake-api:bundle && bun run build:fake-api:schema",
|
|
@@ -69,8 +70,7 @@
|
|
|
69
70
|
"@radix-ui/react-toggle": "^1.1.0",
|
|
70
71
|
"@radix-ui/react-toggle-group": "^1.1.0",
|
|
71
72
|
"@radix-ui/react-tooltip": "^1.1.2",
|
|
72
|
-
"@tscircuit/
|
|
73
|
-
"@tscircuit/eval": "^0.0.170",
|
|
73
|
+
"@tscircuit/eval": "^0.0.198",
|
|
74
74
|
"@tscircuit/footprinter": "^0.0.124",
|
|
75
75
|
"@tscircuit/layout": "^0.0.29",
|
|
76
76
|
"@tscircuit/math-utils": "^0.0.10",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"@tailwindcss/typography": "^0.5.16",
|
|
148
148
|
"@tscircuit/core": "^0.0.384",
|
|
149
149
|
"@tscircuit/prompt-benchmarks": "^0.0.28",
|
|
150
|
-
"@tscircuit/runframe": "^0.0.
|
|
150
|
+
"@tscircuit/runframe": "^0.0.485",
|
|
151
151
|
"@types/babel__standalone": "^7.1.7",
|
|
152
152
|
"@types/bun": "^1.1.10",
|
|
153
153
|
"@types/country-list": "^2.1.4",
|
package/src/App.tsx
CHANGED
|
@@ -49,7 +49,6 @@ const lazyImport = (importFn: () => Promise<any>) =>
|
|
|
49
49
|
}
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
const AiPage = lazyImport(() => import("@/pages/ai"))
|
|
53
52
|
const AuthenticatePage = lazyImport(() => import("@/pages/authorize"))
|
|
54
53
|
const DashboardPage = lazyImport(() => import("@/pages/dashboard"))
|
|
55
54
|
const EditorPage = lazyImport(async () => {
|
|
@@ -62,13 +61,10 @@ const EditorPage = lazyImport(async () => {
|
|
|
62
61
|
const LandingPage = lazyImport(() => import("@/pages/landing"))
|
|
63
62
|
const MyOrdersPage = lazyImport(() => import("@/pages/my-orders"))
|
|
64
63
|
const LatestPage = lazyImport(() => import("@/pages/latest"))
|
|
65
|
-
const PreviewPage = lazyImport(() => import("@/pages/preview"))
|
|
66
64
|
const QuickstartPage = lazyImport(() => import("@/pages/quickstart"))
|
|
67
65
|
const SearchPage = lazyImport(() => import("@/pages/search"))
|
|
68
66
|
const SettingsPage = lazyImport(() => import("@/pages/settings"))
|
|
69
67
|
const UserProfilePage = lazyImport(() => import("@/pages/user-profile"))
|
|
70
|
-
const ViewOrderPage = lazyImport(() => import("@/pages/view-order"))
|
|
71
|
-
const ViewSnippetPage = lazyImport(() => import("@/pages/view-snippet"))
|
|
72
68
|
const DevLoginPage = lazyImport(() => import("@/pages/dev-login"))
|
|
73
69
|
const BetaPage = lazyImport(() => import("@/pages/beta"))
|
|
74
70
|
const ViewPackagePage = lazyImport(() => import("@/pages/view-package"))
|
|
@@ -119,22 +115,15 @@ function App() {
|
|
|
119
115
|
<Route path="/legacy-editor" component={EditorPage} />
|
|
120
116
|
<Route path="/quickstart" component={QuickstartPage} />
|
|
121
117
|
<Route path="/dashboard" component={DashboardPage} />
|
|
122
|
-
<Route path="/ai" component={AiPage} />
|
|
123
118
|
<Route path="/latest" component={LatestPage} />
|
|
124
119
|
<Route path="/settings" component={SettingsPage} />
|
|
125
120
|
<Route path="/search" component={SearchPage} />
|
|
126
121
|
<Route path="/trending" component={TrendingPage} />
|
|
127
122
|
<Route path="/authorize" component={AuthenticatePage} />
|
|
128
123
|
<Route path="/my-orders" component={MyOrdersPage} />
|
|
129
|
-
<Route path="/orders/:orderId" component={ViewOrderPage} />
|
|
130
|
-
<Route path="/preview" component={PreviewPage} />
|
|
131
124
|
<Route path="/dev-login" component={DevLoginPage} />
|
|
132
125
|
<Route path="/:username" component={UserProfilePage} />
|
|
133
126
|
<Route path="/:author/:packageName" component={ViewPackagePage} />
|
|
134
|
-
<Route
|
|
135
|
-
path="/snippets/:author/:snippetName"
|
|
136
|
-
component={ViewSnippetPage}
|
|
137
|
-
/>
|
|
138
127
|
<Route component={lazyImport(() => import("@/pages/404"))} />
|
|
139
128
|
</Switch>
|
|
140
129
|
</Suspense>
|
package/src/ContextProviders.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { HelmetProvider } from "react-helmet-async"
|
|
|
3
3
|
import { useEffect } from "react"
|
|
4
4
|
import { useGlobalStore } from "./hooks/use-global-store"
|
|
5
5
|
import { posthog } from "./lib/posthog"
|
|
6
|
+
import { Toaster } from "react-hot-toast"
|
|
6
7
|
|
|
7
8
|
const staffGithubUsernames = [
|
|
8
9
|
"imrishabh18",
|
|
@@ -61,6 +62,7 @@ export const ContextProviders = ({ children }: any) => {
|
|
|
61
62
|
<HelmetProvider>
|
|
62
63
|
<PostHogIdentifier />
|
|
63
64
|
{children}
|
|
65
|
+
<Toaster />
|
|
64
66
|
</HelmetProvider>
|
|
65
67
|
</QueryClientProvider>
|
|
66
68
|
)
|
|
@@ -7,7 +7,7 @@ import { Package, Snippet } from "fake-snippets-api/lib/db/schema"
|
|
|
7
7
|
import React from "react"
|
|
8
8
|
import { useQuery } from "react-query"
|
|
9
9
|
|
|
10
|
-
type SnippetType = "board" | "package" | "model" | "footprint"
|
|
10
|
+
type SnippetType = "board" | "package" | "model" | "footprint"
|
|
11
11
|
|
|
12
12
|
interface Template {
|
|
13
13
|
name: string
|
|
@@ -31,13 +31,13 @@ const CmdKMenu = () => {
|
|
|
31
31
|
|
|
32
32
|
// Search results query
|
|
33
33
|
const { data: searchResults = [], isLoading: isSearching } = useQuery(
|
|
34
|
-
["
|
|
34
|
+
["packageSearch", searchQuery],
|
|
35
35
|
async () => {
|
|
36
36
|
if (!searchQuery) return []
|
|
37
|
-
const { data } = await axios.
|
|
38
|
-
|
|
37
|
+
const { data } = await axios.post("/packages/search", {
|
|
38
|
+
query: searchQuery,
|
|
39
39
|
})
|
|
40
|
-
return data.
|
|
40
|
+
return data.packages || []
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
enabled: Boolean(searchQuery),
|
|
@@ -114,7 +114,7 @@ const CmdKMenu = () => {
|
|
|
114
114
|
/>
|
|
115
115
|
</svg>
|
|
116
116
|
<Command.Input
|
|
117
|
-
placeholder="Search
|
|
117
|
+
placeholder="Search packages and commands..."
|
|
118
118
|
value={searchQuery}
|
|
119
119
|
onValueChange={setSearchQuery}
|
|
120
120
|
className="w-full h-12 bg-transparent border-none outline-none text-gray-900 dark:text-gray-100 placeholder-gray-500"
|
|
@@ -133,27 +133,27 @@ const CmdKMenu = () => {
|
|
|
133
133
|
heading="Search Results"
|
|
134
134
|
className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
135
135
|
>
|
|
136
|
-
{searchResults.map((
|
|
136
|
+
{searchResults.map((pkg: Package) => (
|
|
137
137
|
<Command.Item
|
|
138
|
-
key={
|
|
139
|
-
value={
|
|
138
|
+
key={pkg.package_id}
|
|
139
|
+
value={pkg.name}
|
|
140
140
|
onSelect={() => {
|
|
141
|
-
window.location.href = `/editor?
|
|
141
|
+
window.location.href = `/editor?package_id=${pkg.package_id}`
|
|
142
142
|
setOpen(false)
|
|
143
143
|
}}
|
|
144
144
|
className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
|
|
145
145
|
>
|
|
146
146
|
<div className="flex flex-col">
|
|
147
147
|
<span className="text-gray-900 dark:text-gray-100">
|
|
148
|
-
{
|
|
148
|
+
{pkg.name}
|
|
149
149
|
</span>
|
|
150
|
-
{
|
|
150
|
+
{pkg.description && (
|
|
151
151
|
<span className="text-sm text-gray-500">
|
|
152
|
-
{
|
|
152
|
+
{pkg.description}
|
|
153
153
|
</span>
|
|
154
154
|
)}
|
|
155
155
|
</div>
|
|
156
|
-
<span className="text-sm text-gray-500">
|
|
156
|
+
<span className="text-sm text-gray-500">package</span>
|
|
157
157
|
</Command.Item>
|
|
158
158
|
))}
|
|
159
159
|
</Command.Group>
|
|
@@ -161,7 +161,7 @@ const CmdKMenu = () => {
|
|
|
161
161
|
|
|
162
162
|
{!searchQuery && recentPackages.length > 0 && (
|
|
163
163
|
<Command.Group
|
|
164
|
-
heading="Recent
|
|
164
|
+
heading="Recent Packages"
|
|
165
165
|
className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
166
166
|
>
|
|
167
167
|
{recentPackages.slice(0, 6).map((pkg) => (
|
|
@@ -169,28 +169,28 @@ const CmdKMenu = () => {
|
|
|
169
169
|
key={pkg.package_id}
|
|
170
170
|
value={pkg.unscoped_name}
|
|
171
171
|
onSelect={() => {
|
|
172
|
-
window.location.href = `/editor?
|
|
172
|
+
window.location.href = `/editor?package_id=${pkg.package_id}`
|
|
173
173
|
setOpen(false)
|
|
174
174
|
}}
|
|
175
175
|
className="flex items-center justify-between px-2 py-1.5 rounded-sm text-sm hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 dark:aria-selected:bg-gray-700"
|
|
176
176
|
>
|
|
177
177
|
<div className="flex flex-col">
|
|
178
178
|
<span className="text-gray-900 dark:text-gray-100">
|
|
179
|
-
{pkg.
|
|
179
|
+
{pkg.name}
|
|
180
180
|
</span>
|
|
181
181
|
<span className="text-sm text-gray-500">
|
|
182
182
|
Last edited:{" "}
|
|
183
183
|
{new Date(pkg.updated_at).toLocaleDateString()}
|
|
184
184
|
</span>
|
|
185
185
|
</div>
|
|
186
|
-
<span className="text-sm text-gray-500">
|
|
186
|
+
<span className="text-sm text-gray-500">package</span>
|
|
187
187
|
</Command.Item>
|
|
188
188
|
))}
|
|
189
189
|
</Command.Group>
|
|
190
190
|
)}
|
|
191
191
|
|
|
192
192
|
<Command.Group
|
|
193
|
-
heading="Start Blank
|
|
193
|
+
heading="Start Blank Package"
|
|
194
194
|
className="px-2 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400"
|
|
195
195
|
>
|
|
196
196
|
{blankTemplates.map((template) => (
|
|
@@ -83,10 +83,7 @@ export function DownloadButtonAndMenu({
|
|
|
83
83
|
className="text-xs"
|
|
84
84
|
onClick={async () => {
|
|
85
85
|
try {
|
|
86
|
-
await downloadGltf(
|
|
87
|
-
circuitJson,
|
|
88
|
-
snippetUnscopedName || "circuit",
|
|
89
|
-
)
|
|
86
|
+
await downloadGltf(snippetUnscopedName || "circuit")
|
|
90
87
|
} catch (error: any) {
|
|
91
88
|
toast({
|
|
92
89
|
title: "Error Downloading 3D Model",
|
package/src/components/FAQ.tsx
CHANGED
|
@@ -176,7 +176,9 @@ export const FAQ = () => (
|
|
|
176
176
|
<Accordion type="single" collapsible>
|
|
177
177
|
{QUESTIONS.map((q, i) => (
|
|
178
178
|
<AccordionItem key={i} value={`item-${i + 1}`}>
|
|
179
|
-
<AccordionTrigger>
|
|
179
|
+
<AccordionTrigger className="text-left">
|
|
180
|
+
{q.question}
|
|
181
|
+
</AccordionTrigger>{" "}
|
|
180
182
|
<AccordionContent>{q.answer}</AccordionContent>
|
|
181
183
|
</AccordionItem>
|
|
182
184
|
))}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useState } from "react"
|
|
2
2
|
import { cn } from "@/lib/utils"
|
|
3
|
-
import { File, Folder, PanelRightOpen } from "lucide-react"
|
|
3
|
+
import { File, Folder, PanelRightOpen, Plus } from "lucide-react"
|
|
4
4
|
import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
|
|
5
5
|
import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
|
|
6
|
+
import { Input } from "@/components/ui/input"
|
|
7
|
+
import { CreateFileProps } from "./package-port/CodeAndPreview"
|
|
6
8
|
|
|
7
9
|
type FileName = string
|
|
8
10
|
|
|
@@ -12,6 +14,7 @@ interface FileSidebarProps {
|
|
|
12
14
|
onFileSelect: (filename: FileName) => void
|
|
13
15
|
className?: string
|
|
14
16
|
fileSidebarState: ReturnType<typeof useState<boolean>>
|
|
17
|
+
handleCreateFile: (props: CreateFileProps) => void
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
@@ -20,8 +23,13 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
20
23
|
onFileSelect,
|
|
21
24
|
className,
|
|
22
25
|
fileSidebarState,
|
|
26
|
+
handleCreateFile,
|
|
23
27
|
}) => {
|
|
24
28
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
29
|
+
const [newFileName, setNewFileName] = useState("")
|
|
30
|
+
const [isCreatingFile, setIsCreatingFile] = useState(false)
|
|
31
|
+
const [errorMessage, setErrorMessage] = useState("")
|
|
32
|
+
|
|
25
33
|
const transformFilesToTreeData = (
|
|
26
34
|
files: Record<FileName, string>,
|
|
27
35
|
): TreeDataItem[] => {
|
|
@@ -82,6 +90,17 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
92
|
const treeData = transformFilesToTreeData(files)
|
|
93
|
+
|
|
94
|
+
const handleCreateFileInline = () => {
|
|
95
|
+
handleCreateFile({
|
|
96
|
+
newFileName,
|
|
97
|
+
setErrorMessage,
|
|
98
|
+
onFileSelect,
|
|
99
|
+
setNewFileName,
|
|
100
|
+
setIsCreatingFile,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
return (
|
|
86
105
|
<div
|
|
87
106
|
className={cn(
|
|
@@ -98,6 +117,36 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
98
117
|
>
|
|
99
118
|
<PanelRightOpen />
|
|
100
119
|
</button>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => setIsCreatingFile(true)}
|
|
122
|
+
className="absolute top-2 right-2 text-gray-400 hover:text-gray-600"
|
|
123
|
+
aria-label="Create new file"
|
|
124
|
+
>
|
|
125
|
+
<Plus className="w-5 h-5" />
|
|
126
|
+
</button>
|
|
127
|
+
{isCreatingFile && (
|
|
128
|
+
<div className="p-2">
|
|
129
|
+
<Input
|
|
130
|
+
autoFocus
|
|
131
|
+
value={newFileName}
|
|
132
|
+
onChange={(e) => setNewFileName(e.target.value)}
|
|
133
|
+
onBlur={handleCreateFileInline}
|
|
134
|
+
onKeyDown={(e) => {
|
|
135
|
+
if (e.key === "Enter") {
|
|
136
|
+
handleCreateFileInline()
|
|
137
|
+
} else if (e.key === "Escape") {
|
|
138
|
+
setIsCreatingFile(false)
|
|
139
|
+
setNewFileName("")
|
|
140
|
+
setErrorMessage("")
|
|
141
|
+
}
|
|
142
|
+
}}
|
|
143
|
+
placeholder="Enter file name"
|
|
144
|
+
/>
|
|
145
|
+
{errorMessage && (
|
|
146
|
+
<div className="text-red-500 mt-1">{errorMessage}</div>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
101
150
|
<TreeView
|
|
102
151
|
data={treeData}
|
|
103
152
|
initialSelectedItemId={currentFile}
|
|
@@ -57,10 +57,13 @@ export default function Footer() {
|
|
|
57
57
|
<h3 className="font-semibold uppercase">Explore</h3>
|
|
58
58
|
<footer className="flex flex-col space-y-2">
|
|
59
59
|
<PrefetchPageLink href="/latest" className="hover:underline">
|
|
60
|
-
Latest
|
|
60
|
+
Latest Packages
|
|
61
61
|
</PrefetchPageLink>
|
|
62
62
|
<PrefetchPageLink href="/trending" className="hover:underline">
|
|
63
|
-
Trending
|
|
63
|
+
Trending Packages
|
|
64
|
+
</PrefetchPageLink>
|
|
65
|
+
<PrefetchPageLink href="/search" className="hover:underline">
|
|
66
|
+
Search Packages
|
|
64
67
|
</PrefetchPageLink>
|
|
65
68
|
<a href="https://docs.tscircuit.com" className="hover:underline">
|
|
66
69
|
Docs
|
|
@@ -29,15 +29,26 @@ const SearchButtonComponent = () => {
|
|
|
29
29
|
</Button> */}
|
|
30
30
|
</div>
|
|
31
31
|
) : (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
32
|
+
<>
|
|
33
|
+
<Button
|
|
34
|
+
variant="ghost"
|
|
35
|
+
size="icon"
|
|
36
|
+
onClick={() => setIsExpanded(true)}
|
|
37
|
+
className="h-8 w-8 hidden sm:flex"
|
|
38
|
+
aria-label="Open search"
|
|
39
|
+
>
|
|
40
|
+
<Search className="size-4" />
|
|
41
|
+
</Button>
|
|
42
|
+
<Button
|
|
43
|
+
variant="ghost"
|
|
44
|
+
size="icon"
|
|
45
|
+
onClick={() => (window.location.href = "/search")}
|
|
46
|
+
className="h-8 w-8 flex sm:hidden"
|
|
47
|
+
aria-label="Go to search"
|
|
48
|
+
>
|
|
49
|
+
<Search className="size-4" />
|
|
50
|
+
</Button>
|
|
51
|
+
</>
|
|
41
52
|
)}
|
|
42
53
|
</div>
|
|
43
54
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React
|
|
1
|
+
import React from "react"
|
|
2
2
|
import { Button } from "@/components/ui/button"
|
|
3
3
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|
4
4
|
import {
|
|
@@ -7,39 +7,19 @@ import {
|
|
|
7
7
|
DropdownMenuItem,
|
|
8
8
|
DropdownMenuTrigger,
|
|
9
9
|
} from "@/components/ui/dropdown-menu"
|
|
10
|
-
import { Link, useLocation, useRouter } from "wouter"
|
|
11
10
|
import { User } from "lucide-react"
|
|
12
|
-
import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
|
|
13
11
|
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
14
12
|
import { useAccountBalance } from "@/hooks/use-account-balance"
|
|
15
|
-
import { useIsUsingFakeApi } from "@/hooks/use-is-using-fake-api"
|
|
16
13
|
import { useSignIn } from "@/hooks/use-sign-in"
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
|
|
21
|
-
const [, setLocation] = useLocation()
|
|
15
|
+
export const HeaderLogin = () => {
|
|
22
16
|
const session = useGlobalStore((s) => s.session)
|
|
23
17
|
const isLoggedIn = Boolean(session)
|
|
24
18
|
const setSession = useGlobalStore((s) => s.setSession)
|
|
25
|
-
const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
|
|
26
|
-
const isUsingFakeApi = useIsUsingFakeApi()
|
|
27
19
|
const signIn = useSignIn()
|
|
28
20
|
const { data: accountBalance } = useAccountBalance()
|
|
29
21
|
|
|
30
22
|
if (!isLoggedIn) {
|
|
31
|
-
const handleLogin = () => {
|
|
32
|
-
if (isUsingFakeApi) {
|
|
33
|
-
setSession({
|
|
34
|
-
account_id: "account-1234",
|
|
35
|
-
github_username: "testuser",
|
|
36
|
-
token: "1234",
|
|
37
|
-
session_id: "session-1234",
|
|
38
|
-
})
|
|
39
|
-
} else {
|
|
40
|
-
signIn()
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
23
|
return (
|
|
44
24
|
<div className="flex items-center md:space-x-2 justify-end">
|
|
45
25
|
<Button onClick={() => signIn()} variant="ghost">
|
|
@@ -51,44 +31,47 @@ export const HeaderLogin: React.FC<HeaderLoginProps> = () => {
|
|
|
51
31
|
}
|
|
52
32
|
|
|
53
33
|
return (
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
<
|
|
34
|
+
<DropdownMenu>
|
|
35
|
+
<DropdownMenuTrigger asChild>
|
|
36
|
+
<Avatar className="w-8 h-8 login-avatar">
|
|
37
|
+
<AvatarImage
|
|
38
|
+
src={`https://github.com/${session?.github_username}.png`}
|
|
39
|
+
alt={`${session?.github_username}'s profile picture`}
|
|
40
|
+
/>
|
|
41
|
+
<AvatarFallback aria-label="User avatar fallback">
|
|
42
|
+
<User size={16} aria-hidden="true" />
|
|
43
|
+
</AvatarFallback>
|
|
44
|
+
</Avatar>
|
|
45
|
+
</DropdownMenuTrigger>
|
|
46
|
+
<DropdownMenuContent>
|
|
47
|
+
<DropdownMenuItem asChild className="text-gray-500 text-xs" disabled>
|
|
48
|
+
<div>
|
|
69
49
|
AI Usage $
|
|
70
50
|
{accountBalance?.monthly_ai_budget_used_usd.toFixed(2) ?? "0.00"} /
|
|
71
51
|
$5.00
|
|
72
|
-
</
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
>
|
|
52
|
+
</div>
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
<DropdownMenuItem asChild>
|
|
55
|
+
<a href={`/${session?.github_username}`} className="cursor-pointer">
|
|
76
56
|
My Profile
|
|
77
|
-
</
|
|
78
|
-
|
|
57
|
+
</a>
|
|
58
|
+
</DropdownMenuItem>
|
|
59
|
+
<DropdownMenuItem asChild>
|
|
60
|
+
<a href="/dashboard" className="cursor-pointer">
|
|
79
61
|
Dashboard
|
|
80
|
-
</
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
<DropdownMenuItem onClick={() => setLocation("/settings")}>
|
|
62
|
+
</a>
|
|
63
|
+
</DropdownMenuItem>
|
|
64
|
+
<DropdownMenuItem asChild>
|
|
65
|
+
<a href="/settings" className="cursor-pointer">
|
|
85
66
|
Settings
|
|
86
|
-
</
|
|
87
|
-
|
|
67
|
+
</a>
|
|
68
|
+
</DropdownMenuItem>
|
|
69
|
+
<DropdownMenuItem asChild onClick={() => setSession(null)}>
|
|
70
|
+
<a href="/sign-out" className="cursor-pointer">
|
|
88
71
|
Sign out
|
|
89
|
-
</
|
|
90
|
-
</
|
|
91
|
-
</
|
|
92
|
-
</
|
|
72
|
+
</a>
|
|
73
|
+
</DropdownMenuItem>
|
|
74
|
+
</DropdownMenuContent>
|
|
75
|
+
</DropdownMenu>
|
|
93
76
|
)
|
|
94
77
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
|
|
3
|
+
interface ImageWithFallbackProps
|
|
4
|
+
extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
5
|
+
src: string
|
|
6
|
+
alt: string
|
|
7
|
+
className?: string
|
|
8
|
+
fallbackSrc?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ImageWithFallback({
|
|
12
|
+
src,
|
|
13
|
+
alt,
|
|
14
|
+
className = "",
|
|
15
|
+
fallbackSrc = "/assets/fallback-image.svg",
|
|
16
|
+
...props
|
|
17
|
+
}: ImageWithFallbackProps) {
|
|
18
|
+
const [loading, setLoading] = useState(true)
|
|
19
|
+
const [currentSrc, setCurrentSrc] = useState(src)
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<img
|
|
23
|
+
src={currentSrc}
|
|
24
|
+
alt={alt}
|
|
25
|
+
className={`object-contain h-full w-full ${
|
|
26
|
+
loading ? "animate-pulse bg-gray-200" : ""
|
|
27
|
+
} ${className}`}
|
|
28
|
+
onLoad={() => setLoading(false)}
|
|
29
|
+
onError={() => {
|
|
30
|
+
console.error("PCB image failed to load:", src)
|
|
31
|
+
setCurrentSrc(fallbackSrc)
|
|
32
|
+
setLoading(false)
|
|
33
|
+
}}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|