@tscircuit/fake-snippets 0.0.82 → 0.0.84
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/README.md +5 -2
- package/bun-tests/fake-snippets-api/routes/ai_reviews/create.test.ts +12 -0
- package/bun-tests/fake-snippets-api/routes/ai_reviews/get.test.ts +16 -0
- package/bun-tests/fake-snippets-api/routes/ai_reviews/list.test.ts +14 -0
- package/bun-tests/fake-snippets-api/routes/ai_reviews/process_review.test.ts +16 -0
- package/bun-tests/fake-snippets-api/routes/package_releases/create.test.ts +3 -3
- package/bun.lock +26 -37
- package/dist/bundle.js +590 -427
- package/dist/index.d.ts +83 -11
- package/dist/index.js +50 -2
- package/dist/schema.d.ts +116 -15
- package/dist/schema.js +17 -2
- package/fake-snippets-api/lib/db/db-client.ts +40 -0
- package/fake-snippets-api/lib/db/schema.ts +17 -1
- package/fake-snippets-api/lib/public-mapping/public-map-package-release.ts +14 -1
- package/fake-snippets-api/routes/api/_fake/ai_reviews/process_review.ts +31 -0
- package/fake-snippets-api/routes/api/ai_reviews/create.ts +22 -0
- package/fake-snippets-api/routes/api/ai_reviews/get.ts +24 -0
- package/fake-snippets-api/routes/api/ai_reviews/list.ts +14 -0
- package/fake-snippets-api/routes/api/package_releases/get.ts +11 -3
- package/fake-snippets-api/routes/api/package_releases/list.ts +8 -1
- package/package.json +4 -3
- package/src/App.tsx +0 -2
- package/src/ContextProviders.tsx +1 -1
- package/src/components/Header2.tsx +8 -18
- package/src/components/PackageBuildsPage/package-build-header.tsx +14 -2
- package/src/components/SearchComponent.tsx +46 -8
- package/src/components/ViewPackagePage/hooks/use-toast.tsx +70 -0
- package/src/components/ViewSnippetHeader.tsx +9 -6
- package/src/components/dialogs/edit-package-details-dialog.tsx +5 -10
- package/src/components/dialogs/{import-snippet-dialog.tsx → import-package-dialog.tsx} +25 -24
- package/src/components/package-port/CodeEditorHeader.tsx +7 -6
- package/src/components/ui/toaster.tsx +1 -33
- package/src/hooks/use-current-package-release.ts +10 -1
- package/src/hooks/use-fork-package-mutation.ts +4 -3
- package/src/hooks/use-package-release.ts +15 -14
- package/src/hooks/use-sign-in.ts +10 -8
- package/src/hooks/use-toast.tsx +50 -169
- package/src/hooks/useFileManagement.ts +74 -12
- package/src/hooks/useForkPackageMutation.ts +2 -1
- package/src/hooks/useForkSnippetMutation.ts +2 -1
- package/src/pages/authorize.tsx +164 -8
- package/src/pages/view-package.tsx +1 -0
- package/src/components/ViewPackagePage/hooks/use-toast.ts +0 -191
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState, useCallback } from "react"
|
|
1
|
+
import { useEffect, useMemo, useState, useCallback, useRef } from "react"
|
|
2
2
|
import { isValidFileName } from "@/lib/utils/isValidFileName"
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_CODE,
|
|
@@ -6,11 +6,7 @@ import {
|
|
|
6
6
|
PackageFile,
|
|
7
7
|
} from "../components/package-port/CodeAndPreview"
|
|
8
8
|
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
9
|
-
import {
|
|
10
|
-
usePackageFile,
|
|
11
|
-
usePackageFileById,
|
|
12
|
-
usePackageFiles,
|
|
13
|
-
} from "./use-package-files"
|
|
9
|
+
import { usePackageFiles } from "./use-package-files"
|
|
14
10
|
import { decodeUrlHashToText } from "@/lib/decodeUrlHashToText"
|
|
15
11
|
import { usePackageFilesLoader } from "./usePackageFilesLoader"
|
|
16
12
|
import { useGlobalStore } from "./use-global-store"
|
|
@@ -19,6 +15,7 @@ import { useUpdatePackageFilesMutation } from "./useUpdatePackageFilesMutation"
|
|
|
19
15
|
import { useCreatePackageReleaseMutation } from "./use-create-package-release-mutation"
|
|
20
16
|
import { useCreatePackageMutation } from "./use-create-package-mutation"
|
|
21
17
|
import { findTargetFile } from "@/lib/utils/findTargetFile"
|
|
18
|
+
import { createSnippetUrl } from "@tscircuit/create-snippet-url"
|
|
22
19
|
|
|
23
20
|
export interface ICreateFileProps {
|
|
24
21
|
newFileName: string
|
|
@@ -55,17 +52,19 @@ export function useFileManagement({
|
|
|
55
52
|
const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
|
|
56
53
|
const loggedInUser = useGlobalStore((s) => s.session)
|
|
57
54
|
const { toast } = useToast()
|
|
55
|
+
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
58
56
|
const {
|
|
59
57
|
data: packageFilesWithContent,
|
|
60
58
|
isLoading: isLoadingPackageFilesWithContent,
|
|
61
59
|
} = usePackageFilesLoader(currentPackage)
|
|
62
60
|
const { data: packageFilesMeta, isLoading: isLoadingPackageFiles } =
|
|
63
61
|
usePackageFiles(currentPackage?.latest_package_release_id)
|
|
64
|
-
|
|
65
62
|
const initialCodeContent = useMemo(() => {
|
|
66
63
|
return (
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
(!!decodeUrlHashToText(window.location.toString()) &&
|
|
65
|
+
decodeUrlHashToText(window.location.toString()) !== ""
|
|
66
|
+
? decodeUrlHashToText(window.location.toString())
|
|
67
|
+
: templateCode) || DEFAULT_CODE
|
|
69
68
|
)
|
|
70
69
|
}, [templateCode, currentPackage])
|
|
71
70
|
const manualEditsFileContent = useMemo(() => {
|
|
@@ -193,7 +192,8 @@ export function useFileManagement({
|
|
|
193
192
|
{ path: newFileName, content: "" },
|
|
194
193
|
]
|
|
195
194
|
setLocalFiles(updatedFiles)
|
|
196
|
-
|
|
195
|
+
// immediately select the newly created file
|
|
196
|
+
setCurrentFile(newFileName)
|
|
197
197
|
return {
|
|
198
198
|
newFileCreated: true,
|
|
199
199
|
}
|
|
@@ -233,14 +233,76 @@ export function useFileManagement({
|
|
|
233
233
|
})
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
const saveToUrl = useCallback(
|
|
237
|
+
(files: PackageFile[]) => {
|
|
238
|
+
if (isLoggedIn || !files.length) return
|
|
239
|
+
|
|
240
|
+
if (debounceTimeoutRef.current) {
|
|
241
|
+
clearTimeout(debounceTimeoutRef.current)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
debounceTimeoutRef.current = setTimeout(() => {
|
|
245
|
+
try {
|
|
246
|
+
const mainFile =
|
|
247
|
+
files.find((f) => f.path === currentFile) ||
|
|
248
|
+
files.find((f) => f.path === "index.tsx") ||
|
|
249
|
+
files[0]
|
|
250
|
+
|
|
251
|
+
if (mainFile.content.length > 50000) return
|
|
252
|
+
|
|
253
|
+
const snippetUrl = createSnippetUrl(mainFile.content)
|
|
254
|
+
if (typeof snippetUrl !== "string") return
|
|
255
|
+
|
|
256
|
+
const currentUrl = new URL(window.location.href)
|
|
257
|
+
const urlParts = snippetUrl.split("#")
|
|
258
|
+
|
|
259
|
+
if (urlParts.length > 1 && urlParts[1]) {
|
|
260
|
+
const newHash = urlParts[1]
|
|
261
|
+
if (newHash.length > 8000) return
|
|
262
|
+
|
|
263
|
+
currentUrl.hash = newHash
|
|
264
|
+
const finalUrl = currentUrl.toString()
|
|
265
|
+
|
|
266
|
+
if (finalUrl.length <= 32000) {
|
|
267
|
+
window.history.replaceState(null, "", finalUrl)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn("Failed to save code to URL:", error)
|
|
272
|
+
}
|
|
273
|
+
}, 1000)
|
|
274
|
+
},
|
|
275
|
+
[isLoggedIn, currentFile],
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!isLoggedIn && localFiles.length > 0) {
|
|
280
|
+
saveToUrl(localFiles)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return () => {
|
|
284
|
+
if (debounceTimeoutRef.current) {
|
|
285
|
+
clearTimeout(debounceTimeoutRef.current)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}, [localFiles, saveToUrl, isLoggedIn])
|
|
289
|
+
|
|
236
290
|
const saveFiles = () => {
|
|
237
291
|
if (!isLoggedIn) {
|
|
292
|
+
// For non-logged-in users, trigger immediate URL save
|
|
293
|
+
if (debounceTimeoutRef.current) {
|
|
294
|
+
clearTimeout(debounceTimeoutRef.current)
|
|
295
|
+
}
|
|
296
|
+
saveToUrl(localFiles)
|
|
297
|
+
|
|
238
298
|
toast({
|
|
239
|
-
title: "
|
|
240
|
-
description:
|
|
299
|
+
title: "Code Saved to URL",
|
|
300
|
+
description:
|
|
301
|
+
"Your code has been saved to the URL. Bookmark this page to access your code later.",
|
|
241
302
|
})
|
|
242
303
|
return
|
|
243
304
|
}
|
|
305
|
+
|
|
244
306
|
if (!currentPackage) {
|
|
245
307
|
openNewPackageSaveDialog()
|
|
246
308
|
return
|
|
@@ -38,9 +38,10 @@ export const useForkPackageMutation = ({
|
|
|
38
38
|
},
|
|
39
39
|
onError: (error: any) => {
|
|
40
40
|
console.error("Error forking package:", error)
|
|
41
|
+
const message = error?.data?.error?.message
|
|
41
42
|
toast({
|
|
42
43
|
title: "Error",
|
|
43
|
-
description: "Failed to fork package. Please try again.",
|
|
44
|
+
description: message || "Failed to fork package. Please try again.",
|
|
44
45
|
variant: "destructive",
|
|
45
46
|
})
|
|
46
47
|
},
|
|
@@ -41,9 +41,10 @@ export const useForkSnippetMutation = ({
|
|
|
41
41
|
},
|
|
42
42
|
onError: (error: any) => {
|
|
43
43
|
console.error("Error forking snippet:", error)
|
|
44
|
+
const message = error?.data?.error?.message
|
|
44
45
|
toast({
|
|
45
46
|
title: "Error",
|
|
46
|
-
description: "Failed to fork snippet. Please try again.",
|
|
47
|
+
description: message || "Failed to fork snippet. Please try again.",
|
|
47
48
|
variant: "destructive",
|
|
48
49
|
})
|
|
49
50
|
},
|
package/src/pages/authorize.tsx
CHANGED
|
@@ -6,6 +6,33 @@ import { useEffect, useState } from "react"
|
|
|
6
6
|
import * as jose from "jose"
|
|
7
7
|
import Footer from "@/components/Footer"
|
|
8
8
|
import { useLocation } from "wouter"
|
|
9
|
+
import { useIsUsingFakeApi } from "@/hooks/use-is-using-fake-api"
|
|
10
|
+
import { CheckCircle, AlertCircle, Loader2 } from "lucide-react"
|
|
11
|
+
|
|
12
|
+
const handleRedirect = (
|
|
13
|
+
redirect: string | null,
|
|
14
|
+
fallbackLocation: () => void,
|
|
15
|
+
) => {
|
|
16
|
+
if (redirect) {
|
|
17
|
+
try {
|
|
18
|
+
const decodedRedirect = decodeURIComponent(redirect)
|
|
19
|
+
|
|
20
|
+
if (decodedRedirect.startsWith("/")) {
|
|
21
|
+
window.location.href = decodedRedirect
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const redirectUrl = new URL(decodedRedirect)
|
|
26
|
+
if (redirectUrl.origin === window.location.origin) {
|
|
27
|
+
window.location.href = redirectUrl.href
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
} catch (e) {
|
|
31
|
+
console.warn("Invalid redirect URL:", redirect)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
fallbackLocation()
|
|
35
|
+
}
|
|
9
36
|
|
|
10
37
|
const AuthenticatePageInnerContent = () => {
|
|
11
38
|
const [location, setLocation] = useLocation()
|
|
@@ -13,32 +40,161 @@ const AuthenticatePageInnerContent = () => {
|
|
|
13
40
|
const [message, setMessage] = useState("logging you in...")
|
|
14
41
|
const searchParams = new URLSearchParams(window.location.search.split("?")[1])
|
|
15
42
|
const session_token = searchParams.get("session_token")
|
|
43
|
+
const redirect = searchParams.get("redirect")
|
|
44
|
+
const isUsingFakeApi = useIsUsingFakeApi()
|
|
45
|
+
|
|
46
|
+
const isError = message.includes("error") || message.includes("couldn't")
|
|
47
|
+
const isSuccess =
|
|
48
|
+
message.includes("success") || message.includes("redirecting")
|
|
49
|
+
|
|
16
50
|
useEffect(() => {
|
|
17
51
|
async function login() {
|
|
52
|
+
if (isUsingFakeApi) {
|
|
53
|
+
setSession({
|
|
54
|
+
account_id: "account-1234",
|
|
55
|
+
github_username: "testuser",
|
|
56
|
+
token: "1234",
|
|
57
|
+
session_id: "session-1234",
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
handleRedirect(redirect, () => setLocation("/"))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
18
63
|
if (!session_token) {
|
|
19
|
-
setMessage("couldn't log in - no token")
|
|
64
|
+
setMessage("couldn't log in - no token provided")
|
|
20
65
|
return
|
|
21
66
|
}
|
|
22
|
-
|
|
23
67
|
if (session_token) {
|
|
24
68
|
const decodedToken = jose.decodeJwt(session_token)
|
|
25
69
|
setSession({
|
|
26
70
|
...(decodedToken as any),
|
|
27
71
|
token: session_token,
|
|
28
72
|
})
|
|
29
|
-
|
|
73
|
+
setMessage("success! redirecting you now...")
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
handleRedirect(redirect, () => setLocation("/"))
|
|
76
|
+
}, 1000)
|
|
30
77
|
return
|
|
31
78
|
}
|
|
32
79
|
}
|
|
33
80
|
login().catch((e) => {
|
|
34
|
-
setMessage(`error logging you in
|
|
81
|
+
setMessage(`error logging you in: ${e.message || e.toString()}`)
|
|
35
82
|
})
|
|
36
|
-
}, [session_token])
|
|
83
|
+
}, [session_token, redirect])
|
|
37
84
|
|
|
38
85
|
return (
|
|
39
|
-
<div className="bg-white
|
|
40
|
-
<div
|
|
41
|
-
|
|
86
|
+
<div className="min-h-screen bg-white flex items-center justify-center px-4">
|
|
87
|
+
<div className="w-full max-w-md">
|
|
88
|
+
<div className="bg-gray-50/20 rounded-2xl shadow-xl border border-gray-200 p-8">
|
|
89
|
+
<div className="text-center">
|
|
90
|
+
<div className="mb-6">
|
|
91
|
+
{isError ? (
|
|
92
|
+
<div className="mx-auto w-16 h-16 bg-red-100 rounded-full flex items-center justify-center">
|
|
93
|
+
<AlertCircle className="h-8 w-8 text-red-500" />
|
|
94
|
+
</div>
|
|
95
|
+
) : isSuccess ? (
|
|
96
|
+
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center">
|
|
97
|
+
<CheckCircle className="h-8 w-8 text-green-500" />
|
|
98
|
+
</div>
|
|
99
|
+
) : (
|
|
100
|
+
<div className="mx-auto w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
|
|
101
|
+
<Loader2 className="h-8 w-8 text-blue-500 animate-spin" />
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Title */}
|
|
107
|
+
<h1 className="text-2xl font-semibold text-gray-900 mb-3">
|
|
108
|
+
{isError
|
|
109
|
+
? "Authentication Failed"
|
|
110
|
+
: isSuccess
|
|
111
|
+
? "Success!"
|
|
112
|
+
: "Signing You In"}
|
|
113
|
+
</h1>
|
|
114
|
+
|
|
115
|
+
{/* Message */}
|
|
116
|
+
<div className="text-gray-600">
|
|
117
|
+
{isError ? (
|
|
118
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-left">
|
|
119
|
+
<p className="text-red-800 font-medium mb-2">
|
|
120
|
+
Authentication Error
|
|
121
|
+
</p>
|
|
122
|
+
<p className="text-red-700 text-sm break-words">{message}</p>
|
|
123
|
+
<div className="mt-4 pt-3 border-t border-red-200">
|
|
124
|
+
<p className="text-red-600 text-xs">
|
|
125
|
+
Please try signing in again or contact support if the
|
|
126
|
+
problem persists.
|
|
127
|
+
</p>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
) : isSuccess ? (
|
|
131
|
+
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
132
|
+
<p className="text-green-800 font-medium">
|
|
133
|
+
Authentication successful!
|
|
134
|
+
</p>
|
|
135
|
+
<p className="text-green-700 text-sm mt-1">
|
|
136
|
+
Redirecting you now...
|
|
137
|
+
</p>
|
|
138
|
+
</div>
|
|
139
|
+
) : (
|
|
140
|
+
<div>
|
|
141
|
+
<p className="text-gray-600 mb-4">
|
|
142
|
+
Please wait while we authenticate your account and redirect
|
|
143
|
+
you to your destination.
|
|
144
|
+
</p>
|
|
145
|
+
<div className="flex items-center justify-center text-sm text-gray-500">
|
|
146
|
+
<div className="flex space-x-1">
|
|
147
|
+
<div className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"></div>
|
|
148
|
+
<div
|
|
149
|
+
className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"
|
|
150
|
+
style={{ animationDelay: "0.1s" }}
|
|
151
|
+
></div>
|
|
152
|
+
<div
|
|
153
|
+
className="w-1 h-1 bg-gray-400 rounded-full animate-bounce"
|
|
154
|
+
style={{ animationDelay: "0.2s" }}
|
|
155
|
+
></div>
|
|
156
|
+
</div>
|
|
157
|
+
<span className="ml-2">Processing</span>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* Footer info */}
|
|
166
|
+
<div className="text-center mt-6 text-xs text-gray-500">
|
|
167
|
+
If you encounter any issues, please report them on our{" "}
|
|
168
|
+
<a
|
|
169
|
+
href="https://github.com/tscircuit/tscircuit/issues"
|
|
170
|
+
className="text-blue-500 underline"
|
|
171
|
+
>
|
|
172
|
+
GitHub Issues page
|
|
173
|
+
</a>
|
|
174
|
+
.
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<style
|
|
179
|
+
dangerouslySetInnerHTML={{
|
|
180
|
+
__html: `
|
|
181
|
+
@keyframes pulse-loading {
|
|
182
|
+
0% {
|
|
183
|
+
width: 0%;
|
|
184
|
+
}
|
|
185
|
+
50% {
|
|
186
|
+
width: 60%;
|
|
187
|
+
}
|
|
188
|
+
100% {
|
|
189
|
+
width: 100%;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
.animate-pulse-loading {
|
|
193
|
+
animation: pulse-loading 2s ease-in-out infinite;
|
|
194
|
+
}
|
|
195
|
+
`,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
42
198
|
</div>
|
|
43
199
|
)
|
|
44
200
|
}
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
// Inspired by react-hot-toast library
|
|
4
|
-
import * as React from "react"
|
|
5
|
-
|
|
6
|
-
import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
|
|
7
|
-
|
|
8
|
-
const TOAST_LIMIT = 1
|
|
9
|
-
const TOAST_REMOVE_DELAY = 1000000
|
|
10
|
-
|
|
11
|
-
type ToasterToast = ToastProps & {
|
|
12
|
-
id: string
|
|
13
|
-
title?: React.ReactNode
|
|
14
|
-
description?: React.ReactNode
|
|
15
|
-
action?: ToastActionElement
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const actionTypes = {
|
|
19
|
-
ADD_TOAST: "ADD_TOAST",
|
|
20
|
-
UPDATE_TOAST: "UPDATE_TOAST",
|
|
21
|
-
DISMISS_TOAST: "DISMISS_TOAST",
|
|
22
|
-
REMOVE_TOAST: "REMOVE_TOAST",
|
|
23
|
-
} as const
|
|
24
|
-
|
|
25
|
-
let count = 0
|
|
26
|
-
|
|
27
|
-
function genId() {
|
|
28
|
-
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
29
|
-
return count.toString()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
type ActionType = typeof actionTypes
|
|
33
|
-
|
|
34
|
-
type Action =
|
|
35
|
-
| {
|
|
36
|
-
type: ActionType["ADD_TOAST"]
|
|
37
|
-
toast: ToasterToast
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
type: ActionType["UPDATE_TOAST"]
|
|
41
|
-
toast: Partial<ToasterToast>
|
|
42
|
-
}
|
|
43
|
-
| {
|
|
44
|
-
type: ActionType["DISMISS_TOAST"]
|
|
45
|
-
toastId?: ToasterToast["id"]
|
|
46
|
-
}
|
|
47
|
-
| {
|
|
48
|
-
type: ActionType["REMOVE_TOAST"]
|
|
49
|
-
toastId?: ToasterToast["id"]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
interface State {
|
|
53
|
-
toasts: ToasterToast[]
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
|
57
|
-
|
|
58
|
-
const addToRemoveQueue = (toastId: string) => {
|
|
59
|
-
if (toastTimeouts.has(toastId)) {
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const timeout = setTimeout(() => {
|
|
64
|
-
toastTimeouts.delete(toastId)
|
|
65
|
-
dispatch({
|
|
66
|
-
type: "REMOVE_TOAST",
|
|
67
|
-
toastId: toastId,
|
|
68
|
-
})
|
|
69
|
-
}, TOAST_REMOVE_DELAY)
|
|
70
|
-
|
|
71
|
-
toastTimeouts.set(toastId, timeout)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const reducer = (state: State, action: Action): State => {
|
|
75
|
-
switch (action.type) {
|
|
76
|
-
case "ADD_TOAST":
|
|
77
|
-
return {
|
|
78
|
-
...state,
|
|
79
|
-
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
case "UPDATE_TOAST":
|
|
83
|
-
return {
|
|
84
|
-
...state,
|
|
85
|
-
toasts: state.toasts.map((t) =>
|
|
86
|
-
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
|
87
|
-
),
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
case "DISMISS_TOAST": {
|
|
91
|
-
const { toastId } = action
|
|
92
|
-
|
|
93
|
-
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
94
|
-
// but I'll keep it here for simplicity
|
|
95
|
-
if (toastId) {
|
|
96
|
-
addToRemoveQueue(toastId)
|
|
97
|
-
} else {
|
|
98
|
-
state.toasts.forEach((toast) => {
|
|
99
|
-
addToRemoveQueue(toast.id)
|
|
100
|
-
})
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
...state,
|
|
105
|
-
toasts: state.toasts.map((t) =>
|
|
106
|
-
t.id === toastId || toastId === undefined
|
|
107
|
-
? {
|
|
108
|
-
...t,
|
|
109
|
-
open: false,
|
|
110
|
-
}
|
|
111
|
-
: t,
|
|
112
|
-
),
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
case "REMOVE_TOAST":
|
|
116
|
-
if (action.toastId === undefined) {
|
|
117
|
-
return {
|
|
118
|
-
...state,
|
|
119
|
-
toasts: [],
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return {
|
|
123
|
-
...state,
|
|
124
|
-
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const listeners: Array<(state: State) => void> = []
|
|
130
|
-
|
|
131
|
-
let memoryState: State = { toasts: [] }
|
|
132
|
-
|
|
133
|
-
function dispatch(action: Action) {
|
|
134
|
-
memoryState = reducer(memoryState, action)
|
|
135
|
-
listeners.forEach((listener) => {
|
|
136
|
-
listener(memoryState)
|
|
137
|
-
})
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
type Toast = Omit<ToasterToast, "id">
|
|
141
|
-
|
|
142
|
-
function toast({ ...props }: Toast) {
|
|
143
|
-
const id = genId()
|
|
144
|
-
|
|
145
|
-
const update = (props: ToasterToast) =>
|
|
146
|
-
dispatch({
|
|
147
|
-
type: "UPDATE_TOAST",
|
|
148
|
-
toast: { ...props, id },
|
|
149
|
-
})
|
|
150
|
-
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
151
|
-
|
|
152
|
-
dispatch({
|
|
153
|
-
type: "ADD_TOAST",
|
|
154
|
-
toast: {
|
|
155
|
-
...props,
|
|
156
|
-
id,
|
|
157
|
-
open: true,
|
|
158
|
-
onOpenChange: (open) => {
|
|
159
|
-
if (!open) dismiss()
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
id: id,
|
|
166
|
-
dismiss,
|
|
167
|
-
update,
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function useToast() {
|
|
172
|
-
const [state, setState] = React.useState<State>(memoryState)
|
|
173
|
-
|
|
174
|
-
React.useEffect(() => {
|
|
175
|
-
listeners.push(setState)
|
|
176
|
-
return () => {
|
|
177
|
-
const index = listeners.indexOf(setState)
|
|
178
|
-
if (index > -1) {
|
|
179
|
-
listeners.splice(index, 1)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}, [state])
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
...state,
|
|
186
|
-
toast,
|
|
187
|
-
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export { useToast, toast }
|