@tscircuit/fake-snippets 0.0.67 → 0.0.68

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.
@@ -16,6 +16,7 @@ import {
16
16
  Dialog,
17
17
  DialogContent,
18
18
  DialogDescription,
19
+ DialogFooter,
19
20
  DialogHeader,
20
21
  DialogTitle,
21
22
  } from "../ui/dialog"
@@ -217,151 +218,157 @@ export const EditPackageDetailsDialog = ({
217
218
  </DialogContent>
218
219
  </Dialog>
219
220
  <Dialog open={open !== showConfirmDelete} onOpenChange={onOpenChange}>
220
- <DialogContent className="sm:max-w-[500px] lg:h-[70vh] sm:h-[90vh] overflow-y-auto w-[95vw] p-6 gap-6 rounded-2xl shadow-lg">
221
- <DialogHeader>
222
- <DialogTitle>Edit Package Details</DialogTitle>
223
- <DialogDescription>
224
- Update your package’s description, website, visibility, or delete
225
- it.
226
- </DialogDescription>
227
- </DialogHeader>
221
+ <DialogContent className="sm:max-w-[500px] lg:h-[70vh] sm:h-[90vh] overflow-y-auto w-[95vw] h-[80vh] p-6 gap-6 rounded-2xl shadow-lg">
222
+ <div className="flex flex-col gap-10">
223
+ <DialogHeader>
224
+ <DialogTitle>Edit Package Details</DialogTitle>
225
+ <DialogDescription>
226
+ Update your package's description, website, visibility, or
227
+ delete it.
228
+ </DialogDescription>
229
+ </DialogHeader>
230
+ <div className="">
231
+ <div className="grid gap-2">
232
+ <div className="space-y-1">
233
+ <Label htmlFor="website">Website</Label>
234
+ <Input
235
+ id="website"
236
+ value={formData.website}
237
+ onChange={(e) =>
238
+ setFormData((prev) => ({
239
+ ...prev,
240
+ website: e.target.value,
241
+ }))
242
+ }
243
+ placeholder="https://example.com"
244
+ disabled={updatePackageDetailsMutation.isLoading}
245
+ className="w-full"
246
+ aria-invalid={!!websiteError}
247
+ />
248
+ {websiteError && (
249
+ <p className="text-sm text-red-500">{websiteError}</p>
250
+ )}
251
+ </div>
252
+ <div className="space-y-1">
253
+ <Label htmlFor="visibility">Visibility</Label>
254
+ <Select
255
+ value={formData.visibility}
256
+ onValueChange={(val) => {
257
+ setFormData((prev) => ({
258
+ ...prev,
259
+ visibility: val,
260
+ }))
261
+ }}
262
+ disabled={updatePackageDetailsMutation.isLoading}
263
+ >
264
+ <SelectTrigger className="w-full">
265
+ <SelectValue placeholder="Select visibility" />
266
+ </SelectTrigger>
267
+ <SelectContent className="!z-[999]">
268
+ <SelectItem value="public">public</SelectItem>
269
+ <SelectItem value="private">private</SelectItem>
270
+ </SelectContent>
271
+ </Select>
272
+ </div>
273
+ <div className="space-y-1">
274
+ <Label htmlFor="description">Description</Label>
275
+ <Textarea
276
+ id="description"
277
+ value={formData.description}
278
+ onChange={(e) =>
279
+ setFormData((prev) => ({
280
+ ...prev,
281
+ description: e.target.value,
282
+ }))
283
+ }
284
+ placeholder="Enter package description"
285
+ disabled={updatePackageDetailsMutation.isLoading}
286
+ className="w-full min-h-[80px] resize-none"
287
+ />
288
+ </div>
289
+ <div className="space-y-1">
290
+ <Label htmlFor="license">License</Label>
291
+ <Select
292
+ value={formData.license || "unset"}
293
+ onValueChange={(value) =>
294
+ setFormData((prev) => ({
295
+ ...prev,
296
+ license: value === "unset" ? null : value,
297
+ }))
298
+ }
299
+ disabled={updatePackageDetailsMutation.isLoading}
300
+ >
301
+ <SelectTrigger className="w-full">
302
+ <SelectValue placeholder="Select a license" />
303
+ </SelectTrigger>
304
+ <SelectContent className="!z-[999]">
305
+ <SelectItem value="MIT">MIT</SelectItem>
306
+ <SelectItem value="Apache-2.0">Apache-2.0</SelectItem>
307
+ <SelectItem value="BSD-3-Clause">BSD-3-Clause</SelectItem>
308
+ <SelectItem value="GPL-3.0">GPL-3.0</SelectItem>
309
+ <SelectItem value="unset">Unset</SelectItem>
310
+ </SelectContent>
311
+ </Select>
312
+ </div>
313
+ </div>
228
314
 
229
- <div className="grid gap-4">
230
- <div className="space-y-1">
231
- <Label htmlFor="website">Website</Label>
232
- <Input
233
- id="website"
234
- value={formData.website}
235
- onChange={(e) =>
236
- setFormData((prev) => ({
237
- ...prev,
238
- website: e.target.value,
239
- }))
240
- }
241
- placeholder="https://example.com"
242
- disabled={updatePackageDetailsMutation.isLoading}
243
- className="w-full"
244
- aria-invalid={!!websiteError}
245
- />
246
- {websiteError && (
247
- <p className="text-sm text-red-500">{websiteError}</p>
248
- )}
249
- </div>
250
- <div className="space-y-1">
251
- <Label htmlFor="visibility">Visibility</Label>
252
- <Select
253
- value={formData.visibility}
254
- onValueChange={(val) => {
255
- setFormData((prev) => ({
256
- ...prev,
257
- visibility: val,
258
- }))
259
- }}
260
- disabled={updatePackageDetailsMutation.isLoading}
315
+ <details
316
+ className="mt-2 rounded-md"
317
+ onToggle={(e) => setDangerOpen(e.currentTarget.open)}
261
318
  >
262
- <SelectTrigger className="w-full">
263
- <SelectValue placeholder="Select visibility" />
264
- </SelectTrigger>
265
- <SelectContent className="!z-[999]">
266
- <SelectItem value="public">public</SelectItem>
267
- <SelectItem value="private">private</SelectItem>
268
- </SelectContent>
269
- </Select>
319
+ <summary className="cursor-pointer p-2 font-medium text-sm text-black list-none flex justify-between items-center">
320
+ Danger Zone
321
+ <ChevronDown
322
+ className={`w-4 h-4 mr-1 transition-transform ${dangerOpen ? "rotate-180" : ""}`}
323
+ />
324
+ </summary>
325
+ <div className="p-2 pr-2">
326
+ <div className="flex justify-between items-center">
327
+ <div>
328
+ <p className="text-sm text-muted-foreground">
329
+ Once deleted, it cannot be recovered.
330
+ </p>
331
+ </div>
332
+ <Button
333
+ variant="destructive"
334
+ size="default"
335
+ onClick={() => setShowConfirmDelete(true)}
336
+ disabled={deleting}
337
+ className="shrink-0 lg:w-[115px] w-[70px]"
338
+ >
339
+ {deleting ? "Deleting..." : "Delete"}
340
+ </Button>
341
+ </div>
342
+ </div>
343
+ </details>
270
344
  </div>
271
- <div className="space-y-1">
272
- <Label htmlFor="description">Description</Label>
273
- <Textarea
274
- id="description"
275
- value={formData.description}
276
- onChange={(e) =>
277
- setFormData((prev) => ({
278
- ...prev,
279
- description: e.target.value,
280
- }))
281
- }
282
- placeholder="Enter package description"
345
+ </div>
346
+
347
+ <DialogFooter className="mt-auto">
348
+ <div className="lg:px-2 flex flex-col sm:flex-row justify-end gap-2">
349
+ <Button
350
+ variant="outline"
351
+ onClick={() => onOpenChange(false)}
283
352
  disabled={updatePackageDetailsMutation.isLoading}
284
- className="w-full min-h-[100px] resize-none"
285
- />
286
- </div>
287
- <div className="space-y-1">
288
- <Label htmlFor="license">License</Label>
289
- <Select
290
- value={formData.license || "unset"}
291
- onValueChange={(value) =>
292
- setFormData((prev) => ({
293
- ...prev,
294
- license: value === "unset" ? null : value,
295
- }))
353
+ className="sm:w-auto w-full"
354
+ >
355
+ Cancel
356
+ </Button>
357
+ <Button
358
+ onClick={() => updatePackageDetailsMutation.mutate()}
359
+ disabled={
360
+ updatePackageDetailsMutation.isLoading ||
361
+ !hasChanges ||
362
+ !isFormValid
296
363
  }
297
- disabled={updatePackageDetailsMutation.isLoading}
364
+ className="sm:w-auto lg:w-[115px]"
298
365
  >
299
- <SelectTrigger className="w-full">
300
- <SelectValue placeholder="Select a license" />
301
- </SelectTrigger>
302
- <SelectContent className="!z-[999]">
303
- <SelectItem value="MIT">MIT</SelectItem>
304
- <SelectItem value="Apache-2.0">Apache-2.0</SelectItem>
305
- <SelectItem value="BSD-3-Clause">BSD-3-Clause</SelectItem>
306
- <SelectItem value="GPL-3.0">GPL-3.0</SelectItem>
307
- <SelectItem value="unset">Unset</SelectItem>
308
- </SelectContent>
309
- </Select>
310
- </div>
311
- </div>
312
- <details
313
- className="mt-2 rounded-md"
314
- onToggle={(e) => setDangerOpen(e.currentTarget.open)}
315
- >
316
- <summary className="cursor-pointer p-2 font-medium text-sm text-black list-none flex justify-between items-center">
317
- Danger Zone
318
- <ChevronDown
319
- className={`w-4 h-4 mr-1 transition-transform ${dangerOpen ? "rotate-180" : ""}`}
320
- />
321
- </summary>
322
- <div className="p-2 pr-2">
323
- <div className="flex justify-between items-center">
324
- <div>
325
- <p className="text-sm text-muted-foreground">
326
- Once deleted, it cannot be recovered.
327
- </p>
328
- </div>
329
- <Button
330
- variant="destructive"
331
- size="default"
332
- onClick={() => setShowConfirmDelete(true)}
333
- disabled={deleting}
334
- className="shrink-0 lg:w-[115px] w-[70px]"
335
- >
336
- {deleting ? "Deleting..." : "Delete"}
337
- </Button>
338
- </div>
366
+ {updatePackageDetailsMutation.isLoading
367
+ ? "Updating..."
368
+ : "Save Changes"}
369
+ </Button>
339
370
  </div>
340
- </details>
341
-
342
- <div className=" lg:px-2 flex flex-col sm:flex-row justify-end gap-3">
343
- <Button
344
- variant="outline"
345
- onClick={() => onOpenChange(false)}
346
- disabled={updatePackageDetailsMutation.isLoading}
347
- className="sm:w-auto w-full"
348
- >
349
- Cancel
350
- </Button>
351
- <Button
352
- onClick={() => updatePackageDetailsMutation.mutate()}
353
- disabled={
354
- updatePackageDetailsMutation.isLoading ||
355
- !hasChanges ||
356
- !isFormValid
357
- }
358
- className="sm:w-auto lg:w-[115px]"
359
- >
360
- {updatePackageDetailsMutation.isLoading
361
- ? "Updating..."
362
- : "Save Changes"}
363
- </Button>
364
- </div>
371
+ </DialogFooter>
365
372
  </DialogContent>
366
373
  </Dialog>
367
374
  </div>
@@ -22,6 +22,8 @@ import { usePackageFilesLoader } from "@/hooks/usePackageFilesLoader"
22
22
  import { findTargetFile } from "@/lib/utils/findTargetFile"
23
23
  import { toastManualEditConflicts } from "@/lib/utils/toastManualEditConflicts"
24
24
  import { ManualEditEvent } from "@tscircuit/props"
25
+ import { isValidFileName } from "@/lib/utils/isValidFileName"
26
+ import { useFileManagement } from "@/hooks/useFileManagement"
25
27
 
26
28
  interface Props {
27
29
  pkg?: Package
@@ -32,12 +34,19 @@ export interface PackageFile {
32
34
  content: string
33
35
  }
34
36
 
35
- interface CodeAndPreviewState {
37
+ export interface CreateFileProps {
38
+ newFileName: string
39
+ setErrorMessage: (message: string) => void
40
+ onFileSelect: (fileName: string) => void
41
+ setNewFileName: (fileName: string) => void
42
+ setIsCreatingFile: (isCreatingFile: boolean) => void
43
+ }
44
+
45
+ export interface CodeAndPreviewState {
36
46
  pkgFilesWithContent: PackageFile[]
37
47
  initialFilesLoad: PackageFile[]
38
48
  showPreview: boolean
39
49
  fullScreen: boolean
40
- dts: string
41
50
  lastSavedAt: number
42
51
  circuitJson: null | any
43
52
  isPrivate: boolean
@@ -92,7 +101,6 @@ export function CodeAndPreview({ pkg }: Props) {
92
101
  initialFilesLoad: [],
93
102
  showPreview: true,
94
103
  fullScreen: false,
95
- dts: "",
96
104
  lastSavedAt: Date.now(),
97
105
  circuitJson: null,
98
106
  isPrivate: false,
@@ -148,7 +156,6 @@ export function CodeAndPreview({ pkg }: Props) {
148
156
 
149
157
  if (loadedFiles && !isLoadingFiles) {
150
158
  const processedResults = [...loadedFiles]
151
-
152
159
  setState((prev) => ({
153
160
  ...prev,
154
161
  pkgFilesWithContent: processedResults,
@@ -159,13 +166,7 @@ export function CodeAndPreview({ pkg }: Props) {
159
166
  defaultCode,
160
167
  }))
161
168
  }
162
- }, [
163
- isLoadingFiles,
164
- pkg,
165
- pkgFiles.data,
166
- state.pkgFilesWithContent.length,
167
- defaultCode,
168
- ])
169
+ }, [isLoadingFiles, pkg, pkgFiles.data, defaultCode])
169
170
 
170
171
  const createPackageMutation = useCreatePackageMutation()
171
172
  const { mutate: createRelease } = useCreatePackageReleaseMutation({
@@ -250,10 +251,21 @@ export function CodeAndPreview({ pkg }: Props) {
250
251
  setState((prev) => ({ ...prev, lastSavedAt: Date.now() }))
251
252
 
252
253
  if (pkg) {
253
- updatePackageFilesMutation.mutate({
254
- package_name_with_version: `${pkg.name}@latest`,
255
- ...pkg,
256
- })
254
+ updatePackageFilesMutation.mutate(
255
+ {
256
+ package_name_with_version: `${pkg.name}@latest`,
257
+ ...pkg,
258
+ },
259
+ {
260
+ onSuccess: () => {
261
+ setState((prev) => ({
262
+ ...prev,
263
+ initialFilesLoad: [...prev.pkgFilesWithContent],
264
+ }))
265
+ pkgFiles.refetch()
266
+ },
267
+ },
268
+ )
257
269
  }
258
270
  }
259
271
 
@@ -284,10 +296,17 @@ export function CodeAndPreview({ pkg }: Props) {
284
296
  state.pkgFilesWithContent,
285
297
  ])
286
298
  const mainComponentPath = useMemo(() => {
287
- return state.currentFile?.endsWith(".tsx") &&
299
+ const isReactComponentExported =
300
+ /export function\s+\w+/.test(currentFileCode) ||
301
+ /export const\s+\w+\s*=/.test(currentFileCode) ||
302
+ /export default\s+\w+/.test(currentFileCode) ||
303
+ /export default\s+function\s*(\w*)\s*\(/.test(currentFileCode) ||
304
+ /export default\s*\(\s*\)\s*=>/.test(currentFileCode)
305
+
306
+ return (state.currentFile?.endsWith(".tsx") ||
307
+ state.currentFile?.endsWith(".ts")) &&
288
308
  !!state.pkgFilesWithContent.some((x) => x.path == state.currentFile) &&
289
- (currentFileCode.match(/export function (\w+)/) ||
290
- currentFileCode.match(/export const (\w+) ?=/))
309
+ isReactComponentExported
291
310
  ? state.currentFile
292
311
  : state.defaultComponentFile
293
312
  }, [state.currentFile, state.pkgFilesWithContent, currentFileCode])
@@ -327,6 +346,8 @@ export function CodeAndPreview({ pkg }: Props) {
327
346
  })
328
347
  }
329
348
 
349
+ const { handleCreateFile } = useFileManagement(state, setState)
350
+
330
351
  if ((!pkg && urlParams.package_id) || pkgFiles.isLoading || isLoadingFiles) {
331
352
  return (
332
353
  <div className="flex items-center justify-center h-64">
@@ -363,6 +384,7 @@ export function CodeAndPreview({ pkg }: Props) {
363
384
  )}
364
385
  >
365
386
  <CodeEditor
387
+ handleCreateFile={handleCreateFile}
366
388
  currentFile={state.currentFile}
367
389
  setCurrentFile={(file) =>
368
390
  setState((prev) => ({ ...prev, currentFile: file }))
@@ -379,7 +401,6 @@ export function CodeAndPreview({ pkg }: Props) {
379
401
  ),
380
402
  }))
381
403
  }}
382
- onDtsChange={(dts) => setState((prev) => ({ ...prev, dts }))}
383
404
  pkgFilesLoaded={state.pkgFilesLoaded}
384
405
  />
385
406
  </div>
@@ -28,7 +28,7 @@ import CodeEditorHeader from "@/components/package-port/CodeEditorHeader"
28
28
  import { useCodeCompletionApi } from "@/hooks/use-code-completion-ai-api"
29
29
  import FileSidebar from "../FileSidebar"
30
30
  import { findTargetFile } from "@/lib/utils/findTargetFile"
31
- import type { PackageFile } from "./CodeAndPreview"
31
+ import type { CreateFileProps, PackageFile } from "./CodeAndPreview"
32
32
  import { useShikiHighlighter } from "@/hooks/use-shiki-highlighter"
33
33
 
34
34
  const defaultImports = `
@@ -39,7 +39,6 @@ import type { CommonLayoutProps } from "@tscircuit/props"
39
39
 
40
40
  export const CodeEditor = ({
41
41
  onCodeChange,
42
- onDtsChange,
43
42
  readOnly = false,
44
43
  files = [],
45
44
  isStreaming = false,
@@ -48,10 +47,11 @@ export const CodeEditor = ({
48
47
  pkgFilesLoaded,
49
48
  currentFile,
50
49
  setCurrentFile,
50
+ handleCreateFile,
51
51
  }: {
52
52
  onCodeChange: (code: string, filename?: string) => void
53
- onDtsChange?: (dts: string) => void
54
53
  files: PackageFile[]
54
+ handleCreateFile: (props: CreateFileProps) => void
55
55
  readOnly?: boolean
56
56
  isStreaming?: boolean
57
57
  pkgFilesLoaded?: boolean
@@ -67,9 +67,8 @@ export const CodeEditor = ({
67
67
  const codeCompletionApi = useCodeCompletionApi()
68
68
  const [cursorPosition, setCursorPosition] = useState<number | null>(null)
69
69
  const [code, setCode] = useState(files[0]?.content || "")
70
- const [isCodeEditorReady, setIsCodeEditorReady] = useState(false)
71
70
 
72
- const { highlighter, isLoading } = useShikiHighlighter()
71
+ const { highlighter } = useShikiHighlighter()
73
72
 
74
73
  // Get URL search params for file_path
75
74
  const urlParams = new URLSearchParams(window.location.search)
@@ -191,9 +190,6 @@ export const CodeEditor = ({
191
190
  return fetch(input, init)
192
191
  },
193
192
  delegate: {
194
- finished: () => {
195
- setIsCodeEditorReady(true)
196
- },
197
193
  started: () => {
198
194
  const manualEditsTypeDeclaration = `
199
195
  declare module "manual-edits.json" {
@@ -248,20 +244,6 @@ export const CodeEditor = ({
248
244
  // setCode(newContent)
249
245
  onCodeChange(newContent, currentFile)
250
246
  onFileContentChanged?.(currentFile, newContent)
251
-
252
- // Generate TypeScript declarations for TypeScript/TSX files
253
- if (currentFile.endsWith(".ts") || currentFile.endsWith(".tsx")) {
254
- const { outputFiles } = env.languageService.getEmitOutput(
255
- currentFile,
256
- true,
257
- )
258
- const dtsFile = outputFiles.find((file) =>
259
- file.name.endsWith(".d.ts"),
260
- )
261
- if (dtsFile?.text && onDtsChange) {
262
- onDtsChange(dtsFile.text)
263
- }
264
- }
265
247
  }
266
248
  if (update.selectionSet) {
267
249
  const pos = update.state.selection.main.head
@@ -452,8 +434,6 @@ export const CodeEditor = ({
452
434
 
453
435
  if (currentFile.endsWith(".tsx") || currentFile.endsWith(".ts")) {
454
436
  ata(`${defaultImports}${code}`)
455
- } else if (!!currentFile) {
456
- setIsCodeEditorReady(true)
457
437
  }
458
438
 
459
439
  return () => {
@@ -542,6 +522,7 @@ export const CodeEditor = ({
542
522
  [sidebarOpen, setSidebarOpen] as ReturnType<typeof useState<boolean>>
543
523
  }
544
524
  onFileSelect={handleFileChange}
525
+ handleCreateFile={handleCreateFile}
545
526
  />
546
527
  <div className="flex flex-col flex-1 w-full min-w-0 h-full">
547
528
  {showImportAndFormatButtons && (
@@ -560,9 +541,9 @@ export const CodeEditor = ({
560
541
  )}
561
542
  <div
562
543
  ref={editorRef}
563
- className={`flex-1 overflow-auto [&_.cm-editor]:h-full [&_.cm-scroller]:!h-full ${
564
- !isCodeEditorReady ? "opacity-50" : ""
565
- }`}
544
+ className={
545
+ "flex-1 overflow-auto [&_.cm-editor]:h-full [&_.cm-scroller]:!h-full"
546
+ }
566
547
  />
567
548
  </div>
568
549
  </div>
@@ -39,7 +39,6 @@ import { Link, useLocation } from "wouter"
39
39
  import { useAxios } from "@/hooks/use-axios"
40
40
  import { useToast } from "@/hooks/use-toast"
41
41
  import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
42
- import { useCreateOrderDialog } from "@/components/dialogs/create-order-dialog"
43
42
  import { useFilesDialog } from "@/components/dialogs/files-dialog"
44
43
  import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
45
44
  import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
@@ -81,8 +80,6 @@ export default function EditorNav({
81
80
  } = useUpdatePackageDescriptionDialog()
82
81
  const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
83
82
  useConfirmDeletePackageDialog()
84
- const { Dialog: CreateOrderDialog, openDialog: openCreateOrderDialog } =
85
- useCreateOrderDialog()
86
83
  const { Dialog: FilesDialog, openDialog: openFilesDialog } = useFilesDialog()
87
84
  const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
88
85
  useViewTsFilesDialog()
@@ -358,13 +355,6 @@ export default function EditorNav({
358
355
  </Button>
359
356
  </DropdownMenuTrigger>
360
357
  <DropdownMenuContent>
361
- <DropdownMenuItem
362
- className="text-xs"
363
- onClick={() => openCreateOrderDialog()}
364
- >
365
- <PackageIcon className="mr-2 h-3 w-3" />
366
- Submit Order
367
- </DropdownMenuItem>
368
358
  <DropdownMenuItem
369
359
  className="text-xs"
370
360
  onClick={() => openFilesDialog()}
@@ -519,8 +509,8 @@ export default function EditorNav({
519
509
  <DeleteDialog
520
510
  packageId={pkg?.package_id ?? ""}
521
511
  packageName={pkg?.unscoped_name ?? ""}
512
+ packageOwner={pkg?.owner_github_username ?? ""}
522
513
  />
523
- <CreateOrderDialog />
524
514
  <FilesDialog snippetId={pkg?.package_id ?? ""} />
525
515
  <ViewTsFilesDialog />
526
516
  </nav>
@@ -0,0 +1,59 @@
1
+ import { Dispatch, SetStateAction } from "react"
2
+ import { isValidFileName } from "@/lib/utils/isValidFileName"
3
+ import {
4
+ CodeAndPreviewState,
5
+ CreateFileProps,
6
+ } from "../components/package-port/CodeAndPreview"
7
+
8
+ export function useFileManagement(
9
+ state: CodeAndPreviewState,
10
+ setState: Dispatch<SetStateAction<CodeAndPreviewState>>,
11
+ ) {
12
+ const handleCreateFile = async ({
13
+ newFileName,
14
+ setErrorMessage,
15
+ onFileSelect,
16
+ setNewFileName,
17
+ setIsCreatingFile,
18
+ }: CreateFileProps) => {
19
+ newFileName = newFileName.trim()
20
+ if (!newFileName) {
21
+ setErrorMessage("File name cannot be empty")
22
+ return
23
+ }
24
+ if (!isValidFileName(newFileName)) {
25
+ setErrorMessage(
26
+ 'Invalid file name. Avoid using special characters like <>:"/\\|?*',
27
+ )
28
+ return
29
+ }
30
+ setErrorMessage("")
31
+
32
+ const fileExists = state.pkgFilesWithContent.some(
33
+ (file) => file.path === newFileName,
34
+ )
35
+
36
+ if (fileExists) {
37
+ setErrorMessage("A file with this name already exists")
38
+ return
39
+ }
40
+
41
+ setState((prev) => {
42
+ const updatedFiles = [
43
+ ...prev.pkgFilesWithContent,
44
+ { path: newFileName, content: "" },
45
+ ]
46
+ return {
47
+ ...prev,
48
+ pkgFilesWithContent: updatedFiles,
49
+ } as CodeAndPreviewState
50
+ })
51
+ onFileSelect(newFileName)
52
+ setIsCreatingFile(false)
53
+ setNewFileName("")
54
+ }
55
+
56
+ return {
57
+ handleCreateFile,
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ export const isValidFileName = (name: string) => {
2
+ // Basic checks for file naming conventions
3
+ const invalidChars = /[<>:"/\\|?*]/
4
+ return name.length > 0 && !invalidChars.test(name)
5
+ }
@@ -194,6 +194,7 @@ export const DashboardPage = () => {
194
194
  <DeleteDialog
195
195
  packageId={packageToDelete.package_id}
196
196
  packageName={packageToDelete.unscoped_name}
197
+ packageOwner={packageToDelete.owner_github_username ?? ""}
197
198
  />
198
199
  )}
199
200
  </div>