@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.
Files changed (43) hide show
  1. package/CLAUDE.md +92 -0
  2. package/api/generated-index.js +84 -25
  3. package/biome.json +7 -1
  4. package/bun-tests/fake-snippets-api/routes/package_builds/get.test.ts +0 -15
  5. package/bun-tests/fake-snippets-api/routes/package_builds/list.test.ts +0 -12
  6. package/dist/bundle.js +360 -434
  7. package/dist/index.d.ts +26 -15
  8. package/dist/index.js +40 -21
  9. package/dist/schema.d.ts +32 -24
  10. package/dist/schema.js +7 -5
  11. package/fake-snippets-api/lib/db/db-client.ts +19 -1
  12. package/fake-snippets-api/lib/db/schema.ts +6 -3
  13. package/fake-snippets-api/lib/db/seed.ts +23 -12
  14. package/fake-snippets-api/lib/public-mapping/public-map-package-build.ts +0 -3
  15. package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +3 -0
  16. package/fake-snippets-api/routes/api/package_builds/list.ts +0 -1
  17. package/package.json +3 -2
  18. package/src/App.tsx +27 -9
  19. package/src/components/FileSidebar.tsx +14 -159
  20. package/src/components/PackageBreadcrumb.tsx +111 -0
  21. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -1
  22. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +18 -2
  23. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +18 -8
  24. package/src/components/preview/BuildsList.tsx +84 -167
  25. package/src/components/preview/ConnectedPackagesList.tsx +92 -62
  26. package/src/components/preview/ConnectedRepoOverview.tsx +171 -155
  27. package/src/components/preview/{ConnectedRepoDashboard.tsx → PackageReleasesDashboard.tsx} +31 -69
  28. package/src/components/preview/index.tsx +20 -154
  29. package/src/hooks/use-package-builds.ts +0 -48
  30. package/src/hooks/use-package-release-by-id-or-version.ts +36 -0
  31. package/src/hooks/use-package-release.ts +32 -0
  32. package/src/index.css +24 -0
  33. package/src/lib/utils/isUuid.ts +5 -0
  34. package/src/lib/utils/transformFilesToTreeData.tsx +195 -0
  35. package/src/pages/404.tsx +3 -5
  36. package/src/pages/preview-release.tsx +269 -0
  37. package/src/pages/release-builds.tsx +99 -0
  38. package/src/pages/release-detail.tsx +120 -0
  39. package/src/pages/releases.tsx +55 -0
  40. package/fake-snippets-api/routes/api/package_builds/latest.ts +0 -109
  41. package/src/hooks/use-snippets-base-api-url.ts +0 -3
  42. package/src/pages/preview-build.tsx +0 -380
  43. 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
- }: { heading?: string }) {
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
+ }