@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.
- package/bun.lock +335 -15
- package/dist/bundle.js +7 -1
- package/fake-snippets-api/routes/api/package_files/create_or_update.ts +9 -0
- package/fake-snippets-api/routes/api/packages/update.ts +1 -0
- package/package.json +5 -5
- package/src/App.tsx +8 -0
- package/src/components/CodeEditor.tsx +5 -3
- package/src/components/CodeEditorHeader.tsx +1 -1
- package/src/components/DownloadButtonAndMenu.tsx +3 -2
- package/src/components/EditorNav.tsx +3 -3
- package/src/components/FileSidebar.tsx +84 -0
- package/src/components/dialogs/rename-package-dialog.tsx +81 -0
- package/src/components/dialogs/update-package-description-dialog.tsx +96 -0
- package/src/components/package-port/CodeAndPreview.tsx +417 -0
- package/src/components/package-port/CodeEditor.tsx +510 -0
- package/src/components/package-port/CodeEditorHeader.tsx +153 -0
- package/src/components/package-port/EditorNav.tsx +518 -0
- package/src/components/ui/tree-view.tsx +490 -0
- package/src/hooks/use-package.ts +23 -0
- package/src/hooks/useForkPackageMutation.ts +49 -0
- package/src/hooks/usePackageFilesLoader.ts +56 -0
- package/src/hooks/useUpdatePackageFilesMutation.ts +86 -0
- package/src/hooks/useUpdatePackageMutation.ts +63 -0
- package/src/lib/utils/checkIfManualEditsImported.ts +1 -1
- package/src/pages/package-editor.tsx +44 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
3
|
+
import { ChevronRight } from "lucide-react"
|
|
4
|
+
import { cva } from "class-variance-authority"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const treeVariants = cva(
|
|
8
|
+
"group hover:before:opacity-100 before:absolute before:rounded-lg before:left-0 before:w-full before:opacity-0 before:bg-slate-100/70 before:h-[2rem] before:-z-10' dark:before:bg-slate-800/70",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const selectedTreeVariants = cva(
|
|
12
|
+
"before:opacity-100 before:bg-slate-100/70 text-accent-foreground' dark:before:bg-slate-800/70",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
const dragOverVariants = cva(
|
|
16
|
+
"before:opacity-100 before:bg-slate-900/20 text-primary-foreground' dark:before:bg-slate-50/20",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
interface TreeDataItem {
|
|
20
|
+
id: string
|
|
21
|
+
name: string
|
|
22
|
+
icon?: any
|
|
23
|
+
selectedIcon?: any
|
|
24
|
+
openIcon?: any
|
|
25
|
+
children?: TreeDataItem[]
|
|
26
|
+
actions?: React.ReactNode
|
|
27
|
+
onClick?: () => void
|
|
28
|
+
draggable?: boolean
|
|
29
|
+
droppable?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type TreeProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
33
|
+
data: TreeDataItem[] | TreeDataItem
|
|
34
|
+
initialSelectedItemId?: string
|
|
35
|
+
onSelectChange?: (item: TreeDataItem | undefined) => void
|
|
36
|
+
expandAll?: boolean
|
|
37
|
+
defaultNodeIcon?: any
|
|
38
|
+
defaultLeafIcon?: any
|
|
39
|
+
onDocumentDrag?: (sourceItem: TreeDataItem, targetItem: TreeDataItem) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TreeView = React.forwardRef<HTMLDivElement, TreeProps>(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
data,
|
|
46
|
+
initialSelectedItemId,
|
|
47
|
+
onSelectChange,
|
|
48
|
+
expandAll,
|
|
49
|
+
defaultLeafIcon,
|
|
50
|
+
defaultNodeIcon,
|
|
51
|
+
className,
|
|
52
|
+
onDocumentDrag,
|
|
53
|
+
...props
|
|
54
|
+
},
|
|
55
|
+
ref,
|
|
56
|
+
) => {
|
|
57
|
+
const [selectedItemId, setSelectedItemId] = React.useState<
|
|
58
|
+
string | undefined
|
|
59
|
+
>(initialSelectedItemId)
|
|
60
|
+
|
|
61
|
+
const [draggedItem, setDraggedItem] = React.useState<TreeDataItem | null>(
|
|
62
|
+
null,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
const handleSelectChange = React.useCallback(
|
|
66
|
+
(item: TreeDataItem | undefined) => {
|
|
67
|
+
setSelectedItemId(item?.id)
|
|
68
|
+
if (onSelectChange) {
|
|
69
|
+
onSelectChange(item)
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
[onSelectChange],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const handleDragStart = React.useCallback((item: TreeDataItem) => {
|
|
76
|
+
setDraggedItem(item)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const handleDrop = React.useCallback(
|
|
80
|
+
(targetItem: TreeDataItem) => {
|
|
81
|
+
if (draggedItem && onDocumentDrag && draggedItem.id !== targetItem.id) {
|
|
82
|
+
onDocumentDrag(draggedItem, targetItem)
|
|
83
|
+
}
|
|
84
|
+
setDraggedItem(null)
|
|
85
|
+
},
|
|
86
|
+
[draggedItem, onDocumentDrag],
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const expandedItemIds = React.useMemo(() => {
|
|
90
|
+
if (!initialSelectedItemId) {
|
|
91
|
+
return [] as string[]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const ids: string[] = []
|
|
95
|
+
|
|
96
|
+
function walkTreeItems(
|
|
97
|
+
items: TreeDataItem[] | TreeDataItem,
|
|
98
|
+
targetId: string,
|
|
99
|
+
) {
|
|
100
|
+
if (items instanceof Array) {
|
|
101
|
+
for (let i = 0; i < items.length; i++) {
|
|
102
|
+
ids.push(items[i]!.id)
|
|
103
|
+
if (walkTreeItems(items[i]!, targetId) && !expandAll) {
|
|
104
|
+
return true
|
|
105
|
+
}
|
|
106
|
+
if (!expandAll) ids.pop()
|
|
107
|
+
}
|
|
108
|
+
} else if (!expandAll && items.id === targetId) {
|
|
109
|
+
return true
|
|
110
|
+
} else if (items.children) {
|
|
111
|
+
return walkTreeItems(items.children, targetId)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
walkTreeItems(data, initialSelectedItemId)
|
|
116
|
+
return ids
|
|
117
|
+
}, [data, expandAll, initialSelectedItemId])
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div className={cn("overflow-hidden relative p-2", className)}>
|
|
121
|
+
<TreeItem
|
|
122
|
+
data={data}
|
|
123
|
+
ref={ref}
|
|
124
|
+
selectedItemId={selectedItemId}
|
|
125
|
+
handleSelectChange={handleSelectChange}
|
|
126
|
+
expandedItemIds={expandedItemIds}
|
|
127
|
+
defaultLeafIcon={defaultLeafIcon}
|
|
128
|
+
defaultNodeIcon={defaultNodeIcon}
|
|
129
|
+
handleDragStart={handleDragStart}
|
|
130
|
+
handleDrop={handleDrop}
|
|
131
|
+
draggedItem={draggedItem}
|
|
132
|
+
{...props}
|
|
133
|
+
/>
|
|
134
|
+
<div
|
|
135
|
+
className="w-full h-[48px]"
|
|
136
|
+
onDrop={(e) => {
|
|
137
|
+
handleDrop({ id: "", name: "parent_div" })
|
|
138
|
+
}}
|
|
139
|
+
></div>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
},
|
|
143
|
+
)
|
|
144
|
+
TreeView.displayName = "TreeView"
|
|
145
|
+
|
|
146
|
+
type TreeItemProps = TreeProps & {
|
|
147
|
+
selectedItemId?: string
|
|
148
|
+
handleSelectChange: (item: TreeDataItem | undefined) => void
|
|
149
|
+
expandedItemIds: string[]
|
|
150
|
+
defaultNodeIcon?: any
|
|
151
|
+
defaultLeafIcon?: any
|
|
152
|
+
handleDragStart?: (item: TreeDataItem) => void
|
|
153
|
+
handleDrop?: (item: TreeDataItem) => void
|
|
154
|
+
draggedItem: TreeDataItem | null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const TreeItem = React.forwardRef<HTMLDivElement, TreeItemProps>(
|
|
158
|
+
(
|
|
159
|
+
{
|
|
160
|
+
className,
|
|
161
|
+
data,
|
|
162
|
+
selectedItemId,
|
|
163
|
+
handleSelectChange,
|
|
164
|
+
expandedItemIds,
|
|
165
|
+
defaultNodeIcon,
|
|
166
|
+
defaultLeafIcon,
|
|
167
|
+
handleDragStart,
|
|
168
|
+
handleDrop,
|
|
169
|
+
draggedItem,
|
|
170
|
+
...props
|
|
171
|
+
},
|
|
172
|
+
ref,
|
|
173
|
+
) => {
|
|
174
|
+
if (!(data instanceof Array)) {
|
|
175
|
+
data = [data]
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
<div ref={ref} role="tree" className={className} {...props}>
|
|
179
|
+
<ul>
|
|
180
|
+
{data.map((item) => (
|
|
181
|
+
<li key={item.id}>
|
|
182
|
+
{item.children ? (
|
|
183
|
+
<TreeNode
|
|
184
|
+
item={item}
|
|
185
|
+
selectedItemId={selectedItemId}
|
|
186
|
+
expandedItemIds={expandedItemIds}
|
|
187
|
+
handleSelectChange={handleSelectChange}
|
|
188
|
+
defaultNodeIcon={defaultNodeIcon}
|
|
189
|
+
defaultLeafIcon={defaultLeafIcon}
|
|
190
|
+
handleDragStart={handleDragStart}
|
|
191
|
+
handleDrop={handleDrop}
|
|
192
|
+
draggedItem={draggedItem}
|
|
193
|
+
/>
|
|
194
|
+
) : (
|
|
195
|
+
<TreeLeaf
|
|
196
|
+
item={item}
|
|
197
|
+
selectedItemId={selectedItemId}
|
|
198
|
+
handleSelectChange={handleSelectChange}
|
|
199
|
+
defaultLeafIcon={defaultLeafIcon}
|
|
200
|
+
handleDragStart={handleDragStart}
|
|
201
|
+
handleDrop={handleDrop}
|
|
202
|
+
draggedItem={draggedItem}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
</li>
|
|
206
|
+
))}
|
|
207
|
+
</ul>
|
|
208
|
+
</div>
|
|
209
|
+
)
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
TreeItem.displayName = "TreeItem"
|
|
213
|
+
|
|
214
|
+
const TreeNode = ({
|
|
215
|
+
item,
|
|
216
|
+
handleSelectChange,
|
|
217
|
+
expandedItemIds,
|
|
218
|
+
selectedItemId,
|
|
219
|
+
defaultNodeIcon,
|
|
220
|
+
defaultLeafIcon,
|
|
221
|
+
handleDragStart,
|
|
222
|
+
handleDrop,
|
|
223
|
+
draggedItem,
|
|
224
|
+
}: {
|
|
225
|
+
item: TreeDataItem
|
|
226
|
+
handleSelectChange: (item: TreeDataItem | undefined) => void
|
|
227
|
+
expandedItemIds: string[]
|
|
228
|
+
selectedItemId?: string
|
|
229
|
+
defaultNodeIcon?: any
|
|
230
|
+
defaultLeafIcon?: any
|
|
231
|
+
handleDragStart?: (item: TreeDataItem) => void
|
|
232
|
+
handleDrop?: (item: TreeDataItem) => void
|
|
233
|
+
draggedItem: TreeDataItem | null
|
|
234
|
+
}) => {
|
|
235
|
+
const [value, setValue] = React.useState(
|
|
236
|
+
expandedItemIds.includes(item.id) ? [item.id] : [],
|
|
237
|
+
)
|
|
238
|
+
const [isDragOver, setIsDragOver] = React.useState(false)
|
|
239
|
+
|
|
240
|
+
const onDragStart = (e: React.DragEvent) => {
|
|
241
|
+
if (!item.draggable) {
|
|
242
|
+
e.preventDefault()
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
e.dataTransfer.setData("text/plain", item.id)
|
|
246
|
+
handleDragStart?.(item)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const onDragOver = (e: React.DragEvent) => {
|
|
250
|
+
if (item.droppable !== false && draggedItem && draggedItem.id !== item.id) {
|
|
251
|
+
e.preventDefault()
|
|
252
|
+
setIsDragOver(true)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const onDragLeave = () => {
|
|
257
|
+
setIsDragOver(false)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const onDrop = (e: React.DragEvent) => {
|
|
261
|
+
e.preventDefault()
|
|
262
|
+
setIsDragOver(false)
|
|
263
|
+
handleDrop?.(item)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<AccordionPrimitive.Root
|
|
268
|
+
type="multiple"
|
|
269
|
+
value={value}
|
|
270
|
+
onValueChange={(s) => setValue(s)}
|
|
271
|
+
>
|
|
272
|
+
<AccordionPrimitive.Item value={item.id}>
|
|
273
|
+
<AccordionTrigger
|
|
274
|
+
className={cn(
|
|
275
|
+
treeVariants(),
|
|
276
|
+
selectedItemId === item.id && selectedTreeVariants(),
|
|
277
|
+
isDragOver && dragOverVariants(),
|
|
278
|
+
)}
|
|
279
|
+
onClick={() => {
|
|
280
|
+
handleSelectChange(item)
|
|
281
|
+
item.onClick?.()
|
|
282
|
+
}}
|
|
283
|
+
draggable={!!item.draggable}
|
|
284
|
+
onDragStart={onDragStart}
|
|
285
|
+
onDragOver={onDragOver}
|
|
286
|
+
onDragLeave={onDragLeave}
|
|
287
|
+
onDrop={onDrop}
|
|
288
|
+
>
|
|
289
|
+
<TreeIcon
|
|
290
|
+
item={item}
|
|
291
|
+
isSelected={selectedItemId === item.id}
|
|
292
|
+
isOpen={value.includes(item.id)}
|
|
293
|
+
default={defaultNodeIcon}
|
|
294
|
+
/>
|
|
295
|
+
<span className="text-sm truncate">{item.name}</span>
|
|
296
|
+
<TreeActions isSelected={selectedItemId === item.id}>
|
|
297
|
+
{item.actions}
|
|
298
|
+
</TreeActions>
|
|
299
|
+
</AccordionTrigger>
|
|
300
|
+
<AccordionContent className="ml-4 pl-1 border-l">
|
|
301
|
+
<TreeItem
|
|
302
|
+
data={item.children ? item.children : item}
|
|
303
|
+
selectedItemId={selectedItemId}
|
|
304
|
+
handleSelectChange={handleSelectChange}
|
|
305
|
+
expandedItemIds={expandedItemIds}
|
|
306
|
+
defaultLeafIcon={defaultLeafIcon}
|
|
307
|
+
defaultNodeIcon={defaultNodeIcon}
|
|
308
|
+
handleDragStart={handleDragStart}
|
|
309
|
+
handleDrop={handleDrop}
|
|
310
|
+
draggedItem={draggedItem}
|
|
311
|
+
/>
|
|
312
|
+
</AccordionContent>
|
|
313
|
+
</AccordionPrimitive.Item>
|
|
314
|
+
</AccordionPrimitive.Root>
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const TreeLeaf = React.forwardRef<
|
|
319
|
+
HTMLDivElement,
|
|
320
|
+
React.HTMLAttributes<HTMLDivElement> & {
|
|
321
|
+
item: TreeDataItem
|
|
322
|
+
selectedItemId?: string
|
|
323
|
+
handleSelectChange: (item: TreeDataItem | undefined) => void
|
|
324
|
+
defaultLeafIcon?: any
|
|
325
|
+
handleDragStart?: (item: TreeDataItem) => void
|
|
326
|
+
handleDrop?: (item: TreeDataItem) => void
|
|
327
|
+
draggedItem: TreeDataItem | null
|
|
328
|
+
}
|
|
329
|
+
>(
|
|
330
|
+
(
|
|
331
|
+
{
|
|
332
|
+
className,
|
|
333
|
+
item,
|
|
334
|
+
selectedItemId,
|
|
335
|
+
handleSelectChange,
|
|
336
|
+
defaultLeafIcon,
|
|
337
|
+
handleDragStart,
|
|
338
|
+
handleDrop,
|
|
339
|
+
draggedItem,
|
|
340
|
+
...props
|
|
341
|
+
},
|
|
342
|
+
ref,
|
|
343
|
+
) => {
|
|
344
|
+
const [isDragOver, setIsDragOver] = React.useState(false)
|
|
345
|
+
|
|
346
|
+
const onDragStart = (e: React.DragEvent) => {
|
|
347
|
+
if (!item.draggable) {
|
|
348
|
+
e.preventDefault()
|
|
349
|
+
return
|
|
350
|
+
}
|
|
351
|
+
e.dataTransfer.setData("text/plain", item.id)
|
|
352
|
+
handleDragStart?.(item)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const onDragOver = (e: React.DragEvent) => {
|
|
356
|
+
if (
|
|
357
|
+
item.droppable !== false &&
|
|
358
|
+
draggedItem &&
|
|
359
|
+
draggedItem.id !== item.id
|
|
360
|
+
) {
|
|
361
|
+
e.preventDefault()
|
|
362
|
+
setIsDragOver(true)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const onDragLeave = () => {
|
|
367
|
+
setIsDragOver(false)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const onDrop = (e: React.DragEvent) => {
|
|
371
|
+
e.preventDefault()
|
|
372
|
+
setIsDragOver(false)
|
|
373
|
+
handleDrop?.(item)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<div
|
|
378
|
+
ref={ref}
|
|
379
|
+
className={cn(
|
|
380
|
+
"ml-[2px] flex text-left items-center py-2 cursor-pointer before:right-1",
|
|
381
|
+
treeVariants(),
|
|
382
|
+
className,
|
|
383
|
+
selectedItemId === item.id && selectedTreeVariants(),
|
|
384
|
+
isDragOver && dragOverVariants(),
|
|
385
|
+
)}
|
|
386
|
+
onClick={() => {
|
|
387
|
+
handleSelectChange(item)
|
|
388
|
+
item.onClick?.()
|
|
389
|
+
}}
|
|
390
|
+
draggable={!!item.draggable}
|
|
391
|
+
onDragStart={onDragStart}
|
|
392
|
+
onDragOver={onDragOver}
|
|
393
|
+
onDragLeave={onDragLeave}
|
|
394
|
+
onDrop={onDrop}
|
|
395
|
+
{...props}
|
|
396
|
+
>
|
|
397
|
+
<TreeIcon
|
|
398
|
+
item={item}
|
|
399
|
+
isSelected={selectedItemId === item.id}
|
|
400
|
+
default={defaultLeafIcon}
|
|
401
|
+
/>
|
|
402
|
+
<span className="flex-grow text-sm truncate">{item.name}</span>
|
|
403
|
+
<TreeActions isSelected={selectedItemId === item.id}>
|
|
404
|
+
{item.actions}
|
|
405
|
+
</TreeActions>
|
|
406
|
+
</div>
|
|
407
|
+
)
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
TreeLeaf.displayName = "TreeLeaf"
|
|
411
|
+
|
|
412
|
+
const AccordionTrigger = React.forwardRef<
|
|
413
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
414
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
415
|
+
>(({ className, children, ...props }, ref) => (
|
|
416
|
+
<AccordionPrimitive.Header>
|
|
417
|
+
<AccordionPrimitive.Trigger
|
|
418
|
+
ref={ref}
|
|
419
|
+
className={cn(
|
|
420
|
+
"flex flex-1 w-full items-center py-2 transition-all first:[&[data-state=open]>svg]:rotate-90",
|
|
421
|
+
className,
|
|
422
|
+
)}
|
|
423
|
+
{...props}
|
|
424
|
+
>
|
|
425
|
+
<ChevronRight className="h-4 w-4 shrink-0 transition-transform duration-200 text-slate-900/50 mr-1 dark:text-slate-50/50" />
|
|
426
|
+
{children}
|
|
427
|
+
</AccordionPrimitive.Trigger>
|
|
428
|
+
</AccordionPrimitive.Header>
|
|
429
|
+
))
|
|
430
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
431
|
+
|
|
432
|
+
const AccordionContent = React.forwardRef<
|
|
433
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
434
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
435
|
+
>(({ className, children, ...props }, ref) => (
|
|
436
|
+
<AccordionPrimitive.Content
|
|
437
|
+
ref={ref}
|
|
438
|
+
className={cn(
|
|
439
|
+
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
|
|
440
|
+
className,
|
|
441
|
+
)}
|
|
442
|
+
{...props}
|
|
443
|
+
>
|
|
444
|
+
<div className="pb-1 pt-0">{children}</div>
|
|
445
|
+
</AccordionPrimitive.Content>
|
|
446
|
+
))
|
|
447
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
448
|
+
|
|
449
|
+
const TreeIcon = ({
|
|
450
|
+
item,
|
|
451
|
+
isOpen,
|
|
452
|
+
isSelected,
|
|
453
|
+
default: defaultIcon,
|
|
454
|
+
}: {
|
|
455
|
+
item: TreeDataItem
|
|
456
|
+
isOpen?: boolean
|
|
457
|
+
isSelected?: boolean
|
|
458
|
+
default?: any
|
|
459
|
+
}) => {
|
|
460
|
+
let Icon = defaultIcon
|
|
461
|
+
if (isSelected && item.selectedIcon) {
|
|
462
|
+
Icon = item.selectedIcon
|
|
463
|
+
} else if (isOpen && item.openIcon) {
|
|
464
|
+
Icon = item.openIcon
|
|
465
|
+
} else if (item.icon) {
|
|
466
|
+
Icon = item.icon
|
|
467
|
+
}
|
|
468
|
+
return Icon ? <Icon className="h-4 w-4 shrink-0 mr-2" /> : <></>
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const TreeActions = ({
|
|
472
|
+
children,
|
|
473
|
+
isSelected,
|
|
474
|
+
}: {
|
|
475
|
+
children: React.ReactNode
|
|
476
|
+
isSelected: boolean
|
|
477
|
+
}) => {
|
|
478
|
+
return (
|
|
479
|
+
<div
|
|
480
|
+
className={cn(
|
|
481
|
+
isSelected ? "block" : "hidden",
|
|
482
|
+
"absolute right-3 group-hover:block",
|
|
483
|
+
)}
|
|
484
|
+
>
|
|
485
|
+
{children}
|
|
486
|
+
</div>
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export { TreeView, type TreeDataItem }
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useQuery } from "react-query"
|
|
2
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
3
|
+
import type { Package, Snippet } from "fake-snippets-api/lib/db/schema"
|
|
4
|
+
|
|
5
|
+
export const usePackage = (packageId: string | null) => {
|
|
6
|
+
const axios = useAxios()
|
|
7
|
+
return useQuery<Package, Error & { status: number }>(
|
|
8
|
+
["packages", packageId],
|
|
9
|
+
async () => {
|
|
10
|
+
if (!packageId) {
|
|
11
|
+
throw new Error("Package ID is required")
|
|
12
|
+
}
|
|
13
|
+
const { data } = await axios.get("/packages/get", {
|
|
14
|
+
params: { package_id: packageId },
|
|
15
|
+
})
|
|
16
|
+
return data.package
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
enabled: Boolean(packageId),
|
|
20
|
+
retry: false,
|
|
21
|
+
},
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useMutation } from "react-query"
|
|
2
|
+
import { useGlobalStore } from "@/hooks/use-global-store"
|
|
3
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
4
|
+
import { useToast } from "@/hooks/use-toast"
|
|
5
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
6
|
+
|
|
7
|
+
export const useForkPackageMutation = ({
|
|
8
|
+
pkg,
|
|
9
|
+
currentCode,
|
|
10
|
+
onSuccess,
|
|
11
|
+
}: {
|
|
12
|
+
pkg: Package
|
|
13
|
+
currentCode?: string
|
|
14
|
+
onSuccess?: (forkedSnippet: Package) => void
|
|
15
|
+
}) => {
|
|
16
|
+
const axios = useAxios()
|
|
17
|
+
const session = useGlobalStore((s) => s.session)
|
|
18
|
+
const { toast } = useToast()
|
|
19
|
+
|
|
20
|
+
return useMutation(
|
|
21
|
+
["createForkSnippet"],
|
|
22
|
+
async () => {
|
|
23
|
+
if (!session) throw new Error("No session")
|
|
24
|
+
if (!pkg) throw new Error("No package to fork")
|
|
25
|
+
|
|
26
|
+
const { data } = await axios.post("/packages/fork", {
|
|
27
|
+
package_id: pkg?.package_id,
|
|
28
|
+
})
|
|
29
|
+
return data.package
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
onSuccess: (forkedPkg: Package) => {
|
|
33
|
+
toast({
|
|
34
|
+
title: `Forked Package`,
|
|
35
|
+
description: `You have successfully forked the package. Redirecting...`,
|
|
36
|
+
})
|
|
37
|
+
onSuccess?.(forkedPkg)
|
|
38
|
+
},
|
|
39
|
+
onError: (error: any) => {
|
|
40
|
+
console.error("Error forking package:", error)
|
|
41
|
+
toast({
|
|
42
|
+
title: "Error",
|
|
43
|
+
description: "Failed to fork package. Please try again.",
|
|
44
|
+
variant: "destructive",
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useQueries } from "react-query"
|
|
2
|
+
import { useAxios } from "./use-axios"
|
|
3
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
4
|
+
import { usePackageFileById, usePackageFiles } from "./use-package-files"
|
|
5
|
+
|
|
6
|
+
export interface PackageFile {
|
|
7
|
+
path: string
|
|
8
|
+
content: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function usePackageFilesLoader(pkg?: Package) {
|
|
12
|
+
const axios = useAxios()
|
|
13
|
+
const pkgFiles = usePackageFiles(pkg?.latest_package_release_id)
|
|
14
|
+
const indexFileFromHook = usePackageFileById(
|
|
15
|
+
pkgFiles.data?.find((x) => x.file_path === "index.tsx")?.package_file_id ??
|
|
16
|
+
null,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const queries = useQueries(
|
|
20
|
+
pkgFiles.data?.map((file) => ({
|
|
21
|
+
queryKey: ["packageFile", file.package_file_id],
|
|
22
|
+
queryFn: async () => {
|
|
23
|
+
if (
|
|
24
|
+
file.file_path === "index.tsx" &&
|
|
25
|
+
indexFileFromHook.data?.content_text
|
|
26
|
+
) {
|
|
27
|
+
return {
|
|
28
|
+
path: file.file_path,
|
|
29
|
+
content: indexFileFromHook.data.content_text,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const response = await axios.post(`/package_files/get`, {
|
|
34
|
+
package_file_id: file.package_file_id,
|
|
35
|
+
})
|
|
36
|
+
const content = response.data.package_file?.content_text ?? ""
|
|
37
|
+
return content ? { path: file.file_path, content } : null
|
|
38
|
+
},
|
|
39
|
+
staleTime: 2,
|
|
40
|
+
})) ?? [],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const isLoading = queries.some((query) => query.isLoading)
|
|
44
|
+
const error = queries.find((query) => query.error)?.error
|
|
45
|
+
|
|
46
|
+
const processedResults = queries
|
|
47
|
+
.map((query) => query.data)
|
|
48
|
+
.filter((x): x is PackageFile => x !== null)
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isLoading,
|
|
53
|
+
error,
|
|
54
|
+
data: processedResults,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useMutation } from "react-query"
|
|
2
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
3
|
+
|
|
4
|
+
interface PackageFile {
|
|
5
|
+
path: string
|
|
6
|
+
content: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface UseUpdatePackageFilesMutationProps {
|
|
10
|
+
pkg: Package | undefined
|
|
11
|
+
pkgFilesWithContent: PackageFile[]
|
|
12
|
+
initialFilesLoad: PackageFile[]
|
|
13
|
+
pkgFiles: any
|
|
14
|
+
axios: any
|
|
15
|
+
toast: any
|
|
16
|
+
loadPkgFiles: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useUpdatePackageFilesMutation({
|
|
20
|
+
pkg,
|
|
21
|
+
pkgFilesWithContent,
|
|
22
|
+
initialFilesLoad,
|
|
23
|
+
pkgFiles,
|
|
24
|
+
axios,
|
|
25
|
+
toast,
|
|
26
|
+
loadPkgFiles,
|
|
27
|
+
}: UseUpdatePackageFilesMutationProps) {
|
|
28
|
+
return useMutation({
|
|
29
|
+
mutationFn: async (
|
|
30
|
+
newpackage: Pick<Package, "package_id" | "name"> & {
|
|
31
|
+
package_name_with_version: string
|
|
32
|
+
},
|
|
33
|
+
) => {
|
|
34
|
+
if (pkg) {
|
|
35
|
+
newpackage = { ...pkg, ...newpackage }
|
|
36
|
+
}
|
|
37
|
+
if (!newpackage) throw new Error("No package to update")
|
|
38
|
+
|
|
39
|
+
let updatedFilesCount = 0
|
|
40
|
+
|
|
41
|
+
for (const file of pkgFilesWithContent) {
|
|
42
|
+
const initialFile = initialFilesLoad.find((x) => x.path === file.path)
|
|
43
|
+
if (file.content && file.content !== initialFile?.content) {
|
|
44
|
+
const updatePkgFilePayload = {
|
|
45
|
+
package_file_id:
|
|
46
|
+
pkgFiles.data?.find((x: any) => x.file_path === file.path)
|
|
47
|
+
?.package_file_id ?? null,
|
|
48
|
+
content_text: file.content,
|
|
49
|
+
file_path: file.path,
|
|
50
|
+
package_name_with_version: `${newpackage.name}`,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await axios.post(
|
|
54
|
+
"/package_files/create_or_update",
|
|
55
|
+
updatePkgFilePayload,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (response.status === 200) {
|
|
59
|
+
updatedFilesCount++
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return updatedFilesCount
|
|
64
|
+
},
|
|
65
|
+
onSuccess: (updatedFilesCount) => {
|
|
66
|
+
if (updatedFilesCount) {
|
|
67
|
+
toast({
|
|
68
|
+
title: `Package's ${updatedFilesCount} files saved`,
|
|
69
|
+
description: "Your changes have been saved successfully.",
|
|
70
|
+
})
|
|
71
|
+
loadPkgFiles()
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
onError: (error: any) => {
|
|
75
|
+
console.error("Error updating pkg files:", error)
|
|
76
|
+
toast({
|
|
77
|
+
title: "Error",
|
|
78
|
+
description:
|
|
79
|
+
error instanceof Error
|
|
80
|
+
? error.message
|
|
81
|
+
: "Failed to update package files. Please try again.",
|
|
82
|
+
variant: "destructive",
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
}
|