gwan-design-system 0.1.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/.github/workflows/bump-version.yml +50 -0
- package/.github/workflows/publish.yml +43 -0
- package/CODEOWNERS +1 -0
- package/README.md +54 -0
- package/eslint.config.mjs +16 -0
- package/next.config.ts +7 -0
- package/package.json +27 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/images/empty.png +0 -0
- package/public/images/empty_state.png +0 -0
- package/public/images/hero.png +0 -0
- package/public/images/hero2.png +0 -0
- package/public/images/logo.png +0 -0
- package/public/images/logo_short.png +0 -0
- package/public/images/profile_picture.png +0 -0
- package/public/images/success.png +0 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/components-library/page.tsx +8 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +108 -0
- package/src/app/layout.tsx +34 -0
- package/src/app/page.tsx +5 -0
- package/src/components/avatar/index.tsx +109 -0
- package/src/components/banner/index.tsx +58 -0
- package/src/components/button/index.tsx +61 -0
- package/src/components/carousel/index.tsx +58 -0
- package/src/components/checkbox/index.tsx +48 -0
- package/src/components/chip/index.tsx +22 -0
- package/src/components/ellipsis/index.tsx +36 -0
- package/src/components/fileUploader/index.tsx +54 -0
- package/src/components/filterDropdown/index.tsx +49 -0
- package/src/components/icons/arrowLeftSVG/index.tsx +14 -0
- package/src/components/icons/checkSVG/index.tsx +14 -0
- package/src/components/icons/chevDownSVG/index.tsx +14 -0
- package/src/components/icons/chevLeftSVG/index.tsx +14 -0
- package/src/components/icons/chevRightSVG/index.tsx +14 -0
- package/src/components/icons/circleSVG/index.tsx +14 -0
- package/src/components/icons/colorsSVG/index.tsx +21 -0
- package/src/components/icons/coversSVG/index.tsx +21 -0
- package/src/components/icons/crossSVG/index.tsx +16 -0
- package/src/components/icons/dashboardSVG/index.tsx +14 -0
- package/src/components/icons/filterSVG/index.tsx +21 -0
- package/src/components/icons/index.tsx +17 -0
- package/src/components/icons/orderInfoSVG/index.tsx +21 -0
- package/src/components/icons/ordersSVG/index.tsx +21 -0
- package/src/components/icons/productsSVG/index.tsx +21 -0
- package/src/components/icons/signOutSVG/index.tsx +21 -0
- package/src/components/icons/templatesSVG/index.tsx +24 -0
- package/src/components/icons/uploadSVG/index.tsx +21 -0
- package/src/components/input/index.tsx +40 -0
- package/src/components/modal/index.tsx +54 -0
- package/src/components/navBar/index.tsx +161 -0
- package/src/components/pagination/index.tsx +69 -0
- package/src/components/radioButton/index.tsx +44 -0
- package/src/components/selectDropdown/index.tsx +90 -0
- package/src/components/snackBar/index.tsx +46 -0
- package/src/components/state/index.tsx +69 -0
- package/src/components/table/index.tsx +51 -0
- package/src/components/tag/index.tsx +33 -0
- package/src/components/timeLine/index.tsx +99 -0
- package/src/components/tooltip/index.tsx +70 -0
- package/src/index.ts +22 -0
- package/src/templates/component-library/avatars/index.tsx +45 -0
- package/src/templates/component-library/banners/index.tsx +35 -0
- package/src/templates/component-library/buttons/index.tsx +122 -0
- package/src/templates/component-library/carousels/index.tsx +38 -0
- package/src/templates/component-library/checkboxes/index.tsx +19 -0
- package/src/templates/component-library/chips/index.tsx +49 -0
- package/src/templates/component-library/ellipsis/index.tsx +20 -0
- package/src/templates/component-library/fileUploaders/index.tsx +21 -0
- package/src/templates/component-library/filterDropdown/index.tsx +48 -0
- package/src/templates/component-library/icons/index.tsx +23 -0
- package/src/templates/component-library/index.tsx +179 -0
- package/src/templates/component-library/input/index.tsx +35 -0
- package/src/templates/component-library/modals/index.tsx +113 -0
- package/src/templates/component-library/navBars/index.tsx +91 -0
- package/src/templates/component-library/pagination/index.tsx +28 -0
- package/src/templates/component-library/radioButtons/index.tsx +33 -0
- package/src/templates/component-library/selectDropdown/index.tsx +90 -0
- package/src/templates/component-library/snackBars/index.tsx +34 -0
- package/src/templates/component-library/states/index.tsx +24 -0
- package/src/templates/component-library/tables/index.tsx +143 -0
- package/src/templates/component-library/tags/index.tsx +15 -0
- package/src/templates/component-library/timeLines/index.tsx +96 -0
- package/src/templates/component-library/tooltips/index.tsx +61 -0
- package/tsconfig.build.json +16 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface IInput {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
inputClassName?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const Input = ({
|
|
12
|
+
label,
|
|
13
|
+
value,
|
|
14
|
+
onChange,
|
|
15
|
+
disabled,
|
|
16
|
+
placeholder,
|
|
17
|
+
inputClassName = "",
|
|
18
|
+
required = false,
|
|
19
|
+
}: IInput) => {
|
|
20
|
+
return (
|
|
21
|
+
<div className="flex flex-col gap-1 relative">
|
|
22
|
+
<label htmlFor={label} className="text-sm text-neutrola-600 mb-2">
|
|
23
|
+
{`${label}${required ? " *" : ""}`}
|
|
24
|
+
</label>
|
|
25
|
+
<input
|
|
26
|
+
id={label}
|
|
27
|
+
placeholder={placeholder}
|
|
28
|
+
value={value}
|
|
29
|
+
onChange={(e) => onChange(e.target.value)}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
className={`border border-neutrola-300 outline-none p-4 rounded-lg ${
|
|
32
|
+
disabled ? "cursor-not-allowed" : "cursor-text"
|
|
33
|
+
} text-sm w-full ${inputClassName}`}
|
|
34
|
+
required={required}
|
|
35
|
+
/>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default Input;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { CrossSVG } from "../icons";
|
|
3
|
+
|
|
4
|
+
export enum MODAL_SIZE {
|
|
5
|
+
SMALL = "w-[600px] h-60",
|
|
6
|
+
MEDIUM = "w-[800px] h-96",
|
|
7
|
+
LARGE = "w-[950px] h-[600px]",
|
|
8
|
+
FULL = "w-full h-full",
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface IModal {
|
|
12
|
+
title: string;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
onClear: () => void;
|
|
15
|
+
size?: MODAL_SIZE;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Modal = ({
|
|
19
|
+
title,
|
|
20
|
+
children,
|
|
21
|
+
onClear,
|
|
22
|
+
size = MODAL_SIZE.MEDIUM,
|
|
23
|
+
}: IModal) => {
|
|
24
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
modalRef.current?.focus();
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="fixed inset-0 flex items-center justify-center bg-[rgba(0,0,0,0.3)]">
|
|
32
|
+
<div
|
|
33
|
+
ref={modalRef}
|
|
34
|
+
tabIndex={-1}
|
|
35
|
+
className={`bg-white p-4 ${
|
|
36
|
+
size !== MODAL_SIZE.FULL && "rounded-lg"
|
|
37
|
+
} absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col gap-4 ${size}`}
|
|
38
|
+
onBlur={() => onClear()}
|
|
39
|
+
>
|
|
40
|
+
<div className="flex flex-row gap-4 items-center">
|
|
41
|
+
<div className="flex-1 text-3xl">{title}</div>
|
|
42
|
+
<div className="size-4 cursor-pointer" onClick={() => onClear()}>
|
|
43
|
+
<CrossSVG />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex-1 flex justify-center items-center">
|
|
47
|
+
{children}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default Modal;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
import Avatar, { AVATAR_VARIANT } from "../avatar";
|
|
3
|
+
import { ReactNode, useEffect, useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import { ChevLeftSVG, ChevRightSVG } from "../icons";
|
|
6
|
+
import Tooltip, { TOOLTIP_POSITION } from "../tooltip";
|
|
7
|
+
|
|
8
|
+
export interface IMenuItem {
|
|
9
|
+
title: string;
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
route: string;
|
|
12
|
+
isActive: boolean;
|
|
13
|
+
isDivider: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface INavBar {
|
|
17
|
+
menuItems: IMenuItem[];
|
|
18
|
+
logoShort: string;
|
|
19
|
+
logoLong: string;
|
|
20
|
+
avatarName: string;
|
|
21
|
+
avatarEmail: string;
|
|
22
|
+
avatarImage: string;
|
|
23
|
+
avatarType: AVATAR_VARIANT;
|
|
24
|
+
menuWidthClass?: string;
|
|
25
|
+
menuHeightClass?: string;
|
|
26
|
+
isCollapsed?: boolean;
|
|
27
|
+
menuBackgroundColor?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const NavBar = ({
|
|
31
|
+
menuItems,
|
|
32
|
+
logoShort,
|
|
33
|
+
logoLong,
|
|
34
|
+
avatarName,
|
|
35
|
+
avatarEmail,
|
|
36
|
+
avatarImage,
|
|
37
|
+
avatarType,
|
|
38
|
+
menuWidthClass = "w-[20rem]",
|
|
39
|
+
menuHeightClass = "h-[100vh]",
|
|
40
|
+
isCollapsed = false,
|
|
41
|
+
menuBackgroundColor = "bg-primary-100",
|
|
42
|
+
}: INavBar) => {
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
const [isActiveMenuItem, setIsActiveMenuItem] = useState<string>("");
|
|
45
|
+
const [isMenuCollapsed, setIsMenuCollapsed] = useState<boolean>(isCollapsed);
|
|
46
|
+
const [isMenuItemsCollapsed, setIsMenuITemsCollapsed] =
|
|
47
|
+
useState<boolean>(false);
|
|
48
|
+
const [showTooltip, setShowTooltip] = useState<number | null>(null);
|
|
49
|
+
const activeClass = "rounded-lg bg-white bg-opacity-35";
|
|
50
|
+
const collapsedClass = "w-[6rem]";
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (menuItems.length > 0) {
|
|
54
|
+
const active = menuItems.find((item) => item.isActive === true);
|
|
55
|
+
setIsActiveMenuItem(active?.title ?? menuItems[0].title);
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!isMenuCollapsed) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
setIsMenuITemsCollapsed(false);
|
|
63
|
+
}, 200);
|
|
64
|
+
} else {
|
|
65
|
+
setIsMenuITemsCollapsed(true);
|
|
66
|
+
}
|
|
67
|
+
}, [isMenuCollapsed]);
|
|
68
|
+
|
|
69
|
+
const handleClick = (menu: IMenuItem) => {
|
|
70
|
+
setIsActiveMenuItem(menu.title);
|
|
71
|
+
router.push(menu.route);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={`transition-[width] duration-300 ease-in-out ${
|
|
77
|
+
isMenuCollapsed ? collapsedClass : menuWidthClass
|
|
78
|
+
} ${menuHeightClass}`}
|
|
79
|
+
>
|
|
80
|
+
<div
|
|
81
|
+
className={`w-full h-full flex flex-col gap-4 p-4 ${menuBackgroundColor}`}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex flex-col gap-4">
|
|
84
|
+
<div className="flex flex-row gap-2 items-center">
|
|
85
|
+
<Image src={logoShort} alt="logo_short" width={60} height={60} />
|
|
86
|
+
{!isMenuItemsCollapsed && (
|
|
87
|
+
<Image
|
|
88
|
+
src={logoLong}
|
|
89
|
+
alt="logo_long"
|
|
90
|
+
width={200}
|
|
91
|
+
height={48}
|
|
92
|
+
className="transition-opacity duration-300"
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
<div className="relative">
|
|
97
|
+
<Avatar
|
|
98
|
+
name={avatarName}
|
|
99
|
+
email={avatarEmail}
|
|
100
|
+
variant={
|
|
101
|
+
isMenuItemsCollapsed ? AVATAR_VARIANT.IMAGE_ONLY : avatarType
|
|
102
|
+
}
|
|
103
|
+
image={avatarImage}
|
|
104
|
+
/>
|
|
105
|
+
<span
|
|
106
|
+
onClick={() => setIsMenuCollapsed(!isMenuCollapsed)}
|
|
107
|
+
className={`cursor-pointer w-8 h-8 rounded-full ${menuBackgroundColor} border border-neutral-300 absolute -right-8 flex items-center justify-center text-black`}
|
|
108
|
+
>
|
|
109
|
+
<div className="size-5">
|
|
110
|
+
{isMenuCollapsed ? <ChevRightSVG /> : <ChevLeftSVG />}
|
|
111
|
+
</div>
|
|
112
|
+
</span>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div className="border-neutral-300 border-b"></div>
|
|
116
|
+
<div className="flex flex-col gap-1">
|
|
117
|
+
{menuItems.map((item, index) => {
|
|
118
|
+
if (!item.isDivider) {
|
|
119
|
+
return (
|
|
120
|
+
<div
|
|
121
|
+
key={`menu_item_${index + 1}`}
|
|
122
|
+
className={`flex flex-row gap-4 items-center p-4 rounded-lg hover:cursor-pointer hover:bg-white hover:bg-opacity-35 hover:rounded-lg ${
|
|
123
|
+
isActiveMenuItem === item.title ? activeClass : ""
|
|
124
|
+
}`}
|
|
125
|
+
onClick={() => handleClick(item)}
|
|
126
|
+
onMouseEnter={() => setShowTooltip(index)}
|
|
127
|
+
onMouseLeave={() => setShowTooltip(null)}
|
|
128
|
+
>
|
|
129
|
+
<div className="w-6 h-6 ml-1 relative">
|
|
130
|
+
{item.icon}
|
|
131
|
+
{isMenuCollapsed && (
|
|
132
|
+
<Tooltip
|
|
133
|
+
position={TOOLTIP_POSITION.RIGHT}
|
|
134
|
+
label={item.title}
|
|
135
|
+
isVisible={showTooltip === index}
|
|
136
|
+
toolTipWidth="w-fit"
|
|
137
|
+
toolTipClass="text-nowrap"
|
|
138
|
+
/>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
{!isMenuItemsCollapsed && (
|
|
142
|
+
<span className="text-nowrap">{item.title}</span>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div
|
|
150
|
+
key={`menu_item_${index + 1}`}
|
|
151
|
+
className="border-neutral-300 border-b my-3"
|
|
152
|
+
></div>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export default NavBar;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import Button, { BUTTON_VARIANTS } from "../button";
|
|
3
|
+
import { ChevLeft } from "../icons/chevLeftSVG";
|
|
4
|
+
import { ChevRight } from "../icons/chevRightSVG";
|
|
5
|
+
import SelectDropdown, { ISelectDropdownOption } from "../selectDropdown";
|
|
6
|
+
|
|
7
|
+
export interface IPaging {
|
|
8
|
+
total: number;
|
|
9
|
+
page: number;
|
|
10
|
+
size: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface IPagination extends IPaging {
|
|
14
|
+
options: ISelectDropdownOption[];
|
|
15
|
+
onChange: (paging: IPaging) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const Pagination = ({ total, page, size, options, onChange }: IPagination) => {
|
|
19
|
+
const [optionDropdown, setOptionDropdown] = useState<string>(size.toString());
|
|
20
|
+
|
|
21
|
+
const onLeft = () => {
|
|
22
|
+
if (page > 1) {
|
|
23
|
+
onChange({ total, page: page - 1, size });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const onRight = () => {
|
|
28
|
+
if (page < Math.ceil(total / size)) {
|
|
29
|
+
onChange({ total, page: page + 1, size });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const handlePageSize = (size: string) => {
|
|
34
|
+
setOptionDropdown(size);
|
|
35
|
+
onChange({ total, page: 1, size: parseInt(size) });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="bg-neutrola-50 flex flex-row gap-4 items-center px-4 py-2 rounded-lg">
|
|
40
|
+
<div className="flex-1">
|
|
41
|
+
{page} of {Math.ceil(total / size)} pages
|
|
42
|
+
</div>
|
|
43
|
+
<div className="w-20">
|
|
44
|
+
<SelectDropdown
|
|
45
|
+
options={options}
|
|
46
|
+
value={optionDropdown}
|
|
47
|
+
onChange={(option) => handlePageSize(option)}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
<div>items per page</div>
|
|
51
|
+
<div className="flex flex-row gap-4 items-center">
|
|
52
|
+
<Button
|
|
53
|
+
onClick={onLeft}
|
|
54
|
+
icon={<ChevLeft />}
|
|
55
|
+
variant={BUTTON_VARIANTS.TERTIARY}
|
|
56
|
+
disabled={page === 1}
|
|
57
|
+
/>
|
|
58
|
+
<Button
|
|
59
|
+
onClick={onRight}
|
|
60
|
+
icon={<ChevRight />}
|
|
61
|
+
variant={BUTTON_VARIANTS.TERTIARY}
|
|
62
|
+
disabled={page === Math.ceil(total / size)}
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default Pagination;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface IRadioButton {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
selectedValue?: string;
|
|
5
|
+
onChange?: (value: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const RadioButton = ({
|
|
9
|
+
label,
|
|
10
|
+
value,
|
|
11
|
+
selectedValue,
|
|
12
|
+
onChange,
|
|
13
|
+
}: IRadioButton) => {
|
|
14
|
+
const isChecked = selectedValue === value;
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
18
|
+
<div
|
|
19
|
+
className={`w-5 h-5 flex items-center justify-center border-2 rounded-full transition-all
|
|
20
|
+
${
|
|
21
|
+
isChecked
|
|
22
|
+
? "border-primary-500 bg-primary-500"
|
|
23
|
+
: "border-gray-400 bg-white"
|
|
24
|
+
}
|
|
25
|
+
`}
|
|
26
|
+
onClick={() => onChange && onChange(value)}
|
|
27
|
+
>
|
|
28
|
+
{isChecked && <div className="w-2.5 h-2.5 bg-white rounded-full"></div>}
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<span className="text-gray-700">{label}</span>
|
|
32
|
+
|
|
33
|
+
<input
|
|
34
|
+
type="radio"
|
|
35
|
+
value={value}
|
|
36
|
+
checked={isChecked}
|
|
37
|
+
onChange={() => onChange && onChange(value)}
|
|
38
|
+
className="hidden"
|
|
39
|
+
/>
|
|
40
|
+
</label>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default RadioButton;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { ChevDownSVG } from "../icons";
|
|
3
|
+
|
|
4
|
+
export interface ISelectDropdownOption {
|
|
5
|
+
value?: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ISelectDropdown {
|
|
10
|
+
options: ISelectDropdownOption[];
|
|
11
|
+
label?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (option: string) => void;
|
|
16
|
+
inputClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SelectDropdown = ({
|
|
20
|
+
options,
|
|
21
|
+
label,
|
|
22
|
+
placeholder = "",
|
|
23
|
+
disabled = false,
|
|
24
|
+
value,
|
|
25
|
+
onChange,
|
|
26
|
+
inputClassName = "",
|
|
27
|
+
}: ISelectDropdown) => {
|
|
28
|
+
const [dropdownValue, setDropdownValue] = useState<string>("");
|
|
29
|
+
const [isOptionsVisible, setIsOptionsVisible] = useState<boolean>(false);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const option = options.find((opt) => opt.label === value);
|
|
33
|
+
if (option) {
|
|
34
|
+
setDropdownValue(option.label);
|
|
35
|
+
onChange(option.label);
|
|
36
|
+
} else {
|
|
37
|
+
setDropdownValue("");
|
|
38
|
+
}
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const handleMouseDown = (val: string) => {
|
|
42
|
+
setDropdownValue(val);
|
|
43
|
+
onChange(val);
|
|
44
|
+
setIsOptionsVisible(false);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="flex flex-col gap-1 relative">
|
|
49
|
+
{label && (
|
|
50
|
+
<label htmlFor={label} className="text-sm text-neutrola-600 mb-2">
|
|
51
|
+
{label}
|
|
52
|
+
</label>
|
|
53
|
+
)}
|
|
54
|
+
<div className="relative">
|
|
55
|
+
<div className="size-5 absolute inset-y-4 right-4 flex items-center text-neutrola-600 pointer-events-none">
|
|
56
|
+
<ChevDownSVG />
|
|
57
|
+
</div>
|
|
58
|
+
<input
|
|
59
|
+
id={label}
|
|
60
|
+
type="text"
|
|
61
|
+
className={`border border-neutrola-300 outline-none p-4 rounded-lg ${
|
|
62
|
+
disabled ? "cursor-not-allowed" : "cursor-pointer"
|
|
63
|
+
} text-sm w-full ${inputClassName}`}
|
|
64
|
+
placeholder={placeholder}
|
|
65
|
+
onClick={() => setIsOptionsVisible(!isOptionsVisible)}
|
|
66
|
+
value={dropdownValue}
|
|
67
|
+
onBlur={() => setIsOptionsVisible(false)}
|
|
68
|
+
readOnly
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
></input>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{isOptionsVisible && (
|
|
74
|
+
<div className="border border-neutrola-300 rounded-lg shadow-lg max-h-96 overflow-y-auto absolute min-w-full top-full bg-white z-10">
|
|
75
|
+
{options.map(({ label, value: val }, index) => (
|
|
76
|
+
<div
|
|
77
|
+
key={`${label}_${val}_${index + 1}`}
|
|
78
|
+
className="p-4 cursor-pointer hover:bg-neutrola-50 text-sm"
|
|
79
|
+
onMouseDown={() => handleMouseDown(label)}
|
|
80
|
+
>
|
|
81
|
+
{label}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export default SelectDropdown;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export enum SNACK_BAR_TYPE {
|
|
2
|
+
DEFAULT = "default",
|
|
3
|
+
SUCCESS = "success",
|
|
4
|
+
DANGER = "danger",
|
|
5
|
+
WARNING = "warning",
|
|
6
|
+
INFO = "info",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ISnackBar {
|
|
10
|
+
type?: SNACK_BAR_TYPE;
|
|
11
|
+
message: string;
|
|
12
|
+
icon?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const Snackbar = ({
|
|
16
|
+
type = SNACK_BAR_TYPE.DEFAULT,
|
|
17
|
+
message,
|
|
18
|
+
icon,
|
|
19
|
+
}: ISnackBar) => {
|
|
20
|
+
const getClassName = () => {
|
|
21
|
+
switch (type) {
|
|
22
|
+
case SNACK_BAR_TYPE.SUCCESS:
|
|
23
|
+
return "bg-greenola-50 text-greenola-800 border-greenola-500";
|
|
24
|
+
case SNACK_BAR_TYPE.DANGER:
|
|
25
|
+
return "bg-redola-50 text-redola-600 border-redola-500";
|
|
26
|
+
case SNACK_BAR_TYPE.WARNING:
|
|
27
|
+
return "bg-yellowla-50 text-yellowla-800 border-yellowla-600";
|
|
28
|
+
case SNACK_BAR_TYPE.INFO:
|
|
29
|
+
return "bg-blueola-50 text-blueola-800 border-blueola-600";
|
|
30
|
+
case SNACK_BAR_TYPE.DEFAULT:
|
|
31
|
+
default:
|
|
32
|
+
return "bg-neutrola-50 text-neutrola-800 border-neutrola-500";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={`${getClassName()} p-4 rounded-lg flex flex-row gap-4 items-center font-normal border`}
|
|
39
|
+
>
|
|
40
|
+
{icon && <div className="size-5">{icon}</div>}
|
|
41
|
+
<div>{message}</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default Snackbar;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import Image from "next/image";
|
|
2
|
+
|
|
3
|
+
export enum STATE_TYPE {
|
|
4
|
+
EMPTY = "EMPTY",
|
|
5
|
+
SUCCESS = "SUCCESS",
|
|
6
|
+
ERROR = "ERROR",
|
|
7
|
+
LOADING = "LOADING",
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IStateBase {
|
|
11
|
+
title: string;
|
|
12
|
+
subTitle: string;
|
|
13
|
+
imageWidth?: number;
|
|
14
|
+
imageHeight?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type IState =
|
|
18
|
+
| ({ type: STATE_TYPE; stateImage?: string } & IStateBase)
|
|
19
|
+
| ({ type?: undefined; stateImage: string } & IStateBase);
|
|
20
|
+
|
|
21
|
+
const State = ({
|
|
22
|
+
type,
|
|
23
|
+
stateImage,
|
|
24
|
+
title,
|
|
25
|
+
subTitle,
|
|
26
|
+
imageWidth = 526,
|
|
27
|
+
imageHeight = 526,
|
|
28
|
+
}: IState) => {
|
|
29
|
+
const typeImage = () => {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case STATE_TYPE.EMPTY:
|
|
32
|
+
return "/images/empty.png";
|
|
33
|
+
case STATE_TYPE.SUCCESS:
|
|
34
|
+
return "/images/success.png";
|
|
35
|
+
case STATE_TYPE.ERROR:
|
|
36
|
+
return "/images/empty.png";
|
|
37
|
+
case STATE_TYPE.LOADING:
|
|
38
|
+
return "/images/empty.png";
|
|
39
|
+
default:
|
|
40
|
+
return "/images/empty.png";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="w-full h-full flex flex-col gap-8 items-center justify-center">
|
|
46
|
+
{stateImage ? (
|
|
47
|
+
<Image
|
|
48
|
+
src={stateImage}
|
|
49
|
+
alt="state image"
|
|
50
|
+
width={imageWidth}
|
|
51
|
+
height={imageHeight}
|
|
52
|
+
/>
|
|
53
|
+
) : (
|
|
54
|
+
<Image
|
|
55
|
+
src={typeImage()}
|
|
56
|
+
alt="state image"
|
|
57
|
+
width={imageWidth}
|
|
58
|
+
height={imageHeight}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
<div className="flex flex-col gap-2">
|
|
62
|
+
<p className="text-3xl text-center font-semibold">{title}</p>
|
|
63
|
+
<p className="w-[300px] text-center">{subTitle}</p>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default State;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { JSX } from "react";
|
|
2
|
+
|
|
3
|
+
export interface ITableColumn {
|
|
4
|
+
header: string;
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
render: (row?: any) => JSX.Element;
|
|
7
|
+
headerClassName?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ITable {
|
|
11
|
+
columns: ITableColumn[];
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
data: any[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Table = ({ columns, data }: ITable) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className="overflow-hidden rounded-lg border border-neutrola-300">
|
|
19
|
+
<table className="w-full border-collapse">
|
|
20
|
+
<thead>
|
|
21
|
+
<tr className="bg-neutrola-300 border border-neutrola-300">
|
|
22
|
+
{columns.map(({ header, headerClassName }, index) => (
|
|
23
|
+
<th
|
|
24
|
+
key={`column_${index}`}
|
|
25
|
+
className={`text-left px-4 py-4 ${headerClassName}`}
|
|
26
|
+
>
|
|
27
|
+
{header}
|
|
28
|
+
</th>
|
|
29
|
+
))}
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
{data.map((row, rowIndex) => (
|
|
34
|
+
<tr
|
|
35
|
+
key={`row_${rowIndex}`}
|
|
36
|
+
className="bg-white hover:bg-neutrola-100"
|
|
37
|
+
>
|
|
38
|
+
{columns.map(({ render }, cellIndex) => (
|
|
39
|
+
<td key={`cell_${cellIndex}`} className="text-left px-4 py-4">
|
|
40
|
+
{render(row)}
|
|
41
|
+
</td>
|
|
42
|
+
))}
|
|
43
|
+
</tr>
|
|
44
|
+
))}
|
|
45
|
+
</tbody>
|
|
46
|
+
</table>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default Table;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export enum TAG_TYPE {
|
|
2
|
+
DEFAULT = "default",
|
|
3
|
+
SUCCESS = "success",
|
|
4
|
+
DANGER = "danger",
|
|
5
|
+
WARNING = "warning",
|
|
6
|
+
INFO = "info",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ITag {
|
|
10
|
+
type: TAG_TYPE;
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const Tag = ({ type, label }: ITag) => {
|
|
15
|
+
const getTagStyle = (type: TAG_TYPE) => {
|
|
16
|
+
switch (type) {
|
|
17
|
+
case TAG_TYPE.SUCCESS:
|
|
18
|
+
return "bg-greenola-50 text-greenola-600";
|
|
19
|
+
case TAG_TYPE.DANGER:
|
|
20
|
+
return "bg-redola-50 text-redola-600";
|
|
21
|
+
case TAG_TYPE.WARNING:
|
|
22
|
+
return "bg-yellowla-50 text-yellowla-600";
|
|
23
|
+
case TAG_TYPE.INFO:
|
|
24
|
+
return "bg-blueola-50 text-blueola-600";
|
|
25
|
+
default:
|
|
26
|
+
return "bg-neutrola-50 text-neutrola-600";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return <div className={`w-fit px-4 py-2 rounded-lg text-sm ${getTagStyle(type)}`}>{label}</div>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default Tag;
|