@tscircuit/fake-snippets 0.0.82 → 0.0.84

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 (44) hide show
  1. package/README.md +5 -2
  2. package/bun-tests/fake-snippets-api/routes/ai_reviews/create.test.ts +12 -0
  3. package/bun-tests/fake-snippets-api/routes/ai_reviews/get.test.ts +16 -0
  4. package/bun-tests/fake-snippets-api/routes/ai_reviews/list.test.ts +14 -0
  5. package/bun-tests/fake-snippets-api/routes/ai_reviews/process_review.test.ts +16 -0
  6. package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +3 -3
  7. package/bun.lock +26 -37
  8. package/dist/bundle.js +590 -427
  9. package/dist/index.d.ts +83 -11
  10. package/dist/index.js +50 -2
  11. package/dist/schema.d.ts +116 -15
  12. package/dist/schema.js +17 -2
  13. package/fake-snippets-api/lib/db/db-client.ts +40 -0
  14. package/fake-snippets-api/lib/db/schema.ts +17 -1
  15. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +14 -1
  16. package/fake-snippets-api/routes/api/_fake/ai_reviews/process_review.ts +31 -0
  17. package/fake-snippets-api/routes/api/ai_reviews/create.ts +22 -0
  18. package/fake-snippets-api/routes/api/ai_reviews/get.ts +24 -0
  19. package/fake-snippets-api/routes/api/ai_reviews/list.ts +14 -0
  20. package/fake-snippets-api/routes/api/package_releases/get.ts +11 -3
  21. package/fake-snippets-api/routes/api/package_releases/list.ts +8 -1
  22. package/package.json +4 -3
  23. package/src/App.tsx +0 -2
  24. package/src/ContextProviders.tsx +1 -1
  25. package/src/components/Header2.tsx +8 -18
  26. package/src/components/PackageBuildsPage/package-build-header.tsx +14 -2
  27. package/src/components/SearchComponent.tsx +46 -8
  28. package/src/components/ViewPackagePage/hooks/use-toast.tsx +70 -0
  29. package/src/components/ViewSnippetHeader.tsx +9 -6
  30. package/src/components/dialogs/edit-package-details-dialog.tsx +5 -10
  31. package/src/components/dialogs/{import-snippet-dialog.tsx → import-package-dialog.tsx} +25 -24
  32. package/src/components/package-port/CodeEditorHeader.tsx +7 -6
  33. package/src/components/ui/toaster.tsx +1 -33
  34. package/src/hooks/use-current-package-release.ts +10 -1
  35. package/src/hooks/use-fork-package-mutation.ts +4 -3
  36. package/src/hooks/use-package-release.ts +15 -14
  37. package/src/hooks/use-sign-in.ts +10 -8
  38. package/src/hooks/use-toast.tsx +50 -169
  39. package/src/hooks/useFileManagement.ts +74 -12
  40. package/src/hooks/useForkPackageMutation.ts +2 -1
  41. package/src/hooks/useForkSnippetMutation.ts +2 -1
  42. package/src/pages/authorize.tsx +164 -8
  43. package/src/pages/view-package.tsx +1 -0
  44. package/src/components/ViewPackagePage/hooks/use-toast.ts +0 -191
@@ -16,6 +16,8 @@ import {
16
16
  type PackageFile,
17
17
  type PackageRelease,
18
18
  packageReleaseSchema,
19
+ type AiReview,
20
+ aiReviewSchema,
19
21
  type Session,
20
22
  type Snippet,
21
23
  databaseSchema,
@@ -1338,4 +1340,42 @@ const initializer = combine(databaseSchema.parse({}), (set, get) => ({
1338
1340
  ),
1339
1341
  }))
1340
1342
  },
1343
+
1344
+ addAiReview: (review: Omit<AiReview, "ai_review_id">): AiReview => {
1345
+ const base = aiReviewSchema.omit({ ai_review_id: true }).parse(review)
1346
+ const newReview = {
1347
+ ai_review_id: crypto.randomUUID(),
1348
+ ...base,
1349
+ }
1350
+ set((state) => ({
1351
+ aiReviews: [...state.aiReviews, newReview],
1352
+ idCounter: state.idCounter + 1,
1353
+ }))
1354
+ return newReview
1355
+ },
1356
+ updateAiReview: (
1357
+ aiReviewId: string,
1358
+ updates: Partial<AiReview>,
1359
+ ): AiReview | undefined => {
1360
+ let updated: AiReview | undefined
1361
+ set((state) => {
1362
+ const index = state.aiReviews.findIndex(
1363
+ (ar) => ar.ai_review_id === aiReviewId,
1364
+ )
1365
+ if (index === -1) return state
1366
+ const aiReviews = [...state.aiReviews]
1367
+ aiReviews[index] = { ...aiReviews[index], ...updates }
1368
+ updated = aiReviews[index]
1369
+ return { ...state, aiReviews }
1370
+ })
1371
+ return updated
1372
+ },
1373
+ getAiReviewById: (aiReviewId: string): AiReview | undefined => {
1374
+ const state = get()
1375
+ return state.aiReviews.find((ar) => ar.ai_review_id === aiReviewId)
1376
+ },
1377
+ listAiReviews: (): AiReview[] => {
1378
+ const state = get()
1379
+ return state.aiReviews
1380
+ },
1341
1381
  }))
@@ -138,6 +138,17 @@ export const orderQuoteSchema = z.object({
138
138
  })
139
139
  export type OrderQuote = z.infer<typeof orderQuoteSchema>
140
140
 
141
+ export const aiReviewSchema = z.object({
142
+ ai_review_id: z.string().uuid(),
143
+ ai_review_text: z.string().nullable(),
144
+ start_processing_at: z.string().datetime().nullable(),
145
+ finished_processing_at: z.string().datetime().nullable(),
146
+ processing_error: z.any().nullable(),
147
+ created_at: z.string().datetime(),
148
+ display_status: z.enum(["pending", "completed", "failed"]),
149
+ })
150
+ export type AiReview = z.infer<typeof aiReviewSchema>
151
+
141
152
  // TODO: Remove this schema after migration to accountPackages is complete
142
153
  export const accountSnippetSchema = z.object({
143
154
  account_id: z.string(),
@@ -203,7 +214,11 @@ export const packageReleaseSchema = z.object({
203
214
  circuit_json_build_is_stale: z.boolean().default(false),
204
215
 
205
216
  // AI Review
206
- ai_review_text: z.string().nullable().default(null),
217
+ ai_review_text: z.string().nullable().default(null).optional(),
218
+ ai_review_started_at: z.string().datetime().nullable().optional(),
219
+ ai_review_completed_at: z.string().datetime().nullable().optional(),
220
+ ai_review_error: z.any().optional().nullable(),
221
+ ai_review_logs: z.array(z.any()).optional().nullable(),
207
222
  ai_review_requested: z.boolean().default(false),
208
223
  })
209
224
  export type PackageRelease = z.infer<typeof packageReleaseSchema>
@@ -310,5 +325,6 @@ export const databaseSchema = z.object({
310
325
  jlcpcbOrderState: z.array(jlcpcbOrderStateSchema).default([]),
311
326
  jlcpcbOrderStepRuns: z.array(jlcpcbOrderStepRunSchema).default([]),
312
327
  orderQuotes: z.array(orderQuoteSchema).default([]),
328
+ aiReviews: z.array(aiReviewSchema).default([]),
313
329
  })
314
330
  export type DatabaseSchema = z.infer<typeof databaseSchema>
@@ -4,11 +4,13 @@ export const publicMapPackageRelease = (
4
4
  internal_package_release: ZT.PackageRelease,
5
5
  options: {
6
6
  include_logs?: boolean
7
+ include_ai_review?: boolean
7
8
  } = {
8
9
  include_logs: false,
10
+ include_ai_review: false,
9
11
  },
10
12
  ): ZT.PackageRelease => {
11
- return {
13
+ const result = {
12
14
  ...internal_package_release,
13
15
  created_at: internal_package_release.created_at,
14
16
  circuit_json_build_error_last_updated_at:
@@ -21,4 +23,15 @@ export const publicMapPackageRelease = (
21
23
  ? internal_package_release.circuit_json_build_logs
22
24
  : [],
23
25
  }
26
+
27
+ // Only include AI review fields when include_ai_review is true
28
+ if (!options.include_ai_review) {
29
+ delete result.ai_review_text
30
+ delete result.ai_review_started_at
31
+ delete result.ai_review_completed_at
32
+ delete result.ai_review_error
33
+ delete result.ai_review_logs
34
+ }
35
+
36
+ return result
24
37
  }
@@ -0,0 +1,31 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["POST"],
7
+ auth: "session",
8
+ jsonBody: z.object({
9
+ ai_review_id: z.string(),
10
+ }),
11
+ jsonResponse: z.object({
12
+ ai_review: aiReviewSchema,
13
+ }),
14
+ })(async (req, ctx) => {
15
+ const { ai_review_id } = req.jsonBody
16
+ const existing = ctx.db.getAiReviewById(ai_review_id)
17
+ if (!existing) {
18
+ return ctx.error(404, {
19
+ error_code: "ai_review_not_found",
20
+ message: "AI review not found",
21
+ })
22
+ }
23
+ const now = new Date().toISOString()
24
+ const updated = ctx.db.updateAiReview(ai_review_id, {
25
+ ai_review_text: "Placeholder AI Review",
26
+ start_processing_at: existing.start_processing_at ?? now,
27
+ finished_processing_at: now,
28
+ display_status: "completed",
29
+ })!
30
+ return ctx.json({ ai_review: updated })
31
+ })
@@ -0,0 +1,22 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["POST"],
7
+ auth: "session",
8
+ jsonResponse: z.object({
9
+ ai_review: aiReviewSchema,
10
+ }),
11
+ })(async (req, ctx) => {
12
+ const ai_review = ctx.db.addAiReview({
13
+ ai_review_text: null,
14
+ start_processing_at: null,
15
+ finished_processing_at: null,
16
+ processing_error: null,
17
+ created_at: new Date().toISOString(),
18
+ display_status: "pending",
19
+ })
20
+
21
+ return ctx.json({ ai_review })
22
+ })
@@ -0,0 +1,24 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["GET"],
7
+ auth: "session",
8
+ queryParams: z.object({
9
+ ai_review_id: z.string(),
10
+ }),
11
+ jsonResponse: z.object({
12
+ ai_review: aiReviewSchema,
13
+ }),
14
+ })(async (req, ctx) => {
15
+ const { ai_review_id } = req.query
16
+ const ai_review = ctx.db.getAiReviewById(ai_review_id)
17
+ if (!ai_review) {
18
+ return ctx.error(404, {
19
+ error_code: "ai_review_not_found",
20
+ message: "AI review not found",
21
+ })
22
+ }
23
+ return ctx.json({ ai_review })
24
+ })
@@ -0,0 +1,14 @@
1
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
2
+ import { z } from "zod"
3
+ import { aiReviewSchema } from "fake-snippets-api/lib/db/schema"
4
+
5
+ export default withRouteSpec({
6
+ methods: ["GET"],
7
+ auth: "session",
8
+ jsonResponse: z.object({
9
+ ai_reviews: z.array(aiReviewSchema),
10
+ }),
11
+ })(async (req, ctx) => {
12
+ const ai_reviews = ctx.db.listAiReviews()
13
+ return ctx.json({ ai_reviews })
14
+ })
@@ -8,6 +8,7 @@ export default withRouteSpec({
8
8
  auth: "none",
9
9
  commonParams: z.object({
10
10
  include_logs: z.boolean().optional().default(false),
11
+ include_ai_review: z.boolean().optional().default(false),
11
12
  }),
12
13
  jsonBody: z.object({
13
14
  package_release_id: z.string().optional(),
@@ -58,7 +59,9 @@ export default withRouteSpec({
58
59
 
59
60
  return ctx.json({
60
61
  ok: true,
61
- package_release: publicMapPackageRelease(packageReleases[0]),
62
+ package_release: publicMapPackageRelease(packageReleases[0], {
63
+ include_ai_review: req.commonParams?.include_ai_review,
64
+ }),
62
65
  })
63
66
  }
64
67
 
@@ -82,7 +85,9 @@ export default withRouteSpec({
82
85
 
83
86
  return ctx.json({
84
87
  ok: true,
85
- package_release: publicMapPackageRelease(packageReleases[0]),
88
+ package_release: publicMapPackageRelease(packageReleases[0], {
89
+ include_ai_review: req.commonParams?.include_ai_review,
90
+ }),
86
91
  })
87
92
  }
88
93
 
@@ -102,7 +107,9 @@ export default withRouteSpec({
102
107
 
103
108
  return ctx.json({
104
109
  ok: true,
105
- package_release: publicMapPackageRelease(pkgRelease),
110
+ package_release: publicMapPackageRelease(pkgRelease, {
111
+ include_ai_review: req.commonParams?.include_ai_review,
112
+ }),
106
113
  })
107
114
  }
108
115
 
@@ -120,6 +127,7 @@ export default withRouteSpec({
120
127
  ok: true,
121
128
  package_release: publicMapPackageRelease(foundRelease, {
122
129
  include_logs: req.commonParams?.include_logs,
130
+ include_ai_review: req.commonParams?.include_ai_review,
123
131
  }),
124
132
  })
125
133
  })
@@ -6,6 +6,9 @@ import { z } from "zod"
6
6
  export default withRouteSpec({
7
7
  methods: ["POST"],
8
8
  auth: "none",
9
+ commonParams: z.object({
10
+ include_ai_review: z.boolean().optional().default(false),
11
+ }),
9
12
  jsonBody: z
10
13
  .object({
11
14
  package_id: z.string().optional(),
@@ -72,6 +75,10 @@ export default withRouteSpec({
72
75
 
73
76
  return ctx.json({
74
77
  ok: true,
75
- package_releases: releases.map((pr) => publicMapPackageRelease(pr)),
78
+ package_releases: releases.map((pr) =>
79
+ publicMapPackageRelease(pr, {
80
+ include_ai_review: req.commonParams?.include_ai_review,
81
+ }),
82
+ ),
76
83
  })
77
84
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.82",
3
+ "version": "0.0.84",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -86,7 +86,7 @@
86
86
  "circuit-json-to-bom-csv": "^0.0.6",
87
87
  "circuit-json-to-gerber": "^0.0.21",
88
88
  "circuit-json-to-pnp-csv": "^0.0.6",
89
- "circuit-json-to-readable-netlist": "^0.0.8",
89
+ "circuit-json-to-readable-netlist": "^0.0.13",
90
90
  "circuit-json-to-tscircuit": "^0.0.4",
91
91
  "class-variance-authority": "^0.7.1",
92
92
  "clsx": "^2.1.1",
@@ -145,8 +145,9 @@
145
145
  "@playwright/test": "^1.48.0",
146
146
  "@tailwindcss/typography": "^0.5.16",
147
147
  "@tscircuit/core": "^0.0.433",
148
+ "@tscircuit/eval": "^0.0.227",
148
149
  "@tscircuit/prompt-benchmarks": "^0.0.28",
149
- "@tscircuit/runframe": "^0.0.562",
150
+ "@tscircuit/runframe": "^0.0.578",
150
151
  "@types/babel__standalone": "^7.1.7",
151
152
  "@types/bun": "^1.1.10",
152
153
  "@types/country-list": "^2.1.4",
package/src/App.tsx CHANGED
@@ -1,5 +1,4 @@
1
1
  import { ComponentType, Suspense, lazy } from "react"
2
- import { Toaster } from "@/components/ui/toaster"
3
2
  import { Route, Switch } from "wouter"
4
3
  import "./components/CmdKMenu"
5
4
  import { ContextProviders } from "./ContextProviders"
@@ -129,7 +128,6 @@ function App() {
129
128
  <Route component={lazyImport(() => import("@/pages/404"))} />
130
129
  </Switch>
131
130
  </Suspense>
132
- <Toaster />
133
131
  </ErrorBoundary>
134
132
  </ContextProviders>
135
133
  )
@@ -64,7 +64,7 @@ export const ContextProviders = ({ children }: any) => {
64
64
  <HelmetProvider>
65
65
  <PostHogIdentifier />
66
66
  {children}
67
- <Toaster />
67
+ <Toaster position="bottom-right" />
68
68
  </HelmetProvider>
69
69
  </QueryClientProvider>
70
70
  )
@@ -15,18 +15,15 @@ const SearchButtonComponent = () => {
15
15
  return (
16
16
  <div className="relative">
17
17
  {isExpanded ? (
18
- <div className="flex items-center gap-2">
19
- <div className="w-32 bg-white">
20
- <SearchComponent autofocus />
18
+ <div className="flex items-center gap-2 ml-8">
19
+ <div className="absolute -top-4 right-3 bg-white">
20
+ <SearchComponent
21
+ autofocus
22
+ closeOnClick={() => {
23
+ setIsExpanded(false)
24
+ }}
25
+ />
21
26
  </div>
22
- {/* <Button
23
- variant="ghost"
24
- size="icon"
25
- onClick={() => setIsExpanded(false)}
26
- className="h-8 w-8"
27
- >
28
- <X className="h-4 w-4" />
29
- </Button> */}
30
27
  </div>
31
28
  ) : (
32
29
  <>
@@ -58,13 +55,6 @@ export const Header2 = () => {
58
55
  const isLoggedIn = useGlobalStore((state) => Boolean(state.session))
59
56
  return (
60
57
  <>
61
- {/* <div className="absolute left-0 top-0 z-[9999999]">
62
- <div className="hidden xl:block">xl</div>
63
- <div className="hidden lg:block xl:hidden">lg</div>
64
- <div className="hidden md:block lg:hidden">md</div>
65
- <div className="hidden sm:block md:hidden">sm</div>
66
- <div className="hidden xs:block sm:hidden">xs</div>
67
- </div> */}
68
58
  <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
69
59
  <div className="container mx-auto flex h-16 items-center justify-between px-2 md:px-6">
70
60
  <div className="flex items-center gap-2">
@@ -1,13 +1,15 @@
1
1
  import { Button } from "@/components/ui/button"
2
2
  import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
3
  import { useRebuildPackageReleaseMutation } from "@/hooks/use-rebuild-package-release-mutation"
4
- import { Github, RefreshCw } from "lucide-react"
4
+ import { Github, RefreshCw, RotateCcw } from "lucide-react"
5
5
  import { useParams } from "wouter"
6
6
  import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
7
7
 
8
8
  export function PackageBuildHeader() {
9
9
  const { author, packageName } = useParams()
10
- const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
10
+ const { packageRelease, refetch, isFetching } = useCurrentPackageRelease({
11
+ include_logs: true,
12
+ })
11
13
  const { mutate: rebuildPackage, isLoading } =
12
14
  useRebuildPackageReleaseMutation()
13
15
 
@@ -54,6 +56,16 @@ export function PackageBuildHeader() {
54
56
  <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
55
57
  {isLoading ? "Rebuilding..." : "Rebuild"}
56
58
  </Button>
59
+ <Button
60
+ variant="outline"
61
+ size="icon"
62
+ aria-label="Reload logs"
63
+ className="border-gray-300 bg-white hover:bg-gray-50"
64
+ onClick={() => refetch()}
65
+ disabled={isFetching}
66
+ >
67
+ <RotateCcw className="w-3 h-3 sm:w-4 sm:h-4" />
68
+ </Button>
57
69
  <DownloadButtonAndMenu
58
70
  snippetUnscopedName={`${author}/${packageName}`}
59
71
  />
@@ -6,10 +6,12 @@ import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
7
  import { useSnippetsBaseApiUrl } from "@/hooks/use-snippets-base-api-url"
8
8
  import { PrefetchPageLink } from "./PrefetchPageLink"
9
+ import { CircuitBoard } from "lucide-react"
9
10
 
10
11
  interface SearchComponentProps {
11
12
  onResultsFetched?: (results: any[]) => void
12
13
  autofocus?: boolean
14
+ closeOnClick?: () => void
13
15
  }
14
16
 
15
17
  const LinkWithNewTabHandling = ({
@@ -45,13 +47,14 @@ const LinkWithNewTabHandling = ({
45
47
  const SearchComponent: React.FC<SearchComponentProps> = ({
46
48
  onResultsFetched,
47
49
  autofocus = false,
50
+ closeOnClick,
48
51
  }) => {
49
52
  const [searchQuery, setSearchQuery] = useState("")
50
53
  const [showResults, setShowResults] = useState(false)
51
54
  const axios = useAxios()
52
55
  const resultsRef = useRef<HTMLDivElement>(null)
53
56
  const inputRef = useRef<HTMLInputElement>(null)
54
- const [location] = useLocation()
57
+ const [location, setLocation] = useLocation()
55
58
  const snippetsBaseApiUrl = useSnippetsBaseApiUrl()
56
59
 
57
60
  const { data: searchResults, isLoading } = useQuery(
@@ -71,7 +74,9 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
71
74
 
72
75
  const handleSearch = (e: React.FormEvent) => {
73
76
  e.preventDefault()
74
- setShowResults(!!searchQuery)
77
+ if (searchQuery.trim()) {
78
+ setLocation(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
79
+ }
75
80
  }
76
81
 
77
82
  // Focus input on mount
@@ -91,18 +96,31 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
91
96
  }
92
97
  }
93
98
 
99
+ const handleEscapeKey = (event: KeyboardEvent) => {
100
+ if (event.key === "Escape") {
101
+ setShowResults(false)
102
+ if (closeOnClick) {
103
+ closeOnClick()
104
+ }
105
+ }
106
+ }
107
+
94
108
  document.addEventListener("mousedown", handleClickOutside)
109
+ document.addEventListener("keydown", handleEscapeKey)
95
110
  return () => {
96
111
  document.removeEventListener("mousedown", handleClickOutside)
112
+ document.removeEventListener("keydown", handleEscapeKey)
97
113
  }
98
- }, [])
114
+ }, [closeOnClick])
99
115
 
100
116
  const shouldOpenInNewTab = location === "/editor" || location === "/ai"
101
117
  const shouldOpenInEditor = location === "/editor" || location === "/ai"
102
118
 
103
119
  return (
104
- <form onSubmit={handleSearch} className="relative">
120
+ <form onSubmit={handleSearch} autoComplete="off" className="relative w-44">
105
121
  <Input
122
+ autoComplete="off"
123
+ spellCheck={false}
106
124
  ref={inputRef}
107
125
  type="search"
108
126
  placeholder="Search"
@@ -112,12 +130,20 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
112
130
  setSearchQuery(e.target.value)
113
131
  setShowResults(!!e.target.value)
114
132
  }}
133
+ onKeyDown={(e) => {
134
+ if (e.key === "Backspace" && !searchQuery && closeOnClick) {
135
+ closeOnClick()
136
+ }
137
+ }}
115
138
  aria-label="Search packages"
116
139
  role="searchbox"
117
140
  />
118
141
  {isLoading && (
119
- <div className="absolute top-full left-0 right-0 mt-2 bg-white shadow-lg rounded-md z-10 p-2 flex items-center justify-center space-x-2">
120
- <span className="text-gray-500 text-sm">Loading...</span>
142
+ <div className="absolute top-full w-lg left-0 right-0 mt-1 bg-white shadow-lg rounded-lg border w-80 grid place-items-center py-4 z-10 p-3">
143
+ <div className="flex items-center space-x-2">
144
+ <div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"></div>
145
+ <span className="text-gray-600 text-sm">Searching...</span>
146
+ </div>
121
147
  </div>
122
148
  )}
123
149
 
@@ -139,12 +165,24 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
139
165
  shouldOpenInNewTab={shouldOpenInNewTab}
140
166
  className="flex"
141
167
  >
142
- <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm">
168
+ <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm bg-gray-50 border flex items-center justify-center">
143
169
  <img
144
170
  src={`${snippetsBaseApiUrl}/snippets/images/${pkg.name}/pcb.svg`}
145
171
  alt={`PCB preview for ${pkg.name}`}
146
172
  className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
173
+ onError={(e) => {
174
+ e.currentTarget.style.display = "none"
175
+ e.currentTarget.nextElementSibling?.classList.remove(
176
+ "hidden",
177
+ )
178
+ e.currentTarget.nextElementSibling?.classList.add(
179
+ "flex",
180
+ )
181
+ }}
147
182
  />
183
+ <div className="w-12 h-12 hidden items-center justify-center">
184
+ <CircuitBoard className="w-6 h-6 text-gray-300" />
185
+ </div>
148
186
  </div>
149
187
  <div className="flex-grow">
150
188
  <div className="font-medium text-blue-600 break-words text-xs">
@@ -161,7 +199,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
161
199
  ))}
162
200
  </ul>
163
201
  ) : (
164
- <Alert variant="default" className="p-4">
202
+ <Alert variant="default" className="p-4 text-center">
165
203
  No results found for "{searchQuery}"
166
204
  </Alert>
167
205
  )}
@@ -0,0 +1,70 @@
1
+ "use client"
2
+ import toastLibrary, { Toaster, type Toast } from "react-hot-toast"
3
+ import React from "react"
4
+
5
+ export interface ToasterToast {
6
+ title?: React.ReactNode
7
+ description?: React.ReactNode
8
+ variant?: "default" | "destructive"
9
+ duration?: number
10
+ }
11
+
12
+ function ToastContent({
13
+ title,
14
+ description,
15
+ variant,
16
+ t,
17
+ }: ToasterToast & { t: Toast }) {
18
+ return (
19
+ <div
20
+ className={`rounded-md border p-4 shadow-lg transition-all ${
21
+ t.visible
22
+ ? "animate-in fade-in slide-in-from-top-full"
23
+ : "animate-out fade-out slide-out-to-right-full"
24
+ } ${
25
+ variant === "destructive"
26
+ ? "border-red-500 bg-red-500 text-slate-50"
27
+ : "border-slate-200 bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50"
28
+ }`}
29
+ >
30
+ {title && <div className="text-sm font-semibold">{title}</div>}
31
+ {description && <div className="text-sm opacity-90">{description}</div>}
32
+ </div>
33
+ )
34
+ }
35
+
36
+ const toast = ({
37
+ duration,
38
+ description,
39
+ variant = "default",
40
+ title,
41
+ }: ToasterToast) => {
42
+ if (description) {
43
+ return toastLibrary.custom(
44
+ (t) => (
45
+ <ToastContent
46
+ title={title}
47
+ description={description}
48
+ variant={variant}
49
+ t={t}
50
+ />
51
+ ),
52
+ { duration },
53
+ )
54
+ }
55
+
56
+ if (variant === "destructive") {
57
+ return toastLibrary.error(<>{title}</>, { duration })
58
+ }
59
+
60
+ return toastLibrary(<>{title}</>, { duration })
61
+ }
62
+
63
+ function useToast() {
64
+ return {
65
+ toast,
66
+ dismiss: toastLibrary.dismiss,
67
+ }
68
+ }
69
+
70
+ export { useToast, toast, Toaster }
@@ -63,18 +63,21 @@ export default function ViewSnippetHeader() {
63
63
  onSuccess?.(forkedSnippet)
64
64
  },
65
65
  onError: (error: any) => {
66
- // Check if the error message contains 'already exists'
67
- if (error.message?.includes("already forked")) {
66
+ const message =
67
+ error?.data?.error?.message ||
68
+ error.message ||
69
+ "Failed to fork snippet. Please try again."
70
+ if (message.includes("already forked")) {
68
71
  toast({
69
72
  title: "Snippet already exists",
70
- description: error.message,
71
- variant: "destructive", // You can style this variant differently
73
+ description: message,
74
+ variant: "destructive",
72
75
  })
73
76
  } else {
74
77
  toast({
75
78
  title: "Error",
76
- description: "Failed to fork snippet. Please try again.",
77
- variant: "destructive", // Use destructive variant for errors
79
+ description: message,
80
+ variant: "destructive",
78
81
  })
79
82
  }
80
83
  console.error("Error forking snippet:", error)