@tscircuit/fake-snippets 0.0.101 → 0.0.103

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 (56) hide show
  1. package/api/generated-index.js +23 -1
  2. package/bun.lock +2 -2
  3. package/dist/bundle.js +530 -367
  4. package/dist/index.d.ts +29 -2
  5. package/dist/index.js +18 -1
  6. package/dist/schema.d.ts +94 -1
  7. package/dist/schema.js +17 -1
  8. package/fake-snippets-api/lib/db/db-client.ts +6 -1
  9. package/fake-snippets-api/lib/db/schema.ts +15 -0
  10. package/fake-snippets-api/lib/public-mapping/public-map-package.ts +2 -0
  11. package/fake-snippets-api/routes/api/github/installations/create_new_installation_redirect.ts +75 -0
  12. package/fake-snippets-api/routes/api/github/repos/list_available.ts +91 -0
  13. package/fake-snippets-api/routes/api/packages/update.ts +6 -0
  14. package/package.json +2 -2
  15. package/src/App.tsx +10 -1
  16. package/src/components/CreateReleaseDialog.tsx +124 -0
  17. package/src/components/FileSidebar.tsx +128 -23
  18. package/src/components/PackageBuildsPage/package-build-header.tsx +9 -1
  19. package/src/components/PageSearchComponent.tsx +2 -2
  20. package/src/components/SearchComponent.tsx +2 -2
  21. package/src/components/SuspenseRunFrame.tsx +2 -2
  22. package/src/components/TrendingPackagesCarousel.tsx +2 -2
  23. package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +1 -0
  24. package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +13 -1
  25. package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +17 -0
  26. package/src/components/dialogs/GitHubRepositorySelector.tsx +183 -0
  27. package/src/components/dialogs/create-use-dialog.tsx +8 -2
  28. package/src/components/dialogs/edit-package-details-dialog.tsx +32 -3
  29. package/src/components/dialogs/view-ts-files-dialog.tsx +178 -33
  30. package/src/components/package-port/CodeAndPreview.tsx +4 -1
  31. package/src/components/package-port/CodeEditor.tsx +42 -35
  32. package/src/components/package-port/CodeEditorHeader.tsx +6 -4
  33. package/src/components/package-port/EditorNav.tsx +94 -37
  34. package/src/components/preview/BuildsList.tsx +241 -0
  35. package/src/components/preview/ConnectedPackagesList.tsx +187 -0
  36. package/src/components/preview/ConnectedRepoDashboard.tsx +243 -0
  37. package/src/components/preview/ConnectedRepoOverview.tsx +454 -0
  38. package/src/components/preview/index.tsx +248 -0
  39. package/src/components/ui/tree-view.tsx +23 -6
  40. package/src/hooks/use-axios.ts +2 -2
  41. package/src/hooks/use-create-release-dialog.ts +160 -0
  42. package/src/hooks/use-package-details-form.ts +7 -0
  43. package/src/hooks/use-packages-base-api-url.ts +1 -1
  44. package/src/hooks/use-sign-in.ts +2 -2
  45. package/src/hooks/useFileManagement.ts +22 -2
  46. package/src/index.css +4 -0
  47. package/src/lib/utils/formatTimeAgo.ts +10 -0
  48. package/src/lib/utils/isValidFileName.ts +15 -3
  49. package/src/pages/dashboard.tsx +2 -2
  50. package/src/pages/dev-login.tsx +2 -2
  51. package/src/pages/latest.tsx +2 -2
  52. package/src/pages/preview-build.tsx +380 -0
  53. package/src/pages/search.tsx +2 -2
  54. package/src/pages/trending.tsx +2 -2
  55. package/src/pages/user-profile.tsx +40 -24
  56. package/src/pages/view-connected-repo.tsx +18 -0
@@ -1,4 +1,4 @@
1
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
1
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
2
2
  import { useHotkeyCombo } from "@/hooks/use-hotkey"
3
3
  import { basicSetup } from "@/lib/codemirror/basic-setup"
4
4
  import {
@@ -49,7 +49,7 @@ import {
49
49
  } from "@/hooks/useFileManagement"
50
50
  import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
51
51
  import { inlineCopilot } from "codemirror-copilot"
52
- import { resolveRelativePath } from "@/lib/utils/resolveRelativePath"
52
+ import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
53
53
 
54
54
  const defaultImports = `
55
55
  import React from "@types/react/jsx-runtime"
@@ -92,7 +92,7 @@ export const CodeEditor = ({
92
92
  const viewRef = useRef<EditorView | null>(null)
93
93
  const ataRef = useRef<ReturnType<typeof setupTypeAcquisition> | null>(null)
94
94
  const lastReceivedTsFileTimeRef = useRef<number>(0)
95
- const apiUrl = usePackagesBaseApiUrl()
95
+ const apiUrl = useApiBaseUrl()
96
96
  const [cursorPosition, setCursorPosition] = useState<number | null>(null)
97
97
  const [code, setCode] = useState(files[0]?.content || "")
98
98
  const [fontSize, setFontSize] = useState(14)
@@ -108,6 +108,8 @@ export const CodeEditor = ({
108
108
  const filePathFromUrl = urlParams.get("file_path")
109
109
  const lineNumberFromUrl = urlParams.get("line")
110
110
  const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = useState(false)
111
+ const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
112
+ useViewTsFilesDialog()
111
113
 
112
114
  const entryPointFileName = useMemo(() => {
113
115
  const entryPointFile = findTargetFile(files, null)
@@ -519,42 +521,47 @@ export const CodeEditor = ({
519
521
  }
520
522
  }
521
523
  }
522
-
523
- // Check for local file imports
524
- const localFileMatches = Array.from(
525
- lineText.matchAll(LOCAL_FILE_IMPORT_PATTERN),
526
- )
527
- for (const match of localFileMatches) {
528
- if (match.index !== undefined) {
529
- const start = lineStart + match.index
530
- const end = start + match[0].length
531
- if (pos >= start && pos <= end) {
532
- const relativePath = match[0]
533
- const resolvedPath = resolveRelativePath(
534
- relativePath,
535
- currentFile || "",
536
- )
537
-
538
- // Add common extensions if not present
539
- let targetPath = resolvedPath
540
- if (!targetPath.includes(".")) {
541
- const extensions = [".tsx", ".ts", ".js", ".jsx"]
542
- for (const ext of extensions) {
543
- if (fileMap[`${targetPath}${ext}`]) {
544
- targetPath = `${targetPath}${ext}`
545
- break
546
- }
547
- }
548
- }
549
-
550
- if (fileMap[targetPath]) {
551
- onFileSelect(targetPath)
524
+ // TypeScript "Go to Definition" functionality
525
+ const facet = view.state.facet(tsFacet)
526
+ if (facet) {
527
+ const { env, path } = facet
528
+ const definitions =
529
+ env.languageService.getDefinitionAtPosition(path, pos)
530
+ if (definitions && definitions.length > 0) {
531
+ const definition = definitions[0]
532
+ const definitionFileName = definition.fileName
533
+ if (definitionFileName) {
534
+ const localFilePath = definitionFileName.startsWith("/")
535
+ ? definitionFileName.replace("/", "")
536
+ : definitionFileName
537
+ if (fileMap[localFilePath]) {
538
+ const definitionContent = fileMap[localFilePath]
539
+ const lines = definitionContent
540
+ ?.substring(0, definition.textSpan.start)
541
+ .split("\n")
542
+ const lineNumber = lines?.length
543
+
544
+ onFileSelect(localFilePath, lineNumber)
545
+ return true
546
+ } else {
547
+ const definitionContent =
548
+ env
549
+ .getSourceFile(definitionFileName)
550
+ ?.getFullText() || ""
551
+ const lines = definitionContent
552
+ .substring(0, definition.textSpan.start)
553
+ .split("\n")
554
+ const lineNumber = lines.length
555
+ openViewTsFilesDialog({
556
+ initialFile: definitionFileName,
557
+ initialLine: lineNumber,
558
+ })
552
559
  return true
553
560
  }
554
- return !!fileMap[targetPath]
555
561
  }
556
562
  }
557
563
  }
564
+
558
565
  return false
559
566
  },
560
567
  keydown: (event) => {
@@ -660,7 +667,6 @@ export const CodeEditor = ({
660
667
  }, [
661
668
  !isStreaming,
662
669
  currentFile,
663
- code !== "",
664
670
  Boolean(highlighter),
665
671
  isSaving,
666
672
  fontSize,
@@ -867,6 +873,7 @@ export const CodeEditor = ({
867
873
  onClose={() => setShowGlobalFindReplace(false)}
868
874
  />
869
875
  )}
876
+ <ViewTsFilesDialog />
870
877
  </div>
871
878
  )
872
879
  }
@@ -27,7 +27,7 @@ import {
27
27
  } from "@/components/ui/tooltip"
28
28
  import { convertRawEasyToTsx, fetchEasyEDAComponent } from "easyeda/browser"
29
29
  import { ComponentSearchResult } from "@tscircuit/runframe/runner"
30
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
30
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
31
31
  import { ICreateFileProps, ICreateFileResult } from "@/hooks/useFileManagement"
32
32
  import { useGlobalStore } from "@/hooks/use-global-store"
33
33
 
@@ -60,7 +60,7 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
60
60
  useImportComponentDialog()
61
61
  const { toast, toastLibrary } = useToast()
62
62
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
63
- const API_BASE = usePackagesBaseApiUrl()
63
+ const API_BASE = useApiBaseUrl()
64
64
  const [aiAutocompleteEnabled, setAiAutocompleteEnabled] = aiAutocompleteState
65
65
  const session = useGlobalStore((s) => s.session)
66
66
 
@@ -184,11 +184,13 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
184
184
  )
185
185
  const tsxComponent = await convertRawEasyToTsx(jlcpcbComponent)
186
186
  let componentName = component.name.replace(/ /g, "-")
187
- if (files[`${componentName}.tsx`] || files[`./${componentName}.tsx`]) {
187
+ let componentPath = `imports/${componentName}.tsx`
188
+ if (files[componentPath] || files[`./${componentPath}`]) {
188
189
  componentName = `${componentName}-1`
190
+ componentPath = `imports/${componentName}.tsx`
189
191
  }
190
192
  const createFileResult = createFile({
191
- newFileName: `${componentName}.tsx`,
193
+ newFileName: componentPath,
192
194
  content: tsxComponent,
193
195
  onError: (error) => {
194
196
  throw error
@@ -45,6 +45,9 @@ import { useForkPackageMutation } from "@/hooks/useForkPackageMutation"
45
45
  import tscircuitCorePkg from "@tscircuit/core/package.json"
46
46
  import { useRenamePackageDialog } from "../dialogs/rename-package-dialog"
47
47
  import { useUpdatePackageDescriptionDialog } from "../dialogs/update-package-description-dialog"
48
+ import { useCreateReleaseDialog } from "@/hooks/use-create-release-dialog"
49
+ import { Tag } from "lucide-react"
50
+ import { CreateReleaseDialog } from "../CreateReleaseDialog"
48
51
 
49
52
  export default function EditorNav({
50
53
  circuitJson,
@@ -58,6 +61,8 @@ export default function EditorNav({
58
61
  onDiscard,
59
62
  packageType,
60
63
  isSaving,
64
+ files,
65
+ packageFilesMeta,
61
66
  }: {
62
67
  pkg?: Package | null
63
68
  circuitJson?: AnyCircuitElement[] | null
@@ -70,6 +75,13 @@ export default function EditorNav({
70
75
  isSaving: boolean
71
76
  onSave: () => void
72
77
  onDiscard?: () => void
78
+ files?: { path: string; content: string }[]
79
+ packageFilesMeta?: {
80
+ created_at: string
81
+ file_path: string
82
+ package_file_id: string
83
+ package_release_id: string
84
+ }[]
73
85
  }) {
74
86
  const [, navigate] = useLocation()
75
87
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
@@ -84,6 +96,18 @@ export default function EditorNav({
84
96
  useConfirmDeletePackageDialog()
85
97
  const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
86
98
  useViewTsFilesDialog()
99
+ const createReleaseDialog = useCreateReleaseDialog({
100
+ packageId: pkg?.package_id ?? "",
101
+ packageName: pkg?.unscoped_name ?? "",
102
+ currentVersion: pkg?.latest_version || undefined,
103
+ onSuccess: () => {
104
+ qc.invalidateQueries({ queryKey: ["packages"] })
105
+ qc.invalidateQueries({ queryKey: ["packages", pkg?.package_id] })
106
+ },
107
+ files: files || [],
108
+ currentPackage: pkg || undefined,
109
+ packageFilesMeta: packageFilesMeta || [],
110
+ })
87
111
 
88
112
  const [isChangingType, setIsChangingType] = useState(false)
89
113
  const [currentType, setCurrentType] = useState(
@@ -229,14 +253,25 @@ export default function EditorNav({
229
253
  </span>
230
254
  )}
231
255
  {pkg.owner_github_username === session?.github_username && (
232
- <Button
233
- variant="ghost"
234
- size="icon"
235
- className="h-6 w-6 ml-2"
236
- onClick={() => openRenameDialog()}
237
- >
238
- <Pencil className="h-3 w-3 text-gray-700" />
239
- </Button>
256
+ <>
257
+ <Button
258
+ variant="ghost"
259
+ size="icon"
260
+ className="h-6 w-6 ml-2"
261
+ onClick={() => openRenameDialog()}
262
+ >
263
+ <Pencil className="h-3 w-3 text-gray-700" />
264
+ </Button>
265
+ <Button
266
+ variant="ghost"
267
+ size="icon"
268
+ className="h-6 w-6 ml-2"
269
+ onClick={() => createReleaseDialog.openDialog()}
270
+ disabled={hasUnsavedChanges || isSaving}
271
+ >
272
+ <Tag className="h-3 w-3 text-gray-700" />
273
+ </Button>
274
+ </>
240
275
  )}
241
276
  {isPrivate && (
242
277
  <div className="relative group">
@@ -363,22 +398,23 @@ export default function EditorNav({
363
398
  <Eye className="mr-1 h-3 w-3" />
364
399
  Public
365
400
  </Button> */}
366
- {pkg && (
367
- <DropdownMenu>
368
- <DropdownMenuTrigger asChild>
369
- <Button variant="ghost" size="icon" className="hidden md:flex">
370
- <MoreVertical className="h-3 w-3" />
371
- </Button>
372
- </DropdownMenuTrigger>
373
- <DropdownMenuContent>
374
- <DropdownMenuItem
375
- className="text-xs"
376
- onClick={() => openViewTsFilesDialog()}
377
- >
378
- <File className="mr-2 h-3 w-3" />
379
- View Files
380
- </DropdownMenuItem>
381
- {session?.github_username === pkg.owner_github_username && (
401
+
402
+ <DropdownMenu>
403
+ <DropdownMenuTrigger asChild>
404
+ <Button variant="ghost" size="icon" className="hidden md:flex">
405
+ <MoreVertical className="h-3 w-3" />
406
+ </Button>
407
+ </DropdownMenuTrigger>
408
+ <DropdownMenuContent>
409
+ <DropdownMenuItem
410
+ className="text-xs"
411
+ onClick={() => openViewTsFilesDialog()}
412
+ >
413
+ <File className="mr-2 h-3 w-3" />
414
+ View Files
415
+ </DropdownMenuItem>
416
+ {pkg &&
417
+ session?.github_username === pkg?.owner_github_username && (
382
418
  <>
383
419
  <DropdownMenuItem
384
420
  className="text-xs"
@@ -436,21 +472,21 @@ export default function EditorNav({
436
472
  </DropdownMenuItem>
437
473
  </DropdownMenuSubContent>
438
474
  </DropdownMenuSub>
475
+ <DropdownMenuItem
476
+ className="text-xs text-red-600"
477
+ onClick={() => openDeleteDialog()}
478
+ >
479
+ <Trash2 className="mr-2 h-3 w-3" />
480
+ Delete Package
481
+ </DropdownMenuItem>
439
482
  </>
440
483
  )}
441
- <DropdownMenuItem
442
- className="text-xs text-red-600"
443
- onClick={() => openDeleteDialog()}
444
- >
445
- <Trash2 className="mr-2 h-3 w-3" />
446
- Delete Package
447
- </DropdownMenuItem>
448
- <DropdownMenuItem className="text-xs text-gray-500" disabled>
449
- @tscircuit/core@{tscircuitCorePkg.version}
450
- </DropdownMenuItem>
451
- </DropdownMenuContent>
452
- </DropdownMenu>
453
- )}
484
+ <DropdownMenuItem className="text-xs text-gray-500" disabled>
485
+ @tscircuit/core@{tscircuitCorePkg.version}
486
+ </DropdownMenuItem>
487
+ </DropdownMenuContent>
488
+ </DropdownMenu>
489
+
454
490
  <Button
455
491
  variant="ghost"
456
492
  size="icon"
@@ -488,6 +524,17 @@ export default function EditorNav({
488
524
  Discard Changes
489
525
  </DropdownMenuItem>
490
526
  )}
527
+ {pkg &&
528
+ session?.github_username === pkg?.owner_github_username && (
529
+ <DropdownMenuItem
530
+ className="text-xs"
531
+ onClick={() => createReleaseDialog.openDialog()}
532
+ disabled={hasUnsavedChanges || isSaving}
533
+ >
534
+ <Tag className="mr-1 h-3 w-3" />
535
+ Create Release
536
+ </DropdownMenuItem>
537
+ )}
491
538
  <DropdownMenuItem
492
539
  className="text-xs"
493
540
  onClick={() => {
@@ -561,6 +608,16 @@ export default function EditorNav({
561
608
  packageOwner={pkg?.owner_github_username ?? ""}
562
609
  />
563
610
  <ViewTsFilesDialog />
611
+ <CreateReleaseDialog
612
+ isOpen={createReleaseDialog.isOpen}
613
+ onClose={createReleaseDialog.closeDialog}
614
+ currentVersion={createReleaseDialog.currentVersion}
615
+ version={createReleaseDialog.version}
616
+ setVersion={createReleaseDialog.setVersion}
617
+ isLoading={createReleaseDialog.isLoading}
618
+ error={createReleaseDialog.error}
619
+ onCreateRelease={createReleaseDialog.handleCreateRelease}
620
+ />
564
621
  </nav>
565
622
  )
566
623
  }
@@ -0,0 +1,241 @@
1
+ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
2
+ import { Badge } from "@/components/ui/badge"
3
+ import { Button } from "@/components/ui/button"
4
+ import {
5
+ Clock,
6
+ GitBranch,
7
+ AlertCircle,
8
+ CheckCircle,
9
+ Loader2,
10
+ MoreHorizontal,
11
+ GitCommit,
12
+ Plus,
13
+ } from "lucide-react"
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ } from "@/components/ui/dropdown-menu"
20
+ import {
21
+ Table,
22
+ TableBody,
23
+ TableCell,
24
+ TableHead,
25
+ TableHeader,
26
+ TableRow,
27
+ } from "@/components/ui/table"
28
+ import { getBuildStatus, MOCK_DEPLOYMENTS, PackageBuild, StatusIcon } from "."
29
+ import { formatTimeAgo } from "@/lib/utils/formatTimeAgo"
30
+ import { Package } from "fake-snippets-api/lib/db/schema"
31
+
32
+ export const BuildsList = ({
33
+ pkg,
34
+ onSelectBuild,
35
+ }: {
36
+ pkg: Package
37
+ onSelectBuild?: (build: PackageBuild) => void
38
+ }) => {
39
+ const builds = MOCK_DEPLOYMENTS
40
+ return (
41
+ <div className="space-y-6">
42
+ <div className="flex items-center justify-between">
43
+ <div>
44
+ <h2 className="text-2xl font-bold text-gray-900">Builds</h2>
45
+ <p className="text-gray-600">Manage and monitor your builds</p>
46
+ </div>
47
+ </div>
48
+
49
+ <Card>
50
+ <CardHeader>
51
+ <CardTitle>Recent Deployments</CardTitle>
52
+ </CardHeader>
53
+ <CardContent>
54
+ <div className="overflow-x-auto">
55
+ <Table>
56
+ <TableHeader>
57
+ <TableRow>
58
+ <TableHead>Status</TableHead>
59
+ <TableHead>Build ID</TableHead>
60
+ <TableHead>Branch</TableHead>
61
+ <TableHead>Commit</TableHead>
62
+ <TableHead>Author</TableHead>
63
+ <TableHead>Created</TableHead>
64
+ <TableHead>Actions</TableHead>
65
+ </TableRow>
66
+ </TableHeader>
67
+ <TableBody>
68
+ {builds.map((build) => {
69
+ const { status, label } = getBuildStatus(build)
70
+
71
+ return (
72
+ <TableRow
73
+ key={build.package_build_id}
74
+ className="cursor-pointer hover:bg-gray-50"
75
+ onClick={() => onSelectBuild?.(build)}
76
+ >
77
+ <TableCell>
78
+ <div className="flex items-center gap-2">
79
+ <StatusIcon status={status} />
80
+ <Badge
81
+ variant={
82
+ status === "success"
83
+ ? "default"
84
+ : status === "error"
85
+ ? "destructive"
86
+ : "secondary"
87
+ }
88
+ className="text-xs"
89
+ >
90
+ {label}
91
+ </Badge>
92
+ </div>
93
+ </TableCell>
94
+ <TableCell>
95
+ <code className="text-sm bg-gray-100 px-2 py-1 rounded">
96
+ {build.package_build_id.slice(-8)}
97
+ </code>
98
+ </TableCell>
99
+ <TableCell>
100
+ <div className="flex items-center gap-2">
101
+ {build.branch_name?.includes("/") ? (
102
+ <GitBranch className="w-3 h-3 text-gray-500" />
103
+ ) : (
104
+ <GitCommit className="w-3 h-3 text-gray-500" />
105
+ )}
106
+ <Badge variant="outline" className="text-xs">
107
+ {build.branch_name || "main"}
108
+ </Badge>
109
+ </div>
110
+ </TableCell>
111
+ <TableCell>
112
+ <div className="max-w-xs">
113
+ <p className="text-sm font-medium truncate">
114
+ {build.commit_message || "No commit message"}
115
+ </p>
116
+ </div>
117
+ </TableCell>
118
+ <TableCell>
119
+ <span className="text-sm text-gray-600">
120
+ {build.commit_author || "Unknown"}
121
+ </span>
122
+ </TableCell>
123
+ <TableCell>
124
+ <span className="text-sm text-gray-600">
125
+ {formatTimeAgo(build.created_at)}
126
+ </span>
127
+ </TableCell>
128
+ <TableCell>
129
+ <div className="flex items-center gap-2">
130
+ <DropdownMenu>
131
+ <DropdownMenuTrigger asChild>
132
+ <Button
133
+ variant="ghost"
134
+ size="sm"
135
+ onClick={(e) => e.stopPropagation()}
136
+ >
137
+ <MoreHorizontal className="w-3 h-3" />
138
+ </Button>
139
+ </DropdownMenuTrigger>
140
+ <DropdownMenuContent align="end">
141
+ <DropdownMenuItem
142
+ onClick={() => onSelectBuild?.(build)}
143
+ >
144
+ View Details
145
+ </DropdownMenuItem>
146
+ <DropdownMenuItem
147
+ onClick={() => onSelectBuild?.(build)}
148
+ >
149
+ View Logs
150
+ </DropdownMenuItem>
151
+ </DropdownMenuContent>
152
+ </DropdownMenu>
153
+ </div>
154
+ </TableCell>
155
+ </TableRow>
156
+ )
157
+ })}
158
+ </TableBody>
159
+ </Table>
160
+ </div>
161
+ </CardContent>
162
+ </Card>
163
+
164
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
165
+ <Card>
166
+ <CardContent className="p-6">
167
+ <div className="flex items-center gap-3">
168
+ <div className="p-2 bg-green-100 rounded-lg">
169
+ <CheckCircle className="w-5 h-5 text-green-600" />
170
+ </div>
171
+ <div>
172
+ <p className="text-sm text-gray-600">Successful</p>
173
+ <p className="text-2xl font-bold text-gray-900">
174
+ {
175
+ builds.filter((d) => getBuildStatus(d).status === "success")
176
+ .length
177
+ }
178
+ </p>
179
+ </div>
180
+ </div>
181
+ </CardContent>
182
+ </Card>
183
+
184
+ <Card>
185
+ <CardContent className="p-6">
186
+ <div className="flex items-center gap-3">
187
+ <div className="p-2 bg-red-100 rounded-lg">
188
+ <AlertCircle className="w-5 h-5 text-red-600" />
189
+ </div>
190
+ <div>
191
+ <p className="text-sm text-gray-600">Failed</p>
192
+ <p className="text-2xl font-bold text-gray-900">
193
+ {
194
+ builds.filter((d) => getBuildStatus(d).status === "error")
195
+ .length
196
+ }
197
+ </p>
198
+ </div>
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+
203
+ <Card>
204
+ <CardContent className="p-6">
205
+ <div className="flex items-center gap-3">
206
+ <div className="p-2 bg-blue-100 rounded-lg">
207
+ <Loader2 className="w-5 h-5 text-blue-600 animate-spin" />
208
+ </div>
209
+ <div>
210
+ <p className="text-sm text-gray-600">Building</p>
211
+ <p className="text-2xl font-bold text-gray-900">
212
+ {
213
+ builds.filter(
214
+ (d) => getBuildStatus(d).status === "building",
215
+ ).length
216
+ }
217
+ </p>
218
+ </div>
219
+ </div>
220
+ </CardContent>
221
+ </Card>
222
+
223
+ <Card>
224
+ <CardContent className="p-6">
225
+ <div className="flex items-center gap-3">
226
+ <div className="p-2 bg-gray-100 rounded-lg">
227
+ <Clock className="w-5 h-5 text-gray-600" />
228
+ </div>
229
+ <div>
230
+ <p className="text-sm text-gray-600">Total</p>
231
+ <p className="text-2xl font-bold text-gray-900">
232
+ {builds.length}
233
+ </p>
234
+ </div>
235
+ </div>
236
+ </CardContent>
237
+ </Card>
238
+ </div>
239
+ </div>
240
+ )
241
+ }