@tscircuit/fake-snippets 0.0.106 → 0.0.108
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/CLAUDE.md +92 -0
- package/api/generated-index.js +84 -25
- 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/dist/bundle.js +360 -434
- package/dist/index.d.ts +26 -15
- package/dist/index.js +40 -21
- package/dist/schema.d.ts +32 -24
- package/dist/schema.js +7 -5
- package/fake-snippets-api/lib/db/db-client.ts +19 -1
- package/fake-snippets-api/lib/db/schema.ts +6 -3
- package/fake-snippets-api/lib/db/seed.ts +23 -12
- 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/fake-snippets-api/routes/api/package_builds/list.ts +0 -1
- package/package.json +3 -2
- package/src/App.tsx +27 -9
- package/src/components/FileSidebar.tsx +14 -159
- package/src/components/PackageBreadcrumb.tsx +111 -0
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -1
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +18 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +18 -8
- package/src/components/preview/BuildsList.tsx +84 -167
- package/src/components/preview/ConnectedPackagesList.tsx +92 -62
- package/src/components/preview/ConnectedRepoOverview.tsx +171 -155
- package/src/components/preview/{ConnectedRepoDashboard.tsx → PackageReleasesDashboard.tsx} +31 -69
- package/src/components/preview/index.tsx +20 -154
- package/src/hooks/use-package-builds.ts +0 -48
- package/src/hooks/use-package-release-by-id-or-version.ts +36 -0
- package/src/hooks/use-package-release.ts +32 -0
- package/src/index.css +24 -0
- package/src/lib/utils/isUuid.ts +5 -0
- package/src/lib/utils/transformFilesToTreeData.tsx +195 -0
- package/src/pages/404.tsx +3 -5
- package/src/pages/preview-release.tsx +269 -0
- package/src/pages/release-builds.tsx +99 -0
- package/src/pages/release-detail.tsx +120 -0
- package/src/pages/releases.tsx +55 -0
- package/fake-snippets-api/routes/api/package_builds/latest.ts +0 -109
- package/src/hooks/use-snippets-base-api-url.ts +0 -3
- package/src/pages/preview-build.tsx +0 -380
- package/src/pages/view-connected-repo.tsx +0 -49
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import type { TreeDataItem } from "@/components/ui/tree-view"
|
|
2
|
+
import type {
|
|
3
|
+
IRenameFileProps,
|
|
4
|
+
IDeleteFileProps,
|
|
5
|
+
} from "@/hooks/useFileManagement"
|
|
6
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
7
|
+
import { File, Folder, MoreVertical, Pencil, Trash2 } from "lucide-react"
|
|
8
|
+
import {
|
|
9
|
+
DropdownMenu,
|
|
10
|
+
DropdownMenuContent,
|
|
11
|
+
DropdownMenuGroup,
|
|
12
|
+
DropdownMenuItem,
|
|
13
|
+
DropdownMenuTrigger,
|
|
14
|
+
} from "@/components/ui/dropdown-menu"
|
|
15
|
+
import { useToast } from "@/hooks/use-toast"
|
|
16
|
+
|
|
17
|
+
type FileName = string
|
|
18
|
+
type TreeNode = Omit<TreeDataItem, "children"> & {
|
|
19
|
+
children?: Record<string, TreeNode>
|
|
20
|
+
}
|
|
21
|
+
interface TransformFilesToTreeDataProps {
|
|
22
|
+
files: Record<FileName, string>
|
|
23
|
+
currentFile: FileName | null
|
|
24
|
+
renamingFile: string | null
|
|
25
|
+
handleRenameFile: (props: IRenameFileProps) => { fileRenamed: boolean }
|
|
26
|
+
handleDeleteFile: (props: IDeleteFileProps) => { fileDeleted: boolean }
|
|
27
|
+
setRenamingFile: (filename: string | null) => void
|
|
28
|
+
onFileSelect: (filename: FileName) => void
|
|
29
|
+
onFolderSelect: (folderPath: string) => void
|
|
30
|
+
canModifyFiles: boolean
|
|
31
|
+
setErrorMessage: (message: string) => void
|
|
32
|
+
setSelectedFolderForCreation: (folder: string | null) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const transformFilesToTreeData = ({
|
|
36
|
+
files,
|
|
37
|
+
currentFile,
|
|
38
|
+
renamingFile,
|
|
39
|
+
handleRenameFile,
|
|
40
|
+
handleDeleteFile,
|
|
41
|
+
setRenamingFile,
|
|
42
|
+
onFileSelect,
|
|
43
|
+
onFolderSelect,
|
|
44
|
+
canModifyFiles,
|
|
45
|
+
setErrorMessage,
|
|
46
|
+
setSelectedFolderForCreation,
|
|
47
|
+
}: TransformFilesToTreeDataProps): TreeDataItem[] => {
|
|
48
|
+
const { toast } = useToast()
|
|
49
|
+
const root: Record<string, TreeNode> = {}
|
|
50
|
+
|
|
51
|
+
Object.keys(files).forEach((filePath) => {
|
|
52
|
+
const hasLeadingSlash = filePath.startsWith("/")
|
|
53
|
+
const pathSegments = (hasLeadingSlash ? filePath.slice(1) : filePath)
|
|
54
|
+
.trim()
|
|
55
|
+
.split("/")
|
|
56
|
+
let currentNode: Record<string, TreeNode> = root
|
|
57
|
+
|
|
58
|
+
pathSegments.forEach((segment, segmentIndex) => {
|
|
59
|
+
const isLeafNode = segmentIndex === pathSegments.length - 1
|
|
60
|
+
const ancestorPath = pathSegments.slice(0, segmentIndex).join("/")
|
|
61
|
+
const relativePath = ancestorPath ? `${ancestorPath}/${segment}` : segment
|
|
62
|
+
const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
|
|
63
|
+
const itemId = absolutePath
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
!currentNode[segment] &&
|
|
67
|
+
(!isHiddenFile(relativePath) ||
|
|
68
|
+
isHiddenFile(
|
|
69
|
+
currentFile?.startsWith("/")
|
|
70
|
+
? currentFile.slice(1)
|
|
71
|
+
: currentFile || "",
|
|
72
|
+
))
|
|
73
|
+
) {
|
|
74
|
+
currentNode[segment] = {
|
|
75
|
+
id: itemId,
|
|
76
|
+
name: segment,
|
|
77
|
+
isRenaming: renamingFile === itemId,
|
|
78
|
+
onRename: (newFilename: string) => {
|
|
79
|
+
const oldPath = itemId
|
|
80
|
+
const pathParts = oldPath.split("/").filter((part) => part !== "")
|
|
81
|
+
let newPath: string
|
|
82
|
+
|
|
83
|
+
if (pathParts.length > 1) {
|
|
84
|
+
const folderPath = pathParts.slice(0, -1).join("/")
|
|
85
|
+
newPath = folderPath + "/" + newFilename
|
|
86
|
+
} else {
|
|
87
|
+
newPath = newFilename
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
|
|
91
|
+
newPath = "/" + newPath
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { fileRenamed } = handleRenameFile({
|
|
95
|
+
oldFilename: itemId,
|
|
96
|
+
newFilename: newPath,
|
|
97
|
+
onError: (error) => {
|
|
98
|
+
toast({
|
|
99
|
+
title: `Error renaming file`,
|
|
100
|
+
description: error.message,
|
|
101
|
+
variant: "destructive",
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
if (fileRenamed) {
|
|
106
|
+
setRenamingFile(null)
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
onCancelRename: () => {
|
|
110
|
+
setRenamingFile(null)
|
|
111
|
+
},
|
|
112
|
+
icon: isLeafNode ? File : Folder,
|
|
113
|
+
onClick: isLeafNode
|
|
114
|
+
? () => {
|
|
115
|
+
onFileSelect(absolutePath)
|
|
116
|
+
setSelectedFolderForCreation(null)
|
|
117
|
+
}
|
|
118
|
+
: () => onFolderSelect(absolutePath),
|
|
119
|
+
draggable: false,
|
|
120
|
+
droppable: !isLeafNode,
|
|
121
|
+
children: isLeafNode ? undefined : {},
|
|
122
|
+
actions: canModifyFiles ? (
|
|
123
|
+
<>
|
|
124
|
+
<DropdownMenu key={itemId}>
|
|
125
|
+
<DropdownMenuTrigger asChild>
|
|
126
|
+
<MoreVertical className="w-4 h-4 text-gray-500 hover:text-gray-700" />
|
|
127
|
+
</DropdownMenuTrigger>
|
|
128
|
+
<DropdownMenuContent
|
|
129
|
+
className="w-fit bg-white shadow-lg rounded-md border-4 z-[100] border-white"
|
|
130
|
+
style={{
|
|
131
|
+
position: "absolute",
|
|
132
|
+
top: "100%",
|
|
133
|
+
left: "0",
|
|
134
|
+
marginTop: "0.5rem",
|
|
135
|
+
width: "8rem",
|
|
136
|
+
padding: "0.01rem",
|
|
137
|
+
}}
|
|
138
|
+
>
|
|
139
|
+
<DropdownMenuGroup>
|
|
140
|
+
{isLeafNode && (
|
|
141
|
+
<DropdownMenuItem
|
|
142
|
+
onClick={() => {
|
|
143
|
+
setRenamingFile(itemId)
|
|
144
|
+
}}
|
|
145
|
+
className="flex items-center px-3 py-1 text-xs text-black hover:bg-gray-100 cursor-pointer"
|
|
146
|
+
>
|
|
147
|
+
<Pencil className="mr-2 h-3 w-3" />
|
|
148
|
+
Rename
|
|
149
|
+
</DropdownMenuItem>
|
|
150
|
+
)}
|
|
151
|
+
<DropdownMenuItem
|
|
152
|
+
onClick={() => {
|
|
153
|
+
const { fileDeleted } = handleDeleteFile({
|
|
154
|
+
filename: itemId,
|
|
155
|
+
onError: (error) => {
|
|
156
|
+
toast({
|
|
157
|
+
title: `Error deleting file ${itemId}`,
|
|
158
|
+
description: error.message,
|
|
159
|
+
})
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
if (fileDeleted) {
|
|
163
|
+
setErrorMessage("")
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
className="flex items-center px-3 py-1 text-xs text-red-600 hover:bg-gray-100 cursor-pointer"
|
|
167
|
+
>
|
|
168
|
+
<Trash2 className="mr-2 h-3 w-3" />
|
|
169
|
+
Delete
|
|
170
|
+
</DropdownMenuItem>
|
|
171
|
+
</DropdownMenuGroup>
|
|
172
|
+
</DropdownMenuContent>
|
|
173
|
+
</DropdownMenu>
|
|
174
|
+
</>
|
|
175
|
+
) : undefined,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (!isLeafNode && currentNode[segment]?.children) {
|
|
180
|
+
currentNode = currentNode[segment].children
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const convertToArray = (items: Record<string, TreeNode>): TreeDataItem[] => {
|
|
186
|
+
return Object.values(items).map((item) => ({
|
|
187
|
+
...item,
|
|
188
|
+
children: item.children ? convertToArray(item.children) : undefined,
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
return convertToArray(root).filter((x) => {
|
|
192
|
+
if (x.children?.length === 0) return false
|
|
193
|
+
return true
|
|
194
|
+
})
|
|
195
|
+
}
|
package/src/pages/404.tsx
CHANGED
|
@@ -5,15 +5,13 @@ import { NotFound } from "@/components/NotFound"
|
|
|
5
5
|
|
|
6
6
|
export function NotFoundPage({
|
|
7
7
|
heading = "Page Not Found",
|
|
8
|
-
|
|
8
|
+
subtitle = "The page you're looking for doesn't exist.",
|
|
9
|
+
}: { heading?: string; subtitle?: string }) {
|
|
9
10
|
return (
|
|
10
11
|
<div className="flex min-h-screen flex-col">
|
|
11
12
|
<Helmet>
|
|
12
13
|
<title>404 - {heading} | tscircuit</title>
|
|
13
|
-
<meta
|
|
14
|
-
name="description"
|
|
15
|
-
content="The page you're looking for doesn't exist."
|
|
16
|
-
/>
|
|
14
|
+
<meta name="description" content={subtitle} />
|
|
17
15
|
</Helmet>
|
|
18
16
|
<Header2 />
|
|
19
17
|
<NotFound heading={heading} />
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { useState } from "react"
|
|
2
|
+
import { useParams } from "wouter"
|
|
3
|
+
import { Loader2, ChevronLeft, ChevronRight } from "lucide-react"
|
|
4
|
+
import Header from "@/components/Header"
|
|
5
|
+
import { SuspenseRunFrame } from "@/components/SuspenseRunFrame"
|
|
6
|
+
import { TreeView } from "@/components/ui/tree-view"
|
|
7
|
+
import { transformFilesToTreeData } from "@/lib/utils/transformFilesToTreeData"
|
|
8
|
+
import { cn } from "@/lib/utils"
|
|
9
|
+
import { PrefetchPageLink } from "@/components/PrefetchPageLink"
|
|
10
|
+
import NotFoundPage from "./404"
|
|
11
|
+
import { getBuildStatus } from "@/components/preview"
|
|
12
|
+
import { usePackageReleaseById } from "@/hooks/use-package-release"
|
|
13
|
+
import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
|
|
14
|
+
import { usePackageBuild } from "@/hooks/use-package-builds"
|
|
15
|
+
import { PackageBuild } from "fake-snippets-api/lib/db/schema"
|
|
16
|
+
import { usePackageByName } from "@/hooks/use-package-by-package-name"
|
|
17
|
+
|
|
18
|
+
const StatusPill = ({ status }: { status: string }) => {
|
|
19
|
+
const color =
|
|
20
|
+
status === "success"
|
|
21
|
+
? "bg-emerald-600"
|
|
22
|
+
: status === "failed"
|
|
23
|
+
? "bg-red-600"
|
|
24
|
+
: status === "building"
|
|
25
|
+
? "bg-blue-600 animate-pulse"
|
|
26
|
+
: "bg-gray-500"
|
|
27
|
+
return <span className={cn("inline-block w-2 h-2 rounded-full", color)} />
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default function PreviewBuildPage() {
|
|
31
|
+
const params = useParams<{
|
|
32
|
+
packageReleaseId: string
|
|
33
|
+
author: string
|
|
34
|
+
packageName: string
|
|
35
|
+
}>()
|
|
36
|
+
const packageReleaseId = params?.packageReleaseId || null
|
|
37
|
+
const author = params?.author || null
|
|
38
|
+
const packageName = params?.packageName || null
|
|
39
|
+
|
|
40
|
+
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
|
41
|
+
const [selectedFile, setSelectedFile] = useState<string | null>("index.tsx")
|
|
42
|
+
const [selectedItemId, setSelectedItemId] = useState<string>("")
|
|
43
|
+
|
|
44
|
+
const { data: packageRelease, isLoading: isLoadingRelease } =
|
|
45
|
+
usePackageReleaseById(packageReleaseId)
|
|
46
|
+
const { data: pkg, isLoading: isLoadingPackage } = usePackageByName(
|
|
47
|
+
author && packageName ? `${author}/${packageName}` : null,
|
|
48
|
+
)
|
|
49
|
+
const { data: build, isLoading: isLoadingBuild } = usePackageBuild(
|
|
50
|
+
packageRelease?.latest_package_build_id || null,
|
|
51
|
+
)
|
|
52
|
+
const { data: buildFiles = [], isLoading: isLoadingFiles } =
|
|
53
|
+
usePackageFilesLoader(pkg)
|
|
54
|
+
|
|
55
|
+
const buildFsMap = Object.fromEntries(
|
|
56
|
+
buildFiles.map((f) => [f.path, f.content]),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if (!packageReleaseId) {
|
|
60
|
+
return <NotFoundPage heading="Package Release Not Found" />
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!packageRelease && !isLoadingRelease) {
|
|
64
|
+
return <NotFoundPage heading="Package Release Not Found" />
|
|
65
|
+
}
|
|
66
|
+
const isLoading =
|
|
67
|
+
isLoadingRelease || isLoadingPackage || isLoadingFiles || isLoadingBuild
|
|
68
|
+
|
|
69
|
+
if (!build && !isLoading) {
|
|
70
|
+
return <NotFoundPage heading="Package Build Not Found" />
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { status } = getBuildStatus(build as PackageBuild)
|
|
74
|
+
|
|
75
|
+
const treeData = transformFilesToTreeData({
|
|
76
|
+
files: buildFsMap,
|
|
77
|
+
currentFile: selectedFile,
|
|
78
|
+
renamingFile: null,
|
|
79
|
+
handleRenameFile: () => ({ fileRenamed: false }),
|
|
80
|
+
handleDeleteFile: () => ({ fileDeleted: false }),
|
|
81
|
+
setRenamingFile: () => {},
|
|
82
|
+
onFileSelect: setSelectedFile,
|
|
83
|
+
onFolderSelect: () => {},
|
|
84
|
+
canModifyFiles: false,
|
|
85
|
+
setErrorMessage: () => {},
|
|
86
|
+
setSelectedFolderForCreation: () => {},
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<Header />
|
|
92
|
+
<div className="flex flex-col h-screen overflow-hidden !-mt-1">
|
|
93
|
+
<div className="flex flex-1 overflow-hidden">
|
|
94
|
+
<aside
|
|
95
|
+
className={cn(
|
|
96
|
+
"relative border-r border-gray-200 rounded-r-lg z-[5] h-full transition-all duration-300 ease-in-out bg-white",
|
|
97
|
+
sidebarCollapsed ? "w-2 md:w-3" : "w-80",
|
|
98
|
+
)}
|
|
99
|
+
>
|
|
100
|
+
<button
|
|
101
|
+
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
102
|
+
className="absolute top-4 -right-3 z-10 bg-white border border-gray-200 rounded-full p-1 hover:bg-gray-50"
|
|
103
|
+
>
|
|
104
|
+
{sidebarCollapsed ? (
|
|
105
|
+
<ChevronRight size={20} />
|
|
106
|
+
) : (
|
|
107
|
+
<ChevronLeft size={20} />
|
|
108
|
+
)}
|
|
109
|
+
</button>
|
|
110
|
+
|
|
111
|
+
{!sidebarCollapsed && (
|
|
112
|
+
<>
|
|
113
|
+
<div className="p-4 border-b border-gray-200">
|
|
114
|
+
<div className="space-y-3">
|
|
115
|
+
<div className="flex items-center justify-between">
|
|
116
|
+
<h2 className="text-lg font-semibold text-gray-900">
|
|
117
|
+
Deployment
|
|
118
|
+
</h2>
|
|
119
|
+
<StatusPill status={status} />
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="space-y-2">
|
|
123
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
124
|
+
<span className="text-xs text-gray-500 uppercase tracking-wide">
|
|
125
|
+
ID
|
|
126
|
+
</span>
|
|
127
|
+
<PrefetchPageLink
|
|
128
|
+
href={`/${pkg?.name}/releases/${build?.package_release_id}`}
|
|
129
|
+
title={build?.package_build_id}
|
|
130
|
+
className="font-mono text-sm truncate text-gray-900 bg-gray-100 w-full px-2 py-1 rounded"
|
|
131
|
+
>
|
|
132
|
+
{build?.package_build_id}
|
|
133
|
+
</PrefetchPageLink>
|
|
134
|
+
</div>
|
|
135
|
+
{packageRelease?.commit_message && (
|
|
136
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
137
|
+
<span className="text-xs text-gray-500 uppercase tracking-wide">
|
|
138
|
+
Commit
|
|
139
|
+
</span>
|
|
140
|
+
<a
|
|
141
|
+
title={packageRelease?.commit_message}
|
|
142
|
+
target="_blank"
|
|
143
|
+
rel="noopener noreferrer"
|
|
144
|
+
href={`https://github.com/${pkg?.github_repo_full_name}/commit/${packageRelease?.commit_message}`}
|
|
145
|
+
className="font-mono text-xs text-gray-600 bg-gray-50 px-2 text-right py-1 rounded truncate"
|
|
146
|
+
>
|
|
147
|
+
{packageRelease?.commit_message}
|
|
148
|
+
</a>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
|
153
|
+
<span className="text-xs text-gray-500 uppercase tracking-wide">
|
|
154
|
+
Status
|
|
155
|
+
</span>
|
|
156
|
+
<span
|
|
157
|
+
className={`text-xs font-medium px-2 py-1 w-fit rounded-full capitalize ${
|
|
158
|
+
status === "success"
|
|
159
|
+
? "bg-emerald-100 text-emerald-800"
|
|
160
|
+
: status === "error"
|
|
161
|
+
? "bg-red-100 text-red-800"
|
|
162
|
+
: status === "building"
|
|
163
|
+
? "bg-blue-100 text-blue-800"
|
|
164
|
+
: "bg-gray-100 text-gray-800"
|
|
165
|
+
}`}
|
|
166
|
+
>
|
|
167
|
+
{status}
|
|
168
|
+
</span>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div className="flex-1 overflow-hidden">
|
|
175
|
+
<div className="px-4 py-3 border-b border-gray-200">
|
|
176
|
+
<h3 className="text-sm font-semibold text-gray-900">
|
|
177
|
+
Files
|
|
178
|
+
</h3>
|
|
179
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
180
|
+
{isLoadingFiles
|
|
181
|
+
? "Loading files..."
|
|
182
|
+
: `${treeData.length} file${treeData.length !== 1 ? "s" : ""}`}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="px-2 py-2 overflow-y-auto select-none">
|
|
186
|
+
{isLoadingFiles ? (
|
|
187
|
+
<div className="flex items-center justify-center py-8">
|
|
188
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
|
189
|
+
<span className="ml-2 text-sm text-gray-500">
|
|
190
|
+
Loading files...
|
|
191
|
+
</span>
|
|
192
|
+
</div>
|
|
193
|
+
) : (
|
|
194
|
+
<TreeView
|
|
195
|
+
selectedItemId={selectedItemId || ""}
|
|
196
|
+
setSelectedItemId={(v) => setSelectedItemId(v || "")}
|
|
197
|
+
data={treeData}
|
|
198
|
+
className="w-full"
|
|
199
|
+
onSelectChange={(item) => {
|
|
200
|
+
if (item && !item.children) {
|
|
201
|
+
setSelectedFile(item.id)
|
|
202
|
+
}
|
|
203
|
+
}}
|
|
204
|
+
/>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</>
|
|
209
|
+
)}
|
|
210
|
+
</aside>
|
|
211
|
+
|
|
212
|
+
<main className="flex-1 overflow-y-auto">
|
|
213
|
+
<div className="flex flex-col h-full overflow-h-hidden">
|
|
214
|
+
{isLoading ? (
|
|
215
|
+
<div className="flex-1 flex items-center justify-center">
|
|
216
|
+
<div className="flex flex-col items-center gap-3 text-gray-500">
|
|
217
|
+
<Loader2 className="w-6 h-6 animate-spin" />
|
|
218
|
+
<p>Loading package contents...</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
) : status === "success" && buildFiles.length > 0 ? (
|
|
222
|
+
<SuspenseRunFrame
|
|
223
|
+
fsMap={buildFsMap}
|
|
224
|
+
mainComponentPath={selectedFile ?? "index.tsx"}
|
|
225
|
+
showRunButton={false}
|
|
226
|
+
className="[&>div]:overflow-y-hidden"
|
|
227
|
+
/>
|
|
228
|
+
) : (
|
|
229
|
+
<div className="flex-1 flex items-center justify-center">
|
|
230
|
+
{status === "building" ? (
|
|
231
|
+
<div className="flex flex-col items-center gap-3 text-gray-500">
|
|
232
|
+
<Loader2 className="w-6 h-6 animate-spin" />
|
|
233
|
+
<p>Building…</p>
|
|
234
|
+
</div>
|
|
235
|
+
) : status === "error" ? (
|
|
236
|
+
<div className="text-center">
|
|
237
|
+
<p className="text-red-600 font-medium mb-2">
|
|
238
|
+
Build Failed
|
|
239
|
+
</p>
|
|
240
|
+
</div>
|
|
241
|
+
) : buildFiles.length === 0 ? (
|
|
242
|
+
<div className="text-center">
|
|
243
|
+
<p className="text-gray-600 font-medium mb-2">
|
|
244
|
+
No files found
|
|
245
|
+
</p>
|
|
246
|
+
<p className="text-sm text-gray-500">
|
|
247
|
+
This package release doesn't have any files to preview.
|
|
248
|
+
</p>
|
|
249
|
+
</div>
|
|
250
|
+
) : (
|
|
251
|
+
<div className="text-center p-4">
|
|
252
|
+
<p className="text-gray-600 font-medium mb-2">
|
|
253
|
+
Build Status: {status}
|
|
254
|
+
</p>
|
|
255
|
+
<p className="text-sm text-gray-500 max-w-lg">
|
|
256
|
+
Please wait while we process this build status. Try
|
|
257
|
+
refreshing the page in a few moments.
|
|
258
|
+
</p>
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
</div>
|
|
264
|
+
</main>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
</>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useParams } from "wouter"
|
|
2
|
+
import NotFoundPage from "./404"
|
|
3
|
+
import { usePackageByName } from "@/hooks/use-package-by-package-name"
|
|
4
|
+
import { usePackageReleaseByIdOrVersion } from "@/hooks/use-package-release-by-id-or-version"
|
|
5
|
+
import { usePackageBuildsByReleaseId } from "@/hooks/use-package-builds"
|
|
6
|
+
import { BuildsList } from "@/components/preview/BuildsList"
|
|
7
|
+
import Header from "@/components/Header"
|
|
8
|
+
import { useLocation } from "wouter"
|
|
9
|
+
import { PackageBreadcrumb } from "@/components/PackageBreadcrumb"
|
|
10
|
+
import { PackageBuild } from "fake-snippets-api/lib/db/schema"
|
|
11
|
+
|
|
12
|
+
export default function ReleaseBuildsPage() {
|
|
13
|
+
const params = useParams<{
|
|
14
|
+
author: string
|
|
15
|
+
packageName: string
|
|
16
|
+
releaseId: string
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const packageName =
|
|
20
|
+
params?.author && params?.packageName
|
|
21
|
+
? `${params.author}/${params.packageName}`
|
|
22
|
+
: null
|
|
23
|
+
|
|
24
|
+
const [, setLocation] = useLocation()
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
data: pkg,
|
|
28
|
+
isLoading: isLoadingPackage,
|
|
29
|
+
error: packageError,
|
|
30
|
+
} = usePackageByName(packageName)
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
data: packageRelease,
|
|
34
|
+
isLoading: isLoadingRelease,
|
|
35
|
+
error: releaseError,
|
|
36
|
+
} = usePackageReleaseByIdOrVersion(params?.releaseId ?? null, packageName)
|
|
37
|
+
|
|
38
|
+
const {
|
|
39
|
+
data: builds,
|
|
40
|
+
isLoading: isLoadingBuilds,
|
|
41
|
+
error: buildsError,
|
|
42
|
+
} = usePackageBuildsByReleaseId(params?.releaseId ?? null)
|
|
43
|
+
|
|
44
|
+
if (isLoadingPackage || isLoadingRelease || isLoadingBuilds) {
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (packageError?.status === 404 || !pkg) {
|
|
49
|
+
return <NotFoundPage heading="Package Not Found" />
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (releaseError?.status === 404 || !packageRelease) {
|
|
53
|
+
return <NotFoundPage heading="Release Not Found" />
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Header />
|
|
59
|
+
<div className="min-h-screen bg-white">
|
|
60
|
+
<div className="bg-gray-50 border-b py-10">
|
|
61
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
62
|
+
<PackageBreadcrumb
|
|
63
|
+
author={params?.author || ""}
|
|
64
|
+
packageName={packageName || ""}
|
|
65
|
+
unscopedName={pkg.unscoped_name}
|
|
66
|
+
currentPage="builds"
|
|
67
|
+
releaseVersion={
|
|
68
|
+
packageRelease.version ||
|
|
69
|
+
`v${packageRelease.package_release_id.slice(-6)}`
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
<div className="flex items-center gap-4">
|
|
73
|
+
<div>
|
|
74
|
+
<h1 className="text-3xl font-bold text-gray-900">
|
|
75
|
+
{pkg.name} - Release Builds
|
|
76
|
+
</h1>
|
|
77
|
+
<p className="text-gray-600 mt-2">
|
|
78
|
+
All builds for release {packageRelease.package_release_id}
|
|
79
|
+
</p>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
86
|
+
<div className="space-y-6">
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<h2 className="text-2xl font-bold text-gray-900">All Builds</h2>
|
|
89
|
+
<p className="text-sm text-gray-600">
|
|
90
|
+
{builds?.length} build{builds?.length !== 1 ? "s" : ""} found
|
|
91
|
+
</p>
|
|
92
|
+
</div>
|
|
93
|
+
{pkg && <BuildsList pkg={pkg} />}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</>
|
|
98
|
+
)
|
|
99
|
+
}
|