@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.
- package/bun-tests/fake-snippets-api/routes/bug_reports/create.test.ts +37 -0
- package/bun-tests/fake-snippets-api/routes/bug_reports/upload_file.test.ts +89 -0
- package/bun-tests/fake-snippets-api/routes/packages/get.test.ts +3 -0
- package/bun.lock +2 -2
- package/dist/bundle.js +778 -508
- package/dist/index.d.ts +161 -6
- package/dist/index.js +102 -3
- package/dist/schema.d.ts +225 -7
- package/dist/schema.js +38 -3
- package/fake-snippets-api/lib/db/db-client.ts +98 -0
- package/fake-snippets-api/lib/db/schema.ts +37 -0
- package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +6 -0
- package/fake-snippets-api/lib/public-mapping/public-map-package.ts +9 -0
- package/fake-snippets-api/routes/api/bug_reports/create.ts +43 -0
- package/fake-snippets-api/routes/api/bug_reports/upload_file.ts +113 -0
- package/package.json +2 -2
- package/src/components/Header.tsx +16 -0
- package/src/components/PackageCard.tsx +7 -4
- package/src/components/PackageSearchResults.tsx +1 -7
- package/src/components/SearchComponent.tsx +64 -53
- package/src/components/TrendingPackagesCarousel.tsx +16 -23
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +3 -2
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +6 -3
- package/src/hooks/use-preview-images.ts +22 -17
- package/src/hooks/useUpdatePackageFilesMutation.ts +8 -0
- package/src/lib/utils/getPackagePreviewImageUrl.ts +15 -0
- package/src/pages/dashboard.tsx +0 -1
- package/src/pages/editor.tsx +12 -9
- package/src/pages/organization-profile.tsx +0 -1
- package/src/pages/package-editor.tsx +13 -9
- 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.
|
|
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.
|
|
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={
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
251
|
-
<div className="
|
|
252
|
-
{pkg.
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
7
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
5
|
-
|
|
4
|
+
cadPreviewUrl?: string | null
|
|
5
|
+
pcbPreviewUrl?: string | null
|
|
6
|
+
schematicPreviewUrl?: string | null
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function usePreviewImages({
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
+
}
|