@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
|
|
1
|
+
import { useState } from "react"
|
|
2
2
|
import {
|
|
3
3
|
Select,
|
|
4
4
|
SelectContent,
|
|
@@ -110,12 +110,12 @@ export const EditPackageDetailsDialog = ({
|
|
|
110
110
|
|
|
111
111
|
const response = await axios.post("/packages/update", {
|
|
112
112
|
package_id: packageId,
|
|
113
|
-
description: formData.description,
|
|
114
|
-
website: formData.website,
|
|
113
|
+
description: formData.description.trim(),
|
|
114
|
+
website: formData.website.trim(),
|
|
115
115
|
is_private: formData.visibility == "private",
|
|
116
116
|
default_view: formData.defaultView,
|
|
117
117
|
...(formData.unscopedPackageName !== unscopedPackageName && {
|
|
118
|
-
name: formData.unscopedPackageName,
|
|
118
|
+
name: formData.unscopedPackageName.trim(),
|
|
119
119
|
}),
|
|
120
120
|
})
|
|
121
121
|
if (response.status !== 200)
|
|
@@ -148,12 +148,7 @@ export const EditPackageDetailsDialog = ({
|
|
|
148
148
|
})
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
-
|
|
152
|
-
"formData.unscopedPackageName",
|
|
153
|
-
formData.unscopedPackageName,
|
|
154
|
-
"unscopedPackageName",
|
|
155
|
-
unscopedPackageName,
|
|
156
|
-
)
|
|
151
|
+
|
|
157
152
|
if (formData.unscopedPackageName !== unscopedPackageName) {
|
|
158
153
|
// Use router for client-side navigation
|
|
159
154
|
window.history.replaceState(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useAxios } from "@/hooks/use-axios"
|
|
2
2
|
import { useDebounce } from "@/hooks/use-debounce"
|
|
3
|
-
import type {
|
|
3
|
+
import type { Package } from "fake-snippets-api/lib/db/schema"
|
|
4
4
|
import { useState } from "react"
|
|
5
5
|
import { useQuery } from "react-query"
|
|
6
6
|
import { Button } from "../ui/button"
|
|
@@ -8,25 +8,25 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from "../ui/dialog"
|
|
|
8
8
|
import { Input } from "../ui/input"
|
|
9
9
|
import { createUseDialog } from "./create-use-dialog"
|
|
10
10
|
|
|
11
|
-
export const
|
|
11
|
+
export const ImportPackageDialog = ({
|
|
12
12
|
open,
|
|
13
13
|
onOpenChange,
|
|
14
|
-
|
|
14
|
+
onPackageSelected,
|
|
15
15
|
}: {
|
|
16
16
|
open: boolean
|
|
17
17
|
onOpenChange: (open: boolean) => any
|
|
18
|
-
|
|
18
|
+
onPackageSelected: (pkg: Package) => any
|
|
19
19
|
}) => {
|
|
20
20
|
const [searchText, setSearchText] = useState("")
|
|
21
21
|
const debouncedSearch = useDebounce(searchText, 300)
|
|
22
22
|
const axios = useAxios()
|
|
23
23
|
const { data: snippets, isLoading } = useQuery(
|
|
24
|
-
["
|
|
24
|
+
["packageSearch", debouncedSearch],
|
|
25
25
|
async () => {
|
|
26
|
-
const response = await axios.
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
return response.data.
|
|
26
|
+
const response = await axios.post("/packages/search", {
|
|
27
|
+
query: debouncedSearch,
|
|
28
|
+
})
|
|
29
|
+
return response.data.packages
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
enabled: debouncedSearch.length > 0,
|
|
@@ -35,41 +35,42 @@ export const ImportSnippetDialog = ({
|
|
|
35
35
|
|
|
36
36
|
return (
|
|
37
37
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
38
|
-
<DialogContent className="z-[100]">
|
|
38
|
+
<DialogContent className="z-[100] p-4 sm:p-6">
|
|
39
39
|
<DialogHeader>
|
|
40
|
-
<DialogTitle>Import
|
|
40
|
+
<DialogTitle>Import Package</DialogTitle>
|
|
41
41
|
</DialogHeader>
|
|
42
42
|
<Input
|
|
43
|
-
placeholder="Search
|
|
43
|
+
placeholder="Search packages..."
|
|
44
44
|
value={searchText}
|
|
45
45
|
onChange={(e) => setSearchText(e.target.value)}
|
|
46
|
+
className="w-full mb-4"
|
|
46
47
|
/>
|
|
47
48
|
<div className="h-64 overflow-y-auto">
|
|
48
49
|
{isLoading ? (
|
|
49
|
-
<div>Loading...</div>
|
|
50
|
+
<div className="text-center">Loading...</div>
|
|
50
51
|
) : (
|
|
51
52
|
<ul className="w-full">
|
|
52
|
-
{snippets?.map((
|
|
53
|
+
{snippets?.map((pkg: Package) => (
|
|
53
54
|
<li
|
|
54
|
-
className="flex items-center my-
|
|
55
|
-
key={
|
|
55
|
+
className="flex flex-col sm:flex-row items-start sm:items-center my-2 text-sm w-full"
|
|
56
|
+
key={pkg.package_id}
|
|
56
57
|
>
|
|
57
58
|
<a
|
|
58
|
-
href={`/${
|
|
59
|
+
href={`/${pkg.name}`}
|
|
59
60
|
target="_blank"
|
|
60
|
-
className="
|
|
61
|
+
className="text-blue-500 hover:underline cursor-pointer flex-shrink-0 mb-1 sm:mb-0 sm:mr-2"
|
|
61
62
|
>
|
|
62
|
-
{
|
|
63
|
+
{pkg.name}
|
|
63
64
|
</a>
|
|
64
|
-
<div className="text-
|
|
65
|
-
{
|
|
65
|
+
<div className="text-gray-500 flex-grow overflow-hidden text-ellipsis whitespace-nowrap mb-1 sm:mb-0">
|
|
66
|
+
{pkg.description}
|
|
66
67
|
</div>
|
|
67
68
|
<Button
|
|
68
69
|
size="sm"
|
|
69
|
-
className="
|
|
70
|
+
className="flex-shrink-0"
|
|
70
71
|
variant="outline"
|
|
71
72
|
onClick={() => {
|
|
72
|
-
|
|
73
|
+
onPackageSelected(pkg)
|
|
73
74
|
onOpenChange(false)
|
|
74
75
|
}}
|
|
75
76
|
>
|
|
@@ -85,4 +86,4 @@ export const ImportSnippetDialog = ({
|
|
|
85
86
|
)
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
export const
|
|
89
|
+
export const useImportPackageDialog = createUseDialog(ImportPackageDialog)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useCallback } from "react"
|
|
2
2
|
import { Button } from "@/components/ui/button"
|
|
3
3
|
import { handleManualEditsImportWithSupportForMultipleFiles } from "@/lib/handleManualEditsImportWithSupportForMultipleFiles"
|
|
4
|
-
import {
|
|
4
|
+
import { useImportPackageDialog } from "@/components/dialogs/import-package-dialog"
|
|
5
5
|
import { useToast } from "@/hooks/use-toast"
|
|
6
6
|
import {
|
|
7
7
|
DropdownMenu,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
SelectValue,
|
|
20
20
|
} from "../ui/select"
|
|
21
21
|
import { isHiddenFile } from "../ViewPackagePage/utils/is-hidden-file"
|
|
22
|
+
import { Package } from "fake-snippets-api/lib/db/schema"
|
|
22
23
|
|
|
23
24
|
export type FileName = string
|
|
24
25
|
|
|
@@ -39,8 +40,8 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
39
40
|
handleFileChange,
|
|
40
41
|
entrypointFileName = "index.tsx",
|
|
41
42
|
}) => {
|
|
42
|
-
const { Dialog:
|
|
43
|
-
|
|
43
|
+
const { Dialog: ImportPackageDialog, openDialog: openImportDialog } =
|
|
44
|
+
useImportPackageDialog()
|
|
44
45
|
const { toast } = useToast()
|
|
45
46
|
const [sidebarOpen, setSidebarOpen] = fileSidebarState
|
|
46
47
|
|
|
@@ -234,9 +235,9 @@ export const CodeEditorHeader: React.FC<CodeEditorHeaderProps> = ({
|
|
|
234
235
|
Format
|
|
235
236
|
</Button>
|
|
236
237
|
</div>
|
|
237
|
-
<
|
|
238
|
-
|
|
239
|
-
const newContent = `import {} from "@tsci/${
|
|
238
|
+
<ImportPackageDialog
|
|
239
|
+
onPackageSelected={(pkg: Package) => {
|
|
240
|
+
const newContent = `import {} from "@tsci/${pkg.owner_github_username}.${pkg.unscoped_name}"\n${files[currentFile || ""]}`
|
|
240
241
|
updateFileContent(currentFile, newContent)
|
|
241
242
|
}}
|
|
242
243
|
/>
|
|
@@ -1,33 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
Toast,
|
|
4
|
-
ToastClose,
|
|
5
|
-
ToastDescription,
|
|
6
|
-
ToastProvider,
|
|
7
|
-
ToastTitle,
|
|
8
|
-
ToastViewport,
|
|
9
|
-
} from "@/components/ui/toast"
|
|
10
|
-
|
|
11
|
-
export function Toaster() {
|
|
12
|
-
const { toasts } = useToast()
|
|
13
|
-
|
|
14
|
-
return (
|
|
15
|
-
<ToastProvider>
|
|
16
|
-
{toasts.map(function ({ id, title, description, action, ...props }) {
|
|
17
|
-
return (
|
|
18
|
-
<Toast key={id} {...props}>
|
|
19
|
-
<div className="grid gap-1">
|
|
20
|
-
{title && <ToastTitle>{title}</ToastTitle>}
|
|
21
|
-
{description && (
|
|
22
|
-
<ToastDescription>{description}</ToastDescription>
|
|
23
|
-
)}
|
|
24
|
-
</div>
|
|
25
|
-
{action}
|
|
26
|
-
<ToastClose />
|
|
27
|
-
</Toast>
|
|
28
|
-
)
|
|
29
|
-
})}
|
|
30
|
-
<ToastViewport />
|
|
31
|
-
</ToastProvider>
|
|
32
|
-
)
|
|
33
|
-
}
|
|
1
|
+
export { Toaster } from "react-hot-toast"
|
|
@@ -4,6 +4,7 @@ import { usePackageRelease } from "./use-package-release"
|
|
|
4
4
|
import { useUrlParams } from "./use-url-params"
|
|
5
5
|
|
|
6
6
|
export const useCurrentPackageRelease = (options?: {
|
|
7
|
+
include_ai_review?: boolean
|
|
7
8
|
include_logs?: boolean
|
|
8
9
|
refetchInterval?: number
|
|
9
10
|
}) => {
|
|
@@ -26,9 +27,17 @@ export const useCurrentPackageRelease = (options?: {
|
|
|
26
27
|
query = { package_id: packageId, is_latest: true }
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (query && options?.include_logs !== undefined) {
|
|
31
|
+
query.include_logs = options.include_logs
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (query && options?.include_ai_review !== undefined) {
|
|
35
|
+
query.include_ai_review = options.include_ai_review
|
|
36
|
+
}
|
|
37
|
+
|
|
29
38
|
const { data: packageRelease, ...rest } = usePackageRelease(query, {
|
|
30
|
-
include_logs: options?.include_logs ?? false,
|
|
31
39
|
refetchInterval: options?.refetchInterval,
|
|
32
40
|
})
|
|
41
|
+
|
|
33
42
|
return { packageRelease, ...rest }
|
|
34
43
|
}
|
|
@@ -42,16 +42,17 @@ export const useForkPackageMutation = ({
|
|
|
42
42
|
onSuccess?.(result)
|
|
43
43
|
},
|
|
44
44
|
onError: (error: any) => {
|
|
45
|
-
|
|
45
|
+
const message = error?.data?.error?.message
|
|
46
|
+
if (error?.data?.error_code === "cannot_fork_own_package") {
|
|
46
47
|
toast({
|
|
47
48
|
title: "Cannot Fork Package",
|
|
48
|
-
description: "You cannot fork your own package.",
|
|
49
|
+
description: message || "You cannot fork your own package.",
|
|
49
50
|
})
|
|
50
51
|
return
|
|
51
52
|
}
|
|
52
53
|
toast({
|
|
53
54
|
title: "Error",
|
|
54
|
-
description: "Failed to fork package. Please try again.",
|
|
55
|
+
description: message || "Failed to fork package. Please try again.",
|
|
55
56
|
variant: "destructive",
|
|
56
57
|
})
|
|
57
58
|
},
|
|
@@ -2,7 +2,7 @@ import type { PackageRelease } from "fake-snippets-api/lib/db/schema"
|
|
|
2
2
|
import { type UseQueryOptions, useQuery } from "react-query"
|
|
3
3
|
import { useAxios } from "./use-axios"
|
|
4
4
|
|
|
5
|
-
type PackageReleaseQuery =
|
|
5
|
+
type PackageReleaseQuery = (
|
|
6
6
|
| {
|
|
7
7
|
package_release_id: string
|
|
8
8
|
}
|
|
@@ -17,29 +17,30 @@ type PackageReleaseQuery =
|
|
|
17
17
|
package_id: string
|
|
18
18
|
is_latest: boolean
|
|
19
19
|
}
|
|
20
|
+
) & {
|
|
21
|
+
include_logs?: boolean | null | undefined
|
|
22
|
+
include_ai_review?: boolean | null | undefined
|
|
23
|
+
}
|
|
20
24
|
|
|
21
25
|
export const usePackageRelease = (
|
|
22
26
|
query: PackageReleaseQuery | null,
|
|
23
|
-
options?: {
|
|
27
|
+
options?: {
|
|
28
|
+
refetchInterval?: number
|
|
29
|
+
},
|
|
24
30
|
) => {
|
|
25
31
|
const axios = useAxios()
|
|
26
32
|
|
|
27
33
|
return useQuery<PackageRelease, Error & { status: number }>(
|
|
28
|
-
["packageRelease", query
|
|
34
|
+
["packageRelease", query],
|
|
29
35
|
async () => {
|
|
30
36
|
if (!query) return
|
|
31
37
|
|
|
32
|
-
const { data } = await axios.post(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
include_logs: true,
|
|
39
|
-
},
|
|
40
|
-
}
|
|
41
|
-
: undefined,
|
|
42
|
-
)
|
|
38
|
+
const { data } = await axios.post("/package_releases/get", query, {
|
|
39
|
+
params: {
|
|
40
|
+
include_logs: query.include_logs,
|
|
41
|
+
include_ai_review: query.include_ai_review,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
43
44
|
|
|
44
45
|
if (!data.package_release) {
|
|
45
46
|
throw new Error("Package release not found")
|
package/src/hooks/use-sign-in.ts
CHANGED
|
@@ -7,16 +7,18 @@ export const useSignIn = () => {
|
|
|
7
7
|
const isUsingFakeApi = useIsUsingFakeApi()
|
|
8
8
|
const setSession = useGlobalStore((s) => s.setSession)
|
|
9
9
|
return () => {
|
|
10
|
+
const currentUrl = window.location.href.replace("127.0.0.1", "localhost")
|
|
11
|
+
const nextUrl = `${window.location.origin.replace("127.0.0.1", "localhost")}/authorize?redirect=${encodeURIComponent(currentUrl)}`
|
|
10
12
|
if (!isUsingFakeApi) {
|
|
11
|
-
|
|
12
|
-
window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${nextUrl}/authorize`
|
|
13
|
+
window.location.href = `${snippetsBaseApiUrl}/internal/oauth/github/authorize?next=${encodeURIComponent(nextUrl)}`
|
|
13
14
|
} else {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
window.location.href = nextUrl
|
|
16
|
+
// setSession({
|
|
17
|
+
// account_id: "account-1234",
|
|
18
|
+
// github_username: "testuser",
|
|
19
|
+
// token: "1234",
|
|
20
|
+
// session_id: "session-1234",
|
|
21
|
+
// })
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
}
|
package/src/hooks/use-toast.tsx
CHANGED
|
@@ -1,191 +1,72 @@
|
|
|
1
|
-
import
|
|
1
|
+
import toastLibrary, { Toaster, type Toast } from "react-hot-toast"
|
|
2
|
+
import React from "react"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const TOAST_LIMIT = 1
|
|
6
|
-
const TOAST_REMOVE_DELAY = 1000000
|
|
7
|
-
|
|
8
|
-
export type ToasterToast = ToastProps & {
|
|
9
|
-
id: string
|
|
4
|
+
export interface ToasterToast {
|
|
10
5
|
title?: React.ReactNode
|
|
11
6
|
description?: React.ReactNode
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const actionTypes = {
|
|
16
|
-
ADD_TOAST: "ADD_TOAST",
|
|
17
|
-
UPDATE_TOAST: "UPDATE_TOAST",
|
|
18
|
-
DISMISS_TOAST: "DISMISS_TOAST",
|
|
19
|
-
REMOVE_TOAST: "REMOVE_TOAST",
|
|
20
|
-
} as const
|
|
21
|
-
|
|
22
|
-
let count = 0
|
|
23
|
-
|
|
24
|
-
function genId() {
|
|
25
|
-
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
|
26
|
-
return count.toString()
|
|
7
|
+
variant?: "default" | "destructive"
|
|
8
|
+
duration?: number
|
|
27
9
|
}
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
11
|
+
function ToastContent({
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
variant,
|
|
15
|
+
t,
|
|
16
|
+
}: ToasterToast & { t: Toast }) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className={`rounded-md border p-4 shadow-lg transition-all ${
|
|
20
|
+
t.visible
|
|
21
|
+
? "animate-in fade-in slide-in-from-top-full"
|
|
22
|
+
: "animate-out fade-out slide-out-to-right-full"
|
|
23
|
+
} ${
|
|
24
|
+
variant === "destructive"
|
|
25
|
+
? "border-red-500 bg-red-500 text-slate-50"
|
|
26
|
+
: "border-slate-200 bg-white text-slate-950 dark:bg-slate-950 dark:text-slate-50"
|
|
27
|
+
}`}
|
|
28
|
+
>
|
|
29
|
+
{title && <div className="text-sm font-semibold">{title}</div>}
|
|
30
|
+
{description && <div className="text-sm opacity-90">{description}</div>}
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
35
|
+
const toast = ({
|
|
36
|
+
duration,
|
|
37
|
+
description,
|
|
38
|
+
variant = "default",
|
|
39
|
+
title,
|
|
40
|
+
}: ToasterToast) => {
|
|
41
|
+
if (description) {
|
|
42
|
+
return toastLibrary.custom(
|
|
43
|
+
(t) => (
|
|
44
|
+
<ToastContent
|
|
45
|
+
title={title}
|
|
46
|
+
description={description}
|
|
47
|
+
variant={variant}
|
|
48
|
+
t={t}
|
|
49
|
+
/>
|
|
50
|
+
),
|
|
51
|
+
{ duration },
|
|
52
|
+
)
|
|
58
53
|
}
|
|
59
54
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
dispatch({
|
|
63
|
-
type: "REMOVE_TOAST",
|
|
64
|
-
toastId: toastId,
|
|
65
|
-
})
|
|
66
|
-
}, TOAST_REMOVE_DELAY)
|
|
67
|
-
|
|
68
|
-
toastTimeouts.set(toastId, timeout)
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export const reducer = (state: State, action: Action): State => {
|
|
72
|
-
switch (action.type) {
|
|
73
|
-
case "ADD_TOAST":
|
|
74
|
-
return {
|
|
75
|
-
...state,
|
|
76
|
-
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
case "UPDATE_TOAST":
|
|
80
|
-
return {
|
|
81
|
-
...state,
|
|
82
|
-
toasts: state.toasts.map((t) =>
|
|
83
|
-
t.id === action.toast.id ? { ...t, ...action.toast } : t,
|
|
84
|
-
),
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
case "DISMISS_TOAST": {
|
|
88
|
-
const { toastId } = action
|
|
89
|
-
|
|
90
|
-
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
|
91
|
-
// but I'll keep it here for simplicity
|
|
92
|
-
if (toastId) {
|
|
93
|
-
addToRemoveQueue(toastId)
|
|
94
|
-
} else {
|
|
95
|
-
state.toasts.forEach((toast) => {
|
|
96
|
-
addToRemoveQueue(toast.id)
|
|
97
|
-
})
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
...state,
|
|
102
|
-
toasts: state.toasts.map((t) =>
|
|
103
|
-
t.id === toastId || toastId === undefined
|
|
104
|
-
? {
|
|
105
|
-
...t,
|
|
106
|
-
open: false,
|
|
107
|
-
}
|
|
108
|
-
: t,
|
|
109
|
-
),
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
case "REMOVE_TOAST":
|
|
113
|
-
if (action.toastId === undefined) {
|
|
114
|
-
return {
|
|
115
|
-
...state,
|
|
116
|
-
toasts: [],
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
...state,
|
|
121
|
-
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
|
122
|
-
}
|
|
55
|
+
if (variant === "destructive") {
|
|
56
|
+
return toastLibrary.error(<>{title}</>, { duration })
|
|
123
57
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const listeners: Array<(state: State) => void> = []
|
|
127
|
-
|
|
128
|
-
let memoryState: State = { toasts: [] }
|
|
129
|
-
|
|
130
|
-
function dispatch(action: Action) {
|
|
131
|
-
memoryState = reducer(memoryState, action)
|
|
132
|
-
listeners.forEach((listener) => {
|
|
133
|
-
listener(memoryState)
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
type Toast = Omit<ToasterToast, "id">
|
|
138
58
|
|
|
139
|
-
|
|
140
|
-
const id = genId()
|
|
141
|
-
|
|
142
|
-
const update = (props: ToasterToast) =>
|
|
143
|
-
dispatch({
|
|
144
|
-
type: "UPDATE_TOAST",
|
|
145
|
-
toast: { ...props, id },
|
|
146
|
-
})
|
|
147
|
-
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
|
148
|
-
|
|
149
|
-
dispatch({
|
|
150
|
-
type: "ADD_TOAST",
|
|
151
|
-
toast: {
|
|
152
|
-
...props,
|
|
153
|
-
id,
|
|
154
|
-
open: true,
|
|
155
|
-
onOpenChange: (open) => {
|
|
156
|
-
if (!open) dismiss()
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
id: id,
|
|
163
|
-
dismiss,
|
|
164
|
-
update,
|
|
165
|
-
}
|
|
59
|
+
return toastLibrary(<>{title}</>, { duration })
|
|
166
60
|
}
|
|
167
61
|
|
|
168
62
|
function useToast() {
|
|
169
|
-
const [state, setState] = React.useState<State>(memoryState)
|
|
170
|
-
|
|
171
|
-
React.useEffect(() => {
|
|
172
|
-
listeners.push(setState)
|
|
173
|
-
return () => {
|
|
174
|
-
const index = listeners.indexOf(setState)
|
|
175
|
-
if (index > -1) {
|
|
176
|
-
listeners.splice(index, 1)
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}, [state])
|
|
180
|
-
|
|
181
63
|
return {
|
|
182
|
-
...state,
|
|
183
64
|
toast,
|
|
184
|
-
dismiss:
|
|
65
|
+
dismiss: toastLibrary.dismiss,
|
|
185
66
|
}
|
|
186
67
|
}
|
|
187
68
|
|
|
188
|
-
export { useToast, toast }
|
|
69
|
+
export { useToast, toast, Toaster }
|
|
189
70
|
|
|
190
71
|
export function useNotImplementedToast() {
|
|
191
72
|
const { toast } = useToast()
|