@tscircuit/fake-snippets 0.0.88 → 0.0.90
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/api/generated-index.js +96 -14
- package/bun-tests/fake-snippets-api/routes/proxy.test.ts +42 -0
- package/bun.lock +196 -215
- package/dist/bundle.js +596 -370
- package/fake-snippets-api/routes/api/autocomplete/create_autocomplete.ts +134 -0
- package/fake-snippets-api/routes/api/proxy.ts +128 -0
- package/package.json +59 -48
- package/renovate.json +2 -1
- package/src/App.tsx +67 -3
- package/src/ContextProviders.tsx +2 -0
- package/src/build-watcher.ts +52 -0
- package/src/components/CircuitJsonImportDialog.tsx +1 -1
- package/src/components/CmdKMenu.tsx +533 -197
- package/src/components/DownloadButtonAndMenu.tsx +104 -26
- package/src/components/FileSidebar.tsx +11 -1
- package/src/components/Header2.tsx +7 -2
- package/src/components/PackageBuildsPage/LogContent.tsx +25 -22
- package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +6 -6
- package/src/components/PackageBuildsPage/build-preview-content.tsx +5 -5
- package/src/components/PackageBuildsPage/package-build-details-panel.tsx +15 -13
- package/src/components/PackageBuildsPage/package-build-header.tsx +19 -28
- package/src/components/PackageCard.tsx +66 -16
- package/src/components/SearchComponent.tsx +2 -2
- package/src/components/SuspenseRunFrame.tsx +14 -2
- package/src/components/ViewPackagePage/components/important-files-view.tsx +90 -17
- package/src/components/ViewPackagePage/components/main-content-header.tsx +26 -2
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +35 -30
- package/src/components/ViewPackagePage/components/sidebar-about-section.tsx +2 -2
- package/src/components/ViewPackagePage/components/sidebar-releases-section.tsx +20 -12
- package/src/components/ViewPackagePage/components/tab-views/files-view.tsx +0 -7
- package/src/components/ViewPackagePage/utils/fuzz-search.ts +121 -0
- package/src/components/ViewPackagePage/utils/is-hidden-file.ts +4 -0
- package/src/components/dialogs/confirm-delete-package-dialog.tsx +1 -1
- package/src/components/dialogs/confirm-discard-changes-dialog.tsx +73 -0
- package/src/components/dialogs/edit-package-details-dialog.tsx +2 -2
- package/src/components/dialogs/view-ts-files-dialog.tsx +478 -42
- package/src/components/package-port/CodeAndPreview.tsx +17 -16
- package/src/components/package-port/CodeEditor.tsx +138 -17
- package/src/components/package-port/CodeEditorHeader.tsx +44 -4
- package/src/components/package-port/EditorNav.tsx +42 -29
- package/src/components/package-port/GlobalFindReplace.tsx +681 -0
- package/src/components/package-port/QuickOpen.tsx +241 -0
- package/src/components/ui/dialog.tsx +1 -1
- package/src/components/ui/tree-view.tsx +1 -1
- package/src/global.d.ts +3 -0
- package/src/hooks/use-ai-review.ts +31 -0
- package/src/hooks/use-code-completion-ai-api.ts +3 -3
- package/src/hooks/use-current-package-release.ts +5 -1
- package/src/hooks/use-delete-package.ts +6 -2
- package/src/hooks/use-download-zip.ts +50 -0
- package/src/hooks/use-hotkey.ts +116 -0
- package/src/hooks/use-package-by-package-id.ts +1 -0
- package/src/hooks/use-package-by-package-name.ts +1 -0
- package/src/hooks/use-package-files.ts +3 -0
- package/src/hooks/use-package-release.ts +5 -1
- package/src/hooks/use-package.ts +1 -0
- package/src/hooks/use-request-ai-review-mutation.ts +14 -6
- package/src/hooks/use-snippet.ts +1 -0
- package/src/hooks/useFileManagement.ts +28 -10
- package/src/hooks/usePackageFilesLoader.ts +3 -1
- package/src/index.css +11 -0
- package/src/lib/decodeUrlHashToFsMap.ts +17 -0
- package/src/lib/download-fns/download-circuit-png.ts +88 -0
- package/src/lib/download-fns/download-png-utils.ts +31 -0
- package/src/lib/encodeFsMapToUrlHash.ts +13 -0
- package/src/lib/populate-query-cache-with-ssr-data.ts +7 -0
- package/src/lib/ts-lib-cache.ts +47 -0
- package/src/lib/types.ts +2 -0
- package/src/lib/utils/findTargetFile.ts +1 -1
- package/src/lib/utils/package-utils.ts +10 -0
- package/src/main.tsx +7 -0
- package/src/pages/dashboard.tsx +18 -5
- package/src/pages/view-package.tsx +15 -7
- package/src/types/package.ts +4 -0
- package/vite.config.ts +100 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|
2
|
+
import {
|
|
3
|
+
Search,
|
|
4
|
+
Code,
|
|
5
|
+
Braces,
|
|
6
|
+
FileIcon,
|
|
7
|
+
Hash,
|
|
8
|
+
BookOpen,
|
|
9
|
+
FileText,
|
|
10
|
+
} from "lucide-react"
|
|
11
|
+
import type { PackageFile } from "@/types/package"
|
|
12
|
+
import { fuzzyMatch } from "../ViewPackagePage/utils/fuzz-search"
|
|
13
|
+
|
|
14
|
+
interface ScoredFile extends PackageFile {
|
|
15
|
+
score: number
|
|
16
|
+
matches: number[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface QuickOpenProps {
|
|
20
|
+
files: PackageFile[]
|
|
21
|
+
currentFile: string | null
|
|
22
|
+
onFileSelect: (path: string) => void
|
|
23
|
+
onClose: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const QuickOpen = ({
|
|
27
|
+
files,
|
|
28
|
+
currentFile,
|
|
29
|
+
onFileSelect,
|
|
30
|
+
onClose,
|
|
31
|
+
}: QuickOpenProps) => {
|
|
32
|
+
const [query, setQuery] = useState("")
|
|
33
|
+
const [selected, setSelected] = useState(0)
|
|
34
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
35
|
+
const selectedItemRef = useRef<HTMLDivElement>(null)
|
|
36
|
+
|
|
37
|
+
const scoredFiles = useMemo((): ScoredFile[] => {
|
|
38
|
+
if (!query) return files.map((f) => ({ ...f, score: 0, matches: [] }))
|
|
39
|
+
|
|
40
|
+
return files
|
|
41
|
+
.map((file) => {
|
|
42
|
+
const { score, matches } = fuzzyMatch(query, file.path)
|
|
43
|
+
return { ...file, score, matches }
|
|
44
|
+
})
|
|
45
|
+
.filter((f) => f.score >= 0)
|
|
46
|
+
.sort((a, b) => {
|
|
47
|
+
// Current file gets priority
|
|
48
|
+
if (a.path === currentFile) return -1
|
|
49
|
+
if (b.path === currentFile) return 1
|
|
50
|
+
// Then by score
|
|
51
|
+
return b.score - a.score
|
|
52
|
+
})
|
|
53
|
+
}, [files, query, currentFile])
|
|
54
|
+
|
|
55
|
+
const handleKeyDown = useCallback(
|
|
56
|
+
(e: React.KeyboardEvent) => {
|
|
57
|
+
switch (e.key) {
|
|
58
|
+
case "ArrowDown":
|
|
59
|
+
e.preventDefault()
|
|
60
|
+
setSelected((prev) => Math.min(prev + 1, scoredFiles.length - 1))
|
|
61
|
+
break
|
|
62
|
+
case "ArrowUp":
|
|
63
|
+
e.preventDefault()
|
|
64
|
+
setSelected((prev) => Math.max(prev - 1, 0))
|
|
65
|
+
break
|
|
66
|
+
case "Enter":
|
|
67
|
+
e.preventDefault()
|
|
68
|
+
if (scoredFiles[selected]) {
|
|
69
|
+
onFileSelect(scoredFiles[selected].path)
|
|
70
|
+
onClose()
|
|
71
|
+
}
|
|
72
|
+
break
|
|
73
|
+
case "Escape":
|
|
74
|
+
e.preventDefault()
|
|
75
|
+
onClose()
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
[scoredFiles, selected, onFileSelect, onClose],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
inputRef.current?.focus()
|
|
84
|
+
}, [])
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
setSelected(0)
|
|
88
|
+
}, [query])
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (selectedItemRef.current) {
|
|
92
|
+
selectedItemRef.current.scrollIntoView({
|
|
93
|
+
behavior: "smooth",
|
|
94
|
+
block: "nearest",
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}, [selected])
|
|
98
|
+
|
|
99
|
+
const getFileIcon = useCallback((path: string) => {
|
|
100
|
+
const ext = path.split(".").pop()?.toLowerCase()
|
|
101
|
+
const iconProps = { size: 16, className: "" }
|
|
102
|
+
|
|
103
|
+
switch (ext) {
|
|
104
|
+
case "tsx":
|
|
105
|
+
iconProps.className = "text-blue-500"
|
|
106
|
+
return <Code {...iconProps} />
|
|
107
|
+
case "ts":
|
|
108
|
+
iconProps.className = "text-blue-600"
|
|
109
|
+
return <Code {...iconProps} />
|
|
110
|
+
case "jsx":
|
|
111
|
+
iconProps.className = "text-cyan-500"
|
|
112
|
+
return <Code {...iconProps} />
|
|
113
|
+
case "js":
|
|
114
|
+
iconProps.className = "text-yellow-500"
|
|
115
|
+
return <Code {...iconProps} />
|
|
116
|
+
case "json":
|
|
117
|
+
iconProps.className = "text-orange-500"
|
|
118
|
+
return <Braces {...iconProps} />
|
|
119
|
+
case "md":
|
|
120
|
+
iconProps.className = "text-gray-600"
|
|
121
|
+
return <BookOpen {...iconProps} />
|
|
122
|
+
case "css":
|
|
123
|
+
iconProps.className = "text-pink-500"
|
|
124
|
+
return <Hash {...iconProps} />
|
|
125
|
+
case "html":
|
|
126
|
+
iconProps.className = "text-red-500"
|
|
127
|
+
return <FileText {...iconProps} />
|
|
128
|
+
default:
|
|
129
|
+
iconProps.className = "text-gray-400"
|
|
130
|
+
return <FileIcon {...iconProps} />
|
|
131
|
+
}
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
134
|
+
const renderHighlighted = useCallback(
|
|
135
|
+
(file: ScoredFile) => {
|
|
136
|
+
if (!query) return file.path
|
|
137
|
+
|
|
138
|
+
const chars = file.path.split("")
|
|
139
|
+
return chars.map((char, i) => (
|
|
140
|
+
<span key={i} className={file.matches.includes(i) ? "bg-blue-200" : ""}>
|
|
141
|
+
{char}
|
|
142
|
+
</span>
|
|
143
|
+
))
|
|
144
|
+
},
|
|
145
|
+
[query],
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
className="fixed inset-0 bg-black/20 backdrop-blur-sm flex items-start justify-center pt-16 sm:pt-20 z-50 p-4"
|
|
151
|
+
onClick={(e) => {
|
|
152
|
+
if (e.target === e.currentTarget) {
|
|
153
|
+
onClose()
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<div
|
|
158
|
+
className="bg-white/95 backdrop-blur-md rounded-xl shadow-2xl border border-slate-200/50 w-full max-w-lg mx-auto"
|
|
159
|
+
onClick={(e) => e.stopPropagation()}
|
|
160
|
+
>
|
|
161
|
+
<div className="p-4 border-b border-slate-100">
|
|
162
|
+
<div className="relative">
|
|
163
|
+
<Search
|
|
164
|
+
size={16}
|
|
165
|
+
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"
|
|
166
|
+
/>
|
|
167
|
+
<input
|
|
168
|
+
ref={inputRef}
|
|
169
|
+
value={query}
|
|
170
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
171
|
+
onKeyDown={handleKeyDown}
|
|
172
|
+
placeholder="Search files..."
|
|
173
|
+
className="w-full pl-10 pr-4 py-2.5 text-sm bg-slate-50/80 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-slate-950/10 focus:border-slate-400 transition-all duration-200"
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="max-h-80 overflow-y-auto py-2">
|
|
179
|
+
{scoredFiles.length === 0 ? (
|
|
180
|
+
<div className="p-6 text-center select-none text-slate-400 text-sm">
|
|
181
|
+
{query ? "No matching files" : "No files"}
|
|
182
|
+
</div>
|
|
183
|
+
) : (
|
|
184
|
+
scoredFiles.map((file, index) => (
|
|
185
|
+
<div
|
|
186
|
+
key={file.path}
|
|
187
|
+
ref={index === selected ? selectedItemRef : null}
|
|
188
|
+
onClick={() => {
|
|
189
|
+
onFileSelect(file.path)
|
|
190
|
+
onClose()
|
|
191
|
+
}}
|
|
192
|
+
className={`px-4 py-2 cursor-pointer flex items-center gap-3 transition-all duration-150 ${
|
|
193
|
+
index === selected
|
|
194
|
+
? "bg-slate-100 border-r-2 border-slate-500"
|
|
195
|
+
: "hover:bg-slate-50"
|
|
196
|
+
}`}
|
|
197
|
+
>
|
|
198
|
+
<div className="flex-shrink-0">{getFileIcon(file.path)}</div>
|
|
199
|
+
<div className="flex-1 min-w-0 text-sm font-mono text-slate-900 truncate">
|
|
200
|
+
{renderHighlighted(file)}
|
|
201
|
+
</div>
|
|
202
|
+
{file.path === currentFile && (
|
|
203
|
+
<span className="text-xs px-2 py-0.5 rounded-full bg-slate-100 text-slate-700">
|
|
204
|
+
current
|
|
205
|
+
</span>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
))
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div className="px-4 select-none py-3 text-xs border-t border-slate-100 bg-slate-50/50 rounded-b-xl">
|
|
213
|
+
<div className="flex items-center justify-between text-slate-500">
|
|
214
|
+
<div className="flex gap-4">
|
|
215
|
+
<span className="flex items-center gap-1.5">
|
|
216
|
+
<kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
|
|
217
|
+
↑↓
|
|
218
|
+
</kbd>
|
|
219
|
+
navigate
|
|
220
|
+
</span>
|
|
221
|
+
<span className="flex items-center gap-1.5">
|
|
222
|
+
<kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
|
|
223
|
+
↵
|
|
224
|
+
</kbd>
|
|
225
|
+
select
|
|
226
|
+
</span>
|
|
227
|
+
</div>
|
|
228
|
+
<span className="flex items-center gap-1.5">
|
|
229
|
+
<kbd className="px-2 py-1 font-mono bg-white border border-slate-200 rounded shadow-sm">
|
|
230
|
+
esc
|
|
231
|
+
</kbd>
|
|
232
|
+
close
|
|
233
|
+
</span>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export default QuickOpen
|
|
@@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<
|
|
|
36
36
|
<DialogPrimitive.Content
|
|
37
37
|
ref={ref}
|
|
38
38
|
className={cn(
|
|
39
|
-
"fixed left-[50%] top-[50%] z-[100] grid w-full
|
|
39
|
+
"fixed left-[50%] max-w-[95vw] md:max-w-lg top-[50%] z-[100] grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-slate-200 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg dark:border-slate-800 dark:bg-slate-950",
|
|
40
40
|
className,
|
|
41
41
|
)}
|
|
42
42
|
{...props}
|
|
@@ -404,7 +404,7 @@ const TreeLeaf = React.forwardRef<
|
|
|
404
404
|
default={defaultLeafIcon}
|
|
405
405
|
/>
|
|
406
406
|
<span className="flex-grow text-sm truncate">{item.name}</span>
|
|
407
|
-
<div onClick={(e) => e.stopPropagation()}>
|
|
407
|
+
<div className="flex items-center" onClick={(e) => e.stopPropagation()}>
|
|
408
408
|
<TreeActions isSelected={true}>{item.actions}</TreeActions>
|
|
409
409
|
</div>
|
|
410
410
|
</div>
|
package/src/global.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { AiReview } from "fake-snippets-api/lib/db/schema"
|
|
2
|
+
import { useQuery } from "react-query"
|
|
3
|
+
import { useAxios } from "./use-axios"
|
|
4
|
+
|
|
5
|
+
export const useAiReview = (
|
|
6
|
+
aiReviewId: string | null,
|
|
7
|
+
options?: {
|
|
8
|
+
refetchInterval?:
|
|
9
|
+
| number
|
|
10
|
+
| false
|
|
11
|
+
| ((data: AiReview | undefined) => number | false)
|
|
12
|
+
},
|
|
13
|
+
) => {
|
|
14
|
+
const axios = useAxios()
|
|
15
|
+
|
|
16
|
+
return useQuery<AiReview, Error & { status: number }>(
|
|
17
|
+
["aiReview", aiReviewId],
|
|
18
|
+
async () => {
|
|
19
|
+
if (!aiReviewId) throw new Error("aiReviewId is required")
|
|
20
|
+
const { data } = await axios.get("/ai_reviews/get", {
|
|
21
|
+
params: { ai_review_id: aiReviewId },
|
|
22
|
+
})
|
|
23
|
+
return data.ai_review as AiReview
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
enabled: Boolean(aiReviewId),
|
|
27
|
+
refetchInterval: options?.refetchInterval,
|
|
28
|
+
refetchOnWindowFocus: false,
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useMemo } from "react"
|
|
2
2
|
|
|
3
3
|
export const useCodeCompletionApi = () => {
|
|
4
|
-
const
|
|
4
|
+
const openrouterApiKey = useMemo(() => {
|
|
5
5
|
return {
|
|
6
|
-
apiKey: import.meta.env.
|
|
6
|
+
apiKey: import.meta.env.VITE_OPENROUTER_API_KEY,
|
|
7
7
|
}
|
|
8
8
|
}, [])
|
|
9
9
|
|
|
10
|
-
return
|
|
10
|
+
return openrouterApiKey
|
|
11
11
|
}
|
|
@@ -2,11 +2,15 @@ import { useParams } from "wouter"
|
|
|
2
2
|
import { useCurrentPackageId } from "./use-current-package-id"
|
|
3
3
|
import { usePackageRelease } from "./use-package-release"
|
|
4
4
|
import { useUrlParams } from "./use-url-params"
|
|
5
|
+
import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
|
|
5
6
|
|
|
6
7
|
export const useCurrentPackageRelease = (options?: {
|
|
7
8
|
include_ai_review?: boolean
|
|
8
9
|
include_logs?: boolean
|
|
9
|
-
refetchInterval?:
|
|
10
|
+
refetchInterval?:
|
|
11
|
+
| number
|
|
12
|
+
| false
|
|
13
|
+
| ((data: PackageRelease | undefined) => number | false)
|
|
10
14
|
}) => {
|
|
11
15
|
const { packageId } = useCurrentPackageId()
|
|
12
16
|
const urlParams = useUrlParams()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMutation } from "react-query"
|
|
1
|
+
import { useMutation, useQueryClient } from "react-query"
|
|
2
2
|
import { useAxios } from "./use-axios"
|
|
3
3
|
import { useToast } from "./use-toast"
|
|
4
4
|
|
|
@@ -7,6 +7,7 @@ export const useDeletePackage = ({
|
|
|
7
7
|
}: { onSuccess?: () => void } = {}) => {
|
|
8
8
|
const axios = useAxios()
|
|
9
9
|
const { toast } = useToast()
|
|
10
|
+
const queryClient = useQueryClient()
|
|
10
11
|
|
|
11
12
|
return useMutation(
|
|
12
13
|
["deletePackage"],
|
|
@@ -22,11 +23,14 @@ export const useDeletePackage = ({
|
|
|
22
23
|
return response.data
|
|
23
24
|
},
|
|
24
25
|
{
|
|
25
|
-
onSuccess: () => {
|
|
26
|
+
onSuccess: (_, variables) => {
|
|
26
27
|
toast({
|
|
27
28
|
title: "Package deleted",
|
|
28
29
|
description: "Package deleted successfully",
|
|
29
30
|
})
|
|
31
|
+
if (variables?.package_id) {
|
|
32
|
+
queryClient.invalidateQueries(["packages", variables.package_id])
|
|
33
|
+
}
|
|
30
34
|
onSuccess?.()
|
|
31
35
|
},
|
|
32
36
|
onError: (error: any) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useCallback } from "react"
|
|
2
|
+
import JSZip from "jszip"
|
|
3
|
+
import { saveAs } from "file-saver"
|
|
4
|
+
import { useAxios } from "@/hooks/use-axios"
|
|
5
|
+
import { Package, PackageFile } from "fake-snippets-api/lib/db/schema"
|
|
6
|
+
import { isHiddenFile } from "@/components/ViewPackagePage/utils/is-hidden-file"
|
|
7
|
+
|
|
8
|
+
export const useDownloadZip = () => {
|
|
9
|
+
const axios = useAxios()
|
|
10
|
+
|
|
11
|
+
const downloadZip = useCallback(
|
|
12
|
+
async (packageInfo: Package, packageFiles: PackageFile[]) => {
|
|
13
|
+
if (!packageInfo || !packageFiles) return
|
|
14
|
+
|
|
15
|
+
const zip = new JSZip()
|
|
16
|
+
|
|
17
|
+
const visibleFiles = packageFiles.filter(
|
|
18
|
+
(file) => !isHiddenFile(file.file_path),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
for (const file of visibleFiles) {
|
|
22
|
+
try {
|
|
23
|
+
const response = await axios.post("/package_files/get", {
|
|
24
|
+
package_file_id: file.package_file_id,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const content = response.data.package_file?.content_text || ""
|
|
28
|
+
|
|
29
|
+
const cleanPath = file.file_path.startsWith("/")
|
|
30
|
+
? file.file_path.slice(1)
|
|
31
|
+
: file.file_path
|
|
32
|
+
|
|
33
|
+
zip.file(cleanPath, content)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(
|
|
36
|
+
`Failed to fetch content for file ${file.file_path}:`,
|
|
37
|
+
error,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const blob = await zip.generateAsync({ type: "blob" })
|
|
43
|
+
const fileName = `${packageInfo.unscoped_name || packageInfo.name}.zip`
|
|
44
|
+
saveAs(blob, fileName)
|
|
45
|
+
},
|
|
46
|
+
[axios],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
return { downloadZip }
|
|
50
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Efficient hook for handling keyboard shortcuts
|
|
5
|
+
*
|
|
6
|
+
* Examples:
|
|
7
|
+
* - useHotkeyCombo("cmd+b", () => toggleSidebar())
|
|
8
|
+
* - useHotkeyCombo("ctrl+s", () => save())
|
|
9
|
+
* - useHotkeyCombo("Escape", () => closeModal())
|
|
10
|
+
* - useHotkey("Enter", () => submit(), { meta: true })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type HotkeyModifiers = {
|
|
14
|
+
ctrl?: boolean
|
|
15
|
+
alt?: boolean
|
|
16
|
+
shift?: boolean
|
|
17
|
+
meta?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type HotkeyCallback = (event: KeyboardEvent) => void
|
|
21
|
+
|
|
22
|
+
type HotkeyOptions = {
|
|
23
|
+
preventDefault?: boolean
|
|
24
|
+
stopPropagation?: boolean
|
|
25
|
+
target?: EventTarget | null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useHotkey = (
|
|
29
|
+
key: string,
|
|
30
|
+
callback: HotkeyCallback,
|
|
31
|
+
modifiers: HotkeyModifiers = {},
|
|
32
|
+
options: HotkeyOptions = {},
|
|
33
|
+
) => {
|
|
34
|
+
const callbackRef = useRef(callback)
|
|
35
|
+
const {
|
|
36
|
+
preventDefault = true,
|
|
37
|
+
stopPropagation = false,
|
|
38
|
+
target = document,
|
|
39
|
+
} = options
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
callbackRef.current = callback
|
|
43
|
+
}, [callback])
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!target) return
|
|
47
|
+
|
|
48
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
49
|
+
const keyMatches = event.key.toLowerCase() === key.toLowerCase()
|
|
50
|
+
|
|
51
|
+
// Check that all required modifiers are pressed
|
|
52
|
+
const ctrlOk = modifiers.ctrl ? event.ctrlKey : true
|
|
53
|
+
const altOk = modifiers.alt ? event.altKey : true
|
|
54
|
+
const shiftOk = modifiers.shift ? event.shiftKey : true
|
|
55
|
+
const metaOk = modifiers.meta ? event.metaKey || event.ctrlKey : true
|
|
56
|
+
|
|
57
|
+
if (keyMatches && ctrlOk && altOk && shiftOk && metaOk) {
|
|
58
|
+
if (preventDefault) {
|
|
59
|
+
event.preventDefault()
|
|
60
|
+
}
|
|
61
|
+
if (stopPropagation) {
|
|
62
|
+
event.stopPropagation()
|
|
63
|
+
}
|
|
64
|
+
callbackRef.current(event)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
target.addEventListener("keydown", handleKeyDown as EventListener)
|
|
69
|
+
return () =>
|
|
70
|
+
target.removeEventListener("keydown", handleKeyDown as EventListener)
|
|
71
|
+
}, [
|
|
72
|
+
key,
|
|
73
|
+
modifiers.ctrl,
|
|
74
|
+
modifiers.alt,
|
|
75
|
+
modifiers.shift,
|
|
76
|
+
modifiers.meta,
|
|
77
|
+
preventDefault,
|
|
78
|
+
stopPropagation,
|
|
79
|
+
target,
|
|
80
|
+
])
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const useHotkeyCombo = (
|
|
84
|
+
combo: string,
|
|
85
|
+
callback: HotkeyCallback,
|
|
86
|
+
options: HotkeyOptions = {},
|
|
87
|
+
) => {
|
|
88
|
+
const modifiers: HotkeyModifiers = {}
|
|
89
|
+
const parts = combo
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.split("+")
|
|
92
|
+
.map((part) => part.trim())
|
|
93
|
+
|
|
94
|
+
const keyPart = parts[parts.length - 1]
|
|
95
|
+
const modifierParts = parts.slice(0, -1)
|
|
96
|
+
|
|
97
|
+
modifierParts.forEach((part) => {
|
|
98
|
+
switch (part) {
|
|
99
|
+
case "ctrl":
|
|
100
|
+
modifiers.ctrl = true
|
|
101
|
+
break
|
|
102
|
+
case "cmd":
|
|
103
|
+
case "meta":
|
|
104
|
+
modifiers.meta = true
|
|
105
|
+
break
|
|
106
|
+
case "alt":
|
|
107
|
+
modifiers.alt = true
|
|
108
|
+
break
|
|
109
|
+
case "shift":
|
|
110
|
+
modifiers.shift = true
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
useHotkey(keyPart, callback, modifiers, options)
|
|
116
|
+
}
|
|
@@ -47,6 +47,7 @@ export const usePackageFile = (
|
|
|
47
47
|
{
|
|
48
48
|
retry: false,
|
|
49
49
|
enabled: Boolean(query),
|
|
50
|
+
refetchOnWindowFocus: false,
|
|
50
51
|
...(opts as any),
|
|
51
52
|
},
|
|
52
53
|
)
|
|
@@ -112,6 +113,8 @@ export const usePackageFiles = (packageReleaseId?: string | null) => {
|
|
|
112
113
|
},
|
|
113
114
|
{
|
|
114
115
|
enabled: Boolean(packageReleaseId),
|
|
116
|
+
refetchOnWindowFocus: false,
|
|
117
|
+
staleTime: 0,
|
|
115
118
|
},
|
|
116
119
|
)
|
|
117
120
|
}
|
|
@@ -25,7 +25,10 @@ type PackageReleaseQuery = (
|
|
|
25
25
|
export const usePackageRelease = (
|
|
26
26
|
query: PackageReleaseQuery | null,
|
|
27
27
|
options?: {
|
|
28
|
-
refetchInterval?:
|
|
28
|
+
refetchInterval?:
|
|
29
|
+
| number
|
|
30
|
+
| false
|
|
31
|
+
| ((data: PackageRelease | undefined) => number | false)
|
|
29
32
|
},
|
|
30
33
|
) => {
|
|
31
34
|
const axios = useAxios()
|
|
@@ -52,6 +55,7 @@ export const usePackageRelease = (
|
|
|
52
55
|
retry: false,
|
|
53
56
|
enabled: Boolean(query),
|
|
54
57
|
refetchInterval: options?.refetchInterval,
|
|
58
|
+
refetchOnWindowFocus: false,
|
|
55
59
|
},
|
|
56
60
|
)
|
|
57
61
|
}
|
package/src/hooks/use-package.ts
CHANGED
|
@@ -1,35 +1,43 @@
|
|
|
1
1
|
import { useMutation, useQueryClient } from "react-query"
|
|
2
2
|
import { useAxios } from "./use-axios"
|
|
3
3
|
import { useToast } from "./use-toast"
|
|
4
|
-
import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
|
|
4
|
+
import type { AiReview, PackageRelease } from "fake-snippets-api/lib/db/schema"
|
|
5
5
|
|
|
6
6
|
export const useRequestAiReviewMutation = ({
|
|
7
7
|
onSuccess,
|
|
8
|
-
}: {
|
|
8
|
+
}: {
|
|
9
|
+
onSuccess?: (packageRelease: PackageRelease, aiReview: AiReview) => void
|
|
10
|
+
} = {}) => {
|
|
9
11
|
const axios = useAxios()
|
|
10
12
|
const { toast } = useToast()
|
|
11
13
|
const queryClient = useQueryClient()
|
|
12
14
|
|
|
13
15
|
return useMutation(
|
|
14
16
|
async ({ package_release_id }: { package_release_id: string }) => {
|
|
15
|
-
await axios.post("/ai_reviews/create", {
|
|
17
|
+
const { data: createData } = await axios.post("/ai_reviews/create", {
|
|
16
18
|
package_release_id,
|
|
17
19
|
})
|
|
20
|
+
const ai_review = createData.ai_review as AiReview
|
|
21
|
+
|
|
18
22
|
const { data } = await axios.post(
|
|
19
23
|
"/package_releases/get",
|
|
20
24
|
{ package_release_id },
|
|
21
25
|
{ params: { include_ai_review: true } },
|
|
22
26
|
)
|
|
23
|
-
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
package_release: data.package_release as PackageRelease,
|
|
30
|
+
ai_review,
|
|
31
|
+
}
|
|
24
32
|
},
|
|
25
33
|
{
|
|
26
|
-
onSuccess: (
|
|
34
|
+
onSuccess: ({ package_release, ai_review }) => {
|
|
27
35
|
toast({
|
|
28
36
|
title: "AI review requested",
|
|
29
37
|
description: "An AI review has been generated.",
|
|
30
38
|
})
|
|
31
39
|
queryClient.invalidateQueries(["packageRelease"])
|
|
32
|
-
onSuccess?.(
|
|
40
|
+
onSuccess?.(package_release, ai_review)
|
|
33
41
|
},
|
|
34
42
|
onError: (error: any) => {
|
|
35
43
|
toast({
|