@banch0u/core-project-test-repository 1.0.30
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/.babelrc +9 -0
- package/index.js +2 -0
- package/package.json +32 -0
- package/src/assets/fonts/Inter/Inter-Black.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-Bold.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-ExtraBold.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-ExtraLight.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-Light.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-Medium.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-Regular.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-SemiBold.ttf +0 -0
- package/src/assets/fonts/Inter/Inter-Thin.ttf +0 -0
- package/src/assets/fonts/fonts.css +53 -0
- package/src/assets/icons/index.js +72 -0
- package/src/components/Button/index.jsx +61 -0
- package/src/components/Button/index.module.scss +142 -0
- package/src/components/ColSort/index.jsx +51 -0
- package/src/components/ColSort/index.module.scss +40 -0
- package/src/components/Filter/index.jsx +220 -0
- package/src/components/Filter/index.module.scss +99 -0
- package/src/components/Loading/index.jsx +16 -0
- package/src/components/Loading/index.module.scss +18 -0
- package/src/components/Pagination/Pagination.module.scss +8 -0
- package/src/components/Pagination/Select.jsx +26 -0
- package/src/components/Pagination/constant.js +22 -0
- package/src/components/Pagination/index.jsx +33 -0
- package/src/hooks/useNotification.js +57 -0
- package/src/index.js +13 -0
- package/src/utils/message.js +37 -0
- package/webpack.config.js +35 -0
package/.babelrc
ADDED
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@banch0u/core-project-test-repository",
|
|
3
|
+
"version": "1.0.30",
|
|
4
|
+
"description": "Shared core features for all projects",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/banch0u/core-project-test-repository.git"
|
|
9
|
+
},
|
|
10
|
+
"private": false,
|
|
11
|
+
"author": "banch0u",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "npx babel src --out-dir dist --copy-files"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@microsoft/signalr": "^8.0.7",
|
|
18
|
+
"@pdftron/webviewer": "^11.2.0",
|
|
19
|
+
"antd": "^5.16.0",
|
|
20
|
+
"react": "^18.2.0",
|
|
21
|
+
"react-dom": "^18.2.0",
|
|
22
|
+
"react-router-dom": "^6.16.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@babel/cli": "^7.26.4",
|
|
26
|
+
"@babel/core": "^7.26.9",
|
|
27
|
+
"@babel/plugin-transform-runtime": "^7.26.9",
|
|
28
|
+
"@babel/preset-env": "^7.26.9",
|
|
29
|
+
"@babel/preset-react": "^7.26.3",
|
|
30
|
+
"babel-loader": "^9.2.1"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: "Inter";
|
|
3
|
+
src: url("./Inter/Inter-Thin.ttf") format("truetype");
|
|
4
|
+
font-weight: 100;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
@font-face {
|
|
8
|
+
font-family: "Inter";
|
|
9
|
+
src: url("./Inter/Inter-ExtraLight.ttf") format("truetype");
|
|
10
|
+
font-weight: 200;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
@font-face {
|
|
14
|
+
font-family: "Inter";
|
|
15
|
+
src: url("./Inter/Inter-Light.ttf") format("truetype");
|
|
16
|
+
font-weight: 300;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: "Inter";
|
|
21
|
+
src: url("./Inter/Inter-Regular.ttf") format("truetype");
|
|
22
|
+
font-weight: 400;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@font-face {
|
|
26
|
+
font-family: "Inter";
|
|
27
|
+
src: url("./Inter/Inter-Medium.ttf") format("truetype");
|
|
28
|
+
font-weight: 500;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@font-face {
|
|
32
|
+
font-family: "Inter";
|
|
33
|
+
src: url("./Inter/Inter-SemiBold.ttf") format("truetype");
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@font-face {
|
|
38
|
+
font-family: "Inter";
|
|
39
|
+
src: url("./Inter/Inter-Bold.ttf") format("truetype");
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@font-face {
|
|
44
|
+
font-family: "Inter";
|
|
45
|
+
src: url("./Inter/Inter-ExtraBold.ttf") format("truetype");
|
|
46
|
+
font-weight: 800;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@font-face {
|
|
50
|
+
font-family: "Inter";
|
|
51
|
+
src: url("./Inter/Inter-Black.ttf") format("truetype");
|
|
52
|
+
font-weight: 900;
|
|
53
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export const SortIcon = () => {
|
|
3
|
+
return (
|
|
4
|
+
<svg
|
|
5
|
+
width="22"
|
|
6
|
+
height="22"
|
|
7
|
+
viewBox="0 0 22 22"
|
|
8
|
+
fill="none"
|
|
9
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
10
|
+
>
|
|
11
|
+
<path
|
|
12
|
+
d="M2.75 6.41663H19.25"
|
|
13
|
+
stroke="#016DAF"
|
|
14
|
+
strokeWidth="1.375"
|
|
15
|
+
strokeLinecap="round"
|
|
16
|
+
/>
|
|
17
|
+
<path
|
|
18
|
+
d="M5.5 11H16.5"
|
|
19
|
+
stroke="#016DAF"
|
|
20
|
+
strokeWidth="1.375"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="M9.16675 15.5834H12.8334"
|
|
25
|
+
stroke="#016DAF"
|
|
26
|
+
strokeWidth="1.375"
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
export const FilterIcon = () => {
|
|
33
|
+
return (
|
|
34
|
+
<svg
|
|
35
|
+
width="24"
|
|
36
|
+
height="24"
|
|
37
|
+
viewBox="0 0 24 24"
|
|
38
|
+
fill="none"
|
|
39
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
40
|
+
>
|
|
41
|
+
<path
|
|
42
|
+
d="M3.34985 2H12.2499C12.9899 2 13.5999 2.61001 13.5999 3.35001V4.82999C13.5999 5.36999 13.2599 6.04 12.9299 6.38L10.0299 8.94C9.62991 9.28 9.35986 9.94999 9.35986 10.49V13.39C9.35986 13.79 9.08988 14.33 8.74988 14.54L7.80987 15.15C6.92987 15.69 5.71985 15.08 5.71985 14V10.43C5.71985 9.95999 5.44987 9.35001 5.17987 9.01001L2.61987 6.31C2.27987 5.97 2.00989 5.36999 2.00989 4.95999V3.41C1.99989 2.61 2.60985 2 3.34985 2Z"
|
|
43
|
+
stroke="#016DAF"
|
|
44
|
+
strokeWidth="1.5"
|
|
45
|
+
strokeMiterlimit="10"
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
/>
|
|
49
|
+
<path
|
|
50
|
+
d="M2 12V15C2 20 4 22 9 22H15C20 22 22 20 22 15V9C22 5.88 21.22 3.91999 19.41 2.89999C18.9 2.60999 17.88 2.38999 16.95 2.23999"
|
|
51
|
+
stroke="#016DAF"
|
|
52
|
+
strokeWidth="1.5"
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
/>
|
|
56
|
+
<path
|
|
57
|
+
d="M13 13H18"
|
|
58
|
+
stroke="#016DAF"
|
|
59
|
+
strokeWidth="1.5"
|
|
60
|
+
strokeLinecap="round"
|
|
61
|
+
strokeLinejoin="round"
|
|
62
|
+
/>
|
|
63
|
+
<path
|
|
64
|
+
d="M11 17H18"
|
|
65
|
+
stroke="#016DAF"
|
|
66
|
+
strokeWidth="1.5"
|
|
67
|
+
strokeLinecap="round"
|
|
68
|
+
strokeLinejoin="round"
|
|
69
|
+
/>
|
|
70
|
+
</svg>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import style from "./index.module.scss";
|
|
3
|
+
const Button = ({
|
|
4
|
+
children,
|
|
5
|
+
onClick,
|
|
6
|
+
color = "blue",
|
|
7
|
+
disabled = false,
|
|
8
|
+
type,
|
|
9
|
+
}) => {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
{color === "blue" ? (
|
|
13
|
+
<button
|
|
14
|
+
disabled={disabled}
|
|
15
|
+
type={type}
|
|
16
|
+
className={style.button}
|
|
17
|
+
onClick={onClick}>
|
|
18
|
+
{children}
|
|
19
|
+
</button>
|
|
20
|
+
) : null}
|
|
21
|
+
{color === "white" ? (
|
|
22
|
+
<button
|
|
23
|
+
disabled={disabled}
|
|
24
|
+
type={type}
|
|
25
|
+
className={style.button_white}
|
|
26
|
+
onClick={onClick}>
|
|
27
|
+
{children}
|
|
28
|
+
</button>
|
|
29
|
+
) : null}
|
|
30
|
+
{color === "green" ? (
|
|
31
|
+
<button
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
type={type}
|
|
34
|
+
className={style.button_green}
|
|
35
|
+
onClick={onClick}>
|
|
36
|
+
{children}
|
|
37
|
+
</button>
|
|
38
|
+
) : null}
|
|
39
|
+
{color === "green-white" ? (
|
|
40
|
+
<button
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
type={type}
|
|
43
|
+
className={style.button_green_white}
|
|
44
|
+
onClick={onClick}>
|
|
45
|
+
{children}
|
|
46
|
+
</button>
|
|
47
|
+
) : null}
|
|
48
|
+
{color === "red" ? (
|
|
49
|
+
<button
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
type={type}
|
|
52
|
+
className={style.button_red}
|
|
53
|
+
onClick={onClick}>
|
|
54
|
+
{children}
|
|
55
|
+
</button>
|
|
56
|
+
) : null}
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export default Button;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
.button {
|
|
2
|
+
border-radius: 6px;
|
|
3
|
+
background: var(--darkBlueColor);
|
|
4
|
+
padding: 5px 10px;
|
|
5
|
+
font-size: 16px;
|
|
6
|
+
font-weight: 400;
|
|
7
|
+
line-height: 24px;
|
|
8
|
+
letter-spacing: 0.5px;
|
|
9
|
+
border: 1px solid var(--darkBlueColor);
|
|
10
|
+
color: #fff;
|
|
11
|
+
cursor: pointer;
|
|
12
|
+
transition: 250ms;
|
|
13
|
+
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
gap: 8px;
|
|
18
|
+
svg {
|
|
19
|
+
path {
|
|
20
|
+
transition: 250ms;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.button:hover {
|
|
26
|
+
background: #005386;
|
|
27
|
+
border: 1px solid #005386;
|
|
28
|
+
transition: 250ms;
|
|
29
|
+
}
|
|
30
|
+
.button_white {
|
|
31
|
+
border-radius: 6px;
|
|
32
|
+
background: transparent;
|
|
33
|
+
padding: 5px 10px;
|
|
34
|
+
font-size: 16px;
|
|
35
|
+
font-weight: 400;
|
|
36
|
+
line-height: 24px;
|
|
37
|
+
letter-spacing: 0.5px;
|
|
38
|
+
border: 1px solid var(--darkBlueColor);
|
|
39
|
+
color: var(--darkBlueColor);
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
transition: 250ms;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
gap: 8px;
|
|
46
|
+
svg {
|
|
47
|
+
path {
|
|
48
|
+
transition: 250ms;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
.button_white:hover {
|
|
53
|
+
border: 1px solid gray;
|
|
54
|
+
color: gray;
|
|
55
|
+
transition: 250ms;
|
|
56
|
+
svg {
|
|
57
|
+
path {
|
|
58
|
+
transition: 250ms;
|
|
59
|
+
stroke: gray;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
.button_white:disabled,
|
|
64
|
+
.button_white[disabled] {
|
|
65
|
+
cursor: not-allowed !important;
|
|
66
|
+
opacity: 0.5 !important;
|
|
67
|
+
border-color: gray !important;
|
|
68
|
+
color: gray !important;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.button_green {
|
|
72
|
+
border-radius: 6px;
|
|
73
|
+
background: #219653;
|
|
74
|
+
padding: 5px 10px;
|
|
75
|
+
font-size: 16px;
|
|
76
|
+
font-weight: 400;
|
|
77
|
+
line-height: 24px;
|
|
78
|
+
letter-spacing: 0.5px;
|
|
79
|
+
border: 1px solid #219653;
|
|
80
|
+
color: #fff;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
transition: 250ms;
|
|
83
|
+
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
justify-content: center;
|
|
87
|
+
gap: 8px;
|
|
88
|
+
svg {
|
|
89
|
+
path {
|
|
90
|
+
transition: 250ms;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.button_red {
|
|
96
|
+
border-radius: 6px;
|
|
97
|
+
background: #eb5757;
|
|
98
|
+
padding: 5px 10px;
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
font-weight: 400;
|
|
101
|
+
line-height: 24px;
|
|
102
|
+
letter-spacing: 0.5px;
|
|
103
|
+
border: 1px solid #eb5757;
|
|
104
|
+
color: #fff;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
transition: 250ms;
|
|
107
|
+
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: center;
|
|
111
|
+
gap: 8px;
|
|
112
|
+
svg {
|
|
113
|
+
path {
|
|
114
|
+
transition: 250ms;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.button_green_white {
|
|
120
|
+
border-radius: 6px;
|
|
121
|
+
background: transparent!important;
|
|
122
|
+
padding: 5px 10px;
|
|
123
|
+
font-size: 16px;
|
|
124
|
+
font-weight: 400;
|
|
125
|
+
line-height: 24px;
|
|
126
|
+
letter-spacing: 0.5px;
|
|
127
|
+
border: 1px solid #219653;
|
|
128
|
+
color: #fff;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
transition: 250ms;
|
|
131
|
+
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
justify-content: center;
|
|
135
|
+
gap: 8px;
|
|
136
|
+
svg {
|
|
137
|
+
path {
|
|
138
|
+
transition: 250ms;
|
|
139
|
+
fill: #219653!important;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Dropdown, Menu, Checkbox } from "antd";
|
|
3
|
+
import style from "./index.module.scss";
|
|
4
|
+
import { SortIcon } from "../../assets/icons";
|
|
5
|
+
import Button from "../Button";
|
|
6
|
+
|
|
7
|
+
const ColSort = ({ columns, selectedColumns, handleColumnToggle }) => {
|
|
8
|
+
const [visible, setVisible] = useState(false);
|
|
9
|
+
|
|
10
|
+
const handleVisibleChange = (isVisible) => {
|
|
11
|
+
setVisible(isVisible);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const menu = (
|
|
15
|
+
<Menu className={style.menu}>
|
|
16
|
+
{columns
|
|
17
|
+
.filter((col) => col.dataIndex !== "filterOnly") // Exclude columns with dataIndex === "filterOnly"
|
|
18
|
+
.map(
|
|
19
|
+
(col) =>
|
|
20
|
+
col.showCheckbox !== false && (
|
|
21
|
+
<Menu.Item key={col.title}>
|
|
22
|
+
<div className={style.menu_item}>
|
|
23
|
+
<Checkbox
|
|
24
|
+
disabled={col.disabled}
|
|
25
|
+
checked={selectedColumns.includes(col.dataIndex)}
|
|
26
|
+
onChange={(e) =>
|
|
27
|
+
handleColumnToggle(e.target.checked, col.dataIndex)
|
|
28
|
+
}
|
|
29
|
+
/>
|
|
30
|
+
<span>{col.title}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</Menu.Item>
|
|
33
|
+
)
|
|
34
|
+
)}
|
|
35
|
+
</Menu>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Dropdown
|
|
40
|
+
overlay={menu}
|
|
41
|
+
trigger={["click"]}
|
|
42
|
+
visible={visible}
|
|
43
|
+
onVisibleChange={handleVisibleChange}>
|
|
44
|
+
<Button color="white">
|
|
45
|
+
<SortIcon />
|
|
46
|
+
</Button>
|
|
47
|
+
</Dropdown>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default ColSort;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.menu {
|
|
2
|
+
width: 254px;
|
|
3
|
+
padding: 10px !important;
|
|
4
|
+
padding-bottom: 5px !important;
|
|
5
|
+
box-shadow: 0px 4px 4px 0px #0000001a !important;
|
|
6
|
+
border: 1px solid #d1d1d1 !important;
|
|
7
|
+
background: #deeaf6 !important;
|
|
8
|
+
|
|
9
|
+
.count {
|
|
10
|
+
margin-bottom: 24px;
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
font-size: 14px;
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
line-height: 21px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
li {
|
|
19
|
+
padding: 0 !important;
|
|
20
|
+
margin-bottom: 5px !important;
|
|
21
|
+
font-family: Inter !important;
|
|
22
|
+
font-size: 14px !important;
|
|
23
|
+
font-weight: 400 !important;
|
|
24
|
+
line-height: 21px !important;
|
|
25
|
+
letter-spacing: -0.4000000059604645px !important;
|
|
26
|
+
text-align: left !important;
|
|
27
|
+
}
|
|
28
|
+
.menu_item {
|
|
29
|
+
display: flex;
|
|
30
|
+
gap: 8px;
|
|
31
|
+
background: #fff;
|
|
32
|
+
padding: 6px;
|
|
33
|
+
border-radius: 4px;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
.filter_icon {
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
width: 22px;
|
|
39
|
+
height: 22px;
|
|
40
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
Dropdown,
|
|
4
|
+
Menu,
|
|
5
|
+
Form,
|
|
6
|
+
Input,
|
|
7
|
+
Select,
|
|
8
|
+
DatePicker,
|
|
9
|
+
TreeSelect,
|
|
10
|
+
} from "antd";
|
|
11
|
+
import style from "./index.module.scss";
|
|
12
|
+
import Button from "../Button";
|
|
13
|
+
import { FilterIcon } from "../../assets/icons";
|
|
14
|
+
|
|
15
|
+
const { Option } = Select;
|
|
16
|
+
const { RangePicker } = DatePicker;
|
|
17
|
+
|
|
18
|
+
const Filter = ({
|
|
19
|
+
columns,
|
|
20
|
+
selectedColumns,
|
|
21
|
+
setQuery,
|
|
22
|
+
disabledElementCount,
|
|
23
|
+
setPage,
|
|
24
|
+
setSelectedTopic,
|
|
25
|
+
}) => {
|
|
26
|
+
const [filterForm] = Form.useForm();
|
|
27
|
+
const [visible, setVisible] = useState(false);
|
|
28
|
+
|
|
29
|
+
const handleOpenChange = (isOpen) => {
|
|
30
|
+
setVisible(isOpen);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const formatDate = (day, month, year) => {
|
|
34
|
+
const formattedDay = String(day).padStart(2, "0");
|
|
35
|
+
const formattedMonth = String(month).padStart(2, "0");
|
|
36
|
+
return `${formattedDay}.${formattedMonth}.${year}`;
|
|
37
|
+
};
|
|
38
|
+
const handleFinish = (values) => {
|
|
39
|
+
const formattedValues = { ...values };
|
|
40
|
+
Object.keys(values).forEach((key) => {
|
|
41
|
+
if (Array.isArray(values[key]) && values[key].length === 2) {
|
|
42
|
+
const [start, end] = values[key];
|
|
43
|
+
if (start && end) {
|
|
44
|
+
const formattedStart = formatDate(
|
|
45
|
+
start.date(),
|
|
46
|
+
start.month() + 1,
|
|
47
|
+
start.year()
|
|
48
|
+
);
|
|
49
|
+
const formattedEnd = formatDate(
|
|
50
|
+
end.date(),
|
|
51
|
+
end.month() + 1,
|
|
52
|
+
end.year()
|
|
53
|
+
);
|
|
54
|
+
formattedValues[key] = `${formattedStart} - ${formattedEnd}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
setQuery(formattedValues);
|
|
59
|
+
setVisible(false);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Filter the columns based on selectedColumns, filterDisable, and filter status
|
|
63
|
+
const filteredColumns = columns.filter(
|
|
64
|
+
(col) =>
|
|
65
|
+
(selectedColumns.includes(col.dataIndex) &&
|
|
66
|
+
col.filter !== false &&
|
|
67
|
+
col.filterDisable !== true) ||
|
|
68
|
+
col.dataIndex === "filterOnly" // Always include "filterOnly" columns in the filter
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const getGrid = () => {
|
|
72
|
+
const elementCount = selectedColumns.length - disabledElementCount;
|
|
73
|
+
if (elementCount >= 5) return style.grid5;
|
|
74
|
+
if (elementCount === 4) return style.grid4;
|
|
75
|
+
if (elementCount === 3) return style.grid3;
|
|
76
|
+
if (elementCount === 2) return style.grid2;
|
|
77
|
+
if (elementCount === 1) return style.grid1;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const menu = (
|
|
81
|
+
<Menu className={style.menu}>
|
|
82
|
+
<div className="filter">
|
|
83
|
+
<Form
|
|
84
|
+
onFinish={handleFinish}
|
|
85
|
+
form={filterForm}
|
|
86
|
+
layout="vertical"
|
|
87
|
+
className={`${style.form} ${getGrid()}`}>
|
|
88
|
+
{[
|
|
89
|
+
...filteredColumns.filter((col) => !col.isDouble), // Non-double elements
|
|
90
|
+
...filteredColumns.filter((col) => col.isDouble), // Double elements
|
|
91
|
+
].map((col) => {
|
|
92
|
+
if (col.showCheckbox === false) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const gridSpanClass = col.isDouble
|
|
97
|
+
? style.doubleGrid
|
|
98
|
+
: style.singleGrid;
|
|
99
|
+
|
|
100
|
+
if (col.type === "select") {
|
|
101
|
+
return (
|
|
102
|
+
<Form.Item
|
|
103
|
+
key={col.dataIndex}
|
|
104
|
+
label={col.title}
|
|
105
|
+
name={col.queryName ? col.queryName : col.dataIndex}
|
|
106
|
+
className={gridSpanClass} // Dynamically apply grid class
|
|
107
|
+
>
|
|
108
|
+
<Select
|
|
109
|
+
showSearch={col.isDouble} // Enable search for double items
|
|
110
|
+
onChange={(value) => {
|
|
111
|
+
if (col.dataIndex === "topic") {
|
|
112
|
+
setSelectedTopic(value);
|
|
113
|
+
}
|
|
114
|
+
}}
|
|
115
|
+
filterOption={(input, option) => {
|
|
116
|
+
if (!option || !option.children) return false; // Ensure option and children exist
|
|
117
|
+
|
|
118
|
+
const optionText = String(option.children); // Convert to string if needed
|
|
119
|
+
const normalizedInput = input.toLowerCase(); // Normalize input to lowercase
|
|
120
|
+
const normalizedOption = optionText
|
|
121
|
+
.replace(/İ/g, "I")
|
|
122
|
+
.toLowerCase(); // Normalize option text
|
|
123
|
+
|
|
124
|
+
return normalizedOption.includes(normalizedInput);
|
|
125
|
+
}}>
|
|
126
|
+
<Option value=""></Option>
|
|
127
|
+
{(col?.selectData || []).map((option, i) => {
|
|
128
|
+
const isIdArray = Array.isArray(option.id);
|
|
129
|
+
return (
|
|
130
|
+
<Option
|
|
131
|
+
key={i}
|
|
132
|
+
value={
|
|
133
|
+
isIdArray
|
|
134
|
+
? JSON.stringify(option.id) // Convert array to string
|
|
135
|
+
: option.id // Use ID directly
|
|
136
|
+
}>
|
|
137
|
+
{option.name} {option.surname} {option.text}
|
|
138
|
+
{/* Display the name */}
|
|
139
|
+
</Option>
|
|
140
|
+
);
|
|
141
|
+
})}
|
|
142
|
+
</Select>
|
|
143
|
+
</Form.Item>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (col.type === "date") {
|
|
147
|
+
return (
|
|
148
|
+
<Form.Item
|
|
149
|
+
key={col.dataIndex}
|
|
150
|
+
label={col.title}
|
|
151
|
+
name={col.queryName ? col.queryName : col.dataIndex}
|
|
152
|
+
className={gridSpanClass} // Dynamically apply grid class
|
|
153
|
+
>
|
|
154
|
+
<RangePicker format="DD.MM.YYYY" placeholder="" />
|
|
155
|
+
</Form.Item>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (col.type === "recursive") {
|
|
159
|
+
return (
|
|
160
|
+
<Form.Item
|
|
161
|
+
key={col.dataIndex}
|
|
162
|
+
label={col.title}
|
|
163
|
+
name={col.queryName ? col.queryName : col.dataIndex}
|
|
164
|
+
className={gridSpanClass} // Dynamically apply grid class
|
|
165
|
+
>
|
|
166
|
+
<TreeSelect
|
|
167
|
+
// style={{ width: "230px", marginTop: "5px" }}
|
|
168
|
+
showSearch
|
|
169
|
+
popupMatchSelectWidth={false}
|
|
170
|
+
allowClear
|
|
171
|
+
treeDefaultExpandAll
|
|
172
|
+
treeData={col.selectData}
|
|
173
|
+
/>
|
|
174
|
+
</Form.Item>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
<Form.Item
|
|
179
|
+
key={col.dataIndex}
|
|
180
|
+
label={col.title}
|
|
181
|
+
name={col.queryName ? col.queryName : col.dataIndex}
|
|
182
|
+
className={gridSpanClass} // Dynamically apply grid class
|
|
183
|
+
>
|
|
184
|
+
<Input />
|
|
185
|
+
</Form.Item>
|
|
186
|
+
);
|
|
187
|
+
})}
|
|
188
|
+
</Form>
|
|
189
|
+
<div className={style.buttons}>
|
|
190
|
+
<Button onClick={() => filterForm.resetFields()} color="white">
|
|
191
|
+
Təmizlə
|
|
192
|
+
</Button>
|
|
193
|
+
<Button
|
|
194
|
+
onClick={() => {
|
|
195
|
+
setPage(1);
|
|
196
|
+
filterForm.submit();
|
|
197
|
+
setVisible(false);
|
|
198
|
+
}}>
|
|
199
|
+
Axtar
|
|
200
|
+
</Button>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</Menu>
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Dropdown
|
|
208
|
+
overlay={menu}
|
|
209
|
+
trigger={["click"]}
|
|
210
|
+
open={visible} // Updated to use open
|
|
211
|
+
onOpenChange={handleOpenChange} // Updated to use onOpenChange
|
|
212
|
+
>
|
|
213
|
+
<Button color="white">
|
|
214
|
+
<FilterIcon /> Filter menyu
|
|
215
|
+
</Button>
|
|
216
|
+
</Dropdown>
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export default Filter;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
.menu {
|
|
2
|
+
padding: 14px !important;
|
|
3
|
+
box-shadow: 0px 4px 4px 0px #0000001a;
|
|
4
|
+
background: #fff !important;
|
|
5
|
+
|
|
6
|
+
.form {
|
|
7
|
+
display: grid;
|
|
8
|
+
gap: 14px; // Space between items
|
|
9
|
+
|
|
10
|
+
&.grid5 {
|
|
11
|
+
grid-template-columns: repeat(5, minmax(170px, 1fr)); // 5 columns
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
&.grid4 {
|
|
15
|
+
grid-template-columns: repeat(4, minmax(170px, 1fr)); // 4 columns
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
&.grid3 {
|
|
19
|
+
grid-template-columns: repeat(3, minmax(170px, 1fr)); // 3 columns
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
&.grid2 {
|
|
23
|
+
grid-template-columns: repeat(2, minmax(170px, 1fr)); // 2 columns
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
&.grid1 {
|
|
27
|
+
grid-template-columns: repeat(1, minmax(170px, 1fr)); // 1 column
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.singleGrid {
|
|
31
|
+
grid-column: span 1; // Single grid item takes one column
|
|
32
|
+
width: 170px; // Ensure single items are 170px wide
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.doubleGrid {
|
|
36
|
+
grid-column: span 2; // Double grid item takes two columns
|
|
37
|
+
width: 334px; // Ensure double items are 334px wide
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
label {
|
|
41
|
+
font-family: Inter;
|
|
42
|
+
font-size: 14px;
|
|
43
|
+
font-weight: 400;
|
|
44
|
+
line-height: 16.94px;
|
|
45
|
+
text-align: left;
|
|
46
|
+
color: #646464;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.input,
|
|
50
|
+
.select,
|
|
51
|
+
.modal_date {
|
|
52
|
+
width: 100%; // Inherit width based on grid column
|
|
53
|
+
height: 34px;
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
padding: 8px 16px;
|
|
56
|
+
font-size: 16px;
|
|
57
|
+
font-weight: 500;
|
|
58
|
+
line-height: 24px;
|
|
59
|
+
text-align: left;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.select > div {
|
|
63
|
+
padding: 5px 16px !important;
|
|
64
|
+
border-radius: 6px !important;
|
|
65
|
+
height: 34px;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.select {
|
|
69
|
+
span {
|
|
70
|
+
font-size: 16px !important;
|
|
71
|
+
font-weight: 500 !important;
|
|
72
|
+
line-height: 24px !important;
|
|
73
|
+
text-align: left !important;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.modal_date {
|
|
78
|
+
input {
|
|
79
|
+
font-size: 16px !important;
|
|
80
|
+
font-weight: 500 !important;
|
|
81
|
+
line-height: 24px !important;
|
|
82
|
+
text-align: left !important;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.buttons {
|
|
88
|
+
display: flex;
|
|
89
|
+
justify-content: flex-end;
|
|
90
|
+
gap: 14px;
|
|
91
|
+
margin-top: 20px;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.filter_icon {
|
|
96
|
+
cursor: pointer;
|
|
97
|
+
width: 22px;
|
|
98
|
+
height: 22px;
|
|
99
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import style from "./index.module.scss";
|
|
3
|
+
import { Spin } from "antd";
|
|
4
|
+
|
|
5
|
+
const Loading = () => {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
<div className={style.overlay}></div>
|
|
9
|
+
<div className={style.spinner}>
|
|
10
|
+
<Spin size="large" />
|
|
11
|
+
</div>
|
|
12
|
+
</>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default Loading;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.spinner {
|
|
2
|
+
position: absolute;
|
|
3
|
+
top: 50%;
|
|
4
|
+
left: 50%;
|
|
5
|
+
transform: translate(-50%, -50%);
|
|
6
|
+
color: red !important;
|
|
7
|
+
z-index: 9999;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.overlay {
|
|
11
|
+
height: 100vh;
|
|
12
|
+
width: 100%;
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: 0;
|
|
15
|
+
left: 0;
|
|
16
|
+
background: #ffffff50;
|
|
17
|
+
z-index: 9999;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Select as AntSelect } from "antd";
|
|
3
|
+
import { counts } from "./constant";
|
|
4
|
+
const { Option } = AntSelect;
|
|
5
|
+
|
|
6
|
+
const Select = ({ setSize, size }) => {
|
|
7
|
+
const handleChange = (value) => {
|
|
8
|
+
setSize(value);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<AntSelect
|
|
13
|
+
onChange={handleChange}
|
|
14
|
+
defaultValue={size}
|
|
15
|
+
style={{ width: 60, marginTop: "10px", marginLeft: "10px" }}
|
|
16
|
+
>
|
|
17
|
+
{counts.map((item) => (
|
|
18
|
+
<Option key={item.id} value={item.value}>
|
|
19
|
+
{item.value}
|
|
20
|
+
</Option>
|
|
21
|
+
))}
|
|
22
|
+
</AntSelect>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default Select;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Pagination as AntPagination } from "antd";
|
|
3
|
+
import style from "./Pagination.module.scss";
|
|
4
|
+
import Select from "./Select";
|
|
5
|
+
|
|
6
|
+
const Pagination = ({ onChange, page = 1, size = 10, total = 0, setSize }) => {
|
|
7
|
+
const setPagination = (page) => {
|
|
8
|
+
onChange(page); // Trigger the page change callback
|
|
9
|
+
|
|
10
|
+
// Find the scrollable part of the Ant Table and scroll to the top
|
|
11
|
+
const tableBody = document.querySelector(".ant-table-body");
|
|
12
|
+
if (tableBody) {
|
|
13
|
+
tableBody.scrollTo({ top: 0, behavior: "smooth" });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className={style.container}>
|
|
19
|
+
<AntPagination
|
|
20
|
+
className={style.pagination}
|
|
21
|
+
current={Number(page)} // Ensure numeric values
|
|
22
|
+
onChange={setPagination}
|
|
23
|
+
total={Number(total)} // Ensure numeric values
|
|
24
|
+
showSizeChanger={false}
|
|
25
|
+
/>
|
|
26
|
+
<div className="pagination_select">
|
|
27
|
+
<Select size={size} setSize={setSize} style={{ marginTop: "10px" }} />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default Pagination;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// hooks/useNotification.js
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import * as signalR from "@microsoft/signalr";
|
|
4
|
+
import { infoMessageBottomRight } from "../utils/message";
|
|
5
|
+
|
|
6
|
+
const useNotification = () => {
|
|
7
|
+
const [notifications, setNotifications] = useState(() => {
|
|
8
|
+
// localStorage'dan bildirimi başlat
|
|
9
|
+
const savedNotifications = localStorage.getItem("notifications");
|
|
10
|
+
return savedNotifications ? JSON.parse(savedNotifications) : [];
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const token = localStorage.getItem("token");
|
|
15
|
+
let baseUrl;
|
|
16
|
+
if (window.location.hostname === "localhost") {
|
|
17
|
+
|
|
18
|
+
baseUrl = process.env.REACT_APP_ROOT;
|
|
19
|
+
} else {
|
|
20
|
+
baseUrl = window.location.origin;
|
|
21
|
+
}
|
|
22
|
+
if (!token) {
|
|
23
|
+
console.error("Token tapılmadı!");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const connection = new signalR.HubConnectionBuilder()
|
|
28
|
+
.withUrl(`${baseUrl}/notify?token=${token}`, {
|
|
29
|
+
transport:
|
|
30
|
+
signalR.HttpTransportType.WebSockets,
|
|
31
|
+
withCredentials: false,
|
|
32
|
+
skipNegotiation: true,
|
|
33
|
+
})
|
|
34
|
+
.configureLogging(signalR.LogLevel.Information)
|
|
35
|
+
.build();
|
|
36
|
+
|
|
37
|
+
connection
|
|
38
|
+
.start()
|
|
39
|
+
.then(() => {
|
|
40
|
+
console.log("SignalR bağlantısı quruldu.");
|
|
41
|
+
connection.on("receive", (message) => {
|
|
42
|
+
console.log("Yeni bildiriş:", message);
|
|
43
|
+
infoMessageBottomRight(message);
|
|
44
|
+
});
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => console.error("SignalR bağlantısı qurula bilmədi:", err));
|
|
47
|
+
|
|
48
|
+
return () => {
|
|
49
|
+
connection.stop();
|
|
50
|
+
console.log("SignalR bağlantısı bağlandı.");
|
|
51
|
+
};
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
return notifications;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default useNotification;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Export the Button component from the components directory as a named export
|
|
2
|
+
import "./assets/fonts/fonts.css";
|
|
3
|
+
export { default as Button } from "./components/Button";
|
|
4
|
+
export { default as ColSort } from "./components/ColSort";
|
|
5
|
+
export { default as Filter } from "./components/Filter";
|
|
6
|
+
export { default as Loading } from "./components/Loading";
|
|
7
|
+
export { default as Pagination } from "./components/Pagination";
|
|
8
|
+
export { default as useNotification } from "./hooks/useNotification";
|
|
9
|
+
export { default as message } from "./utils/message";
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { notification } from "antd";
|
|
2
|
+
|
|
3
|
+
const playNotificationSound = async () => {
|
|
4
|
+
const audio = new Audio("https://assets.mixkit.co/active_storage/sfx/2575/2575-preview.mp3");
|
|
5
|
+
audio.type = "audio/mpeg"; // Audio format
|
|
6
|
+
audio.preload = "auto";
|
|
7
|
+
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
audio.play().catch((error) => console.error("Audio play failed:", error));
|
|
10
|
+
}, 400);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export const errorMessage = ({ value, placeMent }) => {
|
|
15
|
+
return notification.error({
|
|
16
|
+
message: "",
|
|
17
|
+
description: value || "Serverdə problem baş verdi",
|
|
18
|
+
placement: placeMent,
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const succesMessage = (value) => {
|
|
23
|
+
return notification.success({
|
|
24
|
+
message: "",
|
|
25
|
+
description: value || "Uğurla tamamlandı",
|
|
26
|
+
placement: "topRight",
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const infoMessageBottomRight = (value) => {
|
|
31
|
+
playNotificationSound();
|
|
32
|
+
return notification.info({
|
|
33
|
+
message: "Yeni bildiriş",
|
|
34
|
+
description: value || "Bildiriş",
|
|
35
|
+
placement: "bottomRight",
|
|
36
|
+
});
|
|
37
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
entry: './src/index.js', // Your entry file where your components are exported
|
|
5
|
+
output: {
|
|
6
|
+
path: path.resolve(__dirname, 'dist'),
|
|
7
|
+
filename: 'bundle.js', // Your output bundle
|
|
8
|
+
library: '@banch0u/core-project-test-repository', // Exposes your components as a library
|
|
9
|
+
libraryTarget: 'umd', // Can be used in various environments like CommonJS, AMD, etc.
|
|
10
|
+
clean: true, // Cleans the dist folder before each build
|
|
11
|
+
},
|
|
12
|
+
resolve: {
|
|
13
|
+
extensions: ['.js', '.jsx'], // Resolve .js and .jsx files
|
|
14
|
+
},
|
|
15
|
+
module: {
|
|
16
|
+
rules: [
|
|
17
|
+
{
|
|
18
|
+
test: /\.jsx?$/, // Applies to JavaScript/JSX files
|
|
19
|
+
exclude: /node_modules/,
|
|
20
|
+
use: {
|
|
21
|
+
loader: 'babel-loader',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
test: /\.scss$/, // Handles SCSS files
|
|
26
|
+
use: [
|
|
27
|
+
'style-loader', // Adds styles to DOM
|
|
28
|
+
'css-loader', // Translates CSS into CommonJS
|
|
29
|
+
'sass-loader', // Compiles SCSS to CSS
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
devtool: 'source-map', // For better error tracking
|
|
35
|
+
};
|