@tscircuit/fake-snippets 0.0.43 → 0.0.44

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.
@@ -0,0 +1,518 @@
1
+ import { Button } from "@/components/ui/button"
2
+ import { GitFork, Star } from "lucide-react"
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuSub,
8
+ DropdownMenuSubContent,
9
+ DropdownMenuSubTrigger,
10
+ DropdownMenuTrigger,
11
+ } from "@/components/ui/dropdown-menu"
12
+ import { useGlobalStore } from "@/hooks/use-global-store"
13
+ import { encodeTextToUrlHash } from "@/lib/encodeTextToUrlHash"
14
+ import { cn } from "@/lib/utils"
15
+ import { OpenInNewWindowIcon, LockClosedIcon } from "@radix-ui/react-icons"
16
+ import { AnyCircuitElement } from "circuit-json"
17
+ import { Package } from "fake-snippets-api/lib/db/schema"
18
+ import {
19
+ ChevronDown,
20
+ CodeIcon,
21
+ Download,
22
+ Edit2,
23
+ Eye,
24
+ EyeIcon,
25
+ File,
26
+ FilePenLine,
27
+ MoreVertical,
28
+ Package as PackageIcon,
29
+ Pencil,
30
+ Save,
31
+ Share,
32
+ Sidebar,
33
+ Sparkles,
34
+ Trash2,
35
+ } from "lucide-react"
36
+ import { useEffect, useState } from "react"
37
+ import { useQueryClient } from "react-query"
38
+ import { Link, useLocation } from "wouter"
39
+ import { useAxios } from "@/hooks/use-axios"
40
+ import { useToast } from "@/hooks/use-toast"
41
+ import { useConfirmDeletePackageDialog } from "@/components/dialogs/confirm-delete-package-dialog"
42
+ import { useCreateOrderDialog } from "@/components/dialogs/create-order-dialog"
43
+ import { useFilesDialog } from "@/components/dialogs/files-dialog"
44
+ import { useViewTsFilesDialog } from "@/components/dialogs/view-ts-files-dialog"
45
+ import { DownloadButtonAndMenu } from "@/components/DownloadButtonAndMenu"
46
+ import { TypeBadge } from "@/components/TypeBadge"
47
+ import { useForkPackageMutation } from "@/hooks/useForkPackageMutation"
48
+ import tscircuitCorePkg from "@tscircuit/core/package.json"
49
+ import { useRenamePackageDialog } from "../dialogs/rename-package-dialog"
50
+ import { useUpdatePackageDescriptionDialog } from "../dialogs/update-package-description-dialog"
51
+
52
+ export default function EditorNav({
53
+ circuitJson,
54
+ pkg,
55
+ code,
56
+ hasUnsavedChanges,
57
+ onTogglePreview,
58
+ previewOpen,
59
+ onSave,
60
+ packageType,
61
+ isSaving,
62
+ canSave,
63
+ manualEditsFileContent,
64
+ }: {
65
+ pkg?: Package | null
66
+ circuitJson?: AnyCircuitElement[] | null
67
+ code: string
68
+ packageType?: string
69
+ hasUnsavedChanges: boolean
70
+ previewOpen: boolean
71
+ onTogglePreview: () => void
72
+ isSaving: boolean
73
+ onSave: () => void
74
+ canSave: boolean
75
+ manualEditsFileContent: string
76
+ }) {
77
+ const [, navigate] = useLocation()
78
+ const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
79
+ const session = useGlobalStore((s) => s.session)
80
+ const { Dialog: RenameDialog, openDialog: openRenameDialog } =
81
+ useRenamePackageDialog()
82
+ const {
83
+ Dialog: UpdateDescriptionDialog,
84
+ openDialog: openupdateDescriptionDialog,
85
+ } = useUpdatePackageDescriptionDialog()
86
+ const { Dialog: DeleteDialog, openDialog: openDeleteDialog } =
87
+ useConfirmDeletePackageDialog()
88
+ const { Dialog: CreateOrderDialog, openDialog: openCreateOrderDialog } =
89
+ useCreateOrderDialog()
90
+ const { Dialog: FilesDialog, openDialog: openFilesDialog } = useFilesDialog()
91
+ const { Dialog: ViewTsFilesDialog, openDialog: openViewTsFilesDialog } =
92
+ useViewTsFilesDialog()
93
+
94
+ const [isChangingType, setIsChangingType] = useState(false)
95
+ const [currentType, setCurrentType] = useState(
96
+ packageType ?? pkg?.snippet_type,
97
+ )
98
+ const [isPrivate, setIsPrivate] = useState(pkg?.is_private ?? false)
99
+ const axios = useAxios()
100
+ const { toast } = useToast()
101
+ const qc = useQueryClient()
102
+
103
+ const { mutate: forkSnippet, isLoading: isForking } = useForkPackageMutation({
104
+ pkg: pkg!,
105
+ currentCode: code,
106
+ onSuccess: (forkedPackage) => {
107
+ navigate("/p/editor?package_id=" + forkedPackage.package_id)
108
+ setTimeout(() => {
109
+ window.location.reload() //reload the page
110
+ }, 2000)
111
+ },
112
+ })
113
+
114
+ // Update currentType when snippet or packageType changes
115
+ useEffect(() => {
116
+ setCurrentType(packageType ?? pkg?.snippet_type)
117
+ }, [packageType, pkg?.snippet_type])
118
+
119
+ const handleTypeChange = async (newType: string) => {
120
+ if (!pkg || newType === currentType) return
121
+
122
+ try {
123
+ setIsChangingType(true)
124
+
125
+ const response = await axios.post("/packages/update", {
126
+ package_id: pkg.package_id,
127
+ snippet_type: newType,
128
+ })
129
+
130
+ if (response.status === 200) {
131
+ setCurrentType(newType)
132
+ toast({
133
+ title: "Snippet type changed",
134
+ description: `Successfully changed type to "${newType}"`,
135
+ })
136
+
137
+ // Invalidate queries to refetch data
138
+ await Promise.all([
139
+ qc.invalidateQueries({ queryKey: ["packages"] }),
140
+ qc.invalidateQueries({ queryKey: ["packages", pkg.package_id] }),
141
+ ])
142
+
143
+ // Reload the page to ensure all components reflect the new type
144
+ // window.location.reload()
145
+ } else {
146
+ throw new Error("Failed to update snippet type")
147
+ }
148
+ } catch (error: any) {
149
+ console.error("Error changing snippet type:", error)
150
+ toast({
151
+ title: "Error",
152
+ description:
153
+ error.response?.data?.error?.message ||
154
+ "Failed to change the snippet type. Please try again.",
155
+ variant: "destructive",
156
+ })
157
+ // Reset to previous type on error
158
+ setCurrentType(pkg.snippet_type)
159
+ } finally {
160
+ setIsChangingType(false)
161
+ }
162
+ }
163
+
164
+ const updatePackageVisibilityToPrivate = async (isPrivate: boolean) => {
165
+ if (!pkg) return
166
+
167
+ const response = await axios.post("/packages/update", {
168
+ package_id: pkg.package_id,
169
+ is_private: isPrivate,
170
+ })
171
+
172
+ if (response.status === 200) {
173
+ setIsPrivate(isPrivate)
174
+ toast({
175
+ title: "Package visibility changed",
176
+ description: `Successfully changed visibility to ${
177
+ isPrivate ? "private" : "public"
178
+ }`,
179
+ })
180
+ } else {
181
+ setIsPrivate(pkg.is_private ?? false)
182
+ toast({
183
+ title: "Error",
184
+ description: "Failed to update package visibility",
185
+ variant: "destructive",
186
+ })
187
+ throw new Error("Failed to update package visibility")
188
+ }
189
+ }
190
+
191
+ const canSavePackage =
192
+ !pkg || pkg.owner_github_username === session?.github_username
193
+
194
+ const hasManualEditsChangedFromDefault = manualEditsFileContent !== "{}"
195
+
196
+ return (
197
+ <nav className="lg:flex w-screen items-center justify-between px-2 py-3 border-b border-gray-200 bg-white text-sm border-t">
198
+ <div className="lg:flex items-center my-2 ">
199
+ <div className="flex items-center space-x-1">
200
+ {pkg && (
201
+ <>
202
+ <Link
203
+ className="text-blue-500 font-semibold hover:underline"
204
+ href={`/${pkg.owner_github_username}`}
205
+ >
206
+ {pkg.owner_github_username}
207
+ </Link>
208
+ <span className="px-0.5 text-gray-500">/</span>
209
+ <Link
210
+ className="text-blue-500 font-semibold hover:underline"
211
+ href={`/${pkg.name}`}
212
+ >
213
+ {pkg.unscoped_name}
214
+ </Link>
215
+ {pkg.star_count !== undefined && (
216
+ <span className="ml-2 text-gray-500 text-xs flex items-center">
217
+ <Star className="w-3 h-3 mr-1" />
218
+ {pkg.star_count}
219
+ </span>
220
+ )}
221
+ <Button
222
+ variant="ghost"
223
+ size="icon"
224
+ className="h-6 w-6 ml-2"
225
+ onClick={() => openRenameDialog()}
226
+ >
227
+ <Pencil className="h-3 w-3 text-gray-700" />
228
+ </Button>
229
+ {isPrivate && (
230
+ <div className="relative group">
231
+ <LockClosedIcon className="h-3 w-3 text-gray-700" />
232
+ <span className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-1 hidden group-hover:block bg-black text-white text-xs rounded py-1 px-2">
233
+ private
234
+ </span>
235
+ </div>
236
+ )}
237
+ <Link href={`/${pkg.name}`}>
238
+ <Button variant="ghost" size="icon" className="h-6 w-6">
239
+ <OpenInNewWindowIcon className="h-3 w-3 text-gray-700" />
240
+ </Button>
241
+ </Link>
242
+ </>
243
+ )}
244
+ </div>
245
+ <div className="flex items-center space-x-1">
246
+ {!isLoggedIn && (
247
+ <div className="bg-orange-100 text-orange-700 py-1 px-2 text-xs opacity-70">
248
+ Not logged in, can't save
249
+ </div>
250
+ )}
251
+ <Button
252
+ variant="outline"
253
+ size="sm"
254
+ className={"ml-1 h-6 px-2 text-xs save-button"}
255
+ disabled={
256
+ !isLoggedIn ||
257
+ (!canSavePackage && hasManualEditsChangedFromDefault)
258
+ }
259
+ onClick={canSavePackage ? onSave : () => forkSnippet()}
260
+ >
261
+ {canSavePackage ? (
262
+ <>
263
+ <Save className="mr-1 h-3 w-3" />
264
+ Save
265
+ </>
266
+ ) : (
267
+ <>
268
+ <GitFork className="mr-1 h-3 w-3" />
269
+ Fork
270
+ </>
271
+ )}
272
+ </Button>
273
+ {isSaving && (
274
+ <div className="animate-fadeIn bg-blue-100 text-blue-800 text-xs font-medium px-2.5 py-0.5 rounded flex items-center">
275
+ <svg
276
+ className="animate-spin h-3 w-3 mr-2 text-blue-600"
277
+ xmlns="http://www.w3.org/2000/svg"
278
+ fill="none"
279
+ viewBox="0 0 24 24"
280
+ >
281
+ <circle
282
+ className="opacity-25"
283
+ cx="12"
284
+ cy="12"
285
+ r="10"
286
+ stroke="currentColor"
287
+ strokeWidth="4"
288
+ ></circle>
289
+ <path
290
+ className="opacity-75"
291
+ fill="currentColor"
292
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
293
+ ></path>
294
+ </svg>
295
+ Saving...
296
+ </div>
297
+ )}
298
+ {hasUnsavedChanges && !isSaving && isLoggedIn && (
299
+ <div className="animate-fadeIn bg-yellow-100 text-yellow-800 text-xs font-medium px-2.5 py-0.5 rounded">
300
+ {pkg ? "unsaved changes" : "unsaved"}
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+ <div className="flex items-center justify-end -space-x-1">
306
+ <div className="flex mx-2 items-center space-x-1">
307
+ {pkg && <TypeBadge type={`${packageType ?? pkg.snippet_type}`} />}
308
+ <Button
309
+ variant="ghost"
310
+ size="sm"
311
+ disabled={hasUnsavedChanges || isSaving || !pkg}
312
+ onClick={() => navigate(`/ai?snippet_id=${pkg!.package_id}`)}
313
+ >
314
+ <Sparkles className="mr-1 h-3 w-3" />
315
+ Edit with AI
316
+ </Button>
317
+ <DownloadButtonAndMenu
318
+ snippetUnscopedName={pkg?.unscoped_name}
319
+ circuitJson={circuitJson}
320
+ className="hidden md:flex"
321
+ />
322
+ <Button
323
+ variant="ghost"
324
+ size="sm"
325
+ className="hidden md:flex px-2 text-xs"
326
+ onClick={() => {
327
+ const url = encodeTextToUrlHash(code, packageType)
328
+ navigator.clipboard.writeText(url)
329
+ alert("URL copied to clipboard!")
330
+ }}
331
+ >
332
+ <Share className="mr-1 h-3 w-3" />
333
+ Copy URL
334
+ </Button>
335
+ {/* <Button
336
+ variant="ghost"
337
+ size="sm"
338
+ className="hidden md:flex px-2 text-xs"
339
+ >
340
+ <Eye className="mr-1 h-3 w-3" />
341
+ Public
342
+ </Button> */}
343
+ {pkg && (
344
+ <DropdownMenu>
345
+ <DropdownMenuTrigger asChild>
346
+ <Button variant="ghost" size="icon" className="hidden md:flex">
347
+ <MoreVertical className="h-3 w-3" />
348
+ </Button>
349
+ </DropdownMenuTrigger>
350
+ <DropdownMenuContent>
351
+ <DropdownMenuItem
352
+ className="text-xs"
353
+ onClick={() => openCreateOrderDialog()}
354
+ >
355
+ <PackageIcon className="mr-2 h-3 w-3" />
356
+ Submit Order
357
+ </DropdownMenuItem>
358
+ <DropdownMenuItem
359
+ className="text-xs"
360
+ onClick={() => openFilesDialog()}
361
+ >
362
+ <File className="mr-2 h-3 w-3" />
363
+ View Files
364
+ </DropdownMenuItem>
365
+ <DropdownMenuItem
366
+ className="text-xs"
367
+ onClick={() => openupdateDescriptionDialog()}
368
+ >
369
+ <FilePenLine className="mr-2 h-3 w-3" />
370
+ Edit Description
371
+ </DropdownMenuItem>
372
+ <DropdownMenuItem
373
+ className="text-xs"
374
+ onClick={() => openViewTsFilesDialog()}
375
+ >
376
+ <File className="mr-2 h-3 w-3" />
377
+ [Debug] View TS Files
378
+ </DropdownMenuItem>
379
+ <DropdownMenuSub>
380
+ <DropdownMenuSubTrigger
381
+ className="text-xs"
382
+ disabled={isChangingType || hasUnsavedChanges}
383
+ >
384
+ <Edit2 className="mr-2 h-3 w-3" />
385
+ {isChangingType ? "Changing..." : "Change Type"}
386
+ </DropdownMenuSubTrigger>
387
+ <DropdownMenuSubContent>
388
+ <DropdownMenuItem
389
+ className="text-xs"
390
+ disabled={currentType === "board" || isChangingType}
391
+ onClick={() => handleTypeChange("board")}
392
+ >
393
+ Board {currentType === "board" && "✓"}
394
+ </DropdownMenuItem>
395
+ <DropdownMenuItem
396
+ className="text-xs"
397
+ disabled={currentType === "package" || isChangingType}
398
+ onClick={() => handleTypeChange("package")}
399
+ >
400
+ Module {currentType === "package" && "✓"}
401
+ </DropdownMenuItem>
402
+ </DropdownMenuSubContent>
403
+ </DropdownMenuSub>
404
+ <DropdownMenuSub>
405
+ <DropdownMenuSubTrigger className="text-xs">
406
+ <Edit2 className="mr-2 h-3 w-3" />
407
+ Change Package Visibility
408
+ </DropdownMenuSubTrigger>
409
+ <DropdownMenuSubContent>
410
+ <DropdownMenuItem
411
+ className="text-xs"
412
+ disabled={isPrivate}
413
+ onClick={() => updatePackageVisibilityToPrivate(true)}
414
+ >
415
+ Private {isPrivate && "✓"}
416
+ </DropdownMenuItem>
417
+ <DropdownMenuItem
418
+ className="text-xs"
419
+ disabled={!isPrivate}
420
+ onClick={() => updatePackageVisibilityToPrivate(false)}
421
+ >
422
+ Public {!isPrivate && "✓"}
423
+ </DropdownMenuItem>
424
+ </DropdownMenuSubContent>
425
+ </DropdownMenuSub>
426
+ <DropdownMenuItem
427
+ className="text-xs text-red-600"
428
+ onClick={() => openDeleteDialog()}
429
+ >
430
+ <Trash2 className="mr-2 h-3 w-3" />
431
+ Delete Snippet
432
+ </DropdownMenuItem>
433
+ <DropdownMenuItem className="text-xs text-gray-500" disabled>
434
+ @tscircuit/core@{tscircuitCorePkg.version}
435
+ </DropdownMenuItem>
436
+ </DropdownMenuContent>
437
+ </DropdownMenu>
438
+ )}
439
+ <Button
440
+ variant="ghost"
441
+ size="icon"
442
+ className={cn(
443
+ "hidden md:flex",
444
+ !previewOpen
445
+ ? "bg-blue-600 text-white hover:bg-blue-700 hover:text-white"
446
+ : "",
447
+ )}
448
+ onClick={() => onTogglePreview()}
449
+ >
450
+ {previewOpen ? (
451
+ <Sidebar className="h-3 w-3" />
452
+ ) : (
453
+ <EyeIcon className="h-3 w-3" />
454
+ )}
455
+ </Button>
456
+ </div>
457
+ <div className="flex items-center ">
458
+ <DropdownMenu>
459
+ <DropdownMenuTrigger asChild>
460
+ <div className="md:hidden rounded-full p-1 hover:bg-gray-100 cursor-pointer">
461
+ <Button className="md:hidden" variant="secondary" size="sm">
462
+ <ChevronDown className="h-4 w-4" />
463
+ </Button>
464
+ </div>
465
+ </DropdownMenuTrigger>
466
+ <DropdownMenuContent>
467
+ <DropdownMenuItem className="text-xs">
468
+ <Download className="mr-1 h-3 w-3" />
469
+ Download
470
+ </DropdownMenuItem>
471
+ <DropdownMenuItem className="text-xs">
472
+ <Share className="mr-1 h-3 w-3" />
473
+ Copy URL
474
+ </DropdownMenuItem>
475
+ <DropdownMenuItem className="text-xs">
476
+ <Eye className="mr-1 h-3 w-3" />
477
+ Public
478
+ </DropdownMenuItem>
479
+ </DropdownMenuContent>
480
+ </DropdownMenu>
481
+ <Button
482
+ variant="ghost"
483
+ size="sm"
484
+ className="md:hidden"
485
+ onClick={() => onTogglePreview()}
486
+ >
487
+ {previewOpen ? (
488
+ <div className="flex items-center">
489
+ <CodeIcon className="h-3 w-3 mr-1" />
490
+ Show Code
491
+ </div>
492
+ ) : (
493
+ <div className="flex items-center">
494
+ <EyeIcon className="h-3 w-3 mr-1" />
495
+ Show Preview
496
+ </div>
497
+ )}
498
+ </Button>
499
+ </div>
500
+ </div>
501
+ <UpdateDescriptionDialog
502
+ packageId={pkg?.package_id ?? ""}
503
+ currentDescription={pkg?.description ?? ""}
504
+ />
505
+ <RenameDialog
506
+ packageId={pkg?.package_id ?? ""}
507
+ currentName={pkg?.unscoped_name ?? ""}
508
+ />
509
+ <DeleteDialog
510
+ packageId={pkg?.package_id ?? ""}
511
+ packageName={pkg?.unscoped_name ?? ""}
512
+ />
513
+ <CreateOrderDialog />
514
+ <FilesDialog snippetId={pkg?.package_id ?? ""} />
515
+ <ViewTsFilesDialog />
516
+ </nav>
517
+ )
518
+ }