@tscircuit/fake-snippets 0.0.108 → 0.0.109
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.lock +62 -19
- package/dist/bundle.js +3 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -1
- package/dist/schema.d.ts +8 -0
- package/dist/schema.js +2 -1
- package/fake-snippets-api/lib/db/schema.ts +1 -0
- package/package.json +7 -8
- package/src/App.tsx +0 -2
- package/src/components/DownloadButtonAndMenu.tsx +133 -35
- package/src/components/FileSidebar.tsx +31 -34
- package/src/components/Footer.tsx +0 -1
- package/src/components/HeaderLogin.tsx +1 -1
- package/src/components/HiddenFilesDropdown.tsx +0 -2
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +0 -2
- package/src/components/PackageBuildsPage/build-preview-content.tsx +34 -5
- package/src/components/PackageCard.tsx +0 -1
- package/src/components/ViewPackagePage/components/ShikiCodeViewer.tsx +20 -11
- package/src/components/ViewPackagePage/components/important-files-view.tsx +75 -59
- package/src/components/ViewPackagePage/components/main-content-header.tsx +4 -4
- package/src/components/ViewPackagePage/components/main-content-view-selector.tsx +0 -1
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +0 -1
- package/src/components/ViewPackagePage/components/package-header.tsx +14 -17
- package/src/components/ViewPackagePage/components/preview-image-squares.tsx +0 -1
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +21 -20
- package/src/components/ViewPackagePage/components/sidebar.tsx +0 -2
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +18 -17
- package/src/components/ViewPackagePage/components/theme-toggle.tsx +0 -2
- package/src/components/ViewPackagePage/hooks/use-toast.tsx +0 -1
- package/src/components/package-port/CodeAndPreview.tsx +23 -40
- package/src/components/package-port/CodeEditor.tsx +24 -1
- package/src/components/package-port/CodeEditorHeader.tsx +5 -2
- package/src/components/preview/PackageReleasesDashboard.tsx +30 -25
- package/src/hooks/use-current-package-id.ts +5 -30
- package/src/hooks/use-current-package-info.ts +29 -5
- package/src/hooks/use-global-store.ts +1 -1
- package/src/hooks/useFileManagement.ts +153 -34
- package/src/hooks/useOptimizedPackageFilesLoader.ts +149 -0
- package/src/hooks/useUpdatePackageFilesMutation.ts +2 -0
- package/src/lib/download-fns/download-circuit-png.ts +11 -3
- package/src/lib/download-fns/download-gltf-from-circuit-json.ts +44 -0
- package/src/lib/utils/isComponentExported.ts +9 -0
- package/src/pages/authorize.tsx +0 -2
- package/src/pages/landing.tsx +0 -1
- package/src/pages/preview-release.tsx +14 -4
- package/src/pages/view-package.tsx +14 -13
- package/src/components/Footer2.tsx +0 -100
- package/src/components/ShippingInformationForm.tsx +0 -423
- package/src/components/StaticViewSnippetHeader.tsx +0 -70
- package/src/components/ViewPackagePage/components/file-explorer.tsx +0 -67
- package/src/components/ViewPackagePage/components/readme-view.tsx +0 -58
- package/src/components/ViewPackagePage/components/repo-header-button.tsx +0 -36
- package/src/components/ViewPackagePage/components/repo-header.tsx +0 -4
- package/src/components/ViewPackagePage/components/sidebar-contributors-section.tsx +0 -31
- package/src/components/ViewSnippetHeader.tsx +0 -181
- package/src/components/ui/input-otp.tsx +0 -69
- package/src/pages/settings.tsx +0 -25
|
@@ -6,7 +6,7 @@ import { Package } from "fake-snippets-api/lib/db/schema"
|
|
|
6
6
|
import { usePackageFiles } from "./use-package-files"
|
|
7
7
|
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
8
8
|
import { decodeUrlHashToFsMap } from "@/lib/decodeUrlHashToFsMap"
|
|
9
|
-
import {
|
|
9
|
+
import { useOptimizedPackageFilesLoader } from "./useOptimizedPackageFilesLoader"
|
|
10
10
|
import { useGlobalStore } from "./use-global-store"
|
|
11
11
|
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
12
12
|
import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
|
|
@@ -15,6 +15,7 @@ import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
|
15
15
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
16
16
|
import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
|
|
17
17
|
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
18
|
+
import { isComponentExported } from "@/lib/utils/isComponentExported"
|
|
18
19
|
|
|
19
20
|
export interface ICreateFileProps {
|
|
20
21
|
newFileName: string
|
|
@@ -47,27 +48,37 @@ export interface IRenameFileResult {
|
|
|
47
48
|
export function useFileManagement({
|
|
48
49
|
templateCode,
|
|
49
50
|
currentPackage,
|
|
50
|
-
fileChoosen,
|
|
51
51
|
openNewPackageSaveDialog,
|
|
52
52
|
updateLastUpdated,
|
|
53
|
+
urlParams,
|
|
53
54
|
}: {
|
|
54
55
|
templateCode?: string
|
|
55
56
|
currentPackage?: Package
|
|
56
|
-
fileChoosen: string | null
|
|
57
57
|
openNewPackageSaveDialog: () => void
|
|
58
|
+
urlParams: Record<string, string>
|
|
58
59
|
updateLastUpdated: () => void
|
|
59
60
|
}) {
|
|
61
|
+
const fileChosen = urlParams.file_path ?? null
|
|
60
62
|
const [localFiles, setLocalFiles] = useState<PackageFile[]>([])
|
|
61
63
|
const [initialFiles, setInitialFiles] = useState<PackageFile[]>([])
|
|
62
64
|
const [currentFile, setCurrentFile] = useState<string | null>(null)
|
|
63
65
|
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
64
|
-
const loggedInUser = useGlobalStore((s) => s.session)
|
|
65
66
|
const { toast } = useToast()
|
|
66
67
|
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
67
68
|
const {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
priorityFile,
|
|
70
|
+
allFiles: packageFilesWithContent,
|
|
71
|
+
isPriorityLoading,
|
|
72
|
+
areAllFilesLoading: isLoadingPackageFilesWithContent,
|
|
73
|
+
totalFilesCount,
|
|
74
|
+
loadedFilesCount,
|
|
75
|
+
isPriorityFileFetched,
|
|
76
|
+
} = useOptimizedPackageFilesLoader(
|
|
77
|
+
currentPackage,
|
|
78
|
+
fileChosen,
|
|
79
|
+
urlParams.package_id,
|
|
80
|
+
)
|
|
81
|
+
|
|
71
82
|
const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
|
|
72
83
|
usePackageFiles(currentPackage?.latest_package_release_id)
|
|
73
84
|
const initialCodeContent = useMemo(() => {
|
|
@@ -127,7 +138,7 @@ export function useFileManagement({
|
|
|
127
138
|
})
|
|
128
139
|
|
|
129
140
|
useEffect(() => {
|
|
130
|
-
if (!currentPackage ||
|
|
141
|
+
if (!currentPackage || isPriorityLoading) {
|
|
131
142
|
const decodedFsMap = decodeUrlHashToFsMap(window.location.toString())
|
|
132
143
|
|
|
133
144
|
if (decodedFsMap && Object.keys(decodedFsMap).length > 0) {
|
|
@@ -137,38 +148,114 @@ export function useFileManagement({
|
|
|
137
148
|
content: String(content),
|
|
138
149
|
}),
|
|
139
150
|
)
|
|
140
|
-
const targetFile = findTargetFile(filesFromUrl,
|
|
151
|
+
const targetFile = findTargetFile(filesFromUrl, fileChosen)
|
|
141
152
|
setLocalFiles(filesFromUrl)
|
|
142
153
|
setInitialFiles([])
|
|
143
154
|
setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
|
|
144
155
|
return
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
if (!urlParams.package_id) {
|
|
159
|
+
setLocalFiles([
|
|
160
|
+
{
|
|
161
|
+
path: "index.tsx",
|
|
162
|
+
content: initialCodeContent || "",
|
|
163
|
+
},
|
|
164
|
+
])
|
|
165
|
+
setInitialFiles([])
|
|
166
|
+
setCurrentFile("index.tsx")
|
|
167
|
+
}
|
|
155
168
|
return
|
|
156
|
-
} else {
|
|
157
|
-
const targetFile = findTargetFile(
|
|
158
|
-
packageFilesWithContent || [],
|
|
159
|
-
fileChoosen,
|
|
160
|
-
)
|
|
161
|
-
setLocalFiles(packageFilesWithContent || [])
|
|
162
|
-
setInitialFiles(packageFilesWithContent || [])
|
|
163
|
-
setCurrentFile(targetFile?.path || null)
|
|
164
169
|
}
|
|
165
|
-
}, [currentPackage,
|
|
170
|
+
}, [currentPackage, isPriorityLoading])
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (priorityFile && !isPriorityLoading && currentPackage) {
|
|
174
|
+
setLocalFiles((prev) => {
|
|
175
|
+
const existingIndex = prev.findIndex(
|
|
176
|
+
(f) => f.path === priorityFile.path,
|
|
177
|
+
)
|
|
178
|
+
if (existingIndex >= 0) {
|
|
179
|
+
const updated = [...prev]
|
|
180
|
+
updated[existingIndex] = priorityFile
|
|
181
|
+
return updated
|
|
182
|
+
}
|
|
183
|
+
return [...prev, priorityFile]
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
setCurrentFile((prevCurrentFile) => {
|
|
187
|
+
if (fileChosen && priorityFile.path === fileChosen) {
|
|
188
|
+
return priorityFile.path
|
|
189
|
+
} else if (!prevCurrentFile) {
|
|
190
|
+
return priorityFile.path
|
|
191
|
+
} else {
|
|
192
|
+
// If priority file is index.tsx, always update to it
|
|
193
|
+
const isPriorityFileBetter = priorityFile.path === "index.tsx"
|
|
194
|
+
if (isPriorityFileBetter) {
|
|
195
|
+
return priorityFile.path
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return prevCurrentFile
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
}, [priorityFile, isPriorityLoading, currentPackage, fileChosen])
|
|
202
|
+
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
if (packageFilesWithContent.length > 0 && currentPackage) {
|
|
205
|
+
setLocalFiles(packageFilesWithContent)
|
|
206
|
+
setInitialFiles(packageFilesWithContent)
|
|
207
|
+
|
|
208
|
+
if (fileChosen) {
|
|
209
|
+
const targetFile =
|
|
210
|
+
packageFilesWithContent.find((f) => f.path === fileChosen) ||
|
|
211
|
+
findTargetFile(packageFilesWithContent, fileChosen)
|
|
212
|
+
if (targetFile) {
|
|
213
|
+
setCurrentFile((prevCurrentFile) => {
|
|
214
|
+
return targetFile.path !== prevCurrentFile
|
|
215
|
+
? targetFile.path
|
|
216
|
+
: prevCurrentFile
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
setCurrentFile((prevCurrentFile) => {
|
|
221
|
+
if (!prevCurrentFile) {
|
|
222
|
+
// Wait for priority file to load before making selection to avoid flicker
|
|
223
|
+
// Only select if we have a good candidate (tsx/ts file) or priority file isn't loading
|
|
224
|
+
const targetFile = findTargetFile(
|
|
225
|
+
packageFilesWithContent,
|
|
226
|
+
fileChosen,
|
|
227
|
+
)
|
|
228
|
+
// Only consider it a "good enough" candidate if it's index.tsx
|
|
229
|
+
// Otherwise, wait for the actual priority file (index.tsx) to load
|
|
230
|
+
const isTopPriorityFile =
|
|
231
|
+
targetFile && targetFile.path === "index.tsx"
|
|
232
|
+
const shouldWaitForPriority =
|
|
233
|
+
isPriorityLoading && !isTopPriorityFile
|
|
234
|
+
|
|
235
|
+
if (shouldWaitForPriority) {
|
|
236
|
+
return prevCurrentFile // Keep null to avoid flicker
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return targetFile ? targetFile.path : prevCurrentFile
|
|
240
|
+
}
|
|
241
|
+
return prevCurrentFile
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}, [
|
|
246
|
+
packageFilesWithContent.length,
|
|
247
|
+
currentPackage,
|
|
248
|
+
fileChosen,
|
|
249
|
+
isPriorityLoading,
|
|
250
|
+
])
|
|
166
251
|
|
|
167
252
|
const isLoading = useMemo(() => {
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
253
|
+
return isPriorityLoading || isLoadingPackageFiles
|
|
254
|
+
}, [isPriorityLoading, isLoadingPackageFiles])
|
|
255
|
+
|
|
256
|
+
const isFullyLoaded = useMemo(() => {
|
|
257
|
+
return !isLoadingPackageFilesWithContent && !isLoadingPackageFiles
|
|
258
|
+
}, [isLoadingPackageFilesWithContent, isLoadingPackageFiles])
|
|
172
259
|
|
|
173
260
|
const fsMap = useMemo(() => {
|
|
174
261
|
const map = localFiles.reduce(
|
|
@@ -178,6 +265,7 @@ export function useFileManagement({
|
|
|
178
265
|
},
|
|
179
266
|
{} as Record<string, string>,
|
|
180
267
|
)
|
|
268
|
+
|
|
181
269
|
return map
|
|
182
270
|
}, [localFiles, manualEditsFileContent])
|
|
183
271
|
|
|
@@ -215,7 +303,6 @@ export function useFileManagement({
|
|
|
215
303
|
}
|
|
216
304
|
}
|
|
217
305
|
|
|
218
|
-
// Check if file already exists
|
|
219
306
|
const fileExists = localFiles?.some((file) => file.path === newFileName)
|
|
220
307
|
if (fileExists) {
|
|
221
308
|
onError(new Error(`File '${newFileName}' already exists`))
|
|
@@ -224,7 +311,6 @@ export function useFileManagement({
|
|
|
224
311
|
}
|
|
225
312
|
}
|
|
226
313
|
|
|
227
|
-
// Ensure file name is not empty after path construction
|
|
228
314
|
const fileName = newFileName.split("/").pop() || ""
|
|
229
315
|
if (!fileName.trim()) {
|
|
230
316
|
onError(new Error("File name cannot be empty"))
|
|
@@ -282,7 +368,6 @@ export function useFileManagement({
|
|
|
282
368
|
}
|
|
283
369
|
}
|
|
284
370
|
|
|
285
|
-
// Extract just the filename from the path for validation
|
|
286
371
|
const fileNameOnly = newFilename.split("/").pop() || ""
|
|
287
372
|
if (!isValidFileName(fileNameOnly)) {
|
|
288
373
|
onError(new Error("Invalid file name"))
|
|
@@ -395,7 +480,6 @@ export function useFileManagement({
|
|
|
395
480
|
|
|
396
481
|
const saveFiles = () => {
|
|
397
482
|
if (!isLoggedIn) {
|
|
398
|
-
// For non-logged-in users, trigger immediate URL save
|
|
399
483
|
if (debounceTimeoutRef.current) {
|
|
400
484
|
clearTimeout(debounceTimeoutRef.current)
|
|
401
485
|
}
|
|
@@ -433,20 +517,55 @@ export function useFileManagement({
|
|
|
433
517
|
isCreatingRelease,
|
|
434
518
|
])
|
|
435
519
|
|
|
520
|
+
const currentFileCode = useMemo(
|
|
521
|
+
() =>
|
|
522
|
+
localFiles.find((x) => x.path === currentFile)?.content ?? DEFAULT_CODE,
|
|
523
|
+
[localFiles, currentFile],
|
|
524
|
+
)
|
|
525
|
+
const mainComponentPath = useMemo(() => {
|
|
526
|
+
const isComponentExportedInCurrentFile =
|
|
527
|
+
isComponentExported(currentFileCode)
|
|
528
|
+
|
|
529
|
+
const selectedComponent =
|
|
530
|
+
(currentFile?.endsWith(".tsx") || currentFile?.endsWith(".ts")) &&
|
|
531
|
+
!!localFiles.some((x) => x.path === currentFile) &&
|
|
532
|
+
isComponentExportedInCurrentFile
|
|
533
|
+
? currentFile
|
|
534
|
+
: localFiles.find((x) => {
|
|
535
|
+
const isComponentExportedInFile = isComponentExported(x.content)
|
|
536
|
+
return (
|
|
537
|
+
x.path.endsWith(".tsx") ||
|
|
538
|
+
(x.path.endsWith(".ts") && isComponentExportedInFile)
|
|
539
|
+
)
|
|
540
|
+
})?.path || localFiles[0]?.path
|
|
541
|
+
|
|
542
|
+
return selectedComponent
|
|
543
|
+
}, [currentFile, localFiles, currentFileCode, packageFilesWithContent])
|
|
544
|
+
|
|
545
|
+
const priorityFileFetched = useMemo(() => {
|
|
546
|
+
return urlParams.package_id && isPriorityFileFetched
|
|
547
|
+
}, [urlParams.package_id, isPriorityFileFetched])
|
|
548
|
+
|
|
436
549
|
return {
|
|
437
550
|
fsMap,
|
|
438
551
|
createFile,
|
|
552
|
+
priorityFileFetched,
|
|
439
553
|
deleteFile,
|
|
440
554
|
renameFile,
|
|
441
555
|
saveFiles,
|
|
442
556
|
localFiles,
|
|
557
|
+
mainComponentPath,
|
|
558
|
+
currentFileCode,
|
|
443
559
|
initialFiles,
|
|
444
560
|
currentFile,
|
|
445
561
|
setLocalFiles,
|
|
446
562
|
onFileSelect,
|
|
447
563
|
isLoading,
|
|
564
|
+
isFullyLoaded,
|
|
448
565
|
isSaving,
|
|
449
566
|
savePackage,
|
|
450
567
|
packageFilesMeta,
|
|
568
|
+
totalFilesCount,
|
|
569
|
+
loadedFilesCount,
|
|
451
570
|
}
|
|
452
571
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useQuery, useQueries } from "react-query"
|
|
2
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
3
|
+
import { usePackageFiles } from "@/hooks/use-package-files"
|
|
4
|
+
import { usePackageFileById } from "@/hooks/use-package-files"
|
|
5
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
6
|
+
import { useState, useMemo } from "react"
|
|
7
|
+
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
8
|
+
|
|
9
|
+
export interface PackageFile {
|
|
10
|
+
path: string
|
|
11
|
+
content: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OptimizedLoadingState {
|
|
15
|
+
priorityFile: PackageFile | null
|
|
16
|
+
allFiles: PackageFile[]
|
|
17
|
+
isPriorityLoading: boolean
|
|
18
|
+
areAllFilesLoading: boolean
|
|
19
|
+
error: any
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useOptimizedPackageFilesLoader(
|
|
23
|
+
pkg?: Package,
|
|
24
|
+
priorityFilePath?: string | null,
|
|
25
|
+
packageId?: string | null,
|
|
26
|
+
) {
|
|
27
|
+
const axios = useAxios()
|
|
28
|
+
const [loadedFiles, setLoadedFiles] = useState<Map<string, PackageFile>>(
|
|
29
|
+
new Map(),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const pkgFilesRaw = usePackageFiles(pkg?.latest_package_release_id)
|
|
33
|
+
|
|
34
|
+
// Filter out dist/ files to avoid downloading build artifacts
|
|
35
|
+
const pkgFiles = useMemo(
|
|
36
|
+
() => ({
|
|
37
|
+
...pkgFilesRaw,
|
|
38
|
+
data: pkgFilesRaw.data?.filter(
|
|
39
|
+
(file) => !file.file_path.startsWith("dist/"),
|
|
40
|
+
),
|
|
41
|
+
}),
|
|
42
|
+
[pkgFilesRaw.data, pkgFilesRaw.isLoading, pkgFilesRaw.error],
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const targetFilePath = useMemo(() => {
|
|
46
|
+
if (!pkgFiles.data) return priorityFilePath
|
|
47
|
+
|
|
48
|
+
if (priorityFilePath) {
|
|
49
|
+
const exactMatch = pkgFiles.data.find(
|
|
50
|
+
(f) => f.file_path === priorityFilePath,
|
|
51
|
+
)
|
|
52
|
+
if (exactMatch) return exactMatch.file_path
|
|
53
|
+
|
|
54
|
+
const partialMatch = pkgFiles.data.find(
|
|
55
|
+
(f) =>
|
|
56
|
+
f.file_path.includes(priorityFilePath) ||
|
|
57
|
+
priorityFilePath.includes(f.file_path),
|
|
58
|
+
)
|
|
59
|
+
if (partialMatch) return partialMatch.file_path
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check for index.tsx first
|
|
63
|
+
const indexFile = pkgFiles.data.find((f) => f.file_path === "index.tsx")
|
|
64
|
+
if (indexFile) return indexFile.file_path
|
|
65
|
+
|
|
66
|
+
// Fallback to first file
|
|
67
|
+
return pkgFiles.data[0]?.file_path || null
|
|
68
|
+
}, [pkgFiles.data, priorityFilePath])
|
|
69
|
+
|
|
70
|
+
const priorityFileData = pkgFiles.data?.find(
|
|
71
|
+
(file) => file.file_path === targetFilePath,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const priorityFileQuery = useQuery({
|
|
75
|
+
queryKey: ["priorityPackageFile", priorityFileData?.package_file_id],
|
|
76
|
+
queryFn: async () => {
|
|
77
|
+
if (!priorityFileData) return null
|
|
78
|
+
|
|
79
|
+
const response = await axios.post(`/package_files/get`, {
|
|
80
|
+
package_file_id: priorityFileData.package_file_id,
|
|
81
|
+
})
|
|
82
|
+
const content = response.data.package_file?.content_text
|
|
83
|
+
const file = { path: priorityFileData.file_path, content: content ?? "" }
|
|
84
|
+
|
|
85
|
+
setLoadedFiles((prev) => {
|
|
86
|
+
const newMap = new Map(prev)
|
|
87
|
+
newMap.set(file.path, file)
|
|
88
|
+
return newMap
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
return file
|
|
92
|
+
},
|
|
93
|
+
enabled: !!priorityFileData,
|
|
94
|
+
refetchOnWindowFocus: false,
|
|
95
|
+
refetchOnMount: false,
|
|
96
|
+
staleTime: Infinity,
|
|
97
|
+
cacheTime: Infinity,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const remainingFilesQueries = useQueries(
|
|
101
|
+
pkgFiles.data
|
|
102
|
+
?.filter((file) => file.file_path !== targetFilePath)
|
|
103
|
+
?.map((file) => ({
|
|
104
|
+
queryKey: ["packageFile", file.package_file_id],
|
|
105
|
+
queryFn: async () => {
|
|
106
|
+
const response = await axios.post(`/package_files/get`, {
|
|
107
|
+
package_file_id: file.package_file_id,
|
|
108
|
+
})
|
|
109
|
+
const content = response.data.package_file?.content_text
|
|
110
|
+
const fileData = { path: file.file_path, content: content ?? "" }
|
|
111
|
+
|
|
112
|
+
setLoadedFiles((prev) => {
|
|
113
|
+
const newMap = new Map(prev)
|
|
114
|
+
newMap.set(fileData.path, fileData)
|
|
115
|
+
return newMap
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return fileData
|
|
119
|
+
},
|
|
120
|
+
refetchOnWindowFocus: false,
|
|
121
|
+
refetchOnMount: false,
|
|
122
|
+
staleTime: Infinity,
|
|
123
|
+
cacheTime: Infinity,
|
|
124
|
+
})) ?? [],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const allFiles = useMemo(() => {
|
|
128
|
+
return Array.from(loadedFiles.values())
|
|
129
|
+
}, [loadedFiles])
|
|
130
|
+
|
|
131
|
+
const areAllFilesLoading =
|
|
132
|
+
remainingFilesQueries.some((q) => q.isLoading) ||
|
|
133
|
+
priorityFileQuery.isLoading
|
|
134
|
+
const error =
|
|
135
|
+
priorityFileQuery.error || remainingFilesQueries.find((q) => q.error)?.error
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
priorityFile: priorityFileQuery.data || null,
|
|
139
|
+
priorityFileFetched: priorityFileQuery.isFetched,
|
|
140
|
+
allFiles,
|
|
141
|
+
isPriorityLoading: priorityFileQuery.isLoading,
|
|
142
|
+
areAllFilesLoading,
|
|
143
|
+
error,
|
|
144
|
+
isMetaLoading: pkgFiles.isLoading,
|
|
145
|
+
totalFilesCount: pkgFiles.data?.length || 0,
|
|
146
|
+
loadedFilesCount: allFiles.length,
|
|
147
|
+
isPriorityFileFetched: priorityFileQuery.isFetched,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -2,6 +2,7 @@ import { useMutation } from "react-query"
|
|
|
2
2
|
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
3
3
|
import { useAxios } from "./use-axios"
|
|
4
4
|
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
5
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
5
6
|
|
|
6
7
|
interface PackageFile {
|
|
7
8
|
path: string
|
|
@@ -42,6 +43,7 @@ export function useUpdatePackageFilesMutation({
|
|
|
42
43
|
let updatedFilesCount = 0
|
|
43
44
|
|
|
44
45
|
for (const file of localFiles) {
|
|
46
|
+
if (isHiddenFile(file.path)) continue
|
|
45
47
|
const initialFile = initialFiles.find((x) => x.path === file.path)
|
|
46
48
|
if (file.content !== initialFile?.content) {
|
|
47
49
|
const updatePkgFilePayload = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { convertCircuitJsonToSimple3dSvg } from "circuit-json-to-simple-3d"
|
|
1
2
|
import { AnyCircuitElement } from "circuit-json"
|
|
2
3
|
import {
|
|
3
4
|
convertCircuitJsonToAssemblySvg,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
} from "circuit-to-svg"
|
|
7
8
|
import { saveAs } from "file-saver"
|
|
8
9
|
|
|
9
|
-
export type ImageFormat = "schematic" | "pcb" | "assembly"
|
|
10
|
+
export type ImageFormat = "schematic" | "pcb" | "assembly" | "3d"
|
|
10
11
|
|
|
11
12
|
interface DownloadCircuitPngOptions {
|
|
12
13
|
format: ImageFormat
|
|
@@ -65,7 +66,7 @@ export const downloadCircuitPng = async (
|
|
|
65
66
|
if (options.width) svgOptions.width = options.width
|
|
66
67
|
if (options.height) svgOptions.height = options.height
|
|
67
68
|
|
|
68
|
-
switch (options.format) {
|
|
69
|
+
switch (options.format.toLowerCase()) {
|
|
69
70
|
case "schematic":
|
|
70
71
|
svg = convertCircuitJsonToSchematicSvg(circuitJson, svgOptions)
|
|
71
72
|
break
|
|
@@ -76,13 +77,20 @@ export const downloadCircuitPng = async (
|
|
|
76
77
|
svg = convertCircuitJsonToAssemblySvg(circuitJson, svgOptions)
|
|
77
78
|
break
|
|
78
79
|
default:
|
|
79
|
-
|
|
80
|
+
svg = await convertCircuitJsonToSimple3dSvg(circuitJson, {
|
|
81
|
+
background: {
|
|
82
|
+
color: "#fff",
|
|
83
|
+
opacity: 0.0,
|
|
84
|
+
},
|
|
85
|
+
defaultZoomMultiplier: 1.1,
|
|
86
|
+
})
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
blob = await convertSvgToPng(svg)
|
|
83
90
|
const downloadFileName = `${fileName}_${options.format}.png`
|
|
84
91
|
saveAs(blob, downloadFileName)
|
|
85
92
|
} catch (error) {
|
|
93
|
+
console.error(error)
|
|
86
94
|
throw new Error(`Failed to download ${options.format} PNG: ${error}`)
|
|
87
95
|
}
|
|
88
96
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { CircuitJson } from "circuit-json"
|
|
2
|
+
import { saveAs } from "file-saver"
|
|
3
|
+
import {
|
|
4
|
+
convertCircuitJsonToGltf,
|
|
5
|
+
type ConversionOptions,
|
|
6
|
+
} from "circuit-json-to-gltf"
|
|
7
|
+
export const downloadGltfFromCircuitJson = async (
|
|
8
|
+
circuitJson: CircuitJson,
|
|
9
|
+
fileName: string,
|
|
10
|
+
options?: ConversionOptions,
|
|
11
|
+
) => {
|
|
12
|
+
const result = await convertCircuitJsonToGltf(circuitJson, options)
|
|
13
|
+
|
|
14
|
+
let blob: Blob
|
|
15
|
+
let extension = options?.format === "glb" ? ".glb" : ".gltf"
|
|
16
|
+
|
|
17
|
+
if (result instanceof ArrayBuffer) {
|
|
18
|
+
blob = new Blob([result], { type: "model/gltf-binary" })
|
|
19
|
+
extension = options?.format
|
|
20
|
+
? options.format === "glb"
|
|
21
|
+
? ".glb"
|
|
22
|
+
: ".gltf"
|
|
23
|
+
: ".glb"
|
|
24
|
+
} else if (
|
|
25
|
+
typeof ArrayBuffer !== "undefined" &&
|
|
26
|
+
result &&
|
|
27
|
+
typeof (result as any).buffer === "object" &&
|
|
28
|
+
(result as any).byteLength !== undefined
|
|
29
|
+
) {
|
|
30
|
+
const view = result as ArrayBufferView
|
|
31
|
+
blob = new Blob([view], { type: "model/gltf-binary" })
|
|
32
|
+
extension = options?.format
|
|
33
|
+
? options.format === "glb"
|
|
34
|
+
? ".glb"
|
|
35
|
+
: ".gltf"
|
|
36
|
+
: ".glb"
|
|
37
|
+
} else if (typeof result === "string") {
|
|
38
|
+
blob = new Blob([result], { type: "model/gltf+json" })
|
|
39
|
+
} else {
|
|
40
|
+
blob = new Blob([JSON.stringify(result)], { type: "model/gltf+json" })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
saveAs(blob, fileName + extension)
|
|
44
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const isComponentExported = (code: string) => {
|
|
2
|
+
return (
|
|
3
|
+
/export function\s+\w+/.test(code) ||
|
|
4
|
+
/export const\s+\w+\s*=/.test(code) ||
|
|
5
|
+
/export default\s+\w+/.test(code) ||
|
|
6
|
+
/export default\s+function\s*(\w*)\s*\(/.test(code) ||
|
|
7
|
+
/export default\s*\(\s*\)\s*=>/.test(code)
|
|
8
|
+
)
|
|
9
|
+
}
|
package/src/pages/authorize.tsx
CHANGED
package/src/pages/landing.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react"
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
2
|
import { useParams } from "wouter"
|
|
3
3
|
import { Loader2, ChevronLeft, ChevronRight } from "lucide-react"
|
|
4
4
|
import Header from "@/components/Header"
|
|
@@ -14,6 +14,7 @@ import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
|
|
|
14
14
|
import { usePackageBuild } from "@/hooks/use-package-builds"
|
|
15
15
|
import { PackageBuild } from "fake-snippets-api/lib/db/schema"
|
|
16
16
|
import { usePackageByName } from "@/hooks/use-package-by-package-name"
|
|
17
|
+
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
17
18
|
|
|
18
19
|
const StatusPill = ({ status }: { status: string }) => {
|
|
19
20
|
const color =
|
|
@@ -38,7 +39,7 @@ export default function PreviewBuildPage() {
|
|
|
38
39
|
const packageName = params?.packageName || null
|
|
39
40
|
|
|
40
41
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
|
41
|
-
const [selectedFile, setSelectedFile] = useState<string | null>(
|
|
42
|
+
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
|
42
43
|
const [selectedItemId, setSelectedItemId] = useState<string>("")
|
|
43
44
|
|
|
44
45
|
const { data: packageRelease, isLoading: isLoadingRelease } =
|
|
@@ -56,6 +57,15 @@ export default function PreviewBuildPage() {
|
|
|
56
57
|
buildFiles.map((f) => [f.path, f.content]),
|
|
57
58
|
)
|
|
58
59
|
|
|
60
|
+
const targetFile = findTargetFile(buildFiles, selectedFile)
|
|
61
|
+
const mainComponentPath = targetFile?.path ?? null
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!selectedFile && mainComponentPath) {
|
|
65
|
+
setSelectedFile(mainComponentPath)
|
|
66
|
+
}
|
|
67
|
+
}, [mainComponentPath, selectedFile])
|
|
68
|
+
|
|
59
69
|
if (!packageReleaseId) {
|
|
60
70
|
return <NotFoundPage heading="Package Release Not Found" />
|
|
61
71
|
}
|
|
@@ -74,7 +84,7 @@ export default function PreviewBuildPage() {
|
|
|
74
84
|
|
|
75
85
|
const treeData = transformFilesToTreeData({
|
|
76
86
|
files: buildFsMap,
|
|
77
|
-
currentFile: selectedFile,
|
|
87
|
+
currentFile: selectedFile ?? mainComponentPath,
|
|
78
88
|
renamingFile: null,
|
|
79
89
|
handleRenameFile: () => ({ fileRenamed: false }),
|
|
80
90
|
handleDeleteFile: () => ({ fileDeleted: false }),
|
|
@@ -221,7 +231,7 @@ export default function PreviewBuildPage() {
|
|
|
221
231
|
) : status === "success" && buildFiles.length > 0 ? (
|
|
222
232
|
<SuspenseRunFrame
|
|
223
233
|
fsMap={buildFsMap}
|
|
224
|
-
mainComponentPath={
|
|
234
|
+
mainComponentPath={mainComponentPath ?? "index.tsx"}
|
|
225
235
|
showRunButton={false}
|
|
226
236
|
className="[&>div]:overflow-y-hidden"
|
|
227
237
|
/>
|
|
@@ -5,18 +5,19 @@ import { useLocation, useParams } from "wouter"
|
|
|
5
5
|
import { Helmet } from "react-helmet-async"
|
|
6
6
|
import { useEffect, useState } from "react"
|
|
7
7
|
import NotFoundPage from "./404"
|
|
8
|
-
import {
|
|
9
|
-
import { usePackage } from "@/hooks/use-package"
|
|
8
|
+
import { usePackageByName } from "@/hooks/use-package-by-package-name"
|
|
10
9
|
|
|
11
10
|
export const ViewPackagePage = () => {
|
|
12
|
-
const {
|
|
13
|
-
packageId,
|
|
14
|
-
error: packageIdError,
|
|
15
|
-
isLoading: isLoadingPackageId,
|
|
16
|
-
} = useCurrentPackageId()
|
|
17
|
-
const { data: packageInfo } = usePackage(packageId)
|
|
18
11
|
const { author, packageName } = useParams()
|
|
12
|
+
const packageNameFull = `${author}/${packageName}`
|
|
19
13
|
const [, setLocation] = useLocation()
|
|
14
|
+
|
|
15
|
+
// Get package data directly by name - this will also cache by ID
|
|
16
|
+
const {
|
|
17
|
+
data: packageInfo,
|
|
18
|
+
error: packageError,
|
|
19
|
+
isLoading: isLoadingPackage,
|
|
20
|
+
} = usePackageByName(packageNameFull)
|
|
20
21
|
const {
|
|
21
22
|
data: packageRelease,
|
|
22
23
|
error: packageReleaseError,
|
|
@@ -33,11 +34,10 @@ export const ViewPackagePage = () => {
|
|
|
33
34
|
},
|
|
34
35
|
)
|
|
35
36
|
|
|
36
|
-
const { data: packageFiles } =
|
|
37
|
-
packageRelease?.package_release_id
|
|
38
|
-
)
|
|
37
|
+
const { data: packageFiles, isFetched: arePackageFilesFetched } =
|
|
38
|
+
usePackageFiles(packageRelease?.package_release_id)
|
|
39
39
|
|
|
40
|
-
if (!
|
|
40
|
+
if (!isLoadingPackage && packageError) {
|
|
41
41
|
return <NotFoundPage heading="Package Not Found" />
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -51,7 +51,8 @@ export const ViewPackagePage = () => {
|
|
|
51
51
|
<title>{`${author}/${packageName} - tscircuit`}</title>
|
|
52
52
|
</Helmet>
|
|
53
53
|
<RepoPageContent
|
|
54
|
-
packageFiles={packageFiles
|
|
54
|
+
packageFiles={packageFiles ?? []}
|
|
55
|
+
arePackageFilesFetched={arePackageFilesFetched}
|
|
55
56
|
packageInfo={packageInfo}
|
|
56
57
|
packageRelease={packageRelease}
|
|
57
58
|
importantFilePaths={["README.md", "LICENSE", "package.json"]}
|