gbs-add-block 0.0.1
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/index.js +108 -0
- package/package.json +28 -0
- package/source/components/button/index.tsx +25 -0
- package/source/components/checkbox/index.tsx +9 -0
- package/source/components/darkmode/index.tsx +48 -0
- package/source/components/datepicker/DatePickerHelper.ts +42 -0
- package/source/components/datepicker/index.tsx +258 -0
- package/source/components/datepicker/types.ts +9 -0
- package/source/components/dialog/index.tsx +44 -0
- package/source/components/dialog/types.ts +12 -0
- package/source/components/grid/FilterPopup.tsx +85 -0
- package/source/components/grid/Grid.tsx +615 -0
- package/source/components/grid/GridHelperFunctions.ts +218 -0
- package/source/components/grid/index.tsx +26 -0
- package/source/components/grid/type.ts +28 -0
- package/source/components/input/index.tsx +105 -0
- package/source/components/input/types.ts +9 -0
- package/source/components/modal/index.tsx +58 -0
- package/source/components/modal/types.ts +11 -0
- package/source/components/multiselect/index.tsx +213 -0
- package/source/components/multiselect/types.ts +14 -0
- package/source/components/select/index.tsx +220 -0
- package/source/components/select/types.ts +13 -0
- package/source/components/spinner/Stroke.tsx +89 -0
- package/source/components/spinner/index.tsx +54 -0
- package/source/components/spinner/types.ts +9 -0
- package/source/components/toast/Toast.tsx +61 -0
- package/source/components/toast/index.tsx +36 -0
- package/source/components/toast/toastAtom.ts +23 -0
- package/source/components/toast/types.ts +24 -0
- package/source/components/toast/useToast.ts +24 -0
- package/source/fallback/index.tsx +11 -0
- package/source/icon/Icon.tsx +55 -0
- package/source/icon/iconPaths.ts +92 -0
- package/source/index.ts +15 -0
- package/source/types.ts +108 -0
- package/source/utils.ts +10 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, {
|
|
9
|
+
forwardRef,
|
|
10
|
+
memo,
|
|
11
|
+
useCallback,
|
|
12
|
+
useEffect,
|
|
13
|
+
useImperativeHandle,
|
|
14
|
+
useMemo,
|
|
15
|
+
useRef,
|
|
16
|
+
useState,
|
|
17
|
+
} from "react";
|
|
18
|
+
import Icon from "../icon/Icon";
|
|
19
|
+
import { check, search, upDown, x } from "../icon/iconPaths";
|
|
20
|
+
import { getSourceData } from "../utils";
|
|
21
|
+
import type { SelectProps } from "./types";
|
|
22
|
+
|
|
23
|
+
interface ItemsProps {
|
|
24
|
+
value: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const Select = forwardRef<any, SelectProps>((props, ref) => {
|
|
29
|
+
const {
|
|
30
|
+
placeholder = "Select an Item...",
|
|
31
|
+
items,
|
|
32
|
+
lazy = false,
|
|
33
|
+
showSearch = true,
|
|
34
|
+
onSelect,
|
|
35
|
+
selectedItem: initialSelectedItem,
|
|
36
|
+
} = props;
|
|
37
|
+
|
|
38
|
+
const [showPopover, setShowPopover] = useState(false);
|
|
39
|
+
const [workingDataSource, setWorkingDataSource] = useState<ItemsProps[]>([]);
|
|
40
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
41
|
+
const [selectedItem, setSelectedItem] = useState<string | undefined>(
|
|
42
|
+
initialSelectedItem
|
|
43
|
+
);
|
|
44
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
45
|
+
const selectRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
|
|
47
|
+
// This will close the popup if clicked outside of the component
|
|
48
|
+
const handleClickOutside = useCallback((event: MouseEvent) => {
|
|
49
|
+
if (
|
|
50
|
+
selectRef.current &&
|
|
51
|
+
!selectRef.current.contains(event.target as Node)
|
|
52
|
+
) {
|
|
53
|
+
setShowPopover(false);
|
|
54
|
+
}
|
|
55
|
+
}, []);
|
|
56
|
+
|
|
57
|
+
// This will close the popup if clicked outside of the component
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
60
|
+
return () => {
|
|
61
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
62
|
+
};
|
|
63
|
+
}, [handleClickOutside]);
|
|
64
|
+
|
|
65
|
+
// Focus to input feild when popup opens
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (showPopover) {
|
|
68
|
+
inputRef.current?.focus();
|
|
69
|
+
}
|
|
70
|
+
}, [showPopover]);
|
|
71
|
+
|
|
72
|
+
// Handles if items is an API URL
|
|
73
|
+
const getSelectItems = useCallback(async (itemsApi: string) => {
|
|
74
|
+
const itemsData = await getSourceData(itemsApi);
|
|
75
|
+
setWorkingDataSource(itemsData.sourcedata);
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (Array.isArray(items)) {
|
|
80
|
+
setWorkingDataSource(items);
|
|
81
|
+
} else if (typeof items === "string") {
|
|
82
|
+
getSelectItems(items);
|
|
83
|
+
}
|
|
84
|
+
}, [items, getSelectItems]);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
setSelectedItem(initialSelectedItem);
|
|
88
|
+
}, [initialSelectedItem]);
|
|
89
|
+
|
|
90
|
+
//Sets if a default value is passed
|
|
91
|
+
const selectedDisplay = useMemo(() => {
|
|
92
|
+
if (selectedItem && workingDataSource.length > 0) {
|
|
93
|
+
const selected = workingDataSource.find(
|
|
94
|
+
(item) => item.value === selectedItem
|
|
95
|
+
);
|
|
96
|
+
return selected ? selected.label : "";
|
|
97
|
+
}
|
|
98
|
+
return "";
|
|
99
|
+
}, [selectedItem, workingDataSource]);
|
|
100
|
+
|
|
101
|
+
// Toggles Popover
|
|
102
|
+
const togglePopover = useCallback((e: React.MouseEvent) => {
|
|
103
|
+
e.stopPropagation();
|
|
104
|
+
setShowPopover((prev) => !prev);
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
// Handles Selection of items
|
|
108
|
+
const handleSelect = useCallback(
|
|
109
|
+
(value: string) => {
|
|
110
|
+
setSelectedItem(value);
|
|
111
|
+
setShowPopover(false);
|
|
112
|
+
setSearchTerm("");
|
|
113
|
+
if (onSelect) onSelect(value);
|
|
114
|
+
},
|
|
115
|
+
[onSelect]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Clears Selection
|
|
119
|
+
const clearSelected = useCallback(() => {
|
|
120
|
+
setSelectedItem(undefined);
|
|
121
|
+
if (onSelect) onSelect("");
|
|
122
|
+
setShowPopover(false);
|
|
123
|
+
setSearchTerm("");
|
|
124
|
+
}, [onSelect]);
|
|
125
|
+
|
|
126
|
+
// Filtering logic
|
|
127
|
+
const filteredItems = useMemo(() => {
|
|
128
|
+
if (!searchTerm || lazy) return workingDataSource;
|
|
129
|
+
return workingDataSource.filter((item) =>
|
|
130
|
+
Object.values(item).some((val) =>
|
|
131
|
+
val.toString().toLowerCase().includes(searchTerm.toLowerCase())
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
}, [workingDataSource, searchTerm, lazy]);
|
|
135
|
+
|
|
136
|
+
// Making Select Functions Accessible in Paren
|
|
137
|
+
useImperativeHandle(ref, () => ({
|
|
138
|
+
workingDataSource,
|
|
139
|
+
items,
|
|
140
|
+
clearSelected,
|
|
141
|
+
togglePopover,
|
|
142
|
+
getSelectItems,
|
|
143
|
+
selectedDisplay,
|
|
144
|
+
selected: selectedItem,
|
|
145
|
+
}));
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="relative" ref={selectRef}>
|
|
149
|
+
<div className="w-[200px] relative">
|
|
150
|
+
<button
|
|
151
|
+
className="flex items-center border px-4 py-2 w-[200px] justify-between rounded-lg font-medium text-sm dark:bg-black dark:border-white dark:text-white"
|
|
152
|
+
onClick={togglePopover}
|
|
153
|
+
>
|
|
154
|
+
{selectedDisplay || placeholder}
|
|
155
|
+
<Icon
|
|
156
|
+
elements={upDown}
|
|
157
|
+
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
158
|
+
/>
|
|
159
|
+
</button>
|
|
160
|
+
{selectedDisplay && (
|
|
161
|
+
<button
|
|
162
|
+
className="absolute right-8 top-0 h-full flex items-center px-2 z-20"
|
|
163
|
+
onClick={clearSelected}
|
|
164
|
+
>
|
|
165
|
+
<Icon
|
|
166
|
+
elements={x}
|
|
167
|
+
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
168
|
+
/>
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
|
|
173
|
+
{showPopover && (
|
|
174
|
+
<div className="w-[200px] absolute overflow-y-auto border px-2 rounded-lg mt-[1px] scrollbar bg-white z-50 scrollbar h-auto dark:bg-black dark:text-white">
|
|
175
|
+
{showSearch && (
|
|
176
|
+
<div className="flex p-2 gap-1 items-center sticky top-0 bg-white border-b dark:bg-black">
|
|
177
|
+
<Icon
|
|
178
|
+
elements={search}
|
|
179
|
+
svgClass="stroke-gray-500 fill-none dark:stroke-white"
|
|
180
|
+
/>
|
|
181
|
+
<input
|
|
182
|
+
autoComplete="off"
|
|
183
|
+
type="text"
|
|
184
|
+
name="search"
|
|
185
|
+
id="search"
|
|
186
|
+
placeholder="Search a value"
|
|
187
|
+
className="w-full outline-none dark:bg-black"
|
|
188
|
+
ref={inputRef}
|
|
189
|
+
value={searchTerm}
|
|
190
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
191
|
+
/>
|
|
192
|
+
</div>
|
|
193
|
+
)}
|
|
194
|
+
{filteredItems.length > 0 ? (
|
|
195
|
+
filteredItems.map(({ value, label }) => (
|
|
196
|
+
<button
|
|
197
|
+
key={value}
|
|
198
|
+
className="flex items-center w-full px-2 py-1 text-left hover:bg-blue-100 gap-2 rounded-lg mt-1 text-sm dark:hover:bg-blue-600"
|
|
199
|
+
onClick={() => handleSelect(value)}
|
|
200
|
+
>
|
|
201
|
+
<Icon
|
|
202
|
+
elements={check}
|
|
203
|
+
svgClass={`h-4 w-4 fill-none ${
|
|
204
|
+
selectedItem === value ? "stroke-gray-500" : ""
|
|
205
|
+
}`}
|
|
206
|
+
/>
|
|
207
|
+
{label}
|
|
208
|
+
</button>
|
|
209
|
+
))
|
|
210
|
+
) : (
|
|
211
|
+
<div className="text-sm text-center">No Data Found</div>
|
|
212
|
+
)}
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const MemoizedSelect = memo(Select);
|
|
220
|
+
export { MemoizedSelect as Select };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface StrokeProps {
|
|
4
|
+
size?: string;
|
|
5
|
+
strokeColor?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function Stroke(props: StrokeProps) {
|
|
9
|
+
const { size, strokeColor } = props;
|
|
10
|
+
return (
|
|
11
|
+
<svg
|
|
12
|
+
className={`${size} animate-spin ${strokeColor}`}
|
|
13
|
+
viewBox="0 0 256 256"
|
|
14
|
+
>
|
|
15
|
+
<line
|
|
16
|
+
x1="128"
|
|
17
|
+
y1="32"
|
|
18
|
+
x2="128"
|
|
19
|
+
y2="64"
|
|
20
|
+
strokeLinecap="round"
|
|
21
|
+
strokeLinejoin="round"
|
|
22
|
+
strokeWidth="24"
|
|
23
|
+
></line>
|
|
24
|
+
<line
|
|
25
|
+
x1="195.9"
|
|
26
|
+
y1="60.1"
|
|
27
|
+
x2="173.3"
|
|
28
|
+
y2="82.7"
|
|
29
|
+
strokeLinecap="round"
|
|
30
|
+
strokeLinejoin="round"
|
|
31
|
+
strokeWidth="24"
|
|
32
|
+
></line>
|
|
33
|
+
<line
|
|
34
|
+
x1="224"
|
|
35
|
+
y1="128"
|
|
36
|
+
x2="192"
|
|
37
|
+
y2="128"
|
|
38
|
+
strokeLinecap="round"
|
|
39
|
+
strokeLinejoin="round"
|
|
40
|
+
strokeWidth="24"
|
|
41
|
+
></line>
|
|
42
|
+
<line
|
|
43
|
+
x1="195.9"
|
|
44
|
+
y1="195.9"
|
|
45
|
+
x2="173.3"
|
|
46
|
+
y2="173.3"
|
|
47
|
+
strokeLinecap="round"
|
|
48
|
+
strokeLinejoin="round"
|
|
49
|
+
strokeWidth="24"
|
|
50
|
+
></line>
|
|
51
|
+
<line
|
|
52
|
+
x1="128"
|
|
53
|
+
y1="224"
|
|
54
|
+
x2="128"
|
|
55
|
+
y2="192"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
strokeWidth="24"
|
|
59
|
+
></line>
|
|
60
|
+
<line
|
|
61
|
+
x1="60.1"
|
|
62
|
+
y1="195.9"
|
|
63
|
+
x2="82.7"
|
|
64
|
+
y2="173.3"
|
|
65
|
+
strokeLinecap="round"
|
|
66
|
+
strokeLinejoin="round"
|
|
67
|
+
strokeWidth="24"
|
|
68
|
+
></line>
|
|
69
|
+
<line
|
|
70
|
+
x1="32"
|
|
71
|
+
y1="128"
|
|
72
|
+
x2="64"
|
|
73
|
+
y2="128"
|
|
74
|
+
strokeLinecap="round"
|
|
75
|
+
strokeLinejoin="round"
|
|
76
|
+
strokeWidth="24"
|
|
77
|
+
></line>
|
|
78
|
+
<line
|
|
79
|
+
x1="60.1"
|
|
80
|
+
y1="60.1"
|
|
81
|
+
x2="82.7"
|
|
82
|
+
y2="82.7"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
strokeLinejoin="round"
|
|
85
|
+
strokeWidth="24"
|
|
86
|
+
></line>
|
|
87
|
+
</svg>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import Stroke from "./Stroke";
|
|
10
|
+
import type { SpinnerProps } from "./types";
|
|
11
|
+
|
|
12
|
+
export const Spinner: React.FC<SpinnerProps> = ({
|
|
13
|
+
size = "h-10 w-10",
|
|
14
|
+
color = "border-blue-600",
|
|
15
|
+
fullCircleColor = "border-t-red-400",
|
|
16
|
+
dotColor = "bg-blue-500",
|
|
17
|
+
duration = "duration-500",
|
|
18
|
+
type = "circle",
|
|
19
|
+
strokeColor = "stroke-blue-500",
|
|
20
|
+
}) => {
|
|
21
|
+
if (type === "circle") {
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`loader ease-linear rounded-full border-4 border-t-4 ${color} ${size} ${duration} border-t-transparent animate-spin`}
|
|
25
|
+
></div>
|
|
26
|
+
);
|
|
27
|
+
} else if (type === "circle-bg") {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={`${size} animate-spin rounded-full border-4 ${fullCircleColor}`}
|
|
31
|
+
></div>
|
|
32
|
+
);
|
|
33
|
+
} else if (type === "dot") {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex space-x-2 dark:invert">
|
|
36
|
+
<div
|
|
37
|
+
className={`${size} ${dotColor} rounded-full animate-bounce`}
|
|
38
|
+
style={{ animationDelay: "-0.3s" }}
|
|
39
|
+
></div>
|
|
40
|
+
<div
|
|
41
|
+
className={`${size} ${dotColor} rounded-full animate-bounce`}
|
|
42
|
+
style={{ animationDelay: "-0.15s" }}
|
|
43
|
+
></div>
|
|
44
|
+
<div
|
|
45
|
+
className={`${size} ${dotColor} rounded-full animate-bounce`}
|
|
46
|
+
></div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
} else if (type === "stroke") {
|
|
50
|
+
return <Stroke size={size} strokeColor={strokeColor} />;
|
|
51
|
+
} else {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Toast as ToastType } from "./types";
|
|
3
|
+
import { useToastStore } from "./toastAtom";
|
|
4
|
+
import Icon from "../icon/Icon";
|
|
5
|
+
import { circleCheck, info, x, xCircle } from "../icon/iconPaths";
|
|
6
|
+
|
|
7
|
+
interface ToastProps {
|
|
8
|
+
toast: ToastType;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Toast: React.FC<ToastProps> = ({ toast }) => {
|
|
12
|
+
const { dismissToast } = useToastStore();
|
|
13
|
+
const icons = {
|
|
14
|
+
success: (
|
|
15
|
+
<Icon
|
|
16
|
+
elements={circleCheck}
|
|
17
|
+
svgClass="h-4 w-4 stroke-green-500 fill-none dark:stroke-white"
|
|
18
|
+
/>
|
|
19
|
+
),
|
|
20
|
+
error: (
|
|
21
|
+
<Icon
|
|
22
|
+
elements={xCircle}
|
|
23
|
+
svgClass="h-4 w-4 stroke-red-500 fill-none dark:stroke-white"
|
|
24
|
+
/>
|
|
25
|
+
),
|
|
26
|
+
info: (
|
|
27
|
+
<Icon
|
|
28
|
+
elements={info}
|
|
29
|
+
svgClass="h-4 w-4 stroke-blue-500 fill-none dark:stroke-white"
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
className={`bg-white px-2 py-3 rounded-lg text-sm flex justify-between w-80 m-2 border shadow-lg ${
|
|
37
|
+
toast.type === "success"
|
|
38
|
+
? "text-green-500"
|
|
39
|
+
: toast.type === "error"
|
|
40
|
+
? "text-red-500"
|
|
41
|
+
: "text-blue-500"
|
|
42
|
+
}`}
|
|
43
|
+
role="alert"
|
|
44
|
+
>
|
|
45
|
+
<div className="flex gap-2 items-center">
|
|
46
|
+
{icons[toast.type]}
|
|
47
|
+
<div>{toast.message}</div>
|
|
48
|
+
</div>
|
|
49
|
+
{toast.dismissible && (
|
|
50
|
+
<button onClick={() => dismissToast(toast.id)}>
|
|
51
|
+
<Icon
|
|
52
|
+
elements={x}
|
|
53
|
+
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default Toast;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { useToastStore } from "./toastAtom";
|
|
10
|
+
import Toast from "./Toast";
|
|
11
|
+
import { ToastPosition } from "./types";
|
|
12
|
+
|
|
13
|
+
interface ToastsProps {
|
|
14
|
+
position?: ToastPosition;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const positionClasses: Record<ToastPosition, string> = {
|
|
18
|
+
"top-left": "top-0 left-0",
|
|
19
|
+
"top-right": "top-0 right-0",
|
|
20
|
+
"bottom-left": "bottom-0 left-0",
|
|
21
|
+
"bottom-right": "bottom-0 right-0",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Toasts: React.FC<ToastsProps> = ({ position = "top-right" }) => {
|
|
25
|
+
const { toasts } = useToastStore();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<section
|
|
29
|
+
className={`fixed flex flex-col z-50 p-4 ${positionClasses[position]}`}
|
|
30
|
+
>
|
|
31
|
+
{toasts.map((toast) => (
|
|
32
|
+
<Toast key={toast.id} toast={toast} />
|
|
33
|
+
))}
|
|
34
|
+
</section>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { atom, useAtom } from "jotai";
|
|
2
|
+
import { Toast } from "./types";
|
|
3
|
+
|
|
4
|
+
export const toastsAtom = atom<Toast[]>([]);
|
|
5
|
+
|
|
6
|
+
export const useToastStore = () => {
|
|
7
|
+
const [toasts, setToasts] = useAtom(toastsAtom);
|
|
8
|
+
|
|
9
|
+
const addToast = (toast: Omit<Toast, "id">) => {
|
|
10
|
+
const id = Math.floor(Math.random() * 10000);
|
|
11
|
+
const newToast = { ...toast, id };
|
|
12
|
+
setToasts((prev) => [newToast, ...prev]);
|
|
13
|
+
if (toast.timeout) {
|
|
14
|
+
setTimeout(() => dismissToast(id), toast.timeout);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const dismissToast = (id: number) => {
|
|
19
|
+
setToasts((prev) => prev.filter((t) => t.id !== id));
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return { toasts, addToast, dismissToast };
|
|
23
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ToastType = "success" | "error" | "info";
|
|
4
|
+
|
|
5
|
+
export type Toast = {
|
|
6
|
+
id: number;
|
|
7
|
+
type: ToastType;
|
|
8
|
+
message: string | ReactNode;
|
|
9
|
+
dismissible: boolean;
|
|
10
|
+
timeout: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ToastOptions = {
|
|
14
|
+
message: string | ReactNode;
|
|
15
|
+
type?: ToastType;
|
|
16
|
+
dismissible?: boolean;
|
|
17
|
+
timeout?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ToastPosition =
|
|
21
|
+
| "top-left"
|
|
22
|
+
| "top-right"
|
|
23
|
+
| "bottom-left"
|
|
24
|
+
| "bottom-right";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ToastOptions } from "./types";
|
|
2
|
+
import { useToastStore } from "./toastAtom";
|
|
3
|
+
|
|
4
|
+
const useToast = () => {
|
|
5
|
+
const { addToast, dismissToast } = useToastStore();
|
|
6
|
+
|
|
7
|
+
return {
|
|
8
|
+
addToast: ({
|
|
9
|
+
message,
|
|
10
|
+
type = "info",
|
|
11
|
+
dismissible = false,
|
|
12
|
+
timeout = 3000,
|
|
13
|
+
}: ToastOptions) =>
|
|
14
|
+
addToast({
|
|
15
|
+
message,
|
|
16
|
+
type,
|
|
17
|
+
dismissible,
|
|
18
|
+
timeout,
|
|
19
|
+
}),
|
|
20
|
+
dismissToast,
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useToast;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type SvgElement = {
|
|
4
|
+
type: "path" | "circle";
|
|
5
|
+
key: string;
|
|
6
|
+
d?: string;
|
|
7
|
+
cx?: number;
|
|
8
|
+
cy?: number;
|
|
9
|
+
r?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type IconProps = {
|
|
13
|
+
svgClass?: string;
|
|
14
|
+
elements?: SvgElement[];
|
|
15
|
+
dimensions?: any;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function Icon({
|
|
19
|
+
svgClass = "stroke-none fill-none",
|
|
20
|
+
elements = [
|
|
21
|
+
{ type: "path", d: "m11 17-5-5 5-5", key: "13zhaf" },
|
|
22
|
+
{ type: "path", d: "m18 17-5-5 5-5", key: "h8a8et" },
|
|
23
|
+
{ type: "circle", cx: 12, cy: 12, r: 5, key: "circle1" },
|
|
24
|
+
],
|
|
25
|
+
dimensions = { width: "24", height: "24" },
|
|
26
|
+
}: IconProps) {
|
|
27
|
+
return (
|
|
28
|
+
<svg
|
|
29
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
30
|
+
width={dimensions.width}
|
|
31
|
+
height={dimensions.height}
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
strokeWidth="2"
|
|
34
|
+
strokeLinecap="round"
|
|
35
|
+
strokeLinejoin="round"
|
|
36
|
+
className={svgClass}
|
|
37
|
+
>
|
|
38
|
+
{elements.map((element) => {
|
|
39
|
+
if (element.type === "path") {
|
|
40
|
+
return <path d={element.d} key={element.key} />;
|
|
41
|
+
} else if (element.type === "circle") {
|
|
42
|
+
return (
|
|
43
|
+
<circle
|
|
44
|
+
cx={element.cx}
|
|
45
|
+
cy={element.cy}
|
|
46
|
+
r={element.r}
|
|
47
|
+
key={element.key}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
})}
|
|
53
|
+
</svg>
|
|
54
|
+
);
|
|
55
|
+
}
|