gbs-add-block 0.0.13 → 0.0.15-rc1100-beta
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/package.json +1 -1
- package/source/components/grid/Grid.tsx +32 -12
- package/source/components/grid/type.ts +2 -0
- package/source/components/input/index.tsx +4 -1
- package/source/components/multiselect/index.tsx +24 -14
- package/source/components/uploader/Uploader.tsx +115 -5
- package/source/components/uploader/uploaderService.ts +47 -16
- package/source/icon/iconPaths.ts +11 -0
package/package.json
CHANGED
|
@@ -53,7 +53,11 @@ export const Grid = forwardRef((props: GridProps, ref) => {
|
|
|
53
53
|
gridContainerClass = "flex flex-col min-w-screen border rounded-md overflow-hidden dark:bg-black",
|
|
54
54
|
gridColumnStyleSelectAll = "border-b px-4 text-sm dark:text-white",
|
|
55
55
|
gridColumnStyle = "border-b p-2 text-sm dark:text-white",
|
|
56
|
+
rowChange = () => {},
|
|
57
|
+
pageStatus = () => {},
|
|
56
58
|
} = props;
|
|
59
|
+
|
|
60
|
+
// States Handling Grid
|
|
57
61
|
const [workingDataSource, setWorkingDataSource] = useState<any>([]);
|
|
58
62
|
const [fallbackSourceData, setFallbackSourceData] = useState([]);
|
|
59
63
|
const [workingColumns, setWorkingColumns] = useState<any>([]);
|
|
@@ -65,6 +69,14 @@ export const Grid = forwardRef((props: GridProps, ref) => {
|
|
|
65
69
|
const [activeFilterArray, setActiveFilterArray] = useState<any>([]);
|
|
66
70
|
const [selectedRows, setSelectedRows] = useState<any[]>([]);
|
|
67
71
|
|
|
72
|
+
// This will returns the Page Navigation Status of Grid
|
|
73
|
+
// Such as totalPages in the grid, currentpage, etc..
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (pageStatus) {
|
|
76
|
+
pageStatus({ currentPage: currentPage, totalPages: totalPages });
|
|
77
|
+
}
|
|
78
|
+
}, [pageStatus, currentPage, totalPages]);
|
|
79
|
+
|
|
68
80
|
// Function to handle API datasource
|
|
69
81
|
const getGridData = useCallback(async () => {
|
|
70
82
|
try {
|
|
@@ -225,6 +237,9 @@ export const Grid = forwardRef((props: GridProps, ref) => {
|
|
|
225
237
|
<column.template
|
|
226
238
|
rowData={rowData}
|
|
227
239
|
rowIndex={rowIndex + currentPage * pageSettings.pageNumber}
|
|
240
|
+
rowChange={(changes: any) => {
|
|
241
|
+
if (rowChange) rowChange(changes);
|
|
242
|
+
}}
|
|
228
243
|
/>
|
|
229
244
|
);
|
|
230
245
|
}
|
|
@@ -330,19 +345,24 @@ export const Grid = forwardRef((props: GridProps, ref) => {
|
|
|
330
345
|
// RowSelection Handling Logic Ends Here ***
|
|
331
346
|
|
|
332
347
|
// Making Grid Functions Accessible in Parent
|
|
333
|
-
useImperativeHandle(
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
useImperativeHandle(
|
|
349
|
+
ref,
|
|
350
|
+
() => {
|
|
351
|
+
return {
|
|
352
|
+
goToPage,
|
|
353
|
+
nextPage,
|
|
354
|
+
prevPage,
|
|
355
|
+
goToFirstPage,
|
|
356
|
+
goToEndPage,
|
|
357
|
+
handleSearch,
|
|
358
|
+
handleApplyFilter,
|
|
359
|
+
clearFilter,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
[]
|
|
363
|
+
);
|
|
345
364
|
|
|
365
|
+
// JSX Rendering
|
|
346
366
|
return (
|
|
347
367
|
<div className={gridContainerClass}>
|
|
348
368
|
<React.Fragment>
|
|
@@ -74,6 +74,7 @@ export const Input = ({
|
|
|
74
74
|
{/* If Type is password, this will show a password visible toggle button */}
|
|
75
75
|
{props.type && props.type === "password" && (
|
|
76
76
|
<button
|
|
77
|
+
type="button"
|
|
77
78
|
className="absolute inset-y-0 right-0 flex items-center pr-2"
|
|
78
79
|
onClick={toggleTypeForPassword}
|
|
79
80
|
>
|
|
@@ -98,7 +99,9 @@ export const Input = ({
|
|
|
98
99
|
value={otpValues[index]}
|
|
99
100
|
onChange={(e) => updateOtpValue(index, e)}
|
|
100
101
|
onKeyDown={(e) => handleKeydown(index, e)}
|
|
101
|
-
|
|
102
|
+
ref={(ref) => {
|
|
103
|
+
inputRefs.current[index] = ref;
|
|
104
|
+
}}
|
|
102
105
|
className={OTPClass}
|
|
103
106
|
/>
|
|
104
107
|
))}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import React, {
|
|
9
9
|
forwardRef,
|
|
10
10
|
memo,
|
|
11
|
+
useCallback,
|
|
11
12
|
useEffect,
|
|
12
13
|
useImperativeHandle,
|
|
13
14
|
useRef,
|
|
@@ -50,10 +51,6 @@ const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
|
50
51
|
handleDataSource();
|
|
51
52
|
}, [items]);
|
|
52
53
|
|
|
53
|
-
useEffect(() => {
|
|
54
|
-
if (onSelect) onSelect(selected);
|
|
55
|
-
}, [selected, onSelect]);
|
|
56
|
-
|
|
57
54
|
useEffect(() => {
|
|
58
55
|
const handleClickOutside = (event: MouseEvent) => {
|
|
59
56
|
if (
|
|
@@ -87,13 +84,16 @@ const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
|
87
84
|
setShowPopover(!showPopover);
|
|
88
85
|
};
|
|
89
86
|
|
|
90
|
-
const handleSelect = (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
?
|
|
94
|
-
: [...
|
|
95
|
-
|
|
96
|
-
|
|
87
|
+
const handleSelect = useCallback(
|
|
88
|
+
(value: string) => {
|
|
89
|
+
const newSelected = selected.includes(value)
|
|
90
|
+
? selected.filter((item) => item !== value)
|
|
91
|
+
: [...selected, value];
|
|
92
|
+
setSelected(newSelected);
|
|
93
|
+
onSelect?.(newSelected);
|
|
94
|
+
},
|
|
95
|
+
[selected, onSelect]
|
|
96
|
+
);
|
|
97
97
|
|
|
98
98
|
const getSelectedDisplay = () => {
|
|
99
99
|
if (selected.length === 0) return placeholder;
|
|
@@ -127,6 +127,7 @@ const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
|
127
127
|
const clearSelected = () => {
|
|
128
128
|
setSelected([]);
|
|
129
129
|
setShowPopover(false);
|
|
130
|
+
if (onSelect) onSelect([]);
|
|
130
131
|
};
|
|
131
132
|
|
|
132
133
|
useImperativeHandle(ref, () => ({
|
|
@@ -142,19 +143,27 @@ const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
|
142
143
|
return (
|
|
143
144
|
<div className="relative w-[200px]" ref={selectRef}>
|
|
144
145
|
<div className="relative border w-[200px] flex items-center px-4 py-2 rounded-lg">
|
|
145
|
-
<button
|
|
146
|
+
<button
|
|
147
|
+
onClick={togglePopover}
|
|
148
|
+
className="flex-grow text-sm text-left"
|
|
149
|
+
type="button"
|
|
150
|
+
>
|
|
146
151
|
{getSelectedDisplay()}
|
|
147
152
|
</button>
|
|
148
153
|
<div className="flex items-center space-x-2">
|
|
149
154
|
{selected.length > 0 && (
|
|
150
|
-
<button
|
|
155
|
+
<button
|
|
156
|
+
className="flex items-center px-2"
|
|
157
|
+
onClick={clearSelected}
|
|
158
|
+
type="button"
|
|
159
|
+
>
|
|
151
160
|
<Icon
|
|
152
161
|
elements={x}
|
|
153
162
|
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
154
163
|
/>
|
|
155
164
|
</button>
|
|
156
165
|
)}
|
|
157
|
-
<button onClick={togglePopover}>
|
|
166
|
+
<button onClick={togglePopover} type="button">
|
|
158
167
|
<Icon
|
|
159
168
|
elements={upDown}
|
|
160
169
|
svgClass="h-4 w-4 stroke-gray-500 fill-none dark:stroke-white"
|
|
@@ -187,6 +196,7 @@ const MultiSelect = forwardRef<any, MultiSelectProps>((props, ref) => {
|
|
|
187
196
|
{filteredItems.length > 0 ? (
|
|
188
197
|
filteredItems.map(({ value, label }) => (
|
|
189
198
|
<button
|
|
199
|
+
type="button"
|
|
190
200
|
key={value}
|
|
191
201
|
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"
|
|
192
202
|
onClick={() => handleSelect(value)}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Important!!!!
|
|
3
|
+
* Work In Progress Code
|
|
4
|
+
*
|
|
2
5
|
* Copyright (c) Grampro Business Services and affiliates.
|
|
3
6
|
*
|
|
4
7
|
* This source code is licensed under the MIT license found in the
|
|
@@ -6,30 +9,68 @@
|
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import React, { useState, useRef, useEffect } from "react";
|
|
9
|
-
import { upload, x } from "
|
|
10
|
-
import Icon from "
|
|
12
|
+
import { upload, x, circleCheck } from "../icon/iconPaths";
|
|
13
|
+
import Icon from "../icon/Icon";
|
|
11
14
|
import { GetFileIcon } from "./uploaderIcon";
|
|
12
15
|
import AestheticProcessingAnimationWithStyles from "./ProgressAnimation";
|
|
16
|
+
import { sendFiles } from "./uploaderService";
|
|
17
|
+
import { cloudDownload } from "../../icon/iconPaths";
|
|
13
18
|
|
|
14
19
|
export const FileUploader = ({
|
|
15
20
|
showImagePreview = false,
|
|
16
|
-
multiple =
|
|
21
|
+
multiple = false,
|
|
17
22
|
onChange,
|
|
18
|
-
startsUpload = false,
|
|
19
23
|
selectedFiles = [],
|
|
20
24
|
accept,
|
|
21
25
|
fileCount,
|
|
22
26
|
disabled = false,
|
|
23
27
|
inputFileSize, // Max file size in MB
|
|
28
|
+
startUpload = false,
|
|
29
|
+
apiURL = "",
|
|
30
|
+
chunk_size = "",
|
|
31
|
+
uploadedFileIdArray = () => {},
|
|
32
|
+
fileData = [],
|
|
24
33
|
}: any) => {
|
|
25
34
|
const [files, setFiles] = useState<any[]>([]);
|
|
26
35
|
const [previewUrls, setPreviewUrls] = useState<{ [key: string]: string }>({});
|
|
27
36
|
const [isDragging, setIsDragging] = useState(false);
|
|
28
37
|
const [fileCountError, setFileCountError] = useState(false);
|
|
29
38
|
const [fileSizeErrors, setFileSizeErrors] = useState<string[]>([]); // Array for file size errors
|
|
39
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
40
|
+
const [fileUploadStatus, setFileUploadStatus] = useState<string | undefined>(
|
|
41
|
+
undefined
|
|
42
|
+
);
|
|
43
|
+
const [progress, setProgress] = useState(0);
|
|
30
44
|
const fileInputRef = useRef<any>(null);
|
|
31
45
|
const dropZoneRef = useRef<any>(null);
|
|
32
46
|
|
|
47
|
+
// Starts uploads to server
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (startUpload) {
|
|
50
|
+
// Upload Handler
|
|
51
|
+
const handleUpload = async () => {
|
|
52
|
+
if (files.length > 0) {
|
|
53
|
+
setFileUploadStatus(undefined);
|
|
54
|
+
await sendFiles(
|
|
55
|
+
files,
|
|
56
|
+
chunk_size,
|
|
57
|
+
apiURL,
|
|
58
|
+
({ uploading, progress, uploadedFileIds }) => {
|
|
59
|
+
setIsUploading(uploading);
|
|
60
|
+
if (progress !== undefined) setProgress(progress);
|
|
61
|
+
// Returns Uploaded File ID's
|
|
62
|
+
if (uploadedFileIds) uploadedFileIdArray(uploadedFileIds);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
setFileUploadStatus("Upload completed");
|
|
66
|
+
} else {
|
|
67
|
+
setFileUploadStatus("No files selected");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
handleUpload();
|
|
71
|
+
}
|
|
72
|
+
}, [startUpload]);
|
|
73
|
+
|
|
33
74
|
// For Binding values on edit
|
|
34
75
|
useEffect(() => {
|
|
35
76
|
if (selectedFiles && selectedFiles.length > 0) {
|
|
@@ -128,6 +169,22 @@ export const FileUploader = ({
|
|
|
128
169
|
handleFileChange(multiple ? droppedFiles : [droppedFiles[0]]);
|
|
129
170
|
};
|
|
130
171
|
|
|
172
|
+
const handleDownload = (fileId: any, fileName: any) => {
|
|
173
|
+
const downloadUrl = constructDownloadUrl(fileId);
|
|
174
|
+
window.open(downloadUrl, "_blank");
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const constructDownloadUrl = (fileId: any) => {
|
|
178
|
+
const url = new URL("download", apiURL);
|
|
179
|
+
url.searchParams.append("id", fileId);
|
|
180
|
+
return url.href;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const isImageFile = (fileName: any) => {
|
|
184
|
+
const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"];
|
|
185
|
+
return imageExtensions.some((ext) => fileName.toLowerCase().endsWith(ext));
|
|
186
|
+
};
|
|
187
|
+
|
|
131
188
|
return (
|
|
132
189
|
<div className="w-full max-w-md mx-auto">
|
|
133
190
|
<div
|
|
@@ -157,6 +214,18 @@ export const FileUploader = ({
|
|
|
157
214
|
: "Single file upload only"}
|
|
158
215
|
</p>
|
|
159
216
|
|
|
217
|
+
{/* Shows FileUpload Status Here */}
|
|
218
|
+
{fileUploadStatus && (
|
|
219
|
+
<p className="text-xs mt-2 text-green-500 flex gap-1">
|
|
220
|
+
<Icon
|
|
221
|
+
dimensions={{ width: "16", height: "16" }}
|
|
222
|
+
elements={circleCheck}
|
|
223
|
+
svgClass={"stroke-green-500 fill-none dark:stroke-white"}
|
|
224
|
+
/>
|
|
225
|
+
{fileUploadStatus}
|
|
226
|
+
</p>
|
|
227
|
+
)}
|
|
228
|
+
|
|
160
229
|
{/* Display file size errors */}
|
|
161
230
|
{fileSizeErrors.length > 0 && (
|
|
162
231
|
<div className="mt-2 space-y-1">
|
|
@@ -186,7 +255,10 @@ export const FileUploader = ({
|
|
|
186
255
|
</span>
|
|
187
256
|
)}
|
|
188
257
|
|
|
189
|
-
{
|
|
258
|
+
{/* Loading Animation */}
|
|
259
|
+
{isUploading && (
|
|
260
|
+
<AestheticProcessingAnimationWithStyles progressPercentage={progress} />
|
|
261
|
+
)}
|
|
190
262
|
|
|
191
263
|
{files.length > 0 && (
|
|
192
264
|
<div className="mt-4 space-y-2">
|
|
@@ -225,6 +297,44 @@ export const FileUploader = ({
|
|
|
225
297
|
))}
|
|
226
298
|
</div>
|
|
227
299
|
)}
|
|
300
|
+
|
|
301
|
+
{fileData.length > 0 &&
|
|
302
|
+
fileData.map((item: any, index: any) => (
|
|
303
|
+
<div
|
|
304
|
+
key={index}
|
|
305
|
+
className="flex items-center justify-between bg-gray-50 p-2 rounded-md mt-2"
|
|
306
|
+
>
|
|
307
|
+
<div className="flex items-center space-x-2">
|
|
308
|
+
{/* <GetFileIcon
|
|
309
|
+
file={item.FileName}
|
|
310
|
+
showImagePreview={showImagePreview}
|
|
311
|
+
previewUrls={previewUrls}
|
|
312
|
+
/> */}
|
|
313
|
+
{isImageFile(item.FileName) && (
|
|
314
|
+
<img
|
|
315
|
+
src={constructDownloadUrl(item.FileID)}
|
|
316
|
+
alt={item.FileName}
|
|
317
|
+
className="mt-2 w-8 h-8 object-cover rounded-lg"
|
|
318
|
+
/>
|
|
319
|
+
)}
|
|
320
|
+
<div>
|
|
321
|
+
<p className="text-sm font-medium text-gray-700">
|
|
322
|
+
{item.FileName}
|
|
323
|
+
</p>
|
|
324
|
+
<p className="text-xs text-gray-500">
|
|
325
|
+
{(item.FileSize / 1024 / 1024).toFixed(2)} MB
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<button onClick={() => handleDownload(item.FileID, item.FileName)}>
|
|
330
|
+
<Icon
|
|
331
|
+
dimensions={{ width: "18", height: "18" }}
|
|
332
|
+
elements={cloudDownload}
|
|
333
|
+
svgClass={"stroke-red-500 fill-none dark:stroke-white"}
|
|
334
|
+
/>
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
))}
|
|
228
338
|
</div>
|
|
229
339
|
);
|
|
230
340
|
};
|
|
@@ -20,7 +20,13 @@ function processFiles(files: File[], chunkSize: number) {
|
|
|
20
20
|
export async function sendFiles(
|
|
21
21
|
files: File[],
|
|
22
22
|
chunkSize: number,
|
|
23
|
-
apiUrl = "http://localhost:8080/upload"
|
|
23
|
+
apiUrl = "http://localhost:8080/upload",
|
|
24
|
+
isUploading = (status: {
|
|
25
|
+
uploading: boolean;
|
|
26
|
+
progress?: number;
|
|
27
|
+
file?: string;
|
|
28
|
+
uploadedFileIds?: string[];
|
|
29
|
+
}) => {}
|
|
24
30
|
) {
|
|
25
31
|
const processedFiles = processFiles(files, chunkSize);
|
|
26
32
|
|
|
@@ -33,6 +39,10 @@ export async function sendFiles(
|
|
|
33
39
|
// To track chunk index per file
|
|
34
40
|
const chunkIndexTracker: { [key: string]: number } = {};
|
|
35
41
|
|
|
42
|
+
let totalChunks = processedFiles.length;
|
|
43
|
+
let uploadedChunks = 0;
|
|
44
|
+
const uploadedFileIds: string[] = [];
|
|
45
|
+
|
|
36
46
|
for (let i = 0; i < processedFiles.length; i++) {
|
|
37
47
|
const { file, originalFile } = processedFiles[i];
|
|
38
48
|
const formData = new FormData();
|
|
@@ -59,13 +69,12 @@ export async function sendFiles(
|
|
|
59
69
|
chunkIndexTracker[originalFile.name]++;
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
// Don't Remove the following console
|
|
63
|
-
// console.log("FormData contents:");
|
|
64
|
-
// formData.forEach((value, key) => {
|
|
65
|
-
// console.log(`${key}: ${value}`);
|
|
66
|
-
// });
|
|
67
|
-
|
|
68
72
|
try {
|
|
73
|
+
isUploading({
|
|
74
|
+
uploading: true,
|
|
75
|
+
progress: (uploadedChunks / totalChunks) * 100,
|
|
76
|
+
file: originalFile.name,
|
|
77
|
+
});
|
|
69
78
|
const response = await fetch(apiUrl, {
|
|
70
79
|
method: "POST",
|
|
71
80
|
body: formData,
|
|
@@ -76,18 +85,40 @@ export async function sendFiles(
|
|
|
76
85
|
}
|
|
77
86
|
|
|
78
87
|
const data = await response.json();
|
|
88
|
+
if (data.documentId) uploadedFileIds.push(data.documentId);
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
`Chunk ${chunkIndexTracker[originalFile.name]}/${chunkCount} uploaded`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
90
|
+
uploadedChunks++;
|
|
91
|
+
isUploading({
|
|
92
|
+
uploading: true,
|
|
93
|
+
progress: (uploadedChunks / totalChunks) * 100,
|
|
94
|
+
file: originalFile.name,
|
|
95
|
+
});
|
|
89
96
|
} catch (error) {
|
|
90
97
|
console.error("Error uploading file chunk:", error);
|
|
98
|
+
isUploading({
|
|
99
|
+
uploading: false,
|
|
100
|
+
progress: (uploadedChunks / totalChunks) * 100,
|
|
101
|
+
file: originalFile.name,
|
|
102
|
+
});
|
|
91
103
|
}
|
|
92
104
|
}
|
|
105
|
+
|
|
106
|
+
// Indicate that the upload is complete
|
|
107
|
+
isUploading({
|
|
108
|
+
uploading: false,
|
|
109
|
+
progress: 100,
|
|
110
|
+
uploadedFileIds: uploadedFileIds,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// returns all files
|
|
115
|
+
export async function getAllFiles(apiUrl: string) {
|
|
116
|
+
let files: any[] = [];
|
|
117
|
+
return files;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// returns files by ID
|
|
121
|
+
export async function getFilesById(apiUrl: string, documentId: string) {
|
|
122
|
+
let files: any[] = [];
|
|
123
|
+
return files;
|
|
93
124
|
}
|
package/source/icon/iconPaths.ts
CHANGED
|
@@ -166,3 +166,14 @@ export const filespreadsheet: any = [
|
|
|
166
166
|
{ type: "path", d: "M8 17h2", key: "2yhykz" },
|
|
167
167
|
{ type: "path", d: "M14 17h2", key: "10kma7" },
|
|
168
168
|
];
|
|
169
|
+
|
|
170
|
+
export const cloudDownload: any = [
|
|
171
|
+
{ type: "path", d: "m12 21 4-4", key: "1lfcce" },
|
|
172
|
+
{ type: "path", d: "M12 13v8l-4-4", key: "1f5nwf" },
|
|
173
|
+
{
|
|
174
|
+
type: "path",
|
|
175
|
+
|
|
176
|
+
d: "M4.393 15.269A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.436 8.284",
|
|
177
|
+
key: "ui1hmy",
|
|
178
|
+
},
|
|
179
|
+
];
|