@tscircuit/fake-snippets 0.0.107 → 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/api/generated-index.js +82 -22
- package/biome.json +7 -1
- package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +0 -15
- package/bun-tests/fake-snippets-api/routes/package_builds/list.test.ts +0 -12
- package/bun.lock +62 -19
- package/dist/bundle.js +25 -24
- package/dist/index.d.ts +26 -15
- package/dist/index.js +19 -18
- package/dist/schema.d.ts +32 -24
- package/dist/schema.js +7 -6
- package/fake-snippets-api/lib/db/db-client.ts +10 -1
- package/fake-snippets-api/lib/db/schema.ts +4 -3
- package/fake-snippets-api/lib/db/seed.ts +6 -9
- package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +0 -3
- package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +3 -0
- package/package.json +7 -8
- package/src/App.tsx +12 -11
- package/src/components/DownloadButtonAndMenu.tsx +133 -35
- package/src/components/FileSidebar.tsx +45 -193
- package/src/components/Footer.tsx +0 -1
- package/src/components/HeaderLogin.tsx +1 -1
- package/src/components/HiddenFilesDropdown.tsx +0 -2
- package/src/components/PackageBreadcrumb.tsx +1 -1
- 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 +1 -2
- 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-about-section.tsx +18 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +1 -1
- 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/BuildsList.tsx +20 -9
- package/src/components/preview/ConnectedPackagesList.tsx +73 -60
- package/src/components/preview/ConnectedRepoOverview.tsx +160 -154
- package/src/components/preview/PackageReleasesDashboard.tsx +41 -30
- package/src/components/preview/index.tsx +16 -153
- 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/index.css +24 -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/lib/utils/transformFilesToTreeData.tsx +195 -0
- package/src/pages/404.tsx +3 -5
- package/src/pages/authorize.tsx +0 -2
- package/src/pages/landing.tsx +0 -1
- package/src/pages/preview-release.tsx +279 -0
- package/src/pages/release-builds.tsx +0 -8
- package/src/pages/release-detail.tsx +17 -15
- package/src/pages/releases.tsx +5 -1
- 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/hooks/use-snippets-base-api-url.ts +0 -3
- package/src/pages/preview-build.tsx +0 -380
- package/src/pages/settings.tsx +0 -25
|
@@ -1,24 +1,9 @@
|
|
|
1
1
|
import React, { useState } from "react"
|
|
2
2
|
import { cn } from "@/lib/utils"
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
Folder,
|
|
6
|
-
MoreVertical,
|
|
7
|
-
PanelRightOpen,
|
|
8
|
-
Plus,
|
|
9
|
-
Trash2,
|
|
10
|
-
Pencil,
|
|
11
|
-
} from "lucide-react"
|
|
12
|
-
import { TreeView, TreeDataItem } from "@/components/ui/tree-view"
|
|
13
|
-
import { isHiddenFile } from "./ViewPackagePage/utils/is-hidden-file"
|
|
3
|
+
import { PanelRightOpen, Plus, Loader2 } from "lucide-react"
|
|
4
|
+
import { TreeView } from "@/components/ui/tree-view"
|
|
14
5
|
import { Input } from "@/components/ui/input"
|
|
15
|
-
import {
|
|
16
|
-
DropdownMenu,
|
|
17
|
-
DropdownMenuContent,
|
|
18
|
-
DropdownMenuGroup,
|
|
19
|
-
DropdownMenuItem,
|
|
20
|
-
DropdownMenuTrigger,
|
|
21
|
-
} from "./ui/dropdown-menu"
|
|
6
|
+
import { transformFilesToTreeData } from "@/lib/utils/transformFilesToTreeData"
|
|
22
7
|
import type {
|
|
23
8
|
ICreateFileProps,
|
|
24
9
|
ICreateFileResult,
|
|
@@ -27,8 +12,6 @@ import type {
|
|
|
27
12
|
IRenameFileProps,
|
|
28
13
|
IRenameFileResult,
|
|
29
14
|
} from "@/hooks/useFileManagement"
|
|
30
|
-
import { useToast } from "@/hooks/use-toast"
|
|
31
|
-
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
32
15
|
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
33
16
|
type FileName = string
|
|
34
17
|
|
|
@@ -44,6 +27,8 @@ interface FileSidebarProps {
|
|
|
44
27
|
isCreatingFile: boolean
|
|
45
28
|
setIsCreatingFile: React.Dispatch<React.SetStateAction<boolean>>
|
|
46
29
|
pkg?: Package
|
|
30
|
+
isLoadingFiles?: boolean
|
|
31
|
+
loadingProgress?: string | null
|
|
47
32
|
}
|
|
48
33
|
|
|
49
34
|
const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
@@ -57,6 +42,8 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
57
42
|
handleRenameFile,
|
|
58
43
|
isCreatingFile,
|
|
59
44
|
setIsCreatingFile,
|
|
45
|
+
isLoadingFiles = true,
|
|
46
|
+
loadingProgress = null,
|
|
60
47
|
}) => {
|
|
61
48
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
62
49
|
const [newFileName, setNewFileName] = useState("")
|
|
@@ -68,172 +55,25 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
68
55
|
const [selectedItemId, setSelectedItemId] = React.useState<string>(
|
|
69
56
|
currentFile || "",
|
|
70
57
|
)
|
|
71
|
-
const { toast } = useToast()
|
|
72
58
|
const canModifyFiles = true
|
|
73
59
|
|
|
74
60
|
const onFolderSelect = (folderPath: string) => {
|
|
75
61
|
setSelectedFolderForCreation(folderPath)
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
const
|
|
79
|
-
files
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let currentNode: Record<string, TreeNode> = root
|
|
92
|
-
|
|
93
|
-
pathSegments.forEach((segment, segmentIndex) => {
|
|
94
|
-
const isLeafNode = segmentIndex === pathSegments.length - 1
|
|
95
|
-
const ancestorPath = pathSegments.slice(0, segmentIndex).join("/")
|
|
96
|
-
const relativePath = ancestorPath
|
|
97
|
-
? `${ancestorPath}/${segment}`
|
|
98
|
-
: segment
|
|
99
|
-
const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
|
|
100
|
-
const itemId = absolutePath
|
|
101
|
-
|
|
102
|
-
if (
|
|
103
|
-
!currentNode[segment] &&
|
|
104
|
-
(!isHiddenFile(relativePath) ||
|
|
105
|
-
isHiddenFile(
|
|
106
|
-
currentFile?.startsWith("/")
|
|
107
|
-
? currentFile.slice(1)
|
|
108
|
-
: currentFile || "",
|
|
109
|
-
))
|
|
110
|
-
) {
|
|
111
|
-
currentNode[segment] = {
|
|
112
|
-
id: itemId,
|
|
113
|
-
name: segment,
|
|
114
|
-
isRenaming: renamingFile === itemId,
|
|
115
|
-
onRename: (newFilename: string) => {
|
|
116
|
-
const oldPath = itemId
|
|
117
|
-
const pathParts = oldPath.split("/").filter((part) => part !== "")
|
|
118
|
-
let newPath: string
|
|
119
|
-
|
|
120
|
-
if (pathParts.length > 1) {
|
|
121
|
-
const folderPath = pathParts.slice(0, -1).join("/")
|
|
122
|
-
newPath = folderPath + "/" + newFilename
|
|
123
|
-
} else {
|
|
124
|
-
newPath = newFilename
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
|
|
128
|
-
newPath = "/" + newPath
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const { fileRenamed } = handleRenameFile({
|
|
132
|
-
oldFilename: itemId,
|
|
133
|
-
newFilename: newPath,
|
|
134
|
-
onError: (error) => {
|
|
135
|
-
toast({
|
|
136
|
-
title: `Error renaming file`,
|
|
137
|
-
description: error.message,
|
|
138
|
-
variant: "destructive",
|
|
139
|
-
})
|
|
140
|
-
},
|
|
141
|
-
})
|
|
142
|
-
if (fileRenamed) {
|
|
143
|
-
setRenamingFile(null)
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
onCancelRename: () => {
|
|
147
|
-
setRenamingFile(null)
|
|
148
|
-
},
|
|
149
|
-
icon: isLeafNode ? File : Folder,
|
|
150
|
-
onClick: isLeafNode
|
|
151
|
-
? () => {
|
|
152
|
-
onFileSelect(absolutePath)
|
|
153
|
-
setSelectedFolderForCreation(null)
|
|
154
|
-
}
|
|
155
|
-
: () => onFolderSelect(absolutePath),
|
|
156
|
-
draggable: false,
|
|
157
|
-
droppable: !isLeafNode,
|
|
158
|
-
children: isLeafNode ? undefined : {},
|
|
159
|
-
actions: canModifyFiles ? (
|
|
160
|
-
<>
|
|
161
|
-
<DropdownMenu key={itemId}>
|
|
162
|
-
<DropdownMenuTrigger asChild>
|
|
163
|
-
<MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
|
|
164
|
-
</DropdownMenuTrigger>
|
|
165
|
-
<DropdownMenuContent
|
|
166
|
-
className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
|
|
167
|
-
style={{
|
|
168
|
-
position: "absolute",
|
|
169
|
-
top: "100%",
|
|
170
|
-
left: "0",
|
|
171
|
-
marginTop: "0.5rem",
|
|
172
|
-
width: "8rem",
|
|
173
|
-
padding: "0.01rem",
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
176
|
-
<DropdownMenuGroup>
|
|
177
|
-
{isLeafNode && (
|
|
178
|
-
<DropdownMenuItem
|
|
179
|
-
onClick={() => {
|
|
180
|
-
setRenamingFile(itemId)
|
|
181
|
-
}}
|
|
182
|
-
className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
|
|
183
|
-
>
|
|
184
|
-
<Pencil className="mr-2 h-3 w-3" />
|
|
185
|
-
Rename
|
|
186
|
-
</DropdownMenuItem>
|
|
187
|
-
)}
|
|
188
|
-
<DropdownMenuItem
|
|
189
|
-
onClick={() => {
|
|
190
|
-
const { fileDeleted } = handleDeleteFile({
|
|
191
|
-
filename: itemId,
|
|
192
|
-
onError: (error) => {
|
|
193
|
-
toast({
|
|
194
|
-
title: `Error deleting file ${itemId}`,
|
|
195
|
-
description: error.message,
|
|
196
|
-
})
|
|
197
|
-
},
|
|
198
|
-
})
|
|
199
|
-
if (fileDeleted) {
|
|
200
|
-
setErrorMessage("")
|
|
201
|
-
}
|
|
202
|
-
}}
|
|
203
|
-
className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
|
|
204
|
-
>
|
|
205
|
-
<Trash2 className="mr-2 h-3 w-3" />
|
|
206
|
-
Delete
|
|
207
|
-
</DropdownMenuItem>
|
|
208
|
-
</DropdownMenuGroup>
|
|
209
|
-
</DropdownMenuContent>
|
|
210
|
-
</DropdownMenu>
|
|
211
|
-
</>
|
|
212
|
-
) : undefined,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (!isLeafNode && currentNode[segment].children) {
|
|
217
|
-
currentNode = currentNode[segment].children
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
const convertToArray = (
|
|
223
|
-
items: Record<string, TreeNode>,
|
|
224
|
-
): TreeDataItem[] => {
|
|
225
|
-
return Object.values(items).map((item) => ({
|
|
226
|
-
...item,
|
|
227
|
-
children: item.children ? convertToArray(item.children) : undefined,
|
|
228
|
-
}))
|
|
229
|
-
}
|
|
230
|
-
return convertToArray(root).filter((x) => {
|
|
231
|
-
if (x.children?.length === 0) return false
|
|
232
|
-
return true
|
|
233
|
-
})
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const treeData = transformFilesToTreeData(files)
|
|
64
|
+
const treeData = transformFilesToTreeData({
|
|
65
|
+
files,
|
|
66
|
+
currentFile,
|
|
67
|
+
renamingFile,
|
|
68
|
+
handleRenameFile,
|
|
69
|
+
handleDeleteFile,
|
|
70
|
+
setRenamingFile,
|
|
71
|
+
onFileSelect,
|
|
72
|
+
onFolderSelect,
|
|
73
|
+
canModifyFiles,
|
|
74
|
+
setErrorMessage,
|
|
75
|
+
setSelectedFolderForCreation,
|
|
76
|
+
})
|
|
237
77
|
|
|
238
78
|
const getCurrentFolderPath = (): string => {
|
|
239
79
|
if (selectedFolderForCreation) {
|
|
@@ -329,19 +169,31 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
|
|
|
329
169
|
className,
|
|
330
170
|
)}
|
|
331
171
|
>
|
|
332
|
-
<
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
172
|
+
<div className="flex items-center justify-between px-2 pt-2">
|
|
173
|
+
<button
|
|
174
|
+
onClick={toggleSidebar}
|
|
175
|
+
className={`text-gray-400 scale-90 transition-opacity duration-200 ${!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
|
|
176
|
+
>
|
|
177
|
+
<PanelRightOpen />
|
|
178
|
+
</button>
|
|
179
|
+
<div className="flex items-center gap-2">
|
|
180
|
+
{isLoadingFiles && (
|
|
181
|
+
<div className="flex items-center gap-1">
|
|
182
|
+
<Loader2 className="w-3 h-3 animate-spin text-gray-400" />
|
|
183
|
+
{loadingProgress && (
|
|
184
|
+
<span className="text-xs text-gray-400">{loadingProgress}</span>
|
|
185
|
+
)}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
<button
|
|
189
|
+
onClick={() => setIsCreatingFile(true)}
|
|
190
|
+
className="text-gray-400 hover:text-gray-600"
|
|
191
|
+
aria-label="Create new file"
|
|
192
|
+
>
|
|
193
|
+
<Plus className="w-5 h-5" />
|
|
194
|
+
</button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
345
197
|
{isCreatingFile && (
|
|
346
198
|
<div className="p-2">
|
|
347
199
|
<Input
|
|
@@ -43,7 +43,7 @@ export const HeaderLogin = () => {
|
|
|
43
43
|
</AvatarFallback>
|
|
44
44
|
</Avatar>
|
|
45
45
|
</DropdownMenuTrigger>
|
|
46
|
-
<DropdownMenuContent className="ml-1 md:ml-0 md:mr-1">
|
|
46
|
+
<DropdownMenuContent className="ml-1 mr-1 md:ml-0 md:mr-1">
|
|
47
47
|
<DropdownMenuItem asChild className="text-gray-500 text-xs" disabled>
|
|
48
48
|
<div>
|
|
49
49
|
AI Usage $
|
|
@@ -77,7 +77,7 @@ export function PackageBreadcrumb({
|
|
|
77
77
|
{currentPage === "builds" ? (
|
|
78
78
|
<BreadcrumbLink asChild>
|
|
79
79
|
<PrefetchPageLink
|
|
80
|
-
href={`/${packageName}/
|
|
80
|
+
href={`/${packageName}/releases/${releaseVersion}`}
|
|
81
81
|
>
|
|
82
82
|
{releaseVersion}
|
|
83
83
|
</PrefetchPageLink>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
|
|
2
2
|
import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { CircuitBoard } from "lucide-react"
|
|
3
5
|
|
|
4
6
|
export function BuildPreviewContent() {
|
|
5
7
|
const { packageRelease } = useCurrentPackageRelease({ refetchInterval: 2000 })
|
|
6
8
|
const { packageInfo } = useCurrentPackageInfo()
|
|
9
|
+
const [imageError, setImageError] = useState(false)
|
|
10
|
+
const [imageLoading, setImageLoading] = useState(true)
|
|
7
11
|
|
|
8
12
|
if (!packageRelease) {
|
|
9
13
|
return (
|
|
@@ -16,11 +20,36 @@ export function BuildPreviewContent() {
|
|
|
16
20
|
return (
|
|
17
21
|
<div className="flex items-center justify-center w-full h-full">
|
|
18
22
|
<div className="rounded overflow-hidden w-full max-w-full">
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
{imageError ? (
|
|
24
|
+
<div className="flex flex-col items-center justify-center bg-gray-50 border border-gray-300 rounded-lg p-8 sm:p-12 lg:p-16 min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
|
|
25
|
+
<CircuitBoard className="w-12 h-12 sm:w-16 sm:h-16 lg:w-20 lg:h-20 text-gray-400 mb-4" />
|
|
26
|
+
<h3 className="text-lg sm:text-xl font-medium text-gray-600 mb-2">
|
|
27
|
+
Preview Not Available
|
|
28
|
+
</h3>
|
|
29
|
+
<p className="text-sm sm:text-base text-gray-500 text-center max-w-sm">
|
|
30
|
+
The build preview image could not be loaded. This may be because
|
|
31
|
+
the build is still processing or the image is not available.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
) : (
|
|
35
|
+
<>
|
|
36
|
+
{imageLoading && (
|
|
37
|
+
<div className="flex items-center justify-center bg-gray-100 rounded-lg min-h-[240px] sm:min-h-[300px] lg:min-h-[360px]">
|
|
38
|
+
<div className="w-16 h-16 sm:w-20 sm:h-20 bg-gray-200 rounded animate-pulse"></div>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
<img
|
|
42
|
+
src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
|
|
43
|
+
alt="Package build preview"
|
|
44
|
+
className={`object-contain rounded w-full h-auto max-h-[240px] sm:max-h-[300px] lg:max-h-[360px] ${imageLoading ? "hidden" : "block"}`}
|
|
45
|
+
onLoad={() => setImageLoading(false)}
|
|
46
|
+
onError={() => {
|
|
47
|
+
setImageError(true)
|
|
48
|
+
setImageLoading(false)
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
</>
|
|
52
|
+
)}
|
|
24
53
|
</div>
|
|
25
54
|
</div>
|
|
26
55
|
)
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
} from "@/components/ui/dropdown-menu"
|
|
20
20
|
import { SnippetType, SnippetTypeIcon } from "./SnippetTypeIcon"
|
|
21
21
|
import { timeAgo } from "@/lib/utils/timeAgo"
|
|
22
|
-
import { ImageWithFallback } from "./ImageWithFallback"
|
|
23
22
|
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
|
24
23
|
|
|
25
24
|
export interface PackageCardProps {
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
|
|
2
|
-
import {
|
|
3
|
-
import { useQuery } from "react-query"
|
|
2
|
+
import { useMemo } from "react"
|
|
4
3
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
5
4
|
|
|
6
5
|
// Pre-randomized array to avoid flickering on re-renders
|
|
7
|
-
const SKELETON_WIDTHS = [
|
|
8
|
-
|
|
6
|
+
export const SKELETON_WIDTHS = [
|
|
7
|
+
"w-2/3",
|
|
8
|
+
"w-1/4",
|
|
9
|
+
"w-5/6",
|
|
10
|
+
"w-1/3",
|
|
11
|
+
"w-1/2",
|
|
12
|
+
"w-3/4",
|
|
13
|
+
]
|
|
14
|
+
const PLACEHOLDER_SHIKI_HTML = `<pre class="shiki vitesse-light" style="background-color:#ffffff;color:#393a34" tabindex="0"><code><span class="line"></span></code></pre>`
|
|
9
15
|
export const ShikiCodeViewer = ({
|
|
10
16
|
code,
|
|
11
17
|
filePath,
|
|
@@ -24,17 +30,20 @@ export const ShikiCodeViewer = ({
|
|
|
24
30
|
[filePath, code, highlighter],
|
|
25
31
|
)
|
|
26
32
|
|
|
27
|
-
if (
|
|
33
|
+
if (html && html?.trim() !== PLACEHOLDER_SHIKI_HTML) {
|
|
28
34
|
return (
|
|
29
|
-
<div
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
</div>
|
|
35
|
+
<div
|
|
36
|
+
className="text-sm shiki"
|
|
37
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
38
|
+
/>
|
|
34
39
|
)
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
return (
|
|
38
|
-
<div className="text-sm
|
|
43
|
+
<div className="text-sm p-4">
|
|
44
|
+
{SKELETON_WIDTHS.map((w, i) => (
|
|
45
|
+
<Skeleton key={i} className={`h-4 mb-2 ${w}`} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
39
48
|
)
|
|
40
49
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
1
|
import { useState, useEffect, useMemo, useCallback } from "react"
|
|
4
2
|
import {
|
|
5
3
|
Edit,
|
|
@@ -14,7 +12,7 @@ import {
|
|
|
14
12
|
import { Skeleton } from "@/components/ui/skeleton"
|
|
15
13
|
import { Button } from "@/components/ui/button"
|
|
16
14
|
import { usePackageFile } from "@/hooks/use-package-files"
|
|
17
|
-
import { ShikiCodeViewer } from "./ShikiCodeViewer"
|
|
15
|
+
import { ShikiCodeViewer, SKELETON_WIDTHS } from "./ShikiCodeViewer"
|
|
18
16
|
import MarkdownViewer from "./markdown-viewer"
|
|
19
17
|
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
20
18
|
import { useCurrentPackageCircuitJson } from "../hooks/use-current-package-circuit-json"
|
|
@@ -24,12 +22,12 @@ interface PackageFile {
|
|
|
24
22
|
package_release_id: string
|
|
25
23
|
file_path: string
|
|
26
24
|
created_at: string
|
|
27
|
-
content_text?: string
|
|
25
|
+
content_text?: string | null
|
|
28
26
|
}
|
|
29
27
|
|
|
30
28
|
interface ImportantFilesViewProps {
|
|
31
29
|
importantFiles?: PackageFile[]
|
|
32
|
-
|
|
30
|
+
isFetched?: boolean
|
|
33
31
|
onEditClicked?: (file_path?: string | null) => void
|
|
34
32
|
packageAuthorOwner?: string | null
|
|
35
33
|
aiDescription?: string
|
|
@@ -56,7 +54,7 @@ export default function ImportantFilesView({
|
|
|
56
54
|
aiReviewText,
|
|
57
55
|
aiReviewRequested,
|
|
58
56
|
onRequestAiReview,
|
|
59
|
-
|
|
57
|
+
isFetched = false,
|
|
60
58
|
onEditClicked,
|
|
61
59
|
packageAuthorOwner,
|
|
62
60
|
onLicenseFileRequested,
|
|
@@ -130,19 +128,25 @@ export default function ImportantFilesView({
|
|
|
130
128
|
const availableTabs = useMemo((): TabInfo[] => {
|
|
131
129
|
const tabs: TabInfo[] = []
|
|
132
130
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
// Only show AI description tab if there's actual AI content
|
|
132
|
+
if (hasAiContent) {
|
|
133
|
+
tabs.push({
|
|
134
|
+
type: "ai",
|
|
135
|
+
filePath: null,
|
|
136
|
+
label: "Description",
|
|
137
|
+
icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
141
|
+
// Only show AI review tab if there's actual AI review content
|
|
142
|
+
if (hasAiReview || isOwner) {
|
|
143
|
+
tabs.push({
|
|
144
|
+
type: "ai-review",
|
|
145
|
+
filePath: null,
|
|
146
|
+
label: "AI Review",
|
|
147
|
+
icon: <SparklesIcon className="h-3.5 w-3.5 mr-1.5" />,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
146
150
|
|
|
147
151
|
importantFiles.forEach((file) => {
|
|
148
152
|
tabs.push({
|
|
@@ -154,11 +158,11 @@ export default function ImportantFilesView({
|
|
|
154
158
|
})
|
|
155
159
|
|
|
156
160
|
return tabs
|
|
157
|
-
}, [hasAiContent, importantFiles, getFileName, getFileIcon])
|
|
161
|
+
}, [hasAiContent, hasAiReview, importantFiles, getFileName, getFileIcon])
|
|
158
162
|
|
|
159
163
|
// Find default tab with fallback logic
|
|
160
164
|
const getDefaultTab = useCallback((): TabInfo | null => {
|
|
161
|
-
if (
|
|
165
|
+
if (!isFetched || availableTabs.length === 0) return null
|
|
162
166
|
|
|
163
167
|
// Priority 1: README file
|
|
164
168
|
const readmeTab = availableTabs.find(
|
|
@@ -167,12 +171,14 @@ export default function ImportantFilesView({
|
|
|
167
171
|
)
|
|
168
172
|
if (readmeTab) return readmeTab
|
|
169
173
|
|
|
170
|
-
// Priority 2: AI content
|
|
171
|
-
const aiTab = availableTabs.find((tab) => tab.type === "ai")
|
|
174
|
+
// Priority 2: AI content (only if available)
|
|
175
|
+
const aiTab = availableTabs.find((tab) => tab.type === "ai" && hasAiContent)
|
|
172
176
|
if (aiTab) return aiTab
|
|
173
177
|
|
|
174
178
|
// Priority 3: AI review
|
|
175
|
-
const aiReviewTab = availableTabs.find(
|
|
179
|
+
const aiReviewTab = availableTabs.find(
|
|
180
|
+
(tab) => tab.type === "ai-review" && hasAiReview,
|
|
181
|
+
)
|
|
176
182
|
if (aiReviewTab) return aiReviewTab
|
|
177
183
|
|
|
178
184
|
// Priority 4: First file
|
|
@@ -180,24 +186,7 @@ export default function ImportantFilesView({
|
|
|
180
186
|
if (firstFileTab) return firstFileTab
|
|
181
187
|
|
|
182
188
|
return null
|
|
183
|
-
}, [
|
|
184
|
-
|
|
185
|
-
// Handle copy functionality
|
|
186
|
-
const handleCopy = useCallback(() => {
|
|
187
|
-
let textToCopy = ""
|
|
188
|
-
|
|
189
|
-
if (activeTab?.type === "ai-review" && aiReviewText) {
|
|
190
|
-
textToCopy = aiReviewText
|
|
191
|
-
} else if (activeTab?.type === "file" && activeFileContent) {
|
|
192
|
-
textToCopy = activeFileContent
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (textToCopy) {
|
|
196
|
-
navigator.clipboard.writeText(textToCopy)
|
|
197
|
-
setCopyState("copied")
|
|
198
|
-
setTimeout(() => setCopyState("copy"), 500)
|
|
199
|
-
}
|
|
200
|
-
}, [activeTab, aiReviewText])
|
|
189
|
+
}, [isFetched, availableTabs, isReadmeFile])
|
|
201
190
|
|
|
202
191
|
// Handle tab selection with validation
|
|
203
192
|
const selectTab = useCallback(
|
|
@@ -245,11 +234,11 @@ export default function ImportantFilesView({
|
|
|
245
234
|
|
|
246
235
|
// Set default tab when no tab is active
|
|
247
236
|
useEffect(() => {
|
|
248
|
-
if (activeTab === null &&
|
|
237
|
+
if (activeTab === null && isFetched) {
|
|
249
238
|
const defaultTab = getDefaultTab()
|
|
250
239
|
setActiveTab(defaultTab)
|
|
251
240
|
}
|
|
252
|
-
}, [activeTab,
|
|
241
|
+
}, [activeTab, isFetched, getDefaultTab])
|
|
253
242
|
|
|
254
243
|
// Validate active tab still exists (handles file deletion)
|
|
255
244
|
useEffect(() => {
|
|
@@ -273,18 +262,41 @@ export default function ImportantFilesView({
|
|
|
273
262
|
return importantFiles.find((file) => file.file_path === activeTab.filePath)
|
|
274
263
|
}, [activeTab, importantFiles])
|
|
275
264
|
|
|
276
|
-
const { data: activeFileFull } =
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
265
|
+
const { data: activeFileFull, isFetched: isActiveFileFetched } =
|
|
266
|
+
usePackageFile(
|
|
267
|
+
partialActiveFile
|
|
268
|
+
? {
|
|
269
|
+
file_path: partialActiveFile.file_path,
|
|
270
|
+
package_release_id: partialActiveFile.package_release_id,
|
|
271
|
+
}
|
|
272
|
+
: null,
|
|
273
|
+
{
|
|
274
|
+
keepPreviousData: true,
|
|
275
|
+
staleTime: Infinity,
|
|
276
|
+
refetchOnMount: false,
|
|
277
|
+
refetchOnWindowFocus: false,
|
|
278
|
+
refetchOnReconnect: false,
|
|
279
|
+
},
|
|
280
|
+
)
|
|
285
281
|
|
|
286
282
|
const activeFileContent = activeFileFull?.content_text || ""
|
|
287
283
|
|
|
284
|
+
const handleCopy = () => {
|
|
285
|
+
let textToCopy = ""
|
|
286
|
+
|
|
287
|
+
if (activeTab?.type === "ai-review" && aiReviewText) {
|
|
288
|
+
textToCopy = aiReviewText
|
|
289
|
+
} else if (activeTab?.type === "file" && activeFileContent) {
|
|
290
|
+
textToCopy = activeFileContent
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (textToCopy) {
|
|
294
|
+
navigator.clipboard.writeText(textToCopy)
|
|
295
|
+
setCopyState("copied")
|
|
296
|
+
setTimeout(() => setCopyState("copy"), 500)
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
288
300
|
// Render content based on active tab
|
|
289
301
|
const renderAiContent = useCallback(
|
|
290
302
|
() => (
|
|
@@ -369,20 +381,24 @@ export default function ImportantFilesView({
|
|
|
369
381
|
}, [aiReviewText, aiReviewRequested, isOwner, onRequestAiReview])
|
|
370
382
|
|
|
371
383
|
const renderFileContent = useCallback(() => {
|
|
372
|
-
if (!activeTab?.filePath || !activeFileContent) {
|
|
373
|
-
|
|
384
|
+
if (!isActiveFileFetched || !activeTab?.filePath || !activeFileContent) {
|
|
385
|
+
;<div className="text-sm p-4">
|
|
386
|
+
{SKELETON_WIDTHS.map((w, i) => (
|
|
387
|
+
<Skeleton key={i} className={`h-4 mb-2 ${w}`} />
|
|
388
|
+
))}
|
|
389
|
+
</div>
|
|
374
390
|
}
|
|
375
391
|
|
|
376
|
-
if (isMarkdownFile(activeTab
|
|
392
|
+
if (isMarkdownFile(String(activeTab?.filePath))) {
|
|
377
393
|
return <MarkdownViewer markdownContent={activeFileContent} />
|
|
378
394
|
}
|
|
379
395
|
|
|
380
|
-
if (isCodeFile(activeTab
|
|
396
|
+
if (isCodeFile(String(activeTab?.filePath))) {
|
|
381
397
|
return (
|
|
382
|
-
<div className="overflow-x-auto">
|
|
398
|
+
<div className="overflow-x-auto no-scrollbar">
|
|
383
399
|
<ShikiCodeViewer
|
|
384
400
|
code={activeFileContent}
|
|
385
|
-
filePath={activeTab
|
|
401
|
+
filePath={String(activeTab?.filePath)}
|
|
386
402
|
/>
|
|
387
403
|
</div>
|
|
388
404
|
)
|
|
@@ -422,7 +438,7 @@ export default function ImportantFilesView({
|
|
|
422
438
|
[activeTab],
|
|
423
439
|
)
|
|
424
440
|
|
|
425
|
-
if (
|
|
441
|
+
if (!isFetched) {
|
|
426
442
|
return (
|
|
427
443
|
<div className="mt-4 border border-gray-200 dark:border-[#30363d] rounded-md overflow-hidden">
|
|
428
444
|
<div className="flex items-center pl-2 pr-4 py-2 bg-gray-100 dark:bg-[#161b22] border-b border-gray-200 dark:border-[#30363d]">
|