naria-ui 0.1.0
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 +36 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +42 -0
- package/app/layout.tsx +32 -0
- package/app/page.module.css +167 -0
- package/app/page.tsx +95 -0
- package/components/button/Button.tsx +30 -0
- package/components/drawer/Drawer.tsx +98 -0
- package/components/input/Input.tsx +89 -0
- package/components/lib-file-uploader/LibFileUploader.tsx +124 -0
- package/components/lib-pagination/LibPagination.tsx +198 -0
- package/components/modal/Modal.tsx +88 -0
- package/components/select/Select.tsx +210 -0
- package/components/tabs/LibTabs.tsx +31 -0
- package/components/textarea/Textarea.tsx +87 -0
- package/eslint.config.mjs +25 -0
- package/next.config.ts +7 -0
- package/package.json +25 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {FC, useEffect, useRef, useState} from "react";
|
|
3
|
+
import CloudArrowUp from '@/assets/icons/cloud-arrow-up.svg'
|
|
4
|
+
import LibButton from "@/components/lib/lib-button/LibButton";
|
|
5
|
+
import convertFileSize from "@/utils/convert-file-size";
|
|
6
|
+
import Loading from "@/components/shared/loading/Loading";
|
|
7
|
+
|
|
8
|
+
export interface props {
|
|
9
|
+
isLoading: boolean;
|
|
10
|
+
onFileChange?: any;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LibFileUploader: FC<props> = ({isLoading = true, onFileChange}) => {
|
|
14
|
+
const labelRef = useRef(null);
|
|
15
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
16
|
+
const [selectedFile, setSelectedFile] = useState(null);
|
|
17
|
+
|
|
18
|
+
const handleDragEnter = (e) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setIsDragging(true);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const handleDragLeave = (e) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setIsDragging(false);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const handleDragOver = (e) => {
|
|
29
|
+
e.preventDefault();
|
|
30
|
+
setIsDragging(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handleDrop = (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
if (isLoading) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
setIsDragging(false);
|
|
39
|
+
const file = e?.target?.files ? e?.target?.files[0] : e?.dataTransfer?.files[0];
|
|
40
|
+
if (file) {
|
|
41
|
+
setSelectedFile(file);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const onCancel = () => {
|
|
45
|
+
setSelectedFile(null)
|
|
46
|
+
}
|
|
47
|
+
const onUpload = () => {
|
|
48
|
+
onFileChange(selectedFile)
|
|
49
|
+
}
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if(!isLoading) {
|
|
52
|
+
setSelectedFile(null);
|
|
53
|
+
}
|
|
54
|
+
}, [isLoading])
|
|
55
|
+
return (
|
|
56
|
+
|
|
57
|
+
<div className="relative border border-dashed border-secondary-100/40 rounded-2xl overflow-hidden">
|
|
58
|
+
<label
|
|
59
|
+
ref={labelRef}
|
|
60
|
+
className={`py-10 px-6 block ${isDragging ? 'border-secondary-100/100 bg-secondary-100/5' : ''}`}
|
|
61
|
+
onDragEnter={handleDragEnter}
|
|
62
|
+
onDragLeave={handleDragLeave}
|
|
63
|
+
onDragOver={handleDragOver}
|
|
64
|
+
onDrop={handleDrop}
|
|
65
|
+
>
|
|
66
|
+
<div className="flex flex-col md:flex-row justify-center items-center gap-16">
|
|
67
|
+
<div className="flex gap-5 items-center flex-col md:flex-row">
|
|
68
|
+
<CloudArrowUp className="w-10 mt-2.5 shrink-0"/>
|
|
69
|
+
<div>
|
|
70
|
+
<p className="text-base font-bold">جهت انتخاب فایل کلیک کرده یا فایل مورد نظر را اینجا رها
|
|
71
|
+
کنید.</p>
|
|
72
|
+
<p className="text-sm text-secondary-100 mt-2">فایلهای قابل انتخاب: PNG، JPEG، MP4</p>
|
|
73
|
+
<p className="text-sm text-secondary-100 mt-2">حداکثر حجم مجاز: 2 مگابایت</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<input type="file" accept="image/png, image/jpeg, image/jpg, video/mp4"
|
|
78
|
+
value=""
|
|
79
|
+
onChange={handleDrop} className="hidden"/>
|
|
80
|
+
<div className="sm:max-w-[120px] w-full">
|
|
81
|
+
<LibButton theme="btn-info-light" onClick={() => labelRef.current.click()} value="انتخاب فایل"/>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
</label>
|
|
88
|
+
{
|
|
89
|
+
selectedFile && !isLoading ? (
|
|
90
|
+
<div className="absolute bg-light-100 top-0 right-0 w-full h-full flex items-center">
|
|
91
|
+
<div className="text-sm mx-auto w-full max-w-[250px]">
|
|
92
|
+
<p className="flex gap-1"><span className="text-secondary-100 shrink-0">نام فایل:</span>
|
|
93
|
+
<span
|
|
94
|
+
className="inline-block ltr text-ellipsis overflow-hidden truncate">{selectedFile?.name}</span>
|
|
95
|
+
</p>
|
|
96
|
+
<p><span
|
|
97
|
+
className="text-secondary-100">حجم فایل:</span> {convertFileSize(selectedFile?.size)}
|
|
98
|
+
</p>
|
|
99
|
+
<div className="flex mt-4 gap-2">
|
|
100
|
+
|
|
101
|
+
<div className="grow">
|
|
102
|
+
<LibButton value="انصراف" size="btn-sm" theme="btn-danger-light" onClick={onCancel}/>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="grow">
|
|
105
|
+
<LibButton value="تایید" size="btn-sm" theme="btn-success" onClick={onUpload}/>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
) : undefined
|
|
111
|
+
}
|
|
112
|
+
{
|
|
113
|
+
selectedFile && isLoading ? (
|
|
114
|
+
<div className="absolute bg-light-100 top-0 right-0 w-full h-full flex items-center gap-3 justify-center">
|
|
115
|
+
<Loading size="w-6.5 h-6.5" /> <span>در حال بارگزاری...</span>
|
|
116
|
+
</div>
|
|
117
|
+
) : undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export default LibFileUploader;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {FC, useEffect, useState} from "react";
|
|
3
|
+
import ArrowRight from "@/assets/icons/angle-right.svg";
|
|
4
|
+
import ArrowLeft from "@/assets/icons/angle-left.svg";
|
|
5
|
+
import {generateUuid} from '@/utils/generate-uuid';
|
|
6
|
+
|
|
7
|
+
export interface props {
|
|
8
|
+
theme?: "primary" | "secondary" | "warning" | "infoLight" | "dangerLight" | "infoLink" | "outlineSecondary" | "outlinePrimary";
|
|
9
|
+
activeTheme?: "primary" | "secondary" | "warning" | "infoLight" | "dangerLight" | "infoLink" | "outlineSecondary" | "outlinePrimary";
|
|
10
|
+
size?: "md" | "sm";
|
|
11
|
+
totalCount: number;
|
|
12
|
+
pageSize: number;
|
|
13
|
+
onPageChange: any;
|
|
14
|
+
selectedPage?: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sizes = {
|
|
18
|
+
sm: "text-xs h-8 min-w-8",
|
|
19
|
+
md: "text-sm h-10 min-w-10"
|
|
20
|
+
}
|
|
21
|
+
const sizesArrow = {
|
|
22
|
+
sm: "w-2.5",
|
|
23
|
+
md: "w8"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const buttonThemes = {
|
|
27
|
+
primary: `border border-transparent bg-primary-100 text-white hover:bg-primary-200 focus:bg-primary-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
28
|
+
secondary: `border border-transparent bg-secondary-100 text-white hover:bg-secondary-200 focus:bg-secondary-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
29
|
+
success: `border border-transparent bg-success-100 text-white hover:bg-success-200 focus:bg-success-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
30
|
+
warning: `border border-transparent bg-warning-100 text-dark-100 hover:bg-warning-200 focus:bg-warning-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
31
|
+
infoLight: `border border-transparent bg-info-100/10 text-info-100 hover:brightness-80 focus:brightness-80 hover:border-info-100 focus:border-info-100 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
32
|
+
dangerLight: `border border-transparent bg-danger-300 text-danger-100 hover:bg-danger-400 focus:bg-danger-400 hover:border-danger-100 focus:border-danger-100 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
33
|
+
infoLink: `text-info-100 hover:text-info-200 focus:text-info-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
34
|
+
outlinePrimary: `border border-primary-100 text-primary-100 hover:bg-primary-300 focus:bg-primary-300 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
35
|
+
outlineSecondary: `border border-secondary-100 text-secondary-100 enabled:hover:bg-grey-200 focus:bg-grey-200 disabled:text-grey-300 disabled:border-grey-200 disabled:fill-grey-200 disabled:cursor-not-allowed`,
|
|
36
|
+
}
|
|
37
|
+
export const activeThemes = {
|
|
38
|
+
primary: `border border-transparent bg-primary-100 text-white hover:bg-primary-200 focus:bg-primary-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
39
|
+
secondary: `border border-transparent bg-secondary-100 text-white hover:bg-secondary-200 focus:bg-secondary-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
40
|
+
success: `border border-transparent bg-success-100 text-white hover:bg-success-200 focus:bg-success-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
41
|
+
warning: `border border-transparent bg-warning-100 !text-dark-100 hover:bg-warning-200 focus:bg-warning-200 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
42
|
+
infoLight: `border border-transparent bg-info-100/10 text-info-100 hover:brightness-80 focus:brightness-80 hover:border-info-100 focus:border-info-100 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
43
|
+
dangerLight: `border border-transparent bg-danger-300 text-danger-100 hover:bg-danger-400 focus:bg-danger-400 hover:border-danger-100 focus:border-danger-100 disabled:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
44
|
+
infoLink: `text-info-100 hover:text-info-200 focus:text-info-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
45
|
+
outlinePrimary: `transition duration-200 ease border border-primary-100 text-primary-100 hover:bg-primary-300 focus:bg-primary-300 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
46
|
+
outlineSecondary: `border border-secondary-100 text-secondary-100 hover:bg-grey-200 focus:bg-grey-200 disabled:text-grey-300 disabled:cursor-not-allowed`,
|
|
47
|
+
}
|
|
48
|
+
export const dotsThemes = {
|
|
49
|
+
outlineSecondary: `text-secondary-100`,
|
|
50
|
+
}
|
|
51
|
+
export const arrowThemes = {
|
|
52
|
+
outlineSecondary: `fill-secondary-100`,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const LibPagination: FC<props> = ({
|
|
56
|
+
totalCount,
|
|
57
|
+
pageSize,
|
|
58
|
+
onPageChange,
|
|
59
|
+
selectedPage,
|
|
60
|
+
activeTheme = "primary",
|
|
61
|
+
theme = "primary",
|
|
62
|
+
size = "md"
|
|
63
|
+
}) => {
|
|
64
|
+
const [selected, setSelected] = useState(1);
|
|
65
|
+
const [pagesCount, setPagesCount] = useState(1);
|
|
66
|
+
const [list, setList] = useState([]);
|
|
67
|
+
const onPaginationClicked = (item: number) => {
|
|
68
|
+
setSelected(item);
|
|
69
|
+
onPageChange(+item);
|
|
70
|
+
};
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
onPagesCountHandler();
|
|
73
|
+
}, [totalCount]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
setSelected(1);
|
|
76
|
+
onPagesCountHandler();
|
|
77
|
+
}, [pageSize]);
|
|
78
|
+
const onPagesCountHandler = () => {
|
|
79
|
+
const totalPages =
|
|
80
|
+
totalCount < pageSize ? 1 : Math.ceil(totalCount / pageSize);
|
|
81
|
+
setPagesCount(totalPages);
|
|
82
|
+
};
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
generatePages();
|
|
85
|
+
}, [selected, pagesCount, totalCount]);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (selectedPage) {
|
|
88
|
+
setSelected(selectedPage);
|
|
89
|
+
}
|
|
90
|
+
}, [selectedPage]);
|
|
91
|
+
|
|
92
|
+
const generatePages = () => {
|
|
93
|
+
const list: any = [];
|
|
94
|
+
if (totalCount > 0) {
|
|
95
|
+
list.push(
|
|
96
|
+
<button
|
|
97
|
+
className={`rounded-lg flex items-center justify-center ${buttonThemes[theme]} ${sizes[size]} ${arrowThemes[theme]}`}
|
|
98
|
+
disabled={selected === 1}
|
|
99
|
+
onClick={() => onPaginationClicked(selected - 1)}
|
|
100
|
+
key={generateUuid()}>
|
|
101
|
+
<ArrowRight className={`${sizesArrow[size]}`}/>
|
|
102
|
+
</button>
|
|
103
|
+
);
|
|
104
|
+
if (pagesCount <= 5) {
|
|
105
|
+
for (let i = 1; i <= pagesCount; i++) {
|
|
106
|
+
list.push(
|
|
107
|
+
<button
|
|
108
|
+
className={`rounded-lg flex items-center justify-center ${sizes[size]} ${selected === i ? activeThemes[activeTheme] : buttonThemes[theme]}`}
|
|
109
|
+
|
|
110
|
+
onClick={() => onPaginationClicked(i)}
|
|
111
|
+
key={generateUuid()}
|
|
112
|
+
>
|
|
113
|
+
{i}
|
|
114
|
+
</button>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
if (selected > pagesCount - 3) {
|
|
119
|
+
list.push(
|
|
120
|
+
<button
|
|
121
|
+
className={`rounded-lg flex items-center justify-center ${sizes[size]} ${selected === 1 ? activeThemes[activeTheme] : buttonThemes[theme]}`}
|
|
122
|
+
onClick={() => onPaginationClicked(1)}
|
|
123
|
+
key={generateUuid()}
|
|
124
|
+
>
|
|
125
|
+
{1}
|
|
126
|
+
</button>
|
|
127
|
+
);
|
|
128
|
+
list.push(
|
|
129
|
+
<div
|
|
130
|
+
className={`rounded-lg flex items-center justify-center ${dotsThemes[theme]} ${sizes[size]}`}
|
|
131
|
+
key={generateUuid()}>
|
|
132
|
+
...
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
for (let i = pagesCount - 3; i <= pagesCount; i++) {
|
|
136
|
+
list.push(
|
|
137
|
+
<button
|
|
138
|
+
className={`rounded-lg flex items-center justify-center ${sizes[size]} ${selected === i ? activeThemes[activeTheme] : buttonThemes[theme]}`}
|
|
139
|
+
onClick={() => onPaginationClicked(i)}
|
|
140
|
+
key={generateUuid()}
|
|
141
|
+
>
|
|
142
|
+
{i}
|
|
143
|
+
</button>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
const maxPage = selected < 3 ? 4 : selected + 2;
|
|
148
|
+
const fromPage = selected < 3 ? 1 : selected - 1;
|
|
149
|
+
for (let i = fromPage; i <= maxPage; i++) {
|
|
150
|
+
list.push(
|
|
151
|
+
<button
|
|
152
|
+
className={`rounded-lg flex items-center justify-center ${sizes[size]} ${selected === i ? activeThemes[activeTheme] : buttonThemes[theme]}`}
|
|
153
|
+
onClick={() => onPaginationClicked(i)}
|
|
154
|
+
key={generateUuid()}
|
|
155
|
+
>
|
|
156
|
+
{i}
|
|
157
|
+
</button>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
list.push(
|
|
161
|
+
<div
|
|
162
|
+
className={`rounded-lg flex items-center justify-center ${dotsThemes[theme]} ${sizes[size]}`}
|
|
163
|
+
key={generateUuid()}>
|
|
164
|
+
...
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
list.push(
|
|
168
|
+
<button
|
|
169
|
+
className={`rounded-lg flex items-center justify-center ${sizes[size]} ${selected === pagesCount ? activeThemes[activeTheme] : buttonThemes[theme]}`}
|
|
170
|
+
onClick={() => onPaginationClicked(pagesCount)}
|
|
171
|
+
key={generateUuid()}
|
|
172
|
+
>
|
|
173
|
+
{pagesCount}
|
|
174
|
+
</button>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
list.push(
|
|
179
|
+
<button
|
|
180
|
+
className={`rounded-lg flex items-center justify-center ${buttonThemes[theme]} ${sizes[size]} ${arrowThemes[theme]}`}
|
|
181
|
+
disabled={selected === pagesCount}
|
|
182
|
+
onClick={() => onPaginationClicked(selected + 1)}
|
|
183
|
+
key={generateUuid()}
|
|
184
|
+
>
|
|
185
|
+
<ArrowLeft className={`${sizesArrow[size]}`}/>
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
setList(list);
|
|
190
|
+
};
|
|
191
|
+
return (
|
|
192
|
+
<div className="flex items-center justify-center rounded-xl gap-2">
|
|
193
|
+
{list}
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export default LibPagination;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {FC, forwardRef, useEffect, useImperativeHandle, useState} from "react";
|
|
3
|
+
import Close from '@/assets/icons/close.svg'
|
|
4
|
+
import {useWidth} from '@/hooks/use-width';
|
|
5
|
+
import {addNavigation, onHashChanges, removeNavigation} from "@/utils/navigator/navigator-v2";
|
|
6
|
+
|
|
7
|
+
export interface LibModalProps {
|
|
8
|
+
title?: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
size?: string;
|
|
11
|
+
height?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface LibModalRef {
|
|
15
|
+
open: () => void;
|
|
16
|
+
close: () => void;
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const Modal = forwardRef<LibModalRef, LibModalProps>(({title, size, height, children}, ref) => {
|
|
21
|
+
const getDeviceWidth = useWidth();
|
|
22
|
+
const isHashChanged = onHashChanges();
|
|
23
|
+
const [isShow, setIsShow] = useState(false);
|
|
24
|
+
|
|
25
|
+
const open = () => setIsShow(true);
|
|
26
|
+
const close = () => setIsShow(false);
|
|
27
|
+
const toggle = () => setIsShow((v) => !v);
|
|
28
|
+
|
|
29
|
+
useImperativeHandle(ref, () => ({open, close, toggle}), []);
|
|
30
|
+
|
|
31
|
+
const onBackdropClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
32
|
+
if (e.currentTarget === e.target) close();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (isHashChanged) {
|
|
37
|
+
close();
|
|
38
|
+
}
|
|
39
|
+
}, [isHashChanged])
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (getDeviceWidth < 1024) {
|
|
43
|
+
if (isShow) {
|
|
44
|
+
addNavigation()
|
|
45
|
+
} else {
|
|
46
|
+
removeNavigation();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
document.body.style.overflow = isShow ? "hidden" : "auto";
|
|
50
|
+
return () => {
|
|
51
|
+
document.body.style.overflow = "auto";
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
}, [isShow]);
|
|
55
|
+
|
|
56
|
+
return isShow ? (
|
|
57
|
+
<div
|
|
58
|
+
className={`backdrop bg-dark-100/30 fixed items-end justify-center px-4 md:px-10 lg:px-0
|
|
59
|
+
lg:place-items-center flex right-0 top-0 left-0 bottom-0 w-full h-full z-50 duration-200 transition-opacity ${
|
|
60
|
+
isShow ? "opacity-1" : "opacity-0 hidden pointer-events-none"
|
|
61
|
+
}`}
|
|
62
|
+
onClick={onBackdropClick}
|
|
63
|
+
>
|
|
64
|
+
<div
|
|
65
|
+
className={`transform p-3 md:p-5 rounded-t-2xl lg:rounded-2xl bg-grey-100 shadow-xl transition-opacity
|
|
66
|
+
duration-200 lg:h-auto w-full ${
|
|
67
|
+
size || " lg:max-w-2xl"
|
|
68
|
+
} ${isShow ? "opacity-1" : "opacity-0 pointer-events-none"} ${
|
|
69
|
+
getDeviceWidth < 1024 ? `${height ? height : "h-[90vh]"} animate-drawer-bottom-to-top` : `${height ? height : "h-full"} animate-fade-in-translate-y`
|
|
70
|
+
}`}
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-center justify-between">
|
|
73
|
+
<div className="font-bold text-base lg:text-lg">{title ?? ""}</div>
|
|
74
|
+
<button className="inline-block" onClick={close}>
|
|
75
|
+
<Close className="w-6"/>
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
<div
|
|
79
|
+
className="h-full overflow-y-auto mt-3 md:mt-5 max-h-[calc(100%-40px)] lg:max-h-[calc(100vh-90px)]">
|
|
80
|
+
{children}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
) : <></>;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
Modal.displayName = "LibModal";
|
|
88
|
+
export default Modal;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {FC, useEffect, useState} from "react";
|
|
3
|
+
import AngleDown from "@/assets/icons/angle-down.svg";
|
|
4
|
+
import Close from '@/assets/icons/close.svg';
|
|
5
|
+
import Loading from '@/components/shared/loading/Loading';
|
|
6
|
+
import {useWidth} from '@/hooks/use-width';
|
|
7
|
+
import {useRouter} from 'next/navigation';
|
|
8
|
+
|
|
9
|
+
export interface props {
|
|
10
|
+
list?: any[];
|
|
11
|
+
label?: string;
|
|
12
|
+
title: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
api?: any;
|
|
15
|
+
maxHeight?: string;
|
|
16
|
+
theme?: "secondary";
|
|
17
|
+
hasError?: string | null;
|
|
18
|
+
size?: "md" | "sm";
|
|
19
|
+
onSelectChange?: any;
|
|
20
|
+
selected?: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const themes = {
|
|
24
|
+
secondary: {
|
|
25
|
+
input: `border
|
|
26
|
+
bg-transparent
|
|
27
|
+
placeholder:text-secondary-400
|
|
28
|
+
outline-1
|
|
29
|
+
outline-primary-100
|
|
30
|
+
border-secondary-300
|
|
31
|
+
rounded-md
|
|
32
|
+
hover:not:border-primary-100
|
|
33
|
+
focus:outline
|
|
34
|
+
focus:border-primary-100
|
|
35
|
+
|
|
36
|
+
disabled:text-secondary-100
|
|
37
|
+
disabled:border-secondary-500`,
|
|
38
|
+
|
|
39
|
+
label: `text-dark-100
|
|
40
|
+
peer-focus:text-primary-100
|
|
41
|
+
peer-focus:scale-90
|
|
42
|
+
|
|
43
|
+
peer-disabled:text-secondary-400`
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const sizes = {
|
|
47
|
+
sm: {
|
|
48
|
+
input: "text-xs px-2 py-1",
|
|
49
|
+
label: `px-1 text-xs `,
|
|
50
|
+
},
|
|
51
|
+
md: {
|
|
52
|
+
input: "text-sm px-3 py-2.5",
|
|
53
|
+
label: `px-1 text-xs`,
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
const Select: FC<props> = ({
|
|
57
|
+
list, label, hasError,
|
|
58
|
+
title, value, api,
|
|
59
|
+
maxHeight = "max-h-64", theme = "secondary", size = "md",
|
|
60
|
+
onSelectChange,
|
|
61
|
+
selected
|
|
62
|
+
}) => {
|
|
63
|
+
const getDeviceWidth = useWidth();
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
const [isShow, setIsShow] = useState(false);
|
|
66
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
67
|
+
const [localSelected, setLocalSelected] = useState(null);
|
|
68
|
+
const [localList, setLocalList] = useState(null);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (api) {
|
|
71
|
+
setIsLoading(true);
|
|
72
|
+
api().then((res) => {
|
|
73
|
+
setIsLoading(false);
|
|
74
|
+
setLocalList(res?.data?.message ? [] : res.data);
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}, [api]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (getDeviceWidth < 1024) {
|
|
80
|
+
|
|
81
|
+
if (isShow) {
|
|
82
|
+
router.push('#select');
|
|
83
|
+
document.body.style.overflow = 'hidden';
|
|
84
|
+
} else {
|
|
85
|
+
|
|
86
|
+
if (window.location.hash && !document.referrer.includes('#')) {
|
|
87
|
+
router.back();
|
|
88
|
+
}
|
|
89
|
+
document.body.style.overflow = 'auto';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}, [isShow]);
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const handlePopState = () => {
|
|
96
|
+
setIsShow(false);
|
|
97
|
+
};
|
|
98
|
+
window.addEventListener('popstate', handlePopState);
|
|
99
|
+
return () => {
|
|
100
|
+
window.removeEventListener('popstate', handlePopState);
|
|
101
|
+
};
|
|
102
|
+
}, []);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (label?.length) {
|
|
106
|
+
setLocalSelected(localList?.find(item => item[label] === selected));
|
|
107
|
+
} else {
|
|
108
|
+
setLocalSelected(selected);
|
|
109
|
+
}
|
|
110
|
+
}, [selected, localList]);
|
|
111
|
+
|
|
112
|
+
const onToggle = () => {
|
|
113
|
+
setIsShow(prevState => !prevState);
|
|
114
|
+
}
|
|
115
|
+
const onToggleEvent = (e) => {
|
|
116
|
+
if (e?.target?.className.toString()?.includes('backdropSelect')) {
|
|
117
|
+
onToggle()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const onSelect = (item) => {
|
|
121
|
+
setLocalSelected(item);
|
|
122
|
+
onToggle();
|
|
123
|
+
onSelectChange(item);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const getActiveClass = (item) => {
|
|
127
|
+
if (!localSelected) {
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
if (label?.length && item[label] === localSelected[label]) {
|
|
131
|
+
return "bg-grey-100"
|
|
132
|
+
}
|
|
133
|
+
if (item === localSelected) {
|
|
134
|
+
return "bg-grey-100"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return (
|
|
138
|
+
<div className="relative">
|
|
139
|
+
<label
|
|
140
|
+
className={`cursor-pointer
|
|
141
|
+
${hasError && "!text-danger-100"}`}>
|
|
142
|
+
<span className={`${themes[theme].label} ${sizes[size].label}`}>{title}</span>
|
|
143
|
+
<button type="button"
|
|
144
|
+
className={`relative z-20 flex items-center mt-1 text-base justify-between gap-2 w-full ${localSelected ? "text-dark-100" : "text-grey-300"} ${themes[theme].input} ${sizes[size].input} ${hasError && "!border-danger-100 focus:border-danger-100 outline-danger-100"}`}
|
|
145
|
+
onClick={onToggle}>
|
|
146
|
+
{
|
|
147
|
+
localSelected ? (
|
|
148
|
+
value?.length ? localSelected[value] : localSelected
|
|
149
|
+
) : "انتخاب"
|
|
150
|
+
} <AngleDown
|
|
151
|
+
className={`w-4 h-4 transition ease duration-200 ${!isShow ? "rotate-0" : "rotate-180"}`}/>
|
|
152
|
+
</button>
|
|
153
|
+
</label>
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
isShow ? (
|
|
157
|
+
<div
|
|
158
|
+
className={`overflow-hidden ${getDeviceWidth < 1024 ? "fixed top-0 right-0 h-full w-full z-40" : ""}`}>
|
|
159
|
+
<div
|
|
160
|
+
className={`flex flex-col w-full z-40 bg-light-100 w-full shadow-lg border border-secondary-300 ${getDeviceWidth < 1024 ? "relative h-full pt-1 pb-5 px-5" : `animate-fade-in-translate-y mt-1 rounded-lg absolute ${maxHeight}`} overflow-auto`}>
|
|
161
|
+
{
|
|
162
|
+
api && isLoading ? (
|
|
163
|
+
<div className="py-10 flex justify-center">
|
|
164
|
+
<Loading size="w-7 h-7"/>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<>
|
|
168
|
+
{
|
|
169
|
+
getDeviceWidth < 1024 ? (
|
|
170
|
+
<div className="sticky top-0 text-left">
|
|
171
|
+
<button className="p-3" onClick={onToggle}>
|
|
172
|
+
<Close className="w-6"/>
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
) : undefined
|
|
176
|
+
}
|
|
177
|
+
{
|
|
178
|
+
localList?.map((item, index) => {
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<button type="button" onClick={() => onSelect(item)} key={index}
|
|
182
|
+
className={`text-right py-2.5 px-4 text-base hover:bg-grey-100 rounded-lg ${getActiveClass(item)}`}>
|
|
183
|
+
{value?.length ? item[value] : item}
|
|
184
|
+
</button>
|
|
185
|
+
</>
|
|
186
|
+
)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
</>
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
</div>
|
|
193
|
+
<div
|
|
194
|
+
className={`backdropSelect fixed top-0 left-0 bottom-0 w-full h-full z-30`}
|
|
195
|
+
onClick={onToggleEvent}>
|
|
196
|
+
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
) : undefined
|
|
200
|
+
}
|
|
201
|
+
{
|
|
202
|
+
hasError &&
|
|
203
|
+
<p className="text-xs mt-1 text-danger-100">{hasError}</p>
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export default Select;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
import {FC, useEffect, useRef, useState} from "react";
|
|
3
|
+
|
|
4
|
+
interface TabType {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface props {
|
|
10
|
+
tabs: TabType[],
|
|
11
|
+
onTabEmit?: any,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const LibTabs: FC<props> = ({tabs, onTabEmit}) => {
|
|
15
|
+
const [isActive, setIsActive] = useState(0);
|
|
16
|
+
const onTabClicked = (item: TabType, index: number) => {
|
|
17
|
+
setIsActive(index)
|
|
18
|
+
onTabEmit(item)
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex border-2 border-primary-100 rounded-lg overflow-hidden divide-x-2 divide-x-reverse divide-primary-100">
|
|
22
|
+
{
|
|
23
|
+
tabs.map((item: TabType, index: number) => {
|
|
24
|
+
return <button className={`transition font-bold grow p-2 ${isActive === index ? "bg-primary-100 text-light-100" : ""}`} key = {index} onClick={() => onTabClicked(item, index)}>{item.label}</button>
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default LibTabs;
|