@tscircuit/fake-snippets 0.0.87 → 0.0.89
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 +187 -206
- package/dist/bundle.js +207 -101
- package/fake-snippets-api/routes/api/package_releases/create.ts +1 -1
- package/fake-snippets-api/routes/api/proxy.ts +128 -0
- package/package.json +57 -50
- package/renovate.json +2 -1
- package/src/App.tsx +22 -3
- package/src/ContextProviders.tsx +2 -0
- package/src/build-watcher.ts +52 -0
- package/src/components/CmdKMenu.tsx +533 -197
- package/src/components/DownloadButtonAndMenu.tsx +104 -26
- package/src/components/FileSidebar.tsx +11 -1
- package/src/components/Header.tsx +5 -1
- package/src/components/Header2.tsx +7 -2
- package/src/components/HeaderLogin.tsx +1 -1
- 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 +17 -16
- package/src/components/PackageCard.tsx +66 -16
- package/src/components/PrefetchPageLink.tsx +66 -15
- package/src/components/SearchComponent.tsx +2 -2
- package/src/components/SuspenseRunFrame.tsx +14 -2
- package/src/components/ViewPackagePage/components/important-files-view.tsx +97 -22
- package/src/components/ViewPackagePage/components/main-content-header.tsx +27 -3
- package/src/components/ViewPackagePage/components/mobile-sidebar.tsx +2 -2
- package/src/components/ViewPackagePage/components/repo-page-content.tsx +49 -34
- 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/ViewPackagePage/utils/is-package-file-important.ts +18 -5
- 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 +16 -0
- package/src/components/package-port/CodeEditor.tsx +113 -11
- package/src/components/package-port/CodeEditorHeader.tsx +39 -4
- package/src/components/package-port/EditorNav.tsx +41 -15
- 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-current-package-release.ts +5 -1
- 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 +26 -8
- 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 +39 -38
- package/src/lib/ts-lib-cache.ts +47 -0
- package/src/lib/types.ts +2 -0
- package/src/main.tsx +7 -0
- package/src/pages/dashboard.tsx +8 -5
- package/src/pages/user-profile.tsx +1 -1
- package/src/pages/view-package.tsx +15 -7
- 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 "./CodeAndPreview"
|
|
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
|
+
}
|
|
@@ -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()
|
|
@@ -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({
|
package/src/hooks/use-snippet.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
9
9
|
import { usePackageFiles } from "./use-package-files"
|
|
10
10
|
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
11
|
+
import { decodeUrlHashToFsMap } from "@/lib/decodeUrlHashToFsMap"
|
|
11
12
|
import { usePackageFilesLoader } from "./usePackageFilesLoader"
|
|
12
13
|
import { useGlobalStore } from "./use-global-store"
|
|
13
14
|
import { useToast } from "@/components/ViewPackagePage/hooks/use-toast"
|
|
@@ -15,7 +16,7 @@ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
|
|
|
15
16
|
import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
|
|
16
17
|
import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
17
18
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
18
|
-
import {
|
|
19
|
+
import { encodeFsMapToUrlHash } from "@/lib/encodeFsMapToUrlHash"
|
|
19
20
|
|
|
20
21
|
export interface ICreateFileProps {
|
|
21
22
|
newFileName: string
|
|
@@ -117,6 +118,22 @@ export function useFileManagement({
|
|
|
117
118
|
|
|
118
119
|
useEffect(() => {
|
|
119
120
|
if (!currentPackage || isLoadingPackageFilesWithContent) {
|
|
121
|
+
const decodedFsMap = decodeUrlHashToFsMap(window.location.toString())
|
|
122
|
+
|
|
123
|
+
if (decodedFsMap && Object.keys(decodedFsMap).length > 0) {
|
|
124
|
+
const filesFromUrl = Object.entries(decodedFsMap).map(
|
|
125
|
+
([path, content]) => ({
|
|
126
|
+
path,
|
|
127
|
+
content: String(content),
|
|
128
|
+
}),
|
|
129
|
+
)
|
|
130
|
+
const targetFile = findTargetFile(filesFromUrl, fileChoosen)
|
|
131
|
+
setLocalFiles(filesFromUrl)
|
|
132
|
+
setInitialFiles([])
|
|
133
|
+
setCurrentFile(targetFile?.path || filesFromUrl[0]?.path || null)
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
120
137
|
setLocalFiles([
|
|
121
138
|
{
|
|
122
139
|
path: "index.tsx",
|
|
@@ -243,14 +260,15 @@ export function useFileManagement({
|
|
|
243
260
|
|
|
244
261
|
debounceTimeoutRef.current = setTimeout(() => {
|
|
245
262
|
try {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
const map = files.reduce(
|
|
264
|
+
(acc, f) => {
|
|
265
|
+
acc[f.path] = f.content || ""
|
|
266
|
+
return acc
|
|
267
|
+
},
|
|
268
|
+
{} as Record<string, string>,
|
|
269
|
+
)
|
|
252
270
|
|
|
253
|
-
const snippetUrl =
|
|
271
|
+
const snippetUrl = encodeFsMapToUrlHash(map)
|
|
254
272
|
if (typeof snippetUrl !== "string") return
|
|
255
273
|
|
|
256
274
|
const currentUrl = new URL(window.location.href)
|
|
@@ -36,7 +36,9 @@ export function usePackageFilesLoader(pkg?: Package) {
|
|
|
36
36
|
const content = response.data.package_file?.content_text
|
|
37
37
|
return { path: file.file_path, content: content ?? "" }
|
|
38
38
|
},
|
|
39
|
-
|
|
39
|
+
refetchOnWindowFocus: false,
|
|
40
|
+
refetchOnMount: false,
|
|
41
|
+
cacheTime: 0,
|
|
40
42
|
})) ?? [],
|
|
41
43
|
)
|
|
42
44
|
|