@tscircuit/fake-snippets 0.0.118 → 0.0.120

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 (31) hide show
  1. package/bun-tests/fake-snippets-api/routes/bug_reports/create.test.ts +37 -0
  2. package/bun-tests/fake-snippets-api/routes/bug_reports/upload_file.test.ts +89 -0
  3. package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +3 -0
  4. package/bun.lock +2 -2
  5. package/dist/bundle.js +778 -508
  6. package/dist/index.d.ts +161 -6
  7. package/dist/index.js +102 -3
  8. package/dist/schema.d.ts +225 -7
  9. package/dist/schema.js +38 -3
  10. package/fake-snippets-api/lib/db/db-client.ts +98 -0
  11. package/fake-snippets-api/lib/db/schema.ts +37 -0
  12. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +6 -0
  13. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +9 -0
  14. package/fake-snippets-api/routes/api/bug_reports/create.ts +43 -0
  15. package/fake-snippets-api/routes/api/bug_reports/upload_file.ts +113 -0
  16. package/package.json +2 -2
  17. package/src/components/Header.tsx +16 -0
  18. package/src/components/PackageCard.tsx +7 -4
  19. package/src/components/PackageSearchResults.tsx +1 -7
  20. package/src/components/SearchComponent.tsx +64 -53
  21. package/src/components/TrendingPackagesCarousel.tsx +16 -23
  22. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +3 -2
  23. package/src/components/ViewPackagePage/components/preview-image-squares.tsx +6 -3
  24. package/src/hooks/use-preview-images.ts +22 -17
  25. package/src/hooks/useUpdatePackageFilesMutation.ts +8 -0
  26. package/src/lib/utils/getPackagePreviewImageUrl.ts +15 -0
  27. package/src/pages/dashboard.tsx +0 -1
  28. package/src/pages/editor.tsx +12 -9
  29. package/src/pages/organization-profile.tsx +0 -1
  30. package/src/pages/package-editor.tsx +13 -9
  31. package/src/pages/user-profile.tsx +0 -1
@@ -28,6 +28,9 @@ export const publicMapPackage = (internalPackage: {
28
28
  latest_package_release_fs_sha: string | null
29
29
  github_repo_full_name?: string | null
30
30
  allow_pr_previews?: boolean
31
+ latest_pcb_preview_image_url?: string | null
32
+ latest_sch_preview_image_url?: string | null
33
+ latest_cad_preview_image_url?: string | null
31
34
  }): zt.Package => {
32
35
  return {
33
36
  ...internalPackage,
@@ -47,5 +50,11 @@ export const publicMapPackage = (internalPackage: {
47
50
  ? true
48
51
  : (internalPackage.is_unlisted ?? false),
49
52
  allow_pr_previews: Boolean(internalPackage.allow_pr_previews),
53
+ latest_pcb_preview_image_url:
54
+ internalPackage.latest_pcb_preview_image_url ?? null,
55
+ latest_sch_preview_image_url:
56
+ internalPackage.latest_sch_preview_image_url ?? null,
57
+ latest_cad_preview_image_url:
58
+ internalPackage.latest_cad_preview_image_url ?? null,
50
59
  }
51
60
  }
@@ -0,0 +1,43 @@
1
+ import { bugReportSchema } from "fake-snippets-api/lib/db/schema"
2
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
3
+ import { z } from "zod"
4
+
5
+ const requestSchema = z
6
+ .object({
7
+ text: z.string().optional(),
8
+ is_auto_deleted: z.boolean().optional(),
9
+ delete_at: z.string().datetime().optional(),
10
+ })
11
+ .superRefine((data, ctx) => {
12
+ if (data.is_auto_deleted && !data.delete_at) {
13
+ ctx.addIssue({
14
+ code: z.ZodIssueCode.custom,
15
+ message: "delete_at is required when is_auto_deleted is true",
16
+ path: ["delete_at"],
17
+ })
18
+ }
19
+ })
20
+
21
+ export default withRouteSpec({
22
+ methods: ["POST"],
23
+ auth: "session",
24
+ jsonBody: requestSchema,
25
+ jsonResponse: z.object({
26
+ ok: z.literal(true),
27
+ bug_report: bugReportSchema,
28
+ }),
29
+ })(async (req, ctx) => {
30
+ const { text, is_auto_deleted = false, delete_at } = req.jsonBody
31
+
32
+ const bugReport = ctx.db.addBugReport({
33
+ reporter_account_id: ctx.auth.account_id,
34
+ text: text ?? null,
35
+ is_auto_deleted,
36
+ delete_at: is_auto_deleted ? delete_at : null,
37
+ })
38
+
39
+ return ctx.json({
40
+ ok: true,
41
+ bug_report: bugReport,
42
+ })
43
+ })
@@ -0,0 +1,113 @@
1
+ import { Buffer } from "node:buffer"
2
+ import { bugReportFileResponseSchema } from "fake-snippets-api/lib/db/schema"
3
+ import { withRouteSpec } from "fake-snippets-api/lib/middleware/with-winter-spec"
4
+ import { normalizeProjectFilePathAndValidate } from "fake-snippets-api/utils/normalizeProjectFilePath"
5
+ import { z } from "zod"
6
+
7
+ const requestSchema = z
8
+ .object({
9
+ bug_report_id: z.string().uuid(),
10
+ file_path: z.string(),
11
+ content_mimetype: z.string().optional(),
12
+ content_text: z.string().optional(),
13
+ content_base64: z.string().optional(),
14
+ })
15
+ .superRefine((data, ctx) => {
16
+ if (data.content_text !== undefined && data.content_base64 !== undefined) {
17
+ ctx.addIssue({
18
+ code: z.ZodIssueCode.custom,
19
+ message: "Provide either content_text or content_base64, not both",
20
+ path: ["content_text"],
21
+ })
22
+ }
23
+ })
24
+
25
+ const inferMimetype = (filePath: string, provided?: string) => {
26
+ if (provided) return provided
27
+
28
+ const lowerPath = filePath.toLowerCase()
29
+ if (lowerPath.endsWith(".ts") || lowerPath.endsWith(".tsx")) {
30
+ return "text/typescript"
31
+ }
32
+ if (lowerPath.endsWith(".js")) return "application/javascript"
33
+ if (lowerPath.endsWith(".json")) return "application/json"
34
+ if (lowerPath.endsWith(".md")) return "text/markdown"
35
+ if (lowerPath.endsWith(".html")) return "text/html"
36
+ if (lowerPath.endsWith(".css")) return "text/css"
37
+ if (lowerPath.endsWith(".txt")) return "text/plain"
38
+ return "application/octet-stream"
39
+ }
40
+
41
+ export default withRouteSpec({
42
+ methods: ["POST"],
43
+ auth: "session",
44
+ jsonBody: requestSchema,
45
+ jsonResponse: z.object({
46
+ ok: z.literal(true),
47
+ bug_report_file: bugReportFileResponseSchema,
48
+ }),
49
+ })(async (req, ctx) => {
50
+ const {
51
+ bug_report_id,
52
+ file_path,
53
+ content_mimetype,
54
+ content_text,
55
+ content_base64,
56
+ } = req.jsonBody
57
+
58
+ const bugReport = ctx.db.getBugReportById(bug_report_id)
59
+ if (!bugReport) {
60
+ return ctx.error(404, {
61
+ error_code: "bug_report_not_found",
62
+ message: "Bug report not found",
63
+ })
64
+ }
65
+
66
+ if (bugReport.reporter_account_id !== ctx.auth.account_id) {
67
+ return ctx.error(403, {
68
+ error_code: "bug_report_forbidden",
69
+ message: "You do not have access to modify this bug report",
70
+ })
71
+ }
72
+
73
+ let normalizedPath: string
74
+ try {
75
+ normalizedPath = normalizeProjectFilePathAndValidate(file_path)
76
+ } catch (error) {
77
+ return ctx.error(400, {
78
+ error_code: "invalid_file_path",
79
+ message: error instanceof Error ? error.message : "Invalid file path",
80
+ })
81
+ }
82
+
83
+ const mimeType = inferMimetype(normalizedPath, content_mimetype)
84
+ const hasBase64 = content_base64 !== undefined
85
+ const hasText = content_text !== undefined
86
+ const isTextUpload = hasText || !hasBase64
87
+ const storedText = isTextUpload ? (content_text ?? "") : null
88
+ const storedBytes =
89
+ isTextUpload || !hasBase64
90
+ ? null
91
+ : Buffer.from(content_base64 ?? "", "base64")
92
+
93
+ const bugReportFile = ctx.db.addBugReportFile({
94
+ bug_report_id,
95
+ file_path: normalizedPath,
96
+ content_mimetype: mimeType,
97
+ is_text: isTextUpload,
98
+ content_text: storedText,
99
+ content_bytes: storedBytes,
100
+ })
101
+
102
+ return ctx.json({
103
+ ok: true,
104
+ bug_report_file: {
105
+ bug_report_file_id: bugReportFile.bug_report_file_id,
106
+ bug_report_id: bugReportFile.bug_report_id,
107
+ file_path: bugReportFile.file_path,
108
+ content_mimetype: bugReportFile.content_mimetype,
109
+ is_text: bugReportFile.is_text,
110
+ created_at: bugReportFile.created_at,
111
+ },
112
+ })
113
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fake-snippets",
3
- "version": "0.0.118",
3
+ "version": "0.0.120",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -81,7 +81,7 @@
81
81
  "@tscircuit/3d-viewer": "^0.0.407",
82
82
  "@tscircuit/assembly-viewer": "^0.0.5",
83
83
  "@tscircuit/create-snippet-url": "^0.0.8",
84
- "@tscircuit/eval": "^0.0.410",
84
+ "@tscircuit/eval": "^0.0.414",
85
85
  "@tscircuit/layout": "^0.0.29",
86
86
  "@tscircuit/mm": "^0.0.8",
87
87
  "@tscircuit/pcb-viewer": "^1.11.218",
@@ -24,7 +24,23 @@ const HeaderButton = ({
24
24
  alsoHighlightForUrl?: string
25
25
  }) => {
26
26
  const [location] = useLocation()
27
+ const isExternal = /^(https?|mailto|tel):\/?\//i.test(href)
28
+ if (isExternal) {
29
+ return (
30
+ <a
31
+ className={cn("header-button", className)}
32
+ href={href}
33
+ target="_blank"
34
+ rel="noopener noreferrer"
35
+ >
36
+ <Button className={className} variant="ghost">
37
+ {children}
38
+ </Button>
39
+ </a>
40
+ )
41
+ }
27
42
 
43
+ // For internal links, use the Link Component
28
44
  if (location === href || location === alsoHighlightForUrl) {
29
45
  return (
30
46
  <Link className={cn("header-button", className)} href={href}>
@@ -20,12 +20,11 @@ import {
20
20
  import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
21
21
  import { timeAgo } from "@/lib/utils/timeAgo"
22
22
  import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
23
+ import { getPackagePreviewImageUrl } from "@/lib/utils/getPackagePreviewImageUrl"
23
24
 
24
25
  export interface PackageCardProps {
25
26
  /** The package data to display */
26
27
  pkg: Package
27
- /** Base URL for package images */
28
- baseUrl: string
29
28
  /** Whether to show the owner name (useful in starred views) */
30
29
  showOwner?: boolean
31
30
  /** Whether this is the current user's package (enables edit/delete options) */
@@ -46,7 +45,6 @@ export interface PackageCardProps {
46
45
 
47
46
  export const PackageCard: React.FC<PackageCardProps> = ({
48
47
  pkg,
49
- baseUrl,
50
48
  showOwner = false,
51
49
  isCurrentUserPackage = false,
52
50
  onDeleteClick,
@@ -73,6 +71,11 @@ export const PackageCard: React.FC<PackageCardProps> = ({
73
71
 
74
72
  const availableImages = ["pcb", "schematic", "assembly", "3d"]
75
73
 
74
+ const previewImageUrl = getPackagePreviewImageUrl(
75
+ pkg,
76
+ pkg.default_view as "pcb" | "schematic" | "3d",
77
+ )
78
+
76
79
  const cardContent = (
77
80
  <div
78
81
  className={`border p-4 rounded-md hover:shadow-md transition-shadow flex flex-col gap-4 ${className}`}
@@ -82,7 +85,7 @@ export const PackageCard: React.FC<PackageCardProps> = ({
82
85
  className={`${imageSize} flex-shrink-0 rounded-md overflow-hidden bg-gray-50 border flex items-center justify-center`}
83
86
  >
84
87
  <img
85
- src={`${baseUrl}/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/${availableImages.includes(pkg.default_view || "") ? pkg.default_view : "3d"}.png?fs_sha=${pkg.latest_package_release_fs_sha}`}
88
+ src={String(previewImageUrl)}
86
89
  alt={`${pkg.unscoped_name} ${availableImages.includes(pkg.default_view || "") ? pkg.default_view : "3D"} view`}
87
90
  className={`object-cover h-full w-full ${imageTransform}`}
88
91
  onError={(e) => {
@@ -14,16 +14,10 @@ interface PackageSearchResultsProps {
14
14
 
15
15
  const PackageGrid = ({
16
16
  packages,
17
- baseUrl,
18
17
  }: { packages: Package[]; baseUrl: string }) => (
19
18
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
20
19
  {packages.map((pkg) => (
21
- <PackageCard
22
- key={pkg.package_id}
23
- pkg={pkg}
24
- baseUrl={baseUrl}
25
- showOwner={true}
26
- />
20
+ <PackageCard key={pkg.package_id} pkg={pkg} showOwner={true} />
27
21
  ))}
28
22
  </div>
29
23
  )
@@ -4,7 +4,6 @@ import { useLocation } from "wouter"
4
4
  import React, { useEffect, useRef, useState } from "react"
5
5
  import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
- import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
7
  import { Link } from "wouter"
9
8
  import { CircuitBoard } from "lucide-react"
10
9
  import { cn } from "@/lib/utils"
@@ -60,7 +59,6 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
60
59
  const resultsRef = useRef<HTMLDivElement>(null)
61
60
  const inputRef = useRef<HTMLInputElement>(null)
62
61
  const [location, setLocation] = useLocation()
63
- const apiBaseUrl = useApiBaseUrl()
64
62
 
65
63
  const { data: searchResults, isLoading } = useQuery(
66
64
  ["packageSearch", searchQuery],
@@ -202,60 +200,73 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
202
200
  >
203
201
  {searchResults.length > 0 ? (
204
202
  <ul className="divide-y divide-gray-200 no-scrollbar">
205
- {searchResults.map((pkg: any, index: number) => (
206
- <li
207
- key={pkg.package_id}
208
- className={cn(
209
- "p-2 hover:bg-gray-50",
210
- index === highlightedIndex && "bg-gray-100",
211
- )}
212
- >
213
- <LinkWithNewTabHandling
214
- href={
215
- shouldOpenInEditor
216
- ? `/editor?package_id=${pkg.package_id}`
217
- : `/${pkg.name}`
218
- }
219
- shouldOpenInNewTab={shouldOpenInNewTab}
220
- className="flex"
221
- onClick={() => {
222
- setShowResults(false)
223
- if (closeOnClick) closeOnClick()
224
- }}
203
+ {searchResults.map((pkg: any, index: number) => {
204
+ const previewImageUrl =
205
+ pkg.latest_pcb_preview_image_url ??
206
+ pkg.latest_cad_preview_image_url ??
207
+ pkg.latest_sch_preview_image_url ??
208
+ undefined
209
+ const hasPreviewImage = Boolean(previewImageUrl)
210
+
211
+ return (
212
+ <li
213
+ key={pkg.package_id}
214
+ className={cn(
215
+ "p-2 hover:bg-gray-50",
216
+ index === highlightedIndex && "bg-gray-100",
217
+ )}
225
218
  >
226
- <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm bg-gray-50 border flex items-center justify-center">
227
- <img
228
- src={`${apiBaseUrl}/packages/images/${pkg.name}/pcb.svg`}
229
- alt={`PCB preview for ${pkg.name}`}
230
- draggable={false}
231
- className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
232
- onError={(e) => {
233
- e.currentTarget.style.display = "none"
234
- e.currentTarget.nextElementSibling?.classList.remove(
235
- "hidden",
236
- )
237
- e.currentTarget.nextElementSibling?.classList.add(
238
- "flex",
239
- )
240
- }}
241
- />
242
- <div className="w-12 h-12 hidden items-center justify-center">
243
- <CircuitBoard className="w-6 h-6 text-gray-300" />
244
- </div>
245
- </div>
246
- <div className="flex-grow">
247
- <div className="font-medium text-blue-600 break-words text-xs">
248
- {pkg.name}
219
+ <LinkWithNewTabHandling
220
+ href={
221
+ shouldOpenInEditor
222
+ ? `/editor?package_id=${pkg.package_id}`
223
+ : `/${pkg.name}`
224
+ }
225
+ shouldOpenInNewTab={shouldOpenInNewTab}
226
+ className="flex"
227
+ onClick={() => {
228
+ setShowResults(false)
229
+ if (closeOnClick) closeOnClick()
230
+ }}
231
+ >
232
+ <div className="w-12 h-12 overflow-hidden mr-2 flex-shrink-0 rounded-sm bg-gray-50 border flex items-center justify-center">
233
+ {hasPreviewImage ? (
234
+ <img
235
+ src={previewImageUrl}
236
+ alt={`PCB preview for ${pkg.name}`}
237
+ draggable={false}
238
+ className="w-12 h-12 object-contain p-1 scale-[4] rotate-45"
239
+ onError={(e) => {
240
+ e.currentTarget.style.display = "none"
241
+ e.currentTarget.nextElementSibling?.classList.remove(
242
+ "hidden",
243
+ )
244
+ e.currentTarget.nextElementSibling?.classList.add(
245
+ "flex",
246
+ )
247
+ }}
248
+ />
249
+ ) : null}
250
+ <div
251
+ className={`w-12 h-12 ${hasPreviewImage ? "hidden" : "flex"} items-center justify-center`}
252
+ >
253
+ <CircuitBoard className="w-6 h-6 text-gray-300" />
254
+ </div>
249
255
  </div>
250
- {pkg.description && (
251
- <div className="text-xs text-gray-500 break-words h-8 overflow-hidden">
252
- {pkg.description}
256
+ <div className="flex-grow">
257
+ <div className="font-medium text-blue-600 break-words text-xs">
258
+ {pkg.name}
253
259
  </div>
254
- )}
255
- </div>
256
- </LinkWithNewTabHandling>
257
- </li>
258
- ))}
260
+ {pkg.description && (
261
+ <div className="text-xs text-gray-500 break-words h-8 overflow-hidden">
262
+ {pkg.description}
263
+ </div>
264
+ )}
265
+ </div>
266
+ </LinkWithNewTabHandling>
267
+ </li>
268
+ )
269
+ })}
259
270
  </ul>
260
271
  ) : (
261
272
  <Alert variant="default" className="p-4 text-center">
@@ -3,13 +3,14 @@ import { useAxios } from "@/hooks/use-axios"
3
3
  import { StarFilledIcon } from "@radix-ui/react-icons"
4
4
  import { Link } from "wouter"
5
5
  import { Package } from "fake-snippets-api/lib/db/schema"
6
- import { useRef, useState } from "react"
7
- import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
6
+ import { useRef } from "react"
7
+ const CarouselItem = ({ pkg }: { pkg: Package }) => {
8
+ const previewImageUrl =
9
+ pkg.latest_pcb_preview_image_url ??
10
+ pkg.latest_cad_preview_image_url ??
11
+ pkg.latest_sch_preview_image_url ??
12
+ undefined
8
13
 
9
- const CarouselItem = ({
10
- pkg,
11
- apiBaseUrl,
12
- }: { pkg: Package; apiBaseUrl: string }) => {
13
14
  return (
14
15
  <Link href={`/${pkg.name}`}>
15
16
  <div className="flex-shrink-0 w-[200px] bg-white p-3 py-2 rounded-lg shadow-sm border border-gray-200 hover:border-gray-300 transition-colors">
@@ -17,11 +18,13 @@ const CarouselItem = ({
17
18
  {pkg.name}
18
19
  </div>
19
20
  <div className="mb-2 h-24 w-full bg-black rounded overflow-hidden">
20
- <img
21
- src={`${apiBaseUrl}/packages/images/${pkg.owner_github_username}/${pkg.unscoped_name}/pcb.svg?fs_map=${pkg.latest_package_release_fs_sha}`}
22
- alt="PCB preview"
23
- className="w-full h-full object-contain p-2 scale-[3] rotate-45 hover:scale-[3.5] transition-transform"
24
- />
21
+ {previewImageUrl ? (
22
+ <img
23
+ src={previewImageUrl}
24
+ alt="PCB preview"
25
+ className="w-full h-full object-contain p-2 scale-[3] rotate-45 hover:scale-[3.5] transition-transform"
26
+ />
27
+ ) : null}
25
28
  </div>
26
29
  <div className="flex items-center text-xs text-gray-500">
27
30
  <StarFilledIcon className="w-3 h-3 mr-1" />
@@ -35,8 +38,6 @@ const CarouselItem = ({
35
38
  export const TrendingPackagesCarousel = () => {
36
39
  const axios = useAxios()
37
40
  const scrollRef = useRef<HTMLDivElement>(null)
38
- const [isHovered, setIsHovered] = useState(false)
39
- const apiBaseUrl = useApiBaseUrl()
40
41
 
41
42
  const { data: trendingPackages } = useQuery<Package[]>(
42
43
  "trendingPackages",
@@ -59,22 +60,14 @@ export const TrendingPackagesCarousel = () => {
59
60
  <div className="container mx-auto px-4">
60
61
  <h2 className="text-2xl font-semibold mb-6">Trending Packages</h2>
61
62
  </div>
62
- <div
63
- className="flex gap-6 overflow-x-hidden relative"
64
- onMouseEnter={() => setIsHovered(true)}
65
- onMouseLeave={() => setIsHovered(false)}
66
- >
63
+ <div className="flex gap-6 overflow-x-hidden relative">
67
64
  <div
68
65
  ref={scrollRef}
69
66
  className="flex gap-6 transition-transform duration-1000 animate-carousel-left"
70
67
  >
71
68
  {[...(trendingPackages ?? []), ...(trendingPackages ?? [])].map(
72
69
  (pkg, i) => (
73
- <CarouselItem
74
- key={`${pkg.package_id}-${i}`}
75
- pkg={pkg}
76
- apiBaseUrl={apiBaseUrl}
77
- />
70
+ <CarouselItem key={`${pkg.package_id}-${i}`} pkg={pkg} />
78
71
  ),
79
72
  )}
80
73
  </div>
@@ -85,8 +85,9 @@ const MobileSidebar = ({
85
85
  })
86
86
 
87
87
  const { availableViews: pngViews } = usePreviewImages({
88
- packageName: packageInfo?.name,
89
- fsMapHash: packageInfo?.latest_package_release_fs_sha ?? "",
88
+ cadPreviewUrl: packageInfo?.latest_cad_preview_image_url,
89
+ pcbPreviewUrl: packageInfo?.latest_pcb_preview_image_url,
90
+ schematicPreviewUrl: packageInfo?.latest_sch_preview_image_url,
90
91
  })
91
92
 
92
93
  const viewsToRender =
@@ -4,7 +4,9 @@ import type { Package } from "fake-snippets-api/lib/db/schema"
4
4
  interface ViewPlaceholdersProps {
5
5
  packageInfo?: Pick<
6
6
  Package,
7
- "name" | "latest_package_release_fs_sha" | "latest_package_release_id"
7
+ | "latest_cad_preview_image_url"
8
+ | "latest_pcb_preview_image_url"
9
+ | "latest_sch_preview_image_url"
8
10
  >
9
11
  onViewChange?: (view: "3d" | "pcb" | "schematic") => void
10
12
  }
@@ -14,8 +16,9 @@ export default function PreviewImageSquares({
14
16
  onViewChange,
15
17
  }: ViewPlaceholdersProps) {
16
18
  const { availableViews } = usePreviewImages({
17
- packageName: packageInfo?.name,
18
- fsMapHash: packageInfo?.latest_package_release_fs_sha ?? "",
19
+ cadPreviewUrl: packageInfo?.latest_cad_preview_image_url,
20
+ pcbPreviewUrl: packageInfo?.latest_pcb_preview_image_url,
21
+ schematicPreviewUrl: packageInfo?.latest_sch_preview_image_url,
19
22
  })
20
23
  const handleViewClick = (viewId: string) => {
21
24
  onViewChange?.(viewId as "3d" | "pcb" | "schematic")
@@ -1,46 +1,50 @@
1
- import { useState } from "react"
1
+ import { useEffect, useState } from "react"
2
2
 
3
3
  interface UsePreviewImagesProps {
4
- packageName?: string
5
- fsMapHash?: string
4
+ cadPreviewUrl?: string | null
5
+ pcbPreviewUrl?: string | null
6
+ schematicPreviewUrl?: string | null
6
7
  }
7
8
 
8
9
  export function usePreviewImages({
9
- packageName,
10
- fsMapHash,
10
+ cadPreviewUrl,
11
+ pcbPreviewUrl,
12
+ schematicPreviewUrl,
11
13
  }: UsePreviewImagesProps) {
12
14
  const [imageStatus, setImageStatus] = useState<
13
15
  Record<string, "loading" | "loaded" | "error">
14
16
  >({
15
- "3d": "loading",
16
- pcb: "loading",
17
- schematic: "loading",
17
+ "3d": cadPreviewUrl ? "loading" : "error",
18
+ pcb: pcbPreviewUrl ? "loading" : "error",
19
+ schematic: schematicPreviewUrl ? "loading" : "error",
18
20
  })
19
21
 
22
+ useEffect(() => {
23
+ setImageStatus({
24
+ "3d": cadPreviewUrl ? "loading" : "error",
25
+ pcb: pcbPreviewUrl ? "loading" : "error",
26
+ schematic: schematicPreviewUrl ? "loading" : "error",
27
+ })
28
+ }, [cadPreviewUrl, pcbPreviewUrl, schematicPreviewUrl])
29
+
20
30
  const views = [
21
31
  {
22
32
  id: "3d",
23
33
  label: "3D View",
24
34
  backgroundClass: "bg-gray-100",
25
- imageUrl: packageName
26
- ? `https://api.tscircuit.com/packages/images/${packageName}/3d.png?fs_sha=${fsMapHash}`
27
- : undefined,
35
+ imageUrl: cadPreviewUrl ?? undefined,
28
36
  },
29
37
  {
30
38
  id: "pcb",
31
39
  label: "PCB View",
32
40
  backgroundClass: "bg-black",
33
- imageUrl: packageName
34
- ? `https://api.tscircuit.com/packages/images/${packageName}/pcb.png?fs_sha=${fsMapHash}`
35
- : undefined,
41
+ imageUrl: pcbPreviewUrl ?? undefined,
36
42
  },
37
43
  {
38
44
  id: "schematic",
39
45
  label: "Schematic View",
40
46
  backgroundClass: "bg-[#F5F1ED]",
41
- imageUrl: packageName
42
- ? `https://api.tscircuit.com/packages/images/${packageName}/schematic.png?fs_sha=${fsMapHash}`
43
- : undefined,
47
+ imageUrl: schematicPreviewUrl ?? undefined,
44
48
  },
45
49
  ]
46
50
 
@@ -59,6 +63,7 @@ export function usePreviewImages({
59
63
  }
60
64
 
61
65
  const availableViews = views
66
+ .filter((view) => Boolean(view.imageUrl))
62
67
  .map((view) => ({
63
68
  ...view,
64
69
  status: imageStatus[view.id],
@@ -88,6 +88,14 @@ export function useUpdatePackageFilesMutation({
88
88
  }
89
89
  }
90
90
  }
91
+
92
+ if (!currentPackage) {
93
+ await axios.post("/package_releases/update", {
94
+ package_name_with_version: newPackage.package_name_with_version,
95
+ ready_to_build: true,
96
+ })
97
+ }
98
+
91
99
  return updatedFilesCount
92
100
  },
93
101
  onSuccess: (updatedFilesCount) => {
@@ -0,0 +1,15 @@
1
+ import { Package } from "fake-snippets-api/lib/db/schema"
2
+
3
+ export const getPackagePreviewImageUrl = (
4
+ pkg: Package,
5
+ view: "pcb" | "schematic" | "3d" = "pcb",
6
+ ) => {
7
+ switch (view) {
8
+ case "schematic":
9
+ return pkg.latest_sch_preview_image_url
10
+ case "3d":
11
+ return pkg.latest_cad_preview_image_url
12
+ default:
13
+ return pkg.latest_pcb_preview_image_url
14
+ }
15
+ }
@@ -168,7 +168,6 @@ export const DashboardPage = () => {
168
168
  <PackageCard
169
169
  key={pkg.package_id}
170
170
  pkg={pkg}
171
- baseUrl={baseUrl}
172
171
  isCurrentUserPackage={
173
172
  pkg.owner_github_username === currentUser
174
173
  }