@tscircuit/fake-snippets 0.0.101 → 0.0.102

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 +4 -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 +1 -0
  25. package/src/components/dialogs/GitHubRepositorySelector.tsx +123 -0
  26. package/src/components/dialogs/create-use-dialog.tsx +8 -2
  27. package/src/components/dialogs/edit-package-details-dialog.tsx +22 -3
  28. package/src/components/dialogs/view-ts-files-dialog.tsx +178 -33
  29. package/src/components/package-port/CodeAndPreview.tsx +4 -1
  30. package/src/components/package-port/CodeEditor.tsx +42 -35
  31. package/src/components/package-port/CodeEditorHeader.tsx +6 -4
  32. package/src/components/package-port/EditorNav.tsx +94 -37
  33. package/src/components/preview/BuildsList.tsx +238 -0
  34. package/src/components/preview/ConnectedRepoDashboard.tsx +258 -0
  35. package/src/components/preview/ConnectedRepoOverview.tsx +454 -0
  36. package/src/components/preview/ConnectedRepoSettings.tsx +343 -0
  37. package/src/components/preview/ConnectedReposCards.tsx +191 -0
  38. package/src/components/preview/index.tsx +207 -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 +32 -24
  56. package/src/pages/view-connected-repo.tsx +24 -0
package/src/App.tsx CHANGED
@@ -79,6 +79,10 @@ const PackageEditorPage = lazyImport(async () => {
79
79
  ])
80
80
  return editorModule
81
81
  })
82
+ const ViewConnectedRepoPage = lazyImport(
83
+ () => import("@/pages/view-connected-repo"),
84
+ )
85
+ const PreviewBuildPage = lazyImport(() => import("@/pages/preview-build"))
82
86
 
83
87
  class ErrorBoundary extends React.Component<
84
88
  { children: React.ReactNode },
@@ -139,7 +143,7 @@ class ErrorBoundary extends React.Component<
139
143
  this.cleanup() // Clean up listeners before reload
140
144
  this.setState({ reloading: true })
141
145
  this.reloadTimeout = window.setTimeout(() => {
142
- window.location.reload()
146
+ // window.location.reload()
143
147
  }, 500)
144
148
  }
145
149
 
@@ -253,6 +257,11 @@ function App() {
253
257
  <Route path="/my-orders" component={MyOrdersPage} />
254
258
  <Route path="/dev-login" component={DevLoginPage} />
255
259
  <Route path="/:username" component={UserProfilePage} />
260
+ <Route path="/build/:buildId" component={ViewConnectedRepoPage} />
261
+ <Route
262
+ path="/build/:buildId/preview"
263
+ component={PreviewBuildPage}
264
+ />
256
265
  <Route path="/:author/:packageName" component={ViewPackagePage} />
257
266
  <Route
258
267
  path="/:author/:packageName/builds"
@@ -0,0 +1,124 @@
1
+ import { useState } from "react"
2
+ import {
3
+ Dialog,
4
+ DialogContent,
5
+ DialogDescription,
6
+ DialogFooter,
7
+ DialogHeader,
8
+ DialogTitle,
9
+ } from "@/components/ui/dialog"
10
+ import { Button } from "@/components/ui/button"
11
+ import { Input } from "@/components/ui/input"
12
+ import { Label } from "@/components/ui/label"
13
+ import { Alert, AlertDescription } from "@/components/ui/alert"
14
+ import { Loader2, Tag, AlertCircle } from "lucide-react"
15
+
16
+ interface CreateReleaseDialogProps {
17
+ isOpen: boolean
18
+ onClose: () => void
19
+ version: string
20
+ setVersion: (version: string) => void
21
+ currentVersion?: string
22
+ isLoading: boolean
23
+ error: string | null
24
+ onCreateRelease: () => Promise<void>
25
+ }
26
+
27
+ export function CreateReleaseDialog({
28
+ isOpen,
29
+ onClose,
30
+ version,
31
+ setVersion,
32
+ currentVersion,
33
+ isLoading,
34
+ error,
35
+ onCreateRelease,
36
+ }: CreateReleaseDialogProps) {
37
+ const handleSubmit = async (e: React.FormEvent) => {
38
+ e.preventDefault()
39
+ await onCreateRelease()
40
+ }
41
+
42
+ const handleClose = () => {
43
+ onClose()
44
+ }
45
+
46
+ return (
47
+ <Dialog open={isOpen} onOpenChange={handleClose}>
48
+ <DialogContent className="sm:max-w-[425px]">
49
+ <DialogHeader>
50
+ <DialogTitle className="flex items-center gap-2">
51
+ <Tag className="w-5 h-5" />
52
+ Create New Release
53
+ </DialogTitle>
54
+ <DialogDescription>
55
+ Create a new release. This will make your latest changes available
56
+ to users.
57
+ </DialogDescription>
58
+ </DialogHeader>
59
+
60
+ <form onSubmit={handleSubmit} className="space-y-4">
61
+ <div className="space-y-3">
62
+ <div className="flex justify-center">
63
+ <p className="text-sm text-muted-foreground text-center">
64
+ {currentVersion ? (
65
+ <span className="flex items-center justify-center gap-2">
66
+ <span>Current version:</span>
67
+ <span className="font-mono font-medium text-foreground bg-muted px-2 py-1 rounded-md text-xs border">
68
+ {currentVersion}
69
+ </span>
70
+ </span>
71
+ ) : (
72
+ "Follow semantic versioning (e.g., 1.0.0)"
73
+ )}
74
+ </p>
75
+ </div>{" "}
76
+ <Label htmlFor="version" className="text-sm font-medium">
77
+ Version
78
+ </Label>
79
+ <Input
80
+ id="version"
81
+ placeholder="e.g., 1.0.0"
82
+ value={version}
83
+ onChange={(e) => setVersion(e.target.value)}
84
+ disabled={isLoading}
85
+ className="h-10"
86
+ />
87
+ </div>
88
+
89
+ {error && (
90
+ <Alert variant="destructive">
91
+ <AlertCircle className="h-4 w-4" />
92
+ <AlertDescription>{error}</AlertDescription>
93
+ </Alert>
94
+ )}
95
+
96
+ <DialogFooter>
97
+ <Button
98
+ type="button"
99
+ variant="outline"
100
+ onClick={handleClose}
101
+ disabled={isLoading}
102
+ >
103
+ Cancel
104
+ </Button>
105
+ <Button
106
+ type="submit"
107
+ disabled={isLoading || !version.trim()}
108
+ className="min-w-[100px]"
109
+ >
110
+ {isLoading ? (
111
+ <>
112
+ <Loader2 className="w-4 h-4 mr-2 animate-spin" />
113
+ Creating...
114
+ </>
115
+ ) : (
116
+ "Create Release"
117
+ )}
118
+ </Button>
119
+ </DialogFooter>
120
+ </form>
121
+ </DialogContent>
122
+ </Dialog>
123
+ )
124
+ }
@@ -57,16 +57,24 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
57
57
  handleRenameFile,
58
58
  isCreatingFile,
59
59
  setIsCreatingFile,
60
- pkg,
61
60
  }) => {
62
61
  const [sidebarOpen, setSidebarOpen] = fileSidebarState
63
62
  const [newFileName, setNewFileName] = useState("")
64
63
  const [errorMessage, setErrorMessage] = useState("")
65
64
  const [renamingFile, setRenamingFile] = useState<string | null>(null)
65
+ const [selectedFolderForCreation, setSelectedFolderForCreation] = useState<
66
+ string | null
67
+ >(null)
68
+ const [selectedItemId, setSelectedItemId] = React.useState<string>(
69
+ currentFile || "",
70
+ )
66
71
  const { toast } = useToast()
67
- const session = useGlobalStore((s) => s.session)
68
72
  const canModifyFiles = true
69
73
 
74
+ const onFolderSelect = (folderPath: string) => {
75
+ setSelectedFolderForCreation(folderPath)
76
+ }
77
+
70
78
  const transformFilesToTreeData = (
71
79
  files: Record<FileName, string>,
72
80
  ): TreeDataItem[] => {
@@ -90,6 +98,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
90
98
  : segment
91
99
  const absolutePath = hasLeadingSlash ? `/${relativePath}` : relativePath
92
100
  const itemId = absolutePath
101
+
93
102
  if (
94
103
  !currentNode[segment] &&
95
104
  (!isHiddenFile(relativePath) ||
@@ -104,21 +113,17 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
104
113
  name: segment,
105
114
  isRenaming: renamingFile === itemId,
106
115
  onRename: (newFilename: string) => {
107
- // Preserve the folder structure when renaming
108
116
  const oldPath = itemId
109
- const pathParts = oldPath.split("/").filter((part) => part !== "") // Remove empty segments
117
+ const pathParts = oldPath.split("/").filter((part) => part !== "")
110
118
  let newPath: string
111
119
 
112
120
  if (pathParts.length > 1) {
113
- // File is in a folder, preserve the folder structure
114
121
  const folderPath = pathParts.slice(0, -1).join("/")
115
122
  newPath = folderPath + "/" + newFilename
116
123
  } else {
117
- // File is in root, just use the new filename
118
124
  newPath = newFilename
119
125
  }
120
126
 
121
- // Preserve leading slash if original path had one
122
127
  if (oldPath.startsWith("/") && !newPath.startsWith("/")) {
123
128
  newPath = "/" + newPath
124
129
  }
@@ -142,7 +147,12 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
142
147
  setRenamingFile(null)
143
148
  },
144
149
  icon: isLeafNode ? File : Folder,
145
- onClick: isLeafNode ? () => onFileSelect(absolutePath) : undefined,
150
+ onClick: isLeafNode
151
+ ? () => {
152
+ onFileSelect(absolutePath)
153
+ setSelectedFolderForCreation(null)
154
+ }
155
+ : () => onFolderSelect(absolutePath),
146
156
  draggable: false,
147
157
  droppable: !isLeafNode,
148
158
  children: isLeafNode ? undefined : {},
@@ -209,7 +219,6 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
209
219
  })
210
220
  })
211
221
 
212
- // Convert the nested object structure to array structure
213
222
  const convertToArray = (
214
223
  items: Record<string, TreeNode>,
215
224
  ): TreeDataItem[] => {
@@ -226,17 +235,70 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
226
235
 
227
236
  const treeData = transformFilesToTreeData(files)
228
237
 
238
+ const getCurrentFolderPath = (): string => {
239
+ if (selectedFolderForCreation) {
240
+ return selectedFolderForCreation
241
+ }
242
+
243
+ if (!selectedItemId || selectedItemId === "") return ""
244
+
245
+ const hasLeadingSlash = selectedItemId.startsWith("/")
246
+ const normalizedPath = hasLeadingSlash
247
+ ? selectedItemId.slice(1)
248
+ : selectedItemId
249
+ const pathParts = selectedItemId.split("/")
250
+
251
+ if (pathParts.length > 1) {
252
+ const folderPath = pathParts.slice(0, -1).join("/")
253
+ return hasLeadingSlash ? `/${folderPath}` : folderPath
254
+ }
255
+
256
+ return hasLeadingSlash ? "/" : ""
257
+ }
258
+
259
+ const constructFilePath = (fileName: string): string => {
260
+ const trimmedFileName = fileName.trim()
261
+
262
+ if (!trimmedFileName) {
263
+ return ""
264
+ }
265
+
266
+ const currentFolder = getCurrentFolderPath()
267
+
268
+ if (trimmedFileName.startsWith("/")) {
269
+ return trimmedFileName
270
+ }
271
+
272
+ if (!currentFolder || currentFolder === "/") {
273
+ const result =
274
+ currentFolder === "/" ? `/${trimmedFileName}` : trimmedFileName
275
+ return result
276
+ }
277
+
278
+ const result = `${currentFolder}/${trimmedFileName}`
279
+ return result
280
+ }
229
281
  const handleCreateFileInline = () => {
282
+ const finalFileName = constructFilePath(newFileName)
283
+ if (!finalFileName) {
284
+ setErrorMessage("File name cannot be empty")
285
+ return
286
+ }
287
+
230
288
  const { newFileCreated } = handleCreateFile({
231
- newFileName,
289
+ newFileName: finalFileName,
232
290
  onError: (error) => {
233
291
  setErrorMessage(error.message)
234
292
  },
235
293
  })
294
+
236
295
  if (newFileCreated) {
237
296
  setIsCreatingFile(false)
238
297
  setNewFileName("")
239
298
  setErrorMessage("")
299
+ onFileSelect(finalFileName)
300
+ setSelectedItemId(finalFileName)
301
+ setSelectedFolderForCreation(null)
240
302
  }
241
303
  }
242
304
 
@@ -245,6 +307,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
245
307
  setIsCreatingFile(false)
246
308
  setNewFileName("")
247
309
  setErrorMessage("")
310
+ setSelectedFolderForCreation(null)
248
311
  return
249
312
  }
250
313
  handleCreateFileInline()
@@ -255,6 +318,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
255
318
  setErrorMessage("")
256
319
  setIsCreatingFile(false)
257
320
  setNewFileName("")
321
+ setSelectedFolderForCreation(null)
258
322
  }
259
323
 
260
324
  return (
@@ -267,9 +331,7 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
267
331
  >
268
332
  <button
269
333
  onClick={toggleSidebar}
270
- className={`z-[99] mt-2 ml-2 text-gray-400 scale-90 transition-opacity duration-200 ${
271
- !sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"
272
- }`}
334
+ className={`z-[99] mt-2 ml-2 text-gray-400 scale-90 transition-opacity duration-200 ${!sidebarOpen ? "opacity-0 pointer-events-none" : "opacity-100"}`}
273
335
  >
274
336
  <PanelRightOpen />
275
337
  </button>
@@ -286,33 +348,76 @@ const FileSidebar: React.FC<FileSidebarProps> = ({
286
348
  autoFocus
287
349
  value={newFileName}
288
350
  spellCheck={false}
289
- onChange={(e) => setNewFileName(e.target.value)}
351
+ onChange={(e) => {
352
+ setNewFileName(e.target.value)
353
+ if (errorMessage) {
354
+ setErrorMessage("")
355
+ }
356
+ }}
290
357
  onBlur={handleCreateFileBlur}
291
358
  onKeyDown={(e) => {
292
359
  if (e.key === "Enter") {
360
+ e.preventDefault()
293
361
  handleCreateFileInline()
294
362
  } else if (e.key === "Escape") {
363
+ e.preventDefault()
295
364
  setIsCreatingFile(false)
296
365
  setNewFileName("")
297
366
  setErrorMessage("")
367
+ setSelectedFolderForCreation(null)
368
+ } else if (e.key === "Tab") {
369
+ e.preventDefault()
370
+ const currentFolder = getCurrentFolderPath()
371
+ if (currentFolder && !newFileName.includes("/")) {
372
+ const displayPath = currentFolder.startsWith("/")
373
+ ? currentFolder.slice(1)
374
+ : currentFolder
375
+ setNewFileName(`${displayPath}/`)
376
+ }
298
377
  }
299
378
  }}
300
- placeholder="Enter file name"
379
+ placeholder={(() => {
380
+ const currentFolder = getCurrentFolderPath()
381
+ if (!currentFolder || currentFolder === "/") {
382
+ return "Enter file name (root folder)"
383
+ }
384
+ const displayPath = currentFolder.startsWith("/")
385
+ ? currentFolder.slice(1)
386
+ : currentFolder
387
+ return `Enter file name (${displayPath}/)`
388
+ })()}
389
+ className={
390
+ errorMessage ? "border-red-500 focus:border-red-500" : ""
391
+ }
301
392
  />
302
393
  {errorMessage && (
303
- <div className="text-red-500 mt-1">{errorMessage}</div>
394
+ <div className="text-red-500 text-xs mt-1 px-1">{errorMessage}</div>
304
395
  )}
396
+ <div className="text-gray-400 text-xs mt-1 px-1">
397
+ Tip: Use / for subfolders, Tab to auto-complete current folder
398
+ </div>
305
399
  </div>
306
400
  )}
307
- <TreeView
308
- data={treeData}
309
- initialSelectedItemId={currentFile || ""}
310
- onSelectChange={(item) => {
311
- if (item?.onClick) {
312
- item.onClick()
401
+ <div
402
+ onClick={(e) => {
403
+ if (e.target === e.currentTarget) {
404
+ setSelectedFolderForCreation(null)
405
+ setSelectedItemId("")
313
406
  }
314
407
  }}
315
- />
408
+ className="flex-1 border-2 h-full"
409
+ >
410
+ <TreeView
411
+ data={treeData}
412
+ setSelectedItemId={(value) => setSelectedItemId(value || "")}
413
+ selectedItemId={selectedItemId}
414
+ onSelectChange={(item) => {
415
+ if (item?.onClick) {
416
+ item.onClick()
417
+ }
418
+ }}
419
+ />
420
+ </div>
316
421
  </div>
317
422
  )
318
423
  }
@@ -5,6 +5,7 @@ import { Github, RefreshCw } from "lucide-react"
5
5
  import { useParams } from "wouter"
6
6
  import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
7
7
  import { useGlobalStore } from "@/hooks/use-global-store"
8
+ import { useCurrentPackageCircuitJson } from "../ViewPackagePage/hooks/use-current-package-circuit-json"
8
9
 
9
10
  export function PackageBuildHeader() {
10
11
  const { author, packageName } = useParams()
@@ -15,6 +16,8 @@ export function PackageBuildHeader() {
15
16
  const { mutate: rebuildPackage, isLoading } =
16
17
  useRebuildPackageReleaseMutation()
17
18
 
19
+ const { circuitJson } = useCurrentPackageCircuitJson()
20
+
18
21
  return (
19
22
  <div className="border-b border-gray-200 bg-white px-4 sm:px-6 py-4">
20
23
  <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 container mx-auto max-w-7xl">
@@ -63,7 +66,12 @@ export function PackageBuildHeader() {
63
66
  {isLoading ? "Rebuilding..." : "Rebuild"}
64
67
  </Button>
65
68
  )}
66
- <DownloadButtonAndMenu unscopedName={packageName} author={author} />
69
+ <DownloadButtonAndMenu
70
+ offerMultipleImageFormats
71
+ circuitJson={circuitJson}
72
+ unscopedName={packageName}
73
+ author={author}
74
+ />
67
75
  </div>
68
76
  </div>
69
77
  </div>
@@ -3,7 +3,7 @@ import { useAxios } from "@/hooks/use-axios"
3
3
  import { useLocation } from "wouter"
4
4
  import React, { useState } from "react"
5
5
  import { useQuery } from "react-query"
6
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
6
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
7
7
  import { Search } from "lucide-react"
8
8
  import { Button } from "./ui/button"
9
9
  import { PackageCardSkeleton } from "./PackageCardSkeleton"
@@ -20,7 +20,7 @@ const PageSearchComponent: React.FC<PageSearchComponentProps> = ({
20
20
  }) => {
21
21
  const [location, setLocation] = useLocation()
22
22
  const axios = useAxios()
23
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
23
+ const snippetsBaseApiUrl = useApiBaseUrl()
24
24
 
25
25
  // Initialize search query directly from URL
26
26
  const [searchQuery, setSearchQuery] = useState(
@@ -4,7 +4,7 @@ import { useLocation } from "wouter"
4
4
  import React, { useEffect, useRef, useState } from "react"
5
5
  import { useQuery } from "react-query"
6
6
  import { Alert } from "./ui/alert"
7
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
7
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
8
  import { PrefetchPageLink } from "./PrefetchPageLink"
9
9
  import { CircuitBoard } from "lucide-react"
10
10
  import { cn } from "@/lib/utils"
@@ -60,7 +60,7 @@ const SearchComponent: React.FC<SearchComponentProps> = ({
60
60
  const resultsRef = useRef<HTMLDivElement>(null)
61
61
  const inputRef = useRef<HTMLInputElement>(null)
62
62
  const [location, setLocation] = useLocation()
63
- const snippetsBaseApiUrl = usePackagesBaseApiUrl()
63
+ const snippetsBaseApiUrl = useApiBaseUrl()
64
64
 
65
65
  const { data: searchResults, isLoading } = useQuery(
66
66
  ["packageSearch", searchQuery],
@@ -6,7 +6,7 @@ const RunFrame = lazy(async () => {
6
6
  })
7
7
 
8
8
  export const SuspenseRunFrame = (
9
- props: React.ComponentProps<typeof RunFrame>,
9
+ props: React.ComponentProps<typeof RunFrame> & { className?: string },
10
10
  ) => {
11
11
  return (
12
12
  <Suspense
@@ -20,7 +20,7 @@ export const SuspenseRunFrame = (
20
20
  </div>
21
21
  }
22
22
  >
23
- <div className="h-[98vh]">
23
+ <div className={`h-[98vh] ${props.className}`}>
24
24
  <RunFrame {...props} />
25
25
  </div>
26
26
  </Suspense>
@@ -4,7 +4,7 @@ import { StarFilledIcon } from "@radix-ui/react-icons"
4
4
  import { Link } from "wouter"
5
5
  import { Package } from "fake-snippets-api/lib/db/schema"
6
6
  import { useRef, useState } from "react"
7
- import { usePackagesBaseApiUrl } from "@/hooks/use-packages-base-api-url"
7
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
8
8
 
9
9
  const CarouselItem = ({
10
10
  pkg,
@@ -36,7 +36,7 @@ export const TrendingPackagesCarousel = () => {
36
36
  const axios = useAxios()
37
37
  const scrollRef = useRef<HTMLDivElement>(null)
38
38
  const [isHovered, setIsHovered] = useState(false)
39
- const apiBaseUrl = usePackagesBaseApiUrl()
39
+ const apiBaseUrl = useApiBaseUrl()
40
40
 
41
41
  const { data: trendingPackages } = useQuery<Package[]>(
42
42
  "trendingPackages",
@@ -205,6 +205,7 @@ const MobileSidebar = ({
205
205
  currentDescription={
206
206
  packageInfo.description || packageInfo?.ai_description || ""
207
207
  }
208
+ currentGithubRepoFullName={packageInfo.github_repo_full_name}
208
209
  currentLicense={currentLicense}
209
210
  currentWebsite={(packageInfo as any)?.website || ""}
210
211
  isPrivate={Boolean(packageInfo.is_private)}
@@ -171,6 +171,7 @@ export default function SidebarAboutSection({
171
171
  unscopedPackageName={packageInfo.unscoped_name}
172
172
  packageReleaseId={packageInfo.latest_package_release_id}
173
173
  packageId={packageInfo.package_id}
174
+ currentGithubRepoFullName={packageInfo.github_repo_full_name}
174
175
  currentDescription={
175
176
  packageInfo.description || packageInfo?.ai_description || ""
176
177
  }
@@ -0,0 +1,123 @@
1
+ import { useState } from "react"
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "@/components/ui/select"
9
+ import { useAxios } from "@/hooks/use-axios"
10
+ import { useApiBaseUrl } from "@/hooks/use-packages-base-api-url"
11
+ import { useQuery } from "react-query"
12
+ import { Button } from "../ui/button"
13
+ import { Label } from "../ui/label"
14
+ import { Plus } from "lucide-react"
15
+
16
+ interface GitHubRepositorySelectorProps {
17
+ value?: string
18
+ onValueChange?: (value: string) => void
19
+ disabled?: boolean
20
+ open?: boolean
21
+ }
22
+
23
+ export const GitHubRepositorySelector = ({
24
+ value,
25
+ onValueChange,
26
+ disabled = false,
27
+ open = false,
28
+ }: GitHubRepositorySelectorProps) => {
29
+ const axios = useAxios()
30
+ const apiBaseUrl = useApiBaseUrl()
31
+ const [selectedRepository, setSelectedRepository] = useState<string>(
32
+ value || "",
33
+ )
34
+
35
+ // Fetch available repositories
36
+ const { data: repositoriesData, error: repositoriesError } = useQuery(
37
+ ["github-repositories"],
38
+ async () => {
39
+ const response = await axios.get("/github/repos/list_available")
40
+ return response.data
41
+ },
42
+ {
43
+ enabled: open, // Only fetch when needed
44
+ retry: false,
45
+ },
46
+ )
47
+
48
+ const handleConnectMoreRepos = async () => {
49
+ window.location.href = `${apiBaseUrl}/github/installations/create_new_installation_redirect?return_to_page=${window.location.pathname}`
50
+ }
51
+
52
+ const handleValueChange = (newValue: string) => {
53
+ if (newValue === "connect-more") {
54
+ handleConnectMoreRepos()
55
+ } else {
56
+ setSelectedRepository(newValue)
57
+ onValueChange?.(newValue)
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div className="space-y-1">
63
+ <Label htmlFor="repository">GitHub Repository</Label>
64
+ {(repositoriesError as any)?.response?.status === 400 &&
65
+ (repositoriesError as any)?.response?.data?.error_code ===
66
+ "github_not_connected" ? (
67
+ <div className="space-y-2">
68
+ <div className="text-sm text-muted-foreground">
69
+ Connect your GitHub account to link this package to a repository.
70
+ </div>
71
+ <Button
72
+ type="button"
73
+ variant="outline"
74
+ onClick={handleConnectMoreRepos}
75
+ className="w-full"
76
+ disabled={disabled}
77
+ >
78
+ <svg
79
+ className="w-4 h-4 mr-2"
80
+ viewBox="0 0 24 24"
81
+ fill="currentColor"
82
+ >
83
+ <path d="M12 0C5.374 0 0 5.373 0 12 0 17.302 3.438 21.8 8.207 23.387c.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z" />
84
+ </svg>
85
+ Connect GitHub Account
86
+ </Button>
87
+ </div>
88
+ ) : (
89
+ <div className="space-y-2">
90
+ <Select
91
+ value={selectedRepository}
92
+ onValueChange={handleValueChange}
93
+ disabled={disabled}
94
+ >
95
+ <SelectTrigger className="w-full">
96
+ <SelectValue placeholder="Select a repository" />
97
+ </SelectTrigger>
98
+ <SelectContent className="!z-[999]">
99
+ {repositoriesData?.repos?.map((repo: any) => (
100
+ <SelectItem key={repo.full_name} value={repo.full_name}>
101
+ <div className="flex items-center space-x-2">
102
+ <span>{repo.unscoped_name}</span>
103
+ {repo.private && (
104
+ <span className="text-xs text-muted-foreground">
105
+ (private)
106
+ </span>
107
+ )}
108
+ </div>
109
+ </SelectItem>
110
+ ))}
111
+ <SelectItem value="connect-more">
112
+ <div className="flex items-center space-x-2 text-blue-600">
113
+ <Plus className="w-3 h-3" />
114
+ <span>Connect More Repos</span>
115
+ </div>
116
+ </SelectItem>
117
+ </SelectContent>
118
+ </Select>
119
+ </div>
120
+ )}
121
+ </div>
122
+ )
123
+ }