feat
This commit is contained in:
commit
aede56d4f2
8
.env.dev
Normal file
8
.env.dev
Normal file
@ -0,0 +1,8 @@
|
||||
REACT_APP_API_URL=https://cms-api-dev.angkorcoms.com
|
||||
REACT_APP_VERSION=2.0.0
|
||||
REACT_APP_CODE=009
|
||||
|
||||
REACT_APP_AES_SECRETKEY=unionmobilekookhwa20230906open11
|
||||
REACT_APP_AES_IV=unionmobileivhuh
|
||||
REACT_APP_DEV_THUMBNAIL_SERVER=https://d2ngfzyfwzgoim.cloudfront.net/
|
||||
GENERATE_SOURCEMAP=false
|
||||
7
.env.prod
Normal file
7
.env.prod
Normal file
@ -0,0 +1,7 @@
|
||||
REACT_APP_API_URL=https://cms-api.angkorlifes.com
|
||||
REACT_APP_VERSION=2.0.0
|
||||
REACT_APP_CODE=008
|
||||
|
||||
REACT_APP_AES_SECRETKEY=unionmobilekookhwa20230906open11
|
||||
REACT_APP_AES_IV=unionmobileivhuh
|
||||
REACT_APP_PROD_THUMBNAIL_SERVER=https://d2ngfzyfwzgoim.cloudfront.net/
|
||||
8
.gitattributes
vendored
Normal file
8
.gitattributes
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# 자동 줄바꿈 변환 방지
|
||||
* text=auto
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.css text eol=lf
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
coverage
|
||||
.vscode
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "none",
|
||||
"jsxBracketSameLine": false,
|
||||
"printWidth": 120
|
||||
}
|
||||
14
.vscode/launch.json
vendored
Normal file
14
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Launch Chrome against localhost",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"preLaunchTask": "npm: start:dev"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always"
|
||||
}
|
||||
28
.vscode/tasks.json
vendored
Normal file
28
.vscode/tasks.json
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "npm: start:dev",
|
||||
"type": "npm",
|
||||
"script": "start:dev",
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "custom",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "No issues found",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "Files successfully emitted, wating for typecheck results...",
|
||||
"endsPattern": "No issues found"
|
||||
}
|
||||
},
|
||||
"detail": "env-cmd -f .env.dev craco start"
|
||||
}
|
||||
]
|
||||
}
|
||||
46
README.md
Normal file
46
README.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `npm start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `npm test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `npm run build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `npm run eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
14
craco.config.ts
Normal file
14
craco.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
const { CracoAliasPlugin } = require('react-app-alias');
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
{
|
||||
plugin: CracoAliasPlugin,
|
||||
options: {
|
||||
source: 'tsconfig',
|
||||
baseUrl: '.',
|
||||
tsConfigPath: './tsconfig.paths.json'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
45
eslint.config.mjs
Normal file
45
eslint.config.mjs
Normal file
@ -0,0 +1,45 @@
|
||||
import js from '@eslint/js';
|
||||
import react from 'eslint-plugin-react';
|
||||
import reactJsxRuntime from 'eslint-plugin-react/jsx-runtime';
|
||||
import typescript from '@typescript-eslint/eslint-plugin';
|
||||
import prettier from 'eslint-plugin-prettier';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
export default [
|
||||
{
|
||||
root: true,
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
react,
|
||||
prettier,
|
||||
'@typescript-eslint': typescript
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...react.configs.recommended.rules,
|
||||
...reactJsxRuntime.configs.recommended.rules,
|
||||
...typescript.configs.recommended.rules,
|
||||
...prettier.configs.recommended.rules,
|
||||
|
||||
'prettier/prettier': ['error', { singleQuote: true, endOfLine: 'auto' }],
|
||||
'arrow-body-style': 'off',
|
||||
'prefer-arrow-callback': 'off'
|
||||
},
|
||||
ignores: ['craco.config.ts'],
|
||||
environment: {
|
||||
browser: true,
|
||||
es2021: true,
|
||||
jest: true
|
||||
}
|
||||
}
|
||||
];
|
||||
19637
package-lock.json
generated
Normal file
19637
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
78
package.json
Normal file
78
package.json
Normal file
@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "angkoremoticons-cms",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^2.6.1",
|
||||
"@tanstack/react-query": "^5.68.0",
|
||||
"@tanstack/react-query-devtools": "^5.68.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"axios": "^1.8.3",
|
||||
"bootstrap": "^5.3.3",
|
||||
"classnames": "^2.5.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"nvd3": "^1.8.6",
|
||||
"react": "^19.0.0",
|
||||
"react-bootstrap": "^2.10.9",
|
||||
"react-datepicker": "^8.2.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router-dom": "^7.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-select": "^5.10.1",
|
||||
"react-toastify": "^11.0.5",
|
||||
"redux-persist": "^6.0.0",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "env-cmd -f .env.prod craco start",
|
||||
"start:dev": "env-cmd -f .env.dev craco start",
|
||||
"build": "set \"GENERATE_SOURCEMAP=false\" && set \"BUILD_PATH=build/prod\" && env-cmd -f .env.prod craco build",
|
||||
"build:dev": "set \"GENERATE_SOURCEMAP=false\" && set \"BUILD_PATH=build/dev\" && env-cmd -f .env.dev craco build",
|
||||
"test": "craco test",
|
||||
"eject": "craco eject",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^22.13.10",
|
||||
"@types/react": "^19.0.11",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"globals": "^16.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"react-app-alias": "^2.2.2",
|
||||
"sass": "^1.86.0",
|
||||
"typescript-eslint": "^8.26.1"
|
||||
}
|
||||
}
|
||||
79
public/assets/fonts/datta/datta-icons.css
Normal file
79
public/assets/fonts/datta/datta-icons.css
Normal file
@ -0,0 +1,79 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
@font-face {
|
||||
font-family: 'pct';
|
||||
src: url('fonts/pct.eot');
|
||||
src:
|
||||
url('fonts/pct.eot?#iefix') format('embedded-opentype'),
|
||||
url('fonts/pct.woff') format('woff'),
|
||||
url('fonts/pct.ttf') format('truetype'),
|
||||
url('fonts/pct.svg#pct') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[data-icon]:before {
|
||||
font-family: 'pct' !important;
|
||||
content: attr(data-icon);
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
text-transform: none !important;
|
||||
speak: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
[class^='pct-']:before,
|
||||
[class*=' pct-']:before {
|
||||
font-family: 'pct' !important;
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
text-transform: none !important;
|
||||
speak: none;
|
||||
line-height: 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.pct-arrow1:before {
|
||||
content: '\61';
|
||||
}
|
||||
.pct-arrow2:before {
|
||||
content: '\62';
|
||||
}
|
||||
.pct-arrow3:before {
|
||||
content: '\63';
|
||||
}
|
||||
.pct-arrow4:before {
|
||||
content: '\64';
|
||||
}
|
||||
.pct-chat1:before {
|
||||
content: '\65';
|
||||
}
|
||||
.pct-chat2:before {
|
||||
content: '\66';
|
||||
}
|
||||
.pct-chat3:before {
|
||||
content: '\67';
|
||||
}
|
||||
.pct-chat4:before {
|
||||
content: '\68';
|
||||
}
|
||||
.pct-loader1:before {
|
||||
content: '\69';
|
||||
}
|
||||
.pct-arrow-sharp1:before {
|
||||
content: '\6a';
|
||||
}
|
||||
.pct-arrow-sharp2:before {
|
||||
content: '\6b';
|
||||
}
|
||||
.pct-arrow-sharp3:before {
|
||||
content: '\6c';
|
||||
}
|
||||
.pct-arrow-sharp4:before {
|
||||
content: '\6d';
|
||||
}
|
||||
BIN
public/assets/fonts/datta/fonts/pct.eot
Normal file
BIN
public/assets/fonts/datta/fonts/pct.eot
Normal file
Binary file not shown.
23
public/assets/fonts/datta/fonts/pct.svg
Normal file
23
public/assets/fonts/datta/fonts/pct.svg
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>Generated by Fontastic.me</metadata>
|
||||
<defs>
|
||||
<font id="pct" horiz-adv-x="512">
|
||||
<font-face font-family="pct" units-per-em="512" ascent="480" descent="-32"/>
|
||||
<missing-glyph horiz-adv-x="512" />
|
||||
|
||||
<glyph glyph-name="arrow1" unicode="a" d="M318 512c0 0-6-86-35-122-24-37-69-83-69-83 0 0-21-25-20-51 1-26 11-40 18-51 8-12 62-70 71-82 7-13 30-50 35-123z"/>
|
||||
<glyph glyph-name="arrow2" unicode="b" d="M194 512c0 0 6-86 35-122 24-37 69-83 69-83 0 0 21-25 20-51-1-26-11-40-19-51-7-12-61-70-70-82-7-13-30-50-35-123z"/>
|
||||
<glyph glyph-name="arrow3" unicode="c" d="M512 194c0 0-86 6-122 35-37 24-83 69-83 69 0 0-25 21-51 20-26-1-40-11-51-19-12-7-70-61-82-70-13-7-50-30-123-35z"/>
|
||||
<glyph glyph-name="arrow4" unicode="d" d="M512 318c0 0-86-6-122-35-37-24-83-69-83-69 0 0-25-21-51-20-26 1-40 11-51 19-12 7-70 61-82 70-13 7-50 30-123 35z"/>
|
||||
<glyph glyph-name="chat1" unicode="e" d="M345 0l-152 0c0 0-26 4-26 30 0 25 18 38 24 40 6 1 136 103 154 439z"/>
|
||||
<glyph glyph-name="chat2" unicode="f" d="M167 0l152 0c0 0 26 4 26 30 0 25-18 38-24 40-6 1-136 103-154 439z"/>
|
||||
<glyph glyph-name="chat3" unicode="g" d="M167 512l152 0c0 0 26-4 26-30 0-25-18-38-24-40-6-1-136-103-154-439z"/>
|
||||
<glyph glyph-name="chat4" unicode="h" d="M345 512l-152 0c0 0-26-4-26-30 0-25 18-38 24-40 6-1 136-103 154-439z"/>
|
||||
<glyph glyph-name="loader1" unicode="i" d="M275 101c0 0-39-14-29-57 2-11 12-34 47-39 35-5 166 55 192 173 27 118-12 178-35 211-24 33-66 73-142 89-75 16-136-7-161-24-24-18-65-39-96-121-2-7-31-82 14-164 48-80 75-87 130-112-49 29-92 68-113 115-14 42-33 136 16 186 42 48 60 70 158 76 90 2 143-68 158-110 15-43 9-117-18-148-26-30-44-61-121-75z"/>
|
||||
<glyph glyph-name="arrow-sharp1" unicode="j" d="M333 512c0 0-7-86-35-122-24-37-82-83-82-83 0 0-38-25-37-51 1-26 40-57 36-51 54-50 45-38 83-82 8-13 30-50 35-123z"/>
|
||||
<glyph glyph-name="arrow-sharp2" unicode="k" d="M179 512c0 0 7-86 35-122 24-37 82-83 82-83 0 0 38-25 37-51-1-26-40-57-36-51-54-50-45-38-83-82-8-13-30-50-35-123z"/>
|
||||
<glyph glyph-name="arrow-sharp3" unicode="l" d="M0 179c0 0 86 7 122 35 37 24 83 82 83 82 0 0 25 38 51 37 26-1 57-40 51-36 50-54 38-45 82-83 13-8 50-30 123-35z"/>
|
||||
<glyph glyph-name="arrow-sharp4" unicode="m" d="M0 333c0 0 86-7 122-35 37-24 83-82 83-82 0 0 25-38 51-37 26 1 57 40 51 36 50 54 38 45 82 83 13 8 50 30 123 35z"/>
|
||||
</font></defs></svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/assets/fonts/datta/fonts/pct.ttf
Normal file
BIN
public/assets/fonts/datta/fonts/pct.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/datta/fonts/pct.woff
Normal file
BIN
public/assets/fonts/datta/fonts/pct.woff
Normal file
Binary file not shown.
BIN
public/assets/fonts/feather/fonts/feather.eot
Normal file
BIN
public/assets/fonts/feather/fonts/feather.eot
Normal file
Binary file not shown.
759
public/assets/fonts/feather/fonts/feather.svg
Normal file
759
public/assets/fonts/feather/fonts/feather.svg
Normal file
@ -0,0 +1,759 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<!--
|
||||
2013-9-30: Created.
|
||||
-->
|
||||
<svg>
|
||||
<metadata>
|
||||
Created by iconfont
|
||||
</metadata>
|
||||
<defs>
|
||||
|
||||
<font id="feather" horiz-adv-x="1024" >
|
||||
<font-face
|
||||
font-family="feather"
|
||||
font-weight="500"
|
||||
font-stretch="normal"
|
||||
units-per-em="1024"
|
||||
ascent="896"
|
||||
descent="-128"
|
||||
/>
|
||||
<missing-glyph />
|
||||
|
||||
<glyph glyph-name="x" unicode="x" horiz-adv-x="1001"
|
||||
d="M281 543q-27 -1 -53 -1h-83q-18 0 -36.5 -6t-32.5 -18.5t-23 -32t-9 -45.5v-76h912v41q0 16 -0.5 30t-0.5 18q0 13 -5 29t-17 29.5t-31.5 22.5t-49.5 9h-133v-97h-438v97zM955 310v-52q0 -23 0.5 -52t0.5 -58t-10.5 -47.5t-26 -30t-33 -16t-31.5 -4.5q-14 -1 -29.5 -0.5
|
||||
t-29.5 0.5h-32l-45 128h-439l-44 -128h-29h-34q-20 0 -45 1q-25 0 -41 9.5t-25.5 23t-13.5 29.5t-4 30v167h911zM163 247q-12 0 -21 -8.5t-9 -21.5t9 -21.5t21 -8.5q13 0 22 8.5t9 21.5t-9 21.5t-22 8.5zM316 123q-8 -26 -14 -48q-5 -19 -10.5 -37t-7.5 -25t-3 -15t1 -14.5
|
||||
t9.5 -10.5t21.5 -4h37h67h81h80h64h36q23 0 34 12t2 38q-5 13 -9.5 30.5t-9.5 34.5q-5 19 -11 39h-368zM336 498v228q0 11 2.5 23t10 21.5t20.5 15.5t34 6h188q31 0 51.5 -14.5t20.5 -52.5v-227h-327z" />
|
||||
|
||||
|
||||
|
||||
<glyph glyph-name="alert-octagon" unicode="" d="M128 542.976v-317.952L353.024 0h317.952L896 225.024V542.976L670.976 768H353.024L128 542.976zM335.36 853.333333h353.28a42.666667 42.666667 0 0 0 30.165333-12.501333l250.026667-250.026667A42.666667 42.666667 0 0 0 981.333333 560.64v-353.28a42.666667 42.666667 0 0 0-12.501333-30.165333l-250.026667-250.026667a42.666667 42.666667 0 0 0-30.165333-12.501333H335.36a42.666667 42.666667 0 0 0-30.165333 12.501333l-250.026667 250.026667A42.666667 42.666667 0 0 0 42.666667 207.36V560.64a42.666667 42.666667 0 0 0 12.501333 30.165333l250.026667 250.026667A42.666667 42.666667 0 0 0 335.36 853.333333zM512 213.333333m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM469.333333 554.666667v-170.666667a42.666667 42.666667 0 0 1 85.333334 0V554.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="alert-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM469.333333 554.666667v-170.666667a42.666667 42.666667 0 0 1 85.333334 0V554.666667a42.666667 42.666667 0 0 1-85.333334 0zM512 213.333333m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="activity" unicode="" d="M424.490667 781.482667c-12.970667 38.912-68.010667 38.912-80.981334 0L225.28 426.666667H85.333333a42.666667 42.666667 0 0 1 0-85.333334h170.666667a42.666667 42.666667 0 0 1 40.490667 29.184L384 633.088l215.509333-646.570667c12.970667-38.912 68.010667-38.912 80.981334 0L798.72 341.333333H938.666667a42.666667 42.666667 0 0 1 0 85.333334h-170.666667a42.666667 42.666667 0 0 1-40.490667-29.184L640 134.912 424.490667 781.482667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="alert-triangle" unicode="" d="M402.432 753.237333a128.085333 128.085333 0 0 0 219.136 0L983.296 149.333333A128 128 0 0 0 873.386667-42.666667H150.144a128 128 0 0 0-109.098667 192.597334l361.386667 603.306666zM114.602667 106.666667A42.666667 42.666667 0 0 1 150.613333 42.666667h722.304a42.666667 42.666667 0 0 1 36.821334 63.402666L548.48 709.162667a42.666667 42.666667 0 0 1-72.917333 0.085333L114.602667 106.666667zM512 170.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM469.333333 512v-170.666667a42.666667 42.666667 0 0 1 85.333334 0V512a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="align-center" unicode="" d="M768 512H256a42.666667 42.666667 0 1 1 0-85.333333h512a42.666667 42.666667 0 0 1 0 85.333333zM896 682.666667H128a42.666667 42.666667 0 1 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334zM896 341.333333H128a42.666667 42.666667 0 0 1 0-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333zM768 170.666667H256a42.666667 42.666667 0 0 1 0-85.333334h512a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="airplay" unicode="" d="M213.333333 213.333333H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V682.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h682.666666a42.666667 42.666667 0 0 0 42.666667-42.666666v-426.666667a42.666667 42.666667 0 0 0-42.666667-42.666667h-42.666666a42.666667 42.666667 0 0 1 0-85.333333h42.666666a128 128 0 0 1 128 128V682.666667a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-426.666667a128 128 0 0 1 128-128h42.666666a42.666667 42.666667 0 0 1 0 85.333333zM512 189.354667L389.76 42.666667h244.48L512 189.354667zM725.333333-42.666667H298.666667a42.666667 42.666667 0 0 0-32.768 69.973334l213.333333 256a42.666667 42.666667 0 0 0 65.536 0l213.333333-256A42.666667 42.666667 0 0 0 725.333333-42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="align-justify" unicode="" d="M896 512H128a42.666667 42.666667 0 1 1 0-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333zM896 682.666667H128a42.666667 42.666667 0 1 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334zM896 341.333333H128a42.666667 42.666667 0 0 1 0-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333zM896 170.666667H128a42.666667 42.666667 0 0 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="align-left" unicode="" d="M725.333333 512H128a42.666667 42.666667 0 1 1 0-85.333333h597.333333a42.666667 42.666667 0 0 1 0 85.333333zM896 682.666667H128a42.666667 42.666667 0 1 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334zM896 341.333333H128a42.666667 42.666667 0 0 1 0-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333zM725.333333 170.666667H128a42.666667 42.666667 0 0 1 0-85.333334h597.333333a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="align-right" unicode="" d="M896 512H298.666667a42.666667 42.666667 0 1 1 0-85.333333h597.333333a42.666667 42.666667 0 0 1 0 85.333333zM896 682.666667H128a42.666667 42.666667 0 1 1 0-85.333334h768a42.666667 42.666667 0 0 1 0 85.333334zM896 341.333333H128a42.666667 42.666667 0 0 1 0-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333zM896 170.666667H298.666667a42.666667 42.666667 0 0 1 0-85.333334h597.333333a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-down-left" unicode="" d="M737.834667 670.165333l-512-512a42.666667 42.666667 0 0 1 60.330666-60.330666l512 512a42.666667 42.666667 0 1 1-60.330666 60.330666zM298.666667 512a42.666667 42.666667 0 1 1-85.333334 0v-384a42.666667 42.666667 0 0 1 42.666667-42.666667h384a42.666667 42.666667 0 0 1 0 85.333334H298.666667V512z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-down-right" unicode="" d="M225.834667 609.834667l512-512a42.666667 42.666667 0 0 1 60.330666 60.330666l-512 512a42.666667 42.666667 0 0 1-60.330666-60.330666zM384 170.666667a42.666667 42.666667 0 0 1 0-85.333334h384a42.666667 42.666667 0 0 1 42.666667 42.666667V512a42.666667 42.666667 0 0 1-85.333334 0v-341.333333H384z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="anchor" unicode="" d="M554.666667 2.346667000000025A384.170667 384.170667 0 0 1 893.653333 341.33333300000004H810.666667a42.666667 42.666667 0 0 0 0 85.333334h128a42.666667 42.666667 0 0 0 42.666666-42.666667c0-259.2-210.133333-469.333333-469.333333-469.333333S42.666667 124.79999999999995 42.666667 384a42.666667 42.666667 0 0 0 42.666666 42.666667h128a42.666667 42.666667 0 0 0 0-85.333334H130.346667A384.170667 384.170667 0 0 1 469.333333 2.346667000000025V554.666667a42.666667 42.666667 0 0 0 85.333334 0v-552.32zM512 512a170.666667 170.666667 0 1 0 0 341.333333 170.666667 170.666667 0 0 0 0-341.333333z m0 85.333333a85.333333 85.333333 0 1 1 0 170.666667 85.333333 85.333333 0 0 1 0-170.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="aperture" unicode="" d="M456.533333-82.09066700000005a469.76 469.76 0 0 0-375.893333 280.917334A467.797333 467.797333 0 0 0 42.666667 384c0 102.954667 33.152 198.186667 89.386666 275.584a42.410667 42.410667 0 0 0 4.650667 6.272 468.608 468.608 0 0 0 424.917333 184.874667 469.76 469.76 0 0 0 381.781334-281.6C967.808 512.426667 981.333333 449.792 981.333333 384a467.2 467.2 0 0 0-89.386666-275.584 42.410667 42.410667 0 0 0-4.650667-6.272 468.608 468.608 0 0 0-424.917333-184.874667 42.410667 42.410667 0 0 0-5.802667 0.64z m-14.72 88.490667L536.661333 170.66666699999996H192.682667a384.213333 384.213333 0 0 1 249.130666-164.266667z m95.317334-5.589333a383.146667 383.146667 0 0 1 266.794666 133.717333L709.12 298.66666699999996 647.68 192.42666699999995a42.666667 42.666667 0 0 0-0.426667-0.725334L537.173333 0.8533330000000205z m122.709333 383.146666L585.941333 512h-147.882666l-73.898667-128 73.898667-128h147.882666l73.898667 128z m86.698667 20.522667l109.952-190.336c25.301333 51.2 39.509333 108.885333 39.509333 169.856 0 44.885333-7.68 87.978667-21.845333 128h-189.696l61.056-105.728a42.624 42.624 0 0 0 1.024-1.792zM149.845333 256h189.696l-61.056 105.728a42.624 42.624 0 0 0-1.024 1.792L167.509333 553.856A382.421333 382.421333 0 0 1 128 384c0-44.885333 7.68-87.978667 21.845333-128z m70.229334 377.472L314.88 469.333333 376.32 575.573333a42.666667 42.666667 0 0 0 0.426667 0.725334L486.826667 767.146667a383.146667 383.146667 0 0 1-266.794667-133.717334z m362.112 128.128L487.338667 597.333333H831.317333a384.213333 384.213333 0 0 1-249.130666 164.266667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-left" unicode="" d="M853.333333 426.666667H170.666667a42.666667 42.666667 0 0 1 0-85.333334h682.666666a42.666667 42.666667 0 0 1 0 85.333334zM456.832 609.834667a42.666667 42.666667 0 1 1-60.330667 60.330666l-256-256a42.666667 42.666667 0 0 1 0-60.330666l256-256a42.666667 42.666667 0 1 1 60.330667 60.330666L230.997333 384l225.834667 225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-right" unicode="" d="M170.666667 341.333333h682.666666a42.666667 42.666667 0 0 1 0 85.333334H170.666667a42.666667 42.666667 0 0 1 0-85.333334zM567.168 158.165333a42.666667 42.666667 0 0 1 60.330667-60.330666l256 256a42.666667 42.666667 0 0 1 0 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330667-60.330666L793.002667 384l-225.834667-225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-down" unicode="" d="M469.333333 725.333333v-682.666666a42.666667 42.666667 0 0 1 85.333334 0V725.333333a42.666667 42.666667 0 0 1-85.333334 0zM286.165333 328.832a42.666667 42.666667 0 1 1-60.330666-60.330667l256-256a42.666667 42.666667 0 0 1 60.330666 0l256 256a42.666667 42.666667 0 0 1-60.330666 60.330667L512 102.997333l-225.834667 225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-up-left" unicode="" d="M798.165333 158.165333l-512 512a42.666667 42.666667 0 0 1-60.330666-60.330666l512-512a42.666667 42.666667 0 0 1 60.330666 60.330666zM640 597.333333a42.666667 42.666667 0 0 1 0 85.333334H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-384a42.666667 42.666667 0 0 1 85.333334 0V597.333333h341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-up-right" unicode="" d="M286.165333 97.83466699999997l512 512a42.666667 42.666667 0 1 1-60.330666 60.330666l-512-512a42.666667 42.666667 0 0 1 60.330666-60.330666zM725.333333 256a42.666667 42.666667 0 0 1 85.333334 0V640a42.666667 42.666667 0 0 1-42.666667 42.666667H384a42.666667 42.666667 0 1 1 0-85.333334h341.333333v-341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="arrow-up" unicode="" d="M554.666667 42.666667V725.333333a42.666667 42.666667 0 0 1-85.333334 0v-682.666666a42.666667 42.666667 0 0 1 85.333334 0zM737.834667 439.168a42.666667 42.666667 0 0 1 60.330666 60.330667l-256 256a42.666667 42.666667 0 0 1-60.330666 0l-256-256a42.666667 42.666667 0 0 1 60.330666-60.330667L512 665.002667l225.834667-225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="award" unicode="" d="M304.646221 283.538374a341.323094 341.323094 0 1 0 414.792889 0l48.211887-363.210437a42.665387 42.665387 0 0 0-64.254072-42.238733L512-7.0129100000000335l-191.396925-114.812555a42.665387 42.665387 0 0 0-64.211407 42.196067l48.211887 363.210437z m79.9976-45.651964l-31.99904-241.059435 137.382545 82.429527a42.665387 42.665387 0 0 0 43.945348 0l137.382545-82.429527-31.956374 241.059435A340.384455 340.384455 0 0 0 512 213.35381299999995a340.384455 340.384455 0 0 0-127.356179 24.532597zM512 298.68458599999997a255.99232 255.99232 0 1 1 0 511.984641 255.99232 255.99232 0 0 1 0-511.984641z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bar-chart" unicode="" d="M810.666667 725.333333v-682.666666h85.333333V725.333333h-85.333333z m-42.666667 85.333334h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666667v-768a42.666667 42.666667 0 0 0-42.666666-42.666667h-170.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V768a42.666667 42.666667 0 0 0 42.666667 42.666667zM469.333333 512v-469.333333h85.333334V512h-85.333334z m-42.666666 85.333333h170.666666a42.666667 42.666667 0 0 0 42.666667-42.666666v-554.666667a42.666667 42.666667 0 0 0-42.666667-42.666667h-170.666666a42.666667 42.666667 0 0 0-42.666667 42.666667V554.666667a42.666667 42.666667 0 0 0 42.666667 42.666666zM128 42.666667h85.333333v256H128v-256z m-42.666667 341.333333h170.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333a42.666667 42.666667 0 0 0-42.666667-42.666667H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v341.333333a42.666667 42.666667 0 0 0 42.666666 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="at-sign" unicode="" d="M725.333333 384v-42.666667a85.333333 85.333333 0 1 1 170.666667 0v42.666667a384 384 0 1 1-150.528-304.896 42.666667 42.666667 0 1 0 51.882667-67.754667A469.333333 469.333333 0 1 0 981.333333 384v-42.666667a170.666667 170.666667 0 0 0-309.76-98.901333A213.333333 213.333333 0 1 0 725.333333 384z m-213.333333-128a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bar-chart-" unicode="" d="M469.333333 725.333333v-682.666666h85.333334V725.333333h-85.333334z m-42.666666 85.333334h170.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-768a42.666667 42.666667 0 0 0-42.666667-42.666667h-170.666666a42.666667 42.666667 0 0 0-42.666667 42.666667V768a42.666667 42.666667 0 0 0 42.666667 42.666667zM810.666667 512v-469.333333h85.333333V512h-85.333333z m-42.666667 85.333333h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-554.666667a42.666667 42.666667 0 0 0-42.666666-42.666667h-170.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V554.666667a42.666667 42.666667 0 0 0 42.666667 42.666666zM128 42.666667h85.333333v256H128v-256z m-42.666667 341.333333h170.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-341.333333a42.666667 42.666667 0 0 0-42.666667-42.666667H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v341.333333a42.666667 42.666667 0 0 0 42.666666 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="battery-charging" unicode="" d="M213.333333 170.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666666V554.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h136.106667a42.666667 42.666667 0 1 1 0 85.333334H128a128 128 0 0 1-128-128v-341.333334a128 128 0 0 1 128-128h85.333333a42.666667 42.666667 0 0 1 0 85.333334zM640 597.333333h85.333333a42.666667 42.666667 0 0 0 42.666667-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666667-42.666666h-136.106666a42.666667 42.666667 0 0 1 0-85.333334H725.333333a128 128 0 0 1 128 128V554.666667a128 128 0 0 1-128 128h-85.333333a42.666667 42.666667 0 0 1 0-85.333334zM1024 341.333333v85.333334a42.666667 42.666667 0 0 1-85.333333 0v-85.333334a42.666667 42.666667 0 0 1 85.333333 0zM348.501333 151.68a42.666667 42.666667 0 0 1 70.997334-47.36l170.666666 256A42.666667 42.666667 0 0 1 554.666667 426.666667H378.410667l126.421333 189.653333a42.666667 42.666667 0 1 1-70.997333 47.36l-170.666667-256A42.666667 42.666667 0 0 1 298.666667 341.333333h176.256l-126.421334-189.653333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bell-off" unicode="" d="M386.503252 734.704171A255.868761 255.868761 0 0 0 768.131509 511.671902v-170.579174a42.644793 42.644793 0 0 1 85.289586 0V511.671902A341.158348 341.158348 0 0 1 344.626065 808.906112a42.644793 42.644793 0 1 1 41.877187-74.201941zM256.393987 298.447934V511.671902a255.911405 255.911405 0 0 0 28.529367 117.742274A42.644793 42.644793 0 1 1 209.058267 668.604741 341.158348 341.158348 0 0 1 171.1044 511.629257V298.447934a85.289587 85.289587 0 0 0-85.289586-85.289586c-56.84551 0-56.84551-85.289587 0-85.289587h639.671901a42.644793 42.644793 0 0 1 0 85.289587H233.579023c14.49923 25.075139 22.814964 54.201532 22.814964 85.289586z m292.756507-277.105867a42.644793 42.644793 0 0 0-73.775492 0 42.644793 42.644793 0 0 1-73.775493-42.815373 127.93438 127.93438 0 0 1 221.326478 0 42.644793 42.644793 0 0 1-73.775493 42.815373zM13.020151 822.68038l938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738 60.299738l-938.185456 938.185456A42.644793 42.644793 0 1 1 13.020151 822.68038z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="battery" unicode="" d="M85.334187 554.282667v-340.565334c0-23.765333 19.114667-43.050667 42.453333-43.050666h597.76c23.381333 0 42.453333 19.2 42.453333 43.050666V554.24C768.000853 578.048 748.886187 597.333333 725.54752 597.333333H127.78752C104.406187 597.333333 85.334187 578.133333 85.334187 554.282667z m-85.333334 0A128.042667 128.042667 0 0 0 127.78752 682.666667h597.76A128.170667 128.170667 0 0 0 853.334187 554.282667v-340.565334A128.042667 128.042667 0 0 0 725.54752 85.333333H127.78752A128.170667 128.170667 0 0 0 0.000853 213.717333V554.24zM1024.000853 341.333333v85.333334a42.666667 42.666667 0 0 1-85.333333 0v-85.333334a42.666667 42.666667 0 0 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bluetooth" unicode="" d="M554.660072 17.7251l131.648983 131.648983L554.660072 281.023065v-263.297965zM686.309055 618.634876L554.660072 750.283859v-263.297966L686.309055 618.634876z m-439.100123-30.160671a42.660072 42.660072 0 0 0 60.321342 60.321342l469.260794-469.260793a42.660072 42.660072 0 0 0 0-60.321342l-234.630397-234.630397c-26.875845-26.875845-72.820743-7.849453-72.820743 30.160671V853.265273c0 37.967464 45.944898 57.036516 72.820743 30.160671l234.630397-234.630397a42.660072 42.660072 0 0 0 0-60.321342l-469.260794-469.260793a42.660072 42.660072 0 1 0-60.321342 60.321342L554.660072 486.985893v-205.962828L247.208932 588.474205z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bell" unicode="" d="M768 298.66666699999996V512A256 256 0 0 1 256 512v-213.333333c0-31.104-8.32-60.245333-22.826667-85.333334h557.653334A169.898667 169.898667 0 0 0 768 298.66666699999996z m170.666667-170.666667H85.333333c-56.874667 0-56.874667 85.333333 0 85.333333a85.333333 85.333333 0 0 1 85.333334 85.333334V512a341.333333 341.333333 0 0 0 682.666666 0v-213.333333a85.333333 85.333333 0 0 1 85.333334-85.333334c56.874667 0 56.874667-85.333333 0-85.333333zM548.906667 21.418667000000028a42.666667 42.666667 0 0 0-73.813334 0 42.666667 42.666667 0 0 1-73.813333-42.837334 128 128 0 0 1 221.44 0 42.666667 42.666667 0 0 1-73.813333 42.837334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="book" unicode="" d="M853.333333 853.333333a42.666667 42.666667 0 0 0 42.666667-42.666666v-853.333334a42.666667 42.666667 0 0 0-42.666667-42.666666H277.333333A149.333333 149.333333 0 0 0 128 64v640A149.333333 149.333333 0 0 0 277.333333 853.333333H853.333333z m-42.666666-640V768H277.333333A64 64 0 0 1 213.333333 704v-505.045333A148.736 148.736 0 0 0 277.333333 213.33333300000004H810.666667z m0-85.333333H277.333333a64 64 0 0 1 0-128H810.666667v128z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="briefcase" unicode="" d="M298.666667 640V682.666667a128 128 0 0 0 128 128h170.666666a128 128 0 0 0 128-128v-42.666667h128.426667A127.914667 127.914667 0 0 0 981.333333 511.744v-426.154667A128.128 128.128 0 0 0 853.76-42.66666699999996H170.24A127.914667 127.914667 0 0 0 42.666667 85.58933300000001V511.744A128.128 128.128 0 0 0 170.24 640H298.666667z m0-85.333333H170.24C147.2 554.666667 128 535.381333 128 511.744v-426.154667A42.581333 42.581333 0 0 1 170.24 42.66666699999996H298.666667V554.666667z m85.333333 0v-512h256V554.666667H384z m341.333333 0v-512h128.426667c23.04 0 42.24 19.285333 42.24 42.922666V511.744A42.581333 42.581333 0 0 1 853.76 554.666667H725.333333zM384 640h256V682.666667a42.666667 42.666667 0 0 1-42.666667 42.666666h-170.666666a42.666667 42.666667 0 0 1-42.666667-42.666666v-42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="camera-off" unicode="" d="M682.884567 273.415441l328.620778-328.620779a42.644793 42.644793 0 0 0-60.299738-60.299738L878.410944-42.71041300000002H128.459607a127.93438 127.93438 0 0 0-127.93438 127.93438V554.316695a127.93438 127.93438 0 0 0 127.93438 127.93438h24.989849L13.020151 822.68038A42.644793 42.644793 0 1 0 73.319889 882.980118l371.265572-371.265572 1.151409-1.108764 236.038932-236.038932 1.108765-1.151409z m-28.870526-91.686306a213.223967 213.223967 0 0 0-301.157531 301.072241L238.739043 596.961489H128.459607a42.644793 42.644793 0 0 1-42.644793-42.644794v-469.092728a42.644793 42.644793 0 0 1 42.644793-42.644793h664.661751l-139.107317 139.107316z m-60.470317 60.427672L413.326827 422.373704a127.93438 127.93438 0 0 1 180.216897-180.216897z m96.462523 373.781615A42.644793 42.644793 0 0 1 725.486715 596.961489h170.579174a42.644793 42.644793 0 0 0 42.644793-42.644794v-398.302371a42.644793 42.644793 0 0 1 85.289587 0V554.316695a127.93438 127.93438 0 0 1-127.93438 127.93438h-147.764209l-72.624084 108.957448A42.644793 42.644793 0 0 1 640.197128 810.185456H384.328368a42.644793 42.644793 0 1 1 0-85.289587h233.053796l72.624083-108.957447z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="calendar" unicode="" d="M384 768h256V810.666667a42.666667 42.666667 0 0 0 85.333333 0v-42.666667h85.546667A127.786667 127.786667 0 0 0 938.666667 640.213333v-597.76A127.786667 127.786667 0 0 0 810.88-85.33333300000004H213.12A127.786667 127.786667 0 0 0 85.333333 42.45333300000004V640.213333A127.786667 127.786667 0 0 0 213.12 768H298.666667V810.666667a42.666667 42.666667 0 1 0 85.333333 0v-42.666667z m469.333333-341.333333H170.666667v-384.213334c0-23.466667 18.986667-42.453333 42.453333-42.453333h597.76c23.466667 0 42.453333 18.986667 42.453333 42.453333V426.666667zM384 682.666667v-42.666667a42.666667 42.666667 0 1 0-85.333333 0V682.666667H213.12A42.453333 42.453333 0 0 1 170.666667 640.213333V512h682.666666V640.213333A42.453333 42.453333 0 0 1 810.88 682.666667H725.333333v-42.666667a42.666667 42.666667 0 0 0-85.333333 0V682.666667H384z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bookmark" unicode="" d="M768 82.901333V682.666667a42.666667 42.666667 0 0 1-42.666667 42.666666H298.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-599.765334l231.210667 165.12a42.666667 42.666667 0 0 0 49.578666 0L768 82.944zM238.122667-34.730667A42.666667 42.666667 0 0 0 170.666667 0V682.666667a128 128 0 0 0 128 128h426.666666a128 128 0 0 0 128-128v-682.666667a42.666667 42.666667 0 0 0-67.456-34.730667L512 160.896l-273.877333-195.626667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="box" unicode="" d="M568.96 872.362667l341.333333-170.666667A128 128 0 0 0 981.333333 587.093333v-406.613333a128 128 0 0 0-70.954666-114.517333l-341.333334-170.666667a128 128 0 0 0-114.517333 0L113.066667 66.00533299999995A127.872 127.872 0 0 0 42.666667 180.90666699999997V587.093333a128 128 0 0 0 70.954666 114.517334L455.04 872.362667a128 128 0 0 0 113.92 0zM512 474.368l331.050667 165.546667-312.106667 156.032a42.624 42.624 0 0 1-37.845333 0L180.949333 639.9573330000001 512 474.368z m384 96.597333l-341.333333-170.666666v-416.810667l317.653333 158.848A42.666667 42.666667 0 0 1 896 180.48000000000002V570.965333zM469.333333-16.725332999999978v417.024l-341.333333 170.666666v-390.357333c-0.128-16.213333 8.96-31.104 23.338667-38.357333L469.333333-16.725332999999978z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="camera" unicode="" d="M334.165333 616.32A42.666667 42.666667 0 0 0 298.666667 597.333333H128a42.666667 42.666667 0 0 1-42.666667-42.666666v-469.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h768a42.666667 42.666667 0 0 1 42.666667 42.666666V554.666667a42.666667 42.666667 0 0 1-42.666667 42.666666h-170.666667a42.666667 42.666667 0 0 0-35.498666 18.986667L617.173333 725.333333h-210.346666L334.165333 616.32zM896 682.666667a128 128 0 0 0 128-128v-469.333334a128 128 0 0 0-128-128H128a128 128 0 0 0-128 128V554.666667a128 128 0 0 0 128 128h147.84l72.661333 109.013333A42.666667 42.666667 0 0 0 384 810.666667h256a42.666667 42.666667 0 0 0 35.498667-18.986667L748.16 682.666667H896zM512 128a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="check-circle" unicode="" d="M896 423.68V384a384 384 0 1 0-227.712 351.018667 42.666667 42.666667 0 1 1 34.730667 77.909333A469.333333 469.333333 0 1 1 981.333333 384v39.722667a42.666667 42.666667 0 0 1-85.333333 0zM414.165333 456.832a42.666667 42.666667 0 0 1-60.330666-60.330667l128-128a42.666667 42.666667 0 0 1 60.330666 0l469.333334 469.333334a42.666667 42.666667 0 1 1-60.330667 60.330666L512 358.997333l-97.834667 97.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="check" unicode="" d="M200.832 371.498667a42.666667 42.666667 0 1 1-60.330667-60.330667l213.333334-213.333333a42.666667 42.666667 0 0 1 60.330666 0l469.333334 469.333333a42.666667 42.666667 0 1 1-60.330667 60.330667L384 188.330667l-183.168 183.168z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="check-square" unicode="" d="M371.498667 456.832a42.666667 42.666667 0 0 1-60.330667-60.330667l128-128a42.666667 42.666667 0 0 1 60.330667 0l469.333333 469.333334a42.666667 42.666667 0 1 1-60.330667 60.330666L469.333333 358.997333l-97.834666 97.834667zM810.666667 384v-298.666667a42.666667 42.666667 0 0 0-42.666667-42.666666H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666V682.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h469.333333a42.666667 42.666667 0 0 1 0 85.333334H170.666667a128 128 0 0 1-128-128v-597.333334a128 128 0 0 1 128-128h597.333333a128 128 0 0 1 128 128v298.666667a42.666667 42.666667 0 0 1-85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cast" unicode="" d="M76.8 167.253333a170.666667 170.666667 0 0 0 133.12-133.12 42.666667 42.666667 0 0 1 83.626667 17.066667 256 256 0 0 1-199.68 199.68 42.666667 42.666667 0 0 1-17.066667-83.626667z m3.84 172.202667a341.333333 341.333333 0 0 0 301.482667-301.525333 42.666667 42.666667 0 1 1 84.821333 9.472 426.666667 426.666667 0 0 1-376.874667 376.874666 42.666667 42.666667 0 1 1-9.472-84.821333zM128 554.666667V640a42.666667 42.666667 0 0 0 42.666667 42.666667h682.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-512a42.666667 42.666667 0 0 0-42.666667-42.666667h-256a42.666667 42.666667 0 0 1 0-85.333333h256a128 128 0 0 1 128 128V640a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-85.333333a42.666667 42.666667 0 1 1 85.333333 0zM85.333333 42.666667m-42.666666 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevron-down" unicode="" d="M286.165333 542.165333a42.666667 42.666667 0 0 1-60.330666-60.330666l256-256a42.666667 42.666667 0 0 1 60.330666 0l256 256a42.666667 42.666667 0 1 1-60.330666 60.330666L512 316.330667 286.165333 542.165333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevron-left" unicode="" d="M670.165333 609.834667a42.666667 42.666667 0 1 1-60.330666 60.330666l-256-256a42.666667 42.666667 0 0 1 0-60.330666l256-256a42.666667 42.666667 0 0 1 60.330666 60.330666L444.330667 384l225.834666 225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevron-right" unicode="" d="M353.834667 158.165333a42.666667 42.666667 0 0 1 60.330666-60.330666l256 256a42.666667 42.666667 0 0 1 0 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666-60.330666L579.669333 384l-225.834666-225.834667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevron-up" unicode="" d="M737.834667 225.834667a42.666667 42.666667 0 0 1 60.330666 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666 0l-256-256a42.666667 42.666667 0 0 1 60.330666-60.330666L512 451.669333l225.834667-225.834666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevrons-down" unicode="" d="M328.832 371.498667a42.666667 42.666667 0 1 1-60.330667-60.330667l213.333334-213.333333a42.666667 42.666667 0 0 1 60.330666 0l213.333334 213.333333a42.666667 42.666667 0 0 1-60.330667 60.330667L512 188.330667l-183.168 183.168zM328.832 670.165333a42.666667 42.666667 0 0 1-60.330667-60.330666l213.333334-213.333334a42.666667 42.666667 0 0 1 60.330666 0l213.333334 213.333334a42.666667 42.666667 0 1 1-60.330667 60.330666L512 486.997333 328.832 670.165333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevrons-right" unicode="" d="M524.501333 200.832a42.666667 42.666667 0 0 1 60.330667-60.330667l213.333333 213.333334a42.666667 42.666667 0 0 1 0 60.330666l-213.333333 213.333334a42.666667 42.666667 0 0 1-60.330667-60.330667L707.669333 384l-183.168-183.168zM225.834667 200.832a42.666667 42.666667 0 0 1 60.330666-60.330667l213.333334 213.333334a42.666667 42.666667 0 0 1 0 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666-60.330667L409.002667 384l-183.168-183.168z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevrons-up" unicode="" d="M695.168 396.501333a42.666667 42.666667 0 0 1 60.330667 60.330667l-213.333334 213.333333a42.666667 42.666667 0 0 1-60.330666 0l-213.333334-213.333333a42.666667 42.666667 0 0 1 60.330667-60.330667L512 579.669333l183.168-183.168zM512 281.002667l183.168-183.168a42.666667 42.666667 0 0 1 60.330667 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666 0l-213.333334-213.333334a42.666667 42.666667 0 0 1 60.330667-60.330666L512 281.002667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chevrons-left" unicode="" d="M499.498667 567.168a42.666667 42.666667 0 1 1-60.330667 60.330667l-213.333333-213.333334a42.666667 42.666667 0 0 1 0-60.330666l213.333333-213.333334a42.666667 42.666667 0 0 1 60.330667 60.330667L316.330667 384l183.168 183.168zM798.165333 567.168a42.666667 42.666667 0 1 1-60.330666 60.330667l-213.333334-213.333334a42.666667 42.666667 0 0 1 0-60.330666l213.333334-213.333334a42.666667 42.666667 0 0 1 60.330666 60.330667L614.997333 384l183.168 183.168z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="clipboard" unicode="" d="M298.666667 768c0.213333 46.976 38.4 85.333333 85.205333 85.333333h256.256c47.104 0 84.992-37.973333 85.205333-85.333333h42.666667a128 128 0 0 0 128-128v-597.333333a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128V640a128 128 0 0 0 128 128h42.666667z m0-85.333333H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-597.333333a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667V640a42.666667 42.666667 0 0 1-42.666667 42.666667h-42.666667c-0.213333-46.976-38.4-85.333333-85.205333-85.333334H383.872A85.248 85.248 0 0 0 298.666667 682.666667z m85.205333 85.333333c0.341333 0 0.128-0.213333 0.128-0.426667v-84.48C384 682.752 384 682.666667 383.914667 682.666667h256.213333C639.786667 682.666667 640 682.88 640 683.093333V767.573333c0 0.298667 0 0.426667 0.085333 0.426667H383.872z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="chrome" unicode="" d="M462.378667-82.77333299999998a42.410667 42.410667 0 0 0-5.802667 0.682666C223.488-54.65599999999995 42.666667 143.530667 42.666667 384c0 102.954667 33.152 198.186667 89.386666 275.584a42.410667 42.410667 0 0 0 4.650667 6.272A468.608 468.608 0 0 0 512 853.333333a469.418667 469.418667 0 0 0 431.36-284.16c24.448-56.832 37.973333-119.424 37.973333-185.173333 0-259.2-210.133333-469.333333-469.333333-469.333333-16.768 0-33.28 0.853333-49.621333 2.56z m-20.608 89.173333l95.616 165.76a213.333333 213.333333 0 0 0-214.528 113.066667l-155.306667 268.672A382.421333 382.421333 0 0 1 128 384c0-188.074667 135.253333-344.618667 313.770667-377.6z m95.317333-5.589333A384 384 0 0 1 874.154667 512H682.666667c26.794667-35.669333 42.666667-79.957333 42.666666-128 0-41.941333-12.117333-81.066667-33.024-114.090667l-155.221333-269.098666z m82.389333 313.642666l3.2 5.546667c0.554667 0.981333 1.152 1.962667 1.792 2.858667a128 128 0 1 1-226.56 2.986666l3.413334-5.845333c0.554667-0.938667 1.066667-1.92 1.536-2.901333a127.914667 127.914667 0 0 1 216.618666-2.645334zM220.16 633.5146669999999l95.701333-165.589334A213.376 213.376 0 0 0 512 597.333333h319.317333A383.616 383.616 0 0 1 512 768a383.146667 383.146667 0 0 1-291.925333-134.485333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="clock" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM554.666667 640a42.666667 42.666667 0 0 1-85.333334 0v-256a42.666667 42.666667 0 0 1 12.501334-30.165333l128-128a42.666667 42.666667 0 0 1 60.330666 60.330666L554.666667 401.664V640z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud-lightning" unicode="" d="M384.070136 896a383.99632 383.99632 0 0 1-185.939552-719.9931 42.666258 42.666258 0 0 1 41.300938 74.665951A298.663804 298.663804 0 1 0 673.262031 586.669631a42.666258 42.666258 0 0 1 41.300938-31.999693H768.322453a170.665031 170.665031 0 0 0 34.175673-337.916762 42.666258 42.666258 0 0 1 16.98117-83.625865A255.997547 255.997547 0 0 1 768.279787 640.002453h-22.186454a383.99632 383.99632 0 0 1-362.023197 255.997547zM434.160322-61.644156a42.666258 42.666258 0 0 1 70.996653-47.359546l170.665032 255.997547A42.666258 42.666258 0 0 1 640.32368 213.339875h-176.254311l126.420122 189.651516a42.666258 42.666258 0 1 1-70.996653 47.359546l-170.665031-255.997546A42.666258 42.666258 0 0 1 384.326133 128.00736h176.254311l-126.420122-189.651516z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud-drizzle" unicode="" d="M298.831787 85.333333v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a42.666667 42.666667 0 0 1-85.333333 0zM298.831787 341.333333v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a42.666667 42.666667 0 0 1-85.333333 0zM640.16512 85.333333v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a42.666667 42.666667 0 0 1-85.333333 0zM640.16512 341.333333v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a42.666667 42.666667 0 0 1-85.333333 0zM469.498453 0v-85.333333a42.666667 42.666667 0 0 1 85.333334 0v85.333333a42.666667 42.666667 0 0 1-85.333334 0zM469.498453 256v-85.333333a42.666667 42.666667 0 0 1 85.333334 0v85.333333a42.666667 42.666667 0 0 1-85.333334 0zM405.498453 895.36A384 384 0 0 1 144.207787 212.053333a42.666667 42.666667 0 0 1 53.248 66.645334A298.666667 298.666667 0 1 0 673.103787 586.666667a42.666667 42.666667 0 0 1 41.301333-32H768.16512a170.666667 170.666667 0 0 0 68.224-326.997334 42.666667 42.666667 0 0 1 34.218667-78.165333A256 256 0 0 1 768.207787 640h-22.229334A384 384 0 0 1 405.498453 895.36z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud-rain" unicode="" d="M640.16512 341.333333v-341.333333a42.666667 42.666667 0 0 1 85.333333 0v341.333333a42.666667 42.666667 0 0 1-85.333333 0zM298.831787 341.333333v-341.333333a42.666667 42.666667 0 0 1 85.333333 0v341.333333a42.666667 42.666667 0 0 1-85.333333 0zM469.498453 256v-341.333333a42.666667 42.666667 0 0 1 85.333334 0v341.333333a42.666667 42.666667 0 0 1-85.333334 0zM405.498453 895.36A384 384 0 0 1 144.207787 212.053333a42.666667 42.666667 0 0 1 53.248 66.645334A298.666667 298.666667 0 1 0 673.103787 586.666667a42.666667 42.666667 0 0 1 41.301333-32H768.16512a170.666667 170.666667 0 0 0 68.224-326.997334 42.666667 42.666667 0 0 1 34.218667-78.165333A256 256 0 0 1 768.207787 640h-22.229334A384 384 0 0 1 405.498453 895.36z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud-off" unicode="" d="M420.661732 767.370083a42.474214 42.474214 0 0 1-45.971088-38.721472 42.559504 42.559504 0 0 1 38.508249-46.226956 297.3195 297.3195 0 0 0 261.583163-223.885166 42.431569 42.431569 0 0 1 41.109581-32.154174h53.433926a169.38512 169.38512 0 0 0 141.580714-76.248891 171.389425 171.389425 0 0 0 14.840388-160.813516 42.730083 42.730083 0 0 1 22.516451-55.864679 42.303635 42.303635 0 0 1 55.608811 22.60174 257.062815 257.062815 0 0 1-22.260582 241.198952A254.077679 254.077679 0 0 1 769.240273 511.671902h-21.876779a382.054704 382.054704 0 0 1-326.701762 255.698181zM197.544172 719.607914c-153.308032-85.289587-229.215765-264.568299-184.097573-434.763669 45.160836-170.238015 199.748212-287.681777 374.890379-284.86722h380.988585c29.552842 0 58.849815 5.24531 86.568931 15.352126 22.004713 8.059866 33.348228 32.495333 25.373652 54.62798a42.34628 42.34628 0 0 1-54.329467 25.544231c-18.50784-6.737877-37.996511-10.23475-57.655761-10.23475H387.654662a297.276855 297.276855 0 0 0-292.24477 221.539702 299.153226 299.153226 0 0 0 143.158572 338.130567c20.469501 11.38616 27.93234 37.356839 16.631469 57.954274a42.26099 42.26099 0 0 1-57.655761 16.674115zM13.020151 822.68038l938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738 60.299738l-938.185456 938.185456A42.644793 42.644793 0 1 1 13.020151 822.68038z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="codepen" unicode="" d="M535.253333 846.421333l426.666667-277.333333A42.666667 42.666667 0 0 0 981.333333 533.333333v-298.666666a42.666667 42.666667 0 0 0-19.413333-35.754667l-426.666667-277.333333a42.666667 42.666667 0 0 0-46.506666 0l-426.666667 277.333333A42.666667 42.666667 0 0 0 42.666667 234.66666699999996v298.666666a42.666667 42.666667 0 0 0 19.413333 35.754667l426.666667 277.333333a42.666667 42.666667 0 0 0 46.506666 0zM469.333333 732.074667L161.621333 531.9680000000001 298.666667 436.096l170.666666 119.466667V732.032z m85.333334 0V555.52l170.666666-119.466667 137.045334 95.914667L554.666667 732.032zM512 286.72L650.922667 384 512 481.28 373.077333 384 512 286.72z m384 29.866667v134.741333L799.744 384 896 316.58666700000003z m-33.621333-80.64L725.333333 331.904l-170.666666-119.466667v-176.469333l307.712 200.021333z m-700.757334 0L469.333333 35.96799999999996v176.469333l-170.666666 119.466667-137.045334-95.914667zM128 316.58666700000003L224.256 384 128 451.413333v-134.826666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud-snow" unicode="" d="M405.632 852.693333A384 384 0 0 1 144.298667 169.386667a42.666667 42.666667 0 0 1 53.290666 66.645333A298.666667 298.666667 0 1 0 673.194667 544a42.666667 42.666667 0 0 1 41.301333-32h53.76a170.666667 170.666667 0 0 0 68.266667-326.997333 42.666667 42.666667 0 0 1 34.218666-78.165334A256 256 0 0 1 768.341333 597.333333h-22.272a384 384 0 0 1-340.48 255.36zM682.666667 256m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0ZM512 170.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM341.333333 256m-42.666666 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0ZM341.333333 42.666667m-42.666666 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0ZM512-42.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM682.666667 42.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0ZM682.666667 42.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="compass" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM455.253333 440.746667L398.592 270.506667l170.154667 56.746666 56.746666 170.154667-170.154666-56.746667z m278.144 110.677333l-90.453333-271.36a42.666667 42.666667 0 0 0-27.008-27.008l-271.36-90.453333a42.666667 42.666667 0 0 0-53.973333 53.973333l90.453333 271.36a42.666667 42.666667 0 0 0 27.008 27.008l271.36 90.453333a42.666667 42.666667 0 0 0 53.973333-53.973333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="copy" unicode="" d="M426.666667 426.538667v-383.744A42.666667 42.666667 0 0 1 469.461333 0h383.744A42.666667 42.666667 0 0 1 896 42.794667v383.744A42.666667 42.666667 0 0 1 853.205333 469.333333h-383.744A42.666667 42.666667 0 0 1 426.666667 426.538667z m-85.333334 0A128 128 0 0 0 469.461333 554.666667h383.744A128 128 0 0 0 981.333333 426.538667v-383.744A128 128 0 0 0 853.205333-85.333333h-383.744A128 128 0 0 0 341.333333 42.794667v383.744zM213.333333 298.666667H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666666V725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h384a42.666667 42.666667 0 0 0 42.666666-42.666667v-42.666666a42.666667 42.666667 0 0 1 85.333334 0V725.333333a128 128 0 0 1-128 128H170.666667a128 128 0 0 1-128-128v-384a128 128 0 0 1 128-128h42.666666a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-down-right" unicode="" d="M609.834667 72.832a42.666667 42.666667 0 0 1 60.330666-60.330667l213.333334 213.333334a42.666667 42.666667 0 0 1 0 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666-60.330667L793.002667 256l-183.168-183.168zM128 725.333333v-298.666666a213.333333 213.333333 0 0 1 213.333333-213.333334h512a42.666667 42.666667 0 0 1 0 85.333334H341.333333a128 128 0 0 0-128 128V725.333333a42.666667 42.666667 0 1 1-85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-down-left" unicode="" d="M414.165333 439.168a42.666667 42.666667 0 1 1-60.330666 60.330667l-213.333334-213.333334a42.666667 42.666667 0 0 1 0-60.330666l213.333334-213.333334a42.666667 42.666667 0 1 1 60.330666 60.330667L230.997333 256l183.168 183.168zM810.666667 725.333333v-298.666666a128 128 0 0 0-128-128H170.666667a42.666667 42.666667 0 0 1 0-85.333334h512a213.333333 213.333333 0 0 1 213.333333 213.333334V725.333333a42.666667 42.666667 0 0 1-85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-left-down" unicode="" d="M200.832 286.165333a42.666667 42.666667 0 1 1-60.330667-60.330666l213.333334-213.333334a42.666667 42.666667 0 0 1 60.330666 0l213.333334 213.333334a42.666667 42.666667 0 0 1-60.330667 60.330666L384 102.997333l-183.168 183.168zM853.333333 768h-298.666666a213.333333 213.333333 0 0 1-213.333334-213.333333v-512a42.666667 42.666667 0 0 1 85.333334 0V554.666667a128 128 0 0 0 128 128h298.666666a42.666667 42.666667 0 0 1 0 85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-left-up" unicode="" d="M567.168 481.834667a42.666667 42.666667 0 1 1 60.330667 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666 0l-213.333334-213.333334a42.666667 42.666667 0 0 1 60.330667-60.330666L384 665.002667l183.168-183.168zM853.333333 85.333333h-298.666666a128 128 0 0 0-128 128V725.333333a42.666667 42.666667 0 1 1-85.333334 0v-512a213.333333 213.333333 0 0 1 213.333334-213.333333h298.666666a42.666667 42.666667 0 0 1 0 85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-up-left" unicode="" d="M414.165333 695.168a42.666667 42.666667 0 1 1-60.330666 60.330667l-213.333334-213.333334a42.666667 42.666667 0 0 1 0-60.330666l213.333334-213.333334a42.666667 42.666667 0 1 1 60.330666 60.330667L230.997333 512l183.168 183.168zM896 42.666667v298.666666a213.333333 213.333333 0 0 1-213.333333 213.333334H170.666667a42.666667 42.666667 0 1 1 0-85.333334h512a128 128 0 0 0 128-128v-298.666666a42.666667 42.666667 0 0 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-up-right" unicode="" d="M609.834667 328.832a42.666667 42.666667 0 0 1 60.330666-60.330667l213.333334 213.333334a42.666667 42.666667 0 0 1 0 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666-60.330667L793.002667 512l-183.168-183.168zM213.333333 42.666667v298.666666a128 128 0 0 0 128 128h512a42.666667 42.666667 0 0 1 0 85.333334H341.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-298.666666a42.666667 42.666667 0 0 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-right-down" unicode="" d="M456.832 286.16533300000003a42.666667 42.666667 0 1 1-60.330667-60.330666l213.333334-213.333334a42.666667 42.666667 0 0 1 60.330666 0l213.333334 213.333334a42.666667 42.666667 0 0 1-60.330667 60.330666L640 102.99733300000003l-183.168 183.168zM170.666667 682.666667h298.666666a128 128 0 0 0 128-128v-512a42.666667 42.666667 0 0 1 85.333334 0V554.666667a213.333333 213.333333 0 0 1-213.333334 213.333333H170.666667a42.666667 42.666667 0 1 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="corner-right-up" unicode="" d="M823.168 481.834667a42.666667 42.666667 0 1 1 60.330667 60.330666l-213.333334 213.333334a42.666667 42.666667 0 0 1-60.330666 0l-213.333334-213.333334a42.666667 42.666667 0 1 1 60.330667-60.330666L640 665.002667l183.168-183.168zM170.666667 0h298.666666a213.333333 213.333333 0 0 1 213.333334 213.333333V725.333333a42.666667 42.666667 0 0 1-85.333334 0v-512a128 128 0 0 0-128-128H170.666667a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cpu" unicode="" d="M213.333333 640.256v-512.512c0-23.381333 19.029333-42.410667 42.410667-42.410667h512.512a42.453333 42.453333 0 0 1 42.410667 42.410667V640.256A42.453333 42.453333 0 0 1 768.256 682.666667H255.744A42.453333 42.453333 0 0 1 213.333333 640.256z m-85.333333 0A127.786667 127.786667 0 0 0 255.744 768h512.512A127.786667 127.786667 0 0 0 896 640.256v-512.512A127.786667 127.786667 0 0 0 768.256 0H255.744A127.786667 127.786667 0 0 0 128 127.74400000000003V640.256zM426.666667 298.66666699999996h170.666666v170.666666h-170.666666v-170.666666zM384 554.666667h256a42.666667 42.666667 0 0 0 42.666667-42.666667v-256a42.666667 42.666667 0 0 0-42.666667-42.666667H384a42.666667 42.666667 0 0 0-42.666667 42.666667V512a42.666667 42.666667 0 0 0 42.666667 42.666667zM341.333333 853.333333a42.666667 42.666667 0 1 0 85.333334 0v-128a42.666667 42.666667 0 1 0-85.333334 0V853.333333z m256 0a42.666667 42.666667 0 0 0 85.333334 0v-128a42.666667 42.666667 0 0 0-85.333334 0V853.333333zM341.333333 42.66666699999996a42.666667 42.666667 0 0 0 85.333334 0v-128a42.666667 42.666667 0 0 0-85.333334 0v128z m256 0a42.666667 42.666667 0 0 0 85.333334 0v-128a42.666667 42.666667 0 0 0-85.333334 0v128z m256 426.666666a42.666667 42.666667 0 0 0 0 85.333334h128a42.666667 42.666667 0 0 0 0-85.333334h-128z m0-213.333333a42.666667 42.666667 0 0 0 0 85.333333h128a42.666667 42.666667 0 0 0 0-85.333333h-128zM42.666667 469.333333a42.666667 42.666667 0 1 0 0 85.333334h128a42.666667 42.666667 0 1 0 0-85.333334H42.666667z m0-213.333333a42.666667 42.666667 0 0 0 0 85.333333h128a42.666667 42.666667 0 0 0 0-85.333333H42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="credit-card" unicode="" d="M0 640.256A127.616 127.616 0 0 0 127.616 768H896.426667A127.744 127.744 0 0 0 1024 640.256v-512.512A127.616 127.616 0 0 0 896.384 0H127.573333A127.744 127.744 0 0 0 0 127.74400000000003V640.256zM938.666667 512V640.256A42.410667 42.410667 0 0 1 896.384 682.666667H127.573333A42.282667 42.282667 0 0 1 85.333333 640.256V512h853.333334z m0-85.333333H85.333333v-298.922667c0-23.381333 18.986667-42.410667 42.282667-42.410667H896.426667a42.282667 42.282667 0 0 1 42.282666 42.410667V426.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="crosshair" unicode="" d="M469.333333 2.346667000000025V128a42.666667 42.666667 0 0 0 85.333334 0v-125.653333A384.170667 384.170667 0 0 1 893.653333 341.33333300000004H768a42.666667 42.666667 0 0 0 0 85.333334h125.653333A384.170667 384.170667 0 0 1 554.666667 765.653333V640a42.666667 42.666667 0 0 0-85.333334 0V765.653333A384.170667 384.170667 0 0 1 130.346667 426.666667H256a42.666667 42.666667 0 0 0 0-85.333334H130.346667A384.170667 384.170667 0 0 1 469.333333 2.346667000000025zM512-85.33333300000004C252.8-85.33333300000004 42.666667 124.79999999999995 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="disc" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM512 213.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="delete" unicode="" d="M896 768a128 128 0 0 0 128-128v-512a128 128 0 0 0-128-128H341.333333a42.666667 42.666667 0 0 0-32.128 14.549333l-298.666666 341.333334a42.666667 42.666667 0 0 0 0 56.234666l298.666666 341.333334A42.666667 42.666667 0 0 0 341.333333 768h554.666667z m0-682.666667a42.666667 42.666667 0 0 1 42.666667 42.666667V640a42.666667 42.666667 0 0 1-42.666667 42.666667H360.704l-261.333333-298.666667 261.333333-298.666667H896zM737.834667 542.165333l-256-256a42.666667 42.666667 0 0 1 60.330666-60.330666l256 256a42.666667 42.666667 0 1 1-60.330666 60.330666zM481.834667 481.834667l256-256a42.666667 42.666667 0 0 1 60.330666 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="download-cloud" unicode="" d="M512.468907 17.663999999999987l-140.501334 140.501333a42.666667 42.666667 0 1 1-60.330666-60.330666l170.666666-170.666667a42.666667 42.666667 0 0 1 60.330667 0l170.666667 170.666667a42.666667 42.666667 0 0 1-60.330667 60.330666L512.468907 17.663999999999987zM469.80224 341.33333300000004v-384a42.666667 42.666667 0 0 1 85.333333 0v384a42.666667 42.666667 0 0 1-85.333333 0zM427.988907 765.9946669999999a384 384 0 0 1-331.477334-635.989334 42.666667 42.666667 0 0 1 63.914667 56.576A298.666667 298.666667 0 1 0 673.32224 458.666667a42.666667 42.666667 0 0 1 41.344-32.042667H768.468907a170.666667 170.666667 0 0 0 98.346666-310.272 42.666667 42.666667 0 0 1 49.066667-69.802667A256 256 0 0 1 768.468907 512h-22.101334a384 384 0 0 1-318.293333 253.994667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="download" unicode="" d="M85.333333 170.666667v-128a128 128 0 0 1 128-128h597.333334a128 128 0 0 1 128 128v128a42.666667 42.666667 0 0 1-85.333334 0v-128a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v128a42.666667 42.666667 0 0 1-85.333334 0zM512 273.664l-140.501333 140.501333a42.666667 42.666667 0 1 1-60.330667-60.330666l170.666667-170.666667a42.666667 42.666667 0 0 1 60.330666 0l170.666667 170.666667a42.666667 42.666667 0 0 1-60.330667 60.330666L512 273.664zM469.333333 810.666667v-597.333334a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="droplet" unicode="" d="M301.098667 501.034667a290.986667 290.986667 0 0 1-64.853334-320.64c46.250667-109.952 155.136-181.674667 275.968-181.674667s229.717333 71.68 275.968 181.674667a290.986667 290.986667 0 0 1-64.853333 320.64L512 709.162667 301.098667 501.034667z m241.066666 297.301333l241.493334-237.866667a374.144 374.144 0 0 0 83.328-412.245333C807.594667 6.869333 667.562667-85.333333 512.213333-85.333333c-155.306667 0-295.381333 92.16-354.773333 233.557333a374.144 374.144 0 0 0 83.285333 412.202667l241.066667 237.909333a43.093333 43.093333 0 0 0 60.373333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="edit-" unicode="" d="M170.666667 195.669333V42.666667h153.002666l512 512L682.666667 707.669333l-512-512zM712.832 798.165333l213.333333-213.333333a42.666667 42.666667 0 0 0 0-60.330667l-554.666666-554.666666A42.666667 42.666667 0 0 0 341.333333-42.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666667v213.333333a42.666667 42.666667 0 0 0 12.501334 30.165334l554.666666 554.666666a42.666667 42.666667 0 0 0 60.330667 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="edit" unicode="" d="M810.666667 270.506667V42.666667a42.666667 42.666667 0 0 0-42.666667-42.666667H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V640a42.666667 42.666667 0 0 0 42.666667 42.666667h227.84a42.666667 42.666667 0 1 1 0 85.333333H170.666667a128 128 0 0 1-128-128v-597.333333a128 128 0 0 1 128-128h597.333333a128 128 0 0 1 128 128v227.84a42.666667 42.666667 0 0 1-85.333333 0zM384 366.336V256h110.336l384 384L768 750.336l-384-384z m414.165333 474.496l170.666667-170.666667a42.666667 42.666667 0 0 0 0-60.330666l-426.666667-426.666667A42.666667 42.666667 0 0 0 512 170.666667H341.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v170.666667a42.666667 42.666667 0 0 0 12.501333 30.165333l426.666667 426.666667a42.666667 42.666667 0 0 0 60.330666 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="edit-1" unicode="" d="M170.666667 323.669333V213.333333h110.336l426.666666 426.666667L597.333333 750.336l-426.666666-426.666667zM627.498667 840.832l170.666666-170.666667a42.666667 42.666667 0 0 0 0-60.330666l-469.333333-469.333334A42.666667 42.666667 0 0 0 298.666667 128H128a42.666667 42.666667 0 0 0-42.666667 42.666667v170.666666a42.666667 42.666667 0 0 0 12.501334 30.165334l469.333333 469.333333a42.666667 42.666667 0 0 0 60.330667 0zM128-85.333333h768a42.666667 42.666667 0 0 1 0 85.333333H128a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="external-link" unicode="" d="M725.333333 341.333333v-256a42.666667 42.666667 0 0 0-42.666666-42.666666H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666666V554.666667a42.666667 42.666667 0 0 0 42.666666 42.666666h256a42.666667 42.666667 0 0 1 0 85.333334H213.333333a128 128 0 0 1-128-128v-469.333334a128 128 0 0 1 128-128h469.333334a128 128 0 0 1 128 128v256a42.666667 42.666667 0 0 1-85.333334 0zM853.333333 725.333333v-213.333333a42.666667 42.666667 0 0 1 85.333334 0V768a42.666667 42.666667 0 0 1-42.666667 42.666667h-256a42.666667 42.666667 0 0 1 0-85.333334h213.333333zM456.832 268.501333l469.333333 469.333334a42.666667 42.666667 0 1 1-60.330666 60.330666l-469.333334-469.333333a42.666667 42.666667 0 0 1 60.330667-60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="eye" unicode="" d="M109.844053 353.834667c24.533333-37.973333 53.504-75.989333 86.613334-111.36C289.172053 143.744 394.900053 85.333333 512.020053 85.333333c117.12 0 222.890667 58.368 315.52 157.184A789.888 789.888 0 0 1 932.713387 384a789.888 789.888 0 0 1-105.130667 141.482667C734.91072 624.298667 629.140053 682.666667 512.020053 682.666667 394.900053 682.666667 289.129387 624.298667 196.500053 525.482667A789.888 789.888 0 0 1 91.32672 384c5.376-9.301333 11.52-19.413333 18.474667-30.165333zM4.54272 403.072c5.973333 11.989333 17.237333 32 33.621333 57.429333a874.325333 874.325333 0 0 0 96.042667 123.306667C241.556053 698.410667 367.80672 768 512.020053 768c144.213333 0 270.464-69.632 377.813334-184.149333a874.325333 874.325333 0 0 0 96-123.349334c16.426667-25.429333 27.690667-45.44 33.706666-57.429333a42.666667 42.666667 0 0 0 0-38.144c-6.016-11.989333-17.28-32-33.706666-57.429333a874.325333 874.325333 0 0 0-96-123.306667C782.484053 69.589333 656.233387 0 512.020053 0c-144.213333 0-270.464 69.632-377.813333 184.149333a874.325333 874.325333 0 0 0-96 123.349334 648.021333 648.021333 0 0 0-33.706667 57.429333 42.666667 42.666667 0 0 0 0 38.144zM512.020053 213.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="feather" unicode="" d="M230.997333 42.66666699999996l-115.498666-115.498667a42.666667 42.666667 0 1 0-60.330667 60.330667L170.666667 102.99733300000003V448a42.666667 42.666667 0 0 0 12.501333 30.165333l288 288a298.794667 298.794667 0 0 0 422.613333-422.570666l-287.573333-288.426667A42.666667 42.666667 0 0 0 576 42.66666699999996H230.997333z m256 256l241.365334-0.085334 104.96 105.301334a213.504 213.504 0 1 1-301.824 301.952L256 430.336v-242.005333l396.501333 396.501333a42.666667 42.666667 0 0 0 60.330667-60.330667L486.997333 298.66666699999996z m156.373334-85.333334H401.664l-85.333333-85.333333h241.92l85.12 85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="facebook" unicode="" d="M384 597.333333a256 256 0 0 0 256 256h128a42.666667 42.666667 0 0 0 42.666667-42.666666v-170.666667a42.666667 42.666667 0 0 0-42.666667-42.666667h-128v-85.333333h128a42.666667 42.666667 0 0 0 41.386667-53.034667l-42.666667-170.666666A42.666667 42.666667 0 0 0 725.333333 256h-85.333333v-298.666667a42.666667 42.666667 0 0 0-42.666667-42.666666h-170.666666a42.666667 42.666667 0 0 0-42.666667 42.666666v298.666667H298.666667a42.666667 42.666667 0 0 0-42.666667 42.666667v170.666666a42.666667 42.666667 0 0 0 42.666667 42.666667h85.333333V597.333333z m341.333333 170.666667h-85.333333a170.666667 170.666667 0 0 1-170.666667-170.666667v-128a42.666667 42.666667 0 0 0-42.666666-42.666666H341.333333v-85.333334h85.333334a42.666667 42.666667 0 0 0 42.666666-42.666666v-298.666667h85.333334v298.666667a42.666667 42.666667 0 0 0 42.666666 42.666666h94.72l21.333334 85.333334H597.333333a42.666667 42.666667 0 0 0-42.666666 42.666666V597.333333a85.333333 85.333333 0 0 0 85.333333 85.333334h85.333333V768z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="file-minus" unicode="" d="M554.666667 768H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-682.666666a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667V512h-213.333334a42.666667 42.666667 0 0 0-42.666666 42.666667V768z m85.333333-60.330667V597.333333h110.336L640 707.669333zM597.333333 853.333333a42.666667 42.666667 0 0 0 30.165334-12.501333l256-256A42.666667 42.666667 0 0 0 896 554.666667v-512a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128V725.333333a128 128 0 0 0 128 128h341.333333zM384 213.33333300000004a42.666667 42.666667 0 0 0 0 85.333334h256a42.666667 42.666667 0 0 0 0-85.333334H384z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="eye-off" unicode="" d="M284.838065 603.144984a42.644793 42.644793 0 1 1-51.77078 67.805221A829.441232 829.441232 0 0 1 5.642602 403.908509a42.644793 42.644793 0 0 1-0.554382-39.23321c5.970271-11.983187 17.228497-31.983595 33.604097-57.399892 27.164733-42.005122 59.148329-83.967598 95.99343-123.243453C241.937402 69.488038 368.123346-0.06562 512.262748-0.06562a472.546956 472.546956 0 0 1 279.152818 96.590457 42.644793 42.644793 0 1 1-51.68549 67.805222A386.788277 386.788277 0 0 0 511.580431 85.223967c-116.420286 0-222.094084 58.338077-314.718575 157.103419a789.483061 789.483061 0 0 0-104.948837 141.282201 744.151646 744.151646 0 0 0 192.925046 219.535397z m571.78139-327.981107a42.644793 42.644793 0 0 1 65.246534-54.926494 831.573472 831.573472 0 0 1 97.102195 143.414441 42.644793 42.644793 0 0 1 0.511737 39.14792c-5.970271 11.983187-17.228497 31.983595-33.604097 57.399892a873.877107 873.877107 0 0 1-95.99343 123.243453C782.588094 697.987004 656.40215 767.540662 512.262748 767.540662a431.352086 431.352086 0 0 1-99.277079-11.343515 42.644793 42.644793 0 0 1 19.446026-83.072057A346.275723 346.275723 0 0 0 512.177458 682.251075c117.145248 0 222.861691-58.338077 315.443537-157.103419a789.483061 789.483061 0 0 0 105.034127-141.324845 746.283885 746.283885 0 0 0-76.035667-108.658934z m-285.165734 47.250431a85.289587 85.289587 0 1 0-120.514186 120.514187 42.644793 42.644793 0 1 1-58.167498 62.431977 170.579174 170.579174 0 1 1 241.113662-241.113662 42.644793 42.644793 0 1 1-62.431978 58.167498zM13.020151 822.68038l938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738 60.299738l-938.185456 938.185456A42.644793 42.644793 0 1 1 13.020151 822.68038z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="fast-forward" unicode="" d="M580.864 51.626667A42.666667 42.666667 0 0 0 512 85.333333V682.666667a42.666667 42.666667 0 0 0 68.864 33.706666l384-298.666666a42.666667 42.666667 0 0 0 0-67.413334l-384-298.666666zM869.12 384L597.333333 595.413333v-422.826666L869.162667 384zM111.530667 51.626667A42.666667 42.666667 0 0 0 42.666667 85.333333V682.666667a42.666667 42.666667 0 0 0 68.864 33.706666l384-298.666666a42.666667 42.666667 0 0 0 0-67.413334l-384-298.666666zM399.786667 384L128 595.413333v-422.826666L399.829333 384z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="file-text" unicode="" d="M554.666667 768H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-682.666666a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667V512h-213.333334a42.666667 42.666667 0 0 0-42.666666 42.666667V768z m85.333333-60.330667V597.333333h110.336L640 707.669333zM597.333333 853.333333a42.666667 42.666667 0 0 0 30.165334-12.501333l256-256A42.666667 42.666667 0 0 0 896 554.666667v-512a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128V725.333333a128 128 0 0 0 128 128h341.333333z m85.333334-469.333333a42.666667 42.666667 0 0 0 0-85.333333H341.333333a42.666667 42.666667 0 0 0 0 85.333333h341.333334z m0-170.666667a42.666667 42.666667 0 0 0 0-85.333333H341.333333a42.666667 42.666667 0 0 0 0 85.333333h341.333334z m-256 341.333334a42.666667 42.666667 0 0 0 0-85.333334H341.333333a42.666667 42.666667 0 1 0 0 85.333334h85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="film" unicode="" d="M42.666667 717.738667A135.68 135.68 0 0 0 178.261333 853.333333h667.477334A135.68 135.68 0 0 0 981.333333 717.738667v-667.477334A135.68 135.68 0 0 0 845.738667-85.33333300000004H178.261333A135.68 135.68 0 0 0 42.666667 50.261333000000036V717.738667zM341.333333 768v-341.333333h341.333334V768H341.333333z m0-768h341.333334v341.333333H341.333333v-341.333333z m554.666667 426.666667V554.666667h-128v-128h128z m0-85.333334h-128v-128h128v128zM128 341.33333300000004v-128h128v128H128z m0 85.333334h128V554.666667H128v-128z m640 341.333333v-128h128V717.738667A50.346667 50.346667 0 0 1 845.738667 768H768z m128-640h-128v-128h77.738667A50.346667 50.346667 0 0 1 896 50.261333000000036V128zM256 768H178.261333A50.346667 50.346667 0 0 1 128 717.738667V640h128V768z m0-768v128H128v-77.738667C128 22.61333300000001 150.570667 0 178.261333 0H256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="file" unicode="" d="M554.666667 853.333333a42.666667 42.666667 0 0 0 30.165333-12.501333l298.666667-298.666667A42.666667 42.666667 0 0 0 896 512v-469.333333a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128V725.333333a128 128 0 0 0 128 128h298.666667z m-42.666667-341.333333V768H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-682.666666a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667V469.333333h-256a42.666667 42.666667 0 0 0-42.666667 42.666667z m85.333333 195.669333V554.666667h153.002667L597.333333 707.669333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="file-plus" unicode="" d="M554.666667 768H256a42.666667 42.666667 0 0 1-42.666667-42.666667v-682.666666a42.666667 42.666667 0 0 1 42.666667-42.666667h512a42.666667 42.666667 0 0 1 42.666667 42.666667V512h-213.333334a42.666667 42.666667 0 0 0-42.666666 42.666667V768z m85.333333-60.330667V597.333333h110.336L640 707.669333zM597.333333 853.333333a42.666667 42.666667 0 0 0 30.165334-12.501333l256-256A42.666667 42.666667 0 0 0 896 554.666667v-512a128 128 0 0 0-128-128H256a128 128 0 0 0-128 128V725.333333a128 128 0 0 0 128 128h341.333333z m-128-640H384a42.666667 42.666667 0 0 0 0 85.333334h85.333333v85.333333a42.666667 42.666667 0 0 0 85.333334 0v-85.333333h85.333333a42.666667 42.666667 0 0 0 0-85.333334h-85.333333v-85.333333a42.666667 42.666667 0 0 0-85.333334 0v85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="folder" unicode="" d="M170.666667 725.333333a42.666667 42.666667 0 0 1-42.666667-42.666666v-597.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666666V554.666667a42.666667 42.666667 0 0 1-42.666667 42.666666h-384a42.666667 42.666667 0 0 0-35.498666 18.986667L361.173333 725.333333H170.666667z m682.666666-42.666666a128 128 0 0 0 128-128v-469.333334a128 128 0 0 0-128-128H170.666667a128 128 0 0 0-128 128V682.666667a128 128 0 0 0 128 128h213.333333a42.666667 42.666667 0 0 0 35.498667-18.986667L492.16 682.666667H853.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="filter" unicode="" d="M384 348.757333l-331.264 391.68A42.666667 42.666667 0 0 0 85.333333 810.666667h853.333334a42.666667 42.666667 0 0 0 32.597333-70.229334L640 348.757333V0a42.666667 42.666667 0 0 0-61.738667-38.144l-170.666666 85.333333A42.666667 42.666667 0 0 0 384 85.33333300000004v263.424zM177.28 725.333333l281.984-333.397333a42.666667 42.666667 0 0 0 10.069333-27.562667v-252.672l85.333334-42.666666V364.373333a42.666667 42.666667 0 0 0 10.069333 27.562667L846.72 725.333333H177.28z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="flag" unicode="" d="M128-42.66666699999996V768a42.666667 42.666667 0 0 0 12.501333 30.165333c9.386667 9.386667 26.197333 20.608 51.626667 30.762667C230.698667 844.373333 280.064 853.333333 341.333333 853.333333c60.458667 0 101.674667-11.776 186.496-45.738666C603.008 777.557333 636.458667 768 682.666667 768c50.730667 0 89.344 7.04 117.504 18.261333 14.549333 5.845333 21.674667 10.624 22.997333 11.946667C850.048 825.045333 896 805.973333 896 768v-512a42.666667 42.666667 0 0 0-12.501333-30.165333c-9.386667-9.386667-26.197333-20.608-51.626667-30.762667-38.528-15.445333-87.893333-24.405333-149.205333-24.405333-60.458667 0-101.674667 11.776-186.496 45.738666C420.992 246.44266700000003 387.541333 256 341.333333 256c-50.730667 0-89.344-7.04-117.504-18.261333a174.677333 174.677333 0 0 1-10.496-4.608V-42.66666699999996a42.666667 42.666667 0 0 0-85.333333 0z m213.333333 384c60.458667 0 101.674667-11.776 186.496-45.738666 75.178667-30.037333 108.629333-39.594667 154.837334-39.594667 50.730667 0 89.344 7.04 117.504 18.261333 4.053333 1.664 7.552 3.2 10.496 4.608v420.693334c-34.986667-10.794667-77.397333-16.896-128-16.896-60.458667 0-101.674667 11.776-186.496 45.738666C420.992 758.442667 387.541333 768 341.333333 768c-50.730667 0-89.344-7.04-117.504-18.261333A174.677333 174.677333 0 0 1 213.333333 745.130667v-420.693334C248.32 335.23199999999997 290.730667 341.33333300000004 341.333333 341.33333300000004z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="globe" unicode="" d="M512-85.33333300000004C252.8-85.33333300000004 42.666667 124.79999999999995 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m211.2 426.666666a695.466667 695.466667 0 0 0-114.176-328.96A384.341333 384.341333 0 0 1 893.653333 341.33333300000004H723.2z m-422.272 0H130.346667a384.341333 384.341333 0 0 1 284.629333-328.96A696.789333 696.789333 0 0 0 300.928 341.33333300000004z m308.096 414.293334A696.789333 696.789333 0 0 0 723.072 426.666667h170.581333a384.341333 384.341333 0 0 1-284.629333 328.96z m-194.048 0A384.341333 384.341333 0 0 1 130.346667 426.666667H300.8a695.466667 695.466667 0 0 0 114.176 328.96zM386.474667 341.33333300000004A610.133333 610.133333 0 0 1 512 23.46666700000003 609.621333 609.621333 0 0 1 637.653333 341.33333300000004H386.474667z m251.050666 85.333334A610.133333 610.133333 0 0 1 512 744.533333 609.621333 609.621333 0 0 1 386.346667 426.666667h251.178666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="grid" unicode="" d="M170.666667 512h213.333333V725.333333H170.666667v-213.333333zM128 810.666667h298.666667a42.666667 42.666667 0 0 0 42.666666-42.666667v-298.666667a42.666667 42.666667 0 0 0-42.666666-42.666666H128a42.666667 42.666667 0 0 0-42.666667 42.666666V768a42.666667 42.666667 0 0 0 42.666667 42.666667zM640 512h213.333333V725.333333h-213.333333v-213.333333z m-42.666667 298.666667h298.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-298.666667a42.666667 42.666667 0 0 0-42.666667-42.666666h-298.666667a42.666667 42.666667 0 0 0-42.666666 42.666666V768a42.666667 42.666667 0 0 0 42.666666 42.666667zM640 42.666667h213.333333v213.333333h-213.333333v-213.333333z m-42.666667 298.666666h298.666667a42.666667 42.666667 0 0 0 42.666667-42.666666v-298.666667a42.666667 42.666667 0 0 0-42.666667-42.666667h-298.666667a42.666667 42.666667 0 0 0-42.666666 42.666667v298.666667a42.666667 42.666667 0 0 0 42.666666 42.666666zM170.666667 42.666667h213.333333v213.333333H170.666667v-213.333333z m-42.666667 298.666666h298.666667a42.666667 42.666667 0 0 0 42.666666-42.666666v-298.666667a42.666667 42.666667 0 0 0-42.666666-42.666667H128a42.666667 42.666667 0 0 0-42.666667 42.666667v298.666667a42.666667 42.666667 0 0 0 42.666667 42.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="heart" unicode="" d="M527.061333 729.472A277.333333 277.333333 0 0 0 1000.618667 533.333333a277.333333 277.333333 0 0 0-81.28-196.138666l-377.173334-377.173334a42.666667 42.666667 0 0 0-60.330666 0l-377.173334 377.173334a277.376 277.376 0 0 0 392.277334 392.277333l15.061333-15.061333 15.061333 15.061333z m286.72-377.173333l45.226667 45.226666a192 192 0 0 1-135.808 327.893334 192 192 0 0 1-135.808-56.32l-45.226667-45.226667a42.666667 42.666667 0 0 0-60.330666 0l-45.226667 45.226667a192.042667 192.042667 0 0 1-271.616-271.573334L512 50.517333l301.781333 301.781334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="home" unicode="" d="M101.802667 545.7066669999999l384 298.666666a42.666667 42.666667 0 0 0 52.394666 0l384-298.666666A42.666667 42.666667 0 0 0 938.666667 512v-469.333333a128 128 0 0 0-128-128H213.333333a128 128 0 0 0-128 128V512a42.666667 42.666667 0 0 0 16.469334 33.706667zM682.666667 0h128a42.666667 42.666667 0 0 1 42.666666 42.666667V491.093333l-341.333333 265.514667-341.333333-265.472V42.66666699999996a42.666667 42.666667 0 0 1 42.666666-42.666667h128v384a42.666667 42.666667 0 0 0 42.666667 42.666667h256a42.666667 42.666667 0 0 0 42.666667-42.666667v-384z m-256 0h170.666666v341.333333h-170.666666v-341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="github" unicode="" d="M371.754667 83.541333c-91.818667-27.562667-128.896-12.970667-174.805334 43.861334-3.072 3.797333-15.786667 19.968-19.2 24.192-27.477333 33.92-49.408 52.309333-82.048 60.458666a42.666667 42.666667 0 0 1-20.736-82.773333c9.813333-2.474667 19.712-10.752 36.394667-31.36 2.986667-3.626667 15.701333-19.797333 19.2-24.106667 66.304-82.133333 138.026667-110.293333 265.685333-72.021333a42.666667 42.666667 0 0 1-24.490666 81.749333zM960 489.813333a274.773333 274.773333 0 0 1-58.837333 169.856 258.986667 258.986667 0 0 1-12.458667 167.808 42.666667 42.666667 0 0 1-27.093333 24.106667 64.085333 64.085333 0 0 1-8.96 1.877333c-40.064 5.802667-98.858667-10.026667-177.621334-59.989333a613.546667 613.546667 0 0 1-283.392 0C312.874667 843.434667 254.08 859.306667 213.973333 853.461333a64.085333 64.085333 0 0 1-8.96-1.877333 42.666667 42.666667 0 0 1-27.050666-24.106667 258.986667 258.986667 0 0 1-12.458667-167.808A274.773333 274.773333 0 0 1 106.666667 488.32c0-196.778667 87.893333-292.949333 247.466666-329.6a186.453333 186.453333 0 0 1-12.714666-81.92L341.333333-85.333333a42.666667 42.666667 0 0 1 85.333334 0v165.12c-2.133333 31.616 8.106667 59.776 28.117333 80.426666a42.666667 42.666667 0 0 1-25.429333 71.978667C267.52 252.330667 192 315.776 192 488.533333a189.568 189.568 0 0 0 52.224 131.84 42.666667 42.666667 0 0 1 9.088 44.373334 173.653333 173.653333 0 0 0-5.973333 102.058666c2.986667-0.64 6.272-1.493333 9.856-2.56 27.52-7.893333 61.781333-24.490667 103.04-52.181333a42.666667 42.666667 0 0 1 34.944-5.717333 528.213333 528.213333 0 0 0 276.309333 0 42.666667 42.666667 0 0 1 34.944 5.717333c41.258667 27.690667 75.52 44.288 103.04 52.224 3.584 1.024 6.826667 1.877333 9.813333 2.56a173.653333 173.653333 0 0 0-5.930666-102.101333 42.666667 42.666667 0 0 1 9.088-44.373334A189.44 189.44 0 0 0 874.666667 489.813333c0-174.421333-76.032-238.336-236.8-256.256a42.666667 42.666667 0 0 1-25.941334-72.106666 101.12 101.12 0 0 0 28.202667-78.336L640-85.333333a42.666667 42.666667 0 0 1 85.333333 0v165.12a177.237333 177.237333 0 0 1-12.373333 79.658666c158.72 35.2 247.04 132.266667 247.04 330.368z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="image" unicode="" d="M213.333333-42.66666699999996h-0.213333A127.786667 127.786667 0 0 0 85.333333 85.12V682.88A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.66666699999996H213.333333z m640 401.664v323.84A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88v-597.76c0-18.261333 11.52-33.792 27.648-39.808L652.501333 499.498667a42.666667 42.666667 0 0 0 60.330667 0L853.333333 358.997333z m0-120.661333l-170.666666 170.666667L316.330667 42.66666699999996h494.506666c23.466667 0 42.496 18.986667 42.496 42.453333v153.173333zM362.666667 426.666667a106.666667 106.666667 0 1 0 0 213.333333 106.666667 106.666667 0 0 0 0-213.333333z m0 85.333333a21.333333 21.333333 0 1 1 0 42.666667 21.333333 21.333333 0 0 1 0-42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="inbox" unicode="" d="M311.466667 768h401.066666a128 128 0 0 0 117.077334-76.117333l148.053333-333.226667A42.666667 42.666667 0 0 0 981.333333 341.33333300000004v-213.333333a128 128 0 0 0-128-128H170.666667a128 128 0 0 0-128 128v213.333333a42.666667 42.666667 0 0 0 3.669333 17.322667l148.053333 333.226667A128 128 0 0 0 311.466667 768z m561.536-384l-121.386667 273.28A42.666667 42.666667 0 0 1 712.533333 682.666667H311.466667a42.666667 42.666667 0 0 1-39.082667-25.386667L151.04 384H341.333333a42.666667 42.666667 0 0 0 35.498667-18.986667L449.493333 256h125.013334l72.661333 109.013333A42.666667 42.666667 0 0 0 682.666667 384h190.336zM896 128v170.666667h-190.506667l-72.661333-109.013334A42.666667 42.666667 0 0 0 597.333333 170.66666699999996h-170.666666a42.666667 42.666667 0 0 0-35.498667 18.986666L318.506667 298.66666699999996H128v-170.666667a42.666667 42.666667 0 0 1 42.666667-42.666667h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="layers" unicode="" d="M492.928 848.810667a42.666667 42.666667 0 0 0 38.144 0l426.666667-213.333334a42.666667 42.666667 0 0 0 0-76.288l-426.666667-213.333333a42.666667 42.666667 0 0 0-38.144 0l-426.666667 213.333333a42.666667 42.666667 0 0 0 0 76.288l426.666667 213.333334zM180.736 597.333333L512 431.701333 843.264 597.333333 512 762.965333 180.736 597.333333zM104.405333 208.810667a42.666667 42.666667 0 0 1-38.144-76.288l426.666667-213.333334a42.666667 42.666667 0 0 1 38.144 0l426.666667 213.333334a42.666667 42.666667 0 0 1-38.144 76.288L512 5.034667l-407.594667 203.776zM104.405333 422.144a42.666667 42.666667 0 0 1-38.144-76.288l426.666667-213.333333a42.666667 42.666667 0 0 1 38.144 0l426.666667 213.333333a42.666667 42.666667 0 0 1-38.144 76.288L512 218.368l-407.594667 203.776z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="info" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM554.666667 213.333333v170.666667a42.666667 42.666667 0 0 1-85.333334 0v-170.666667a42.666667 42.666667 0 0 1 85.333334 0zM512 554.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="instagram" unicode="" d="M128 597.546667v-427.093334A170.453333 170.453333 0 0 1 298.453333 0h427.093334A170.453333 170.453333 0 0 1 896 170.453333V597.546667A170.453333 170.453333 0 0 1 725.546667 768H298.453333A170.453333 170.453333 0 0 1 128 597.546667z m-85.333333 0A255.786667 255.786667 0 0 0 298.453333 853.333333h427.093334A255.786667 255.786667 0 0 0 981.333333 597.546667v-427.093334A255.786667 255.786667 0 0 0 725.546667-85.333333H298.453333A255.786667 255.786667 0 0 0 42.666667 170.453333V597.546667zM640.426667 404.608a128 128 0 1 1-253.184-37.546667 128 128 0 0 1 253.226666 37.546667z m84.48 12.544a213.333333 213.333333 0 1 0-422.101334-62.592 213.333333 213.333333 0 0 0 422.058667 62.592zM725.333333 640m-42.666666 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="layout" unicode="" d="M85.333333 682.88A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.66666699999996H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88zM853.333333 554.666667V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88V554.666667h682.666666z m0-85.333334H426.666667v-426.666666h384.213333c23.466667 0 42.453333 18.986667 42.453333 42.453333V469.333333zM341.333333 42.66666699999996V469.333333H170.666667v-384.213333c0-23.466667 18.986667-42.453333 42.453333-42.453333H341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="link-" unicode="" d="M640 554.666667h128a170.666667 170.666667 0 1 0 0-341.333334h-128a42.666667 42.666667 0 0 1 0-85.333333h128a256 256 0 1 1 0 512h-128a42.666667 42.666667 0 0 1 0-85.333333z m-256-341.333334H256a170.666667 170.666667 0 0 0 0 341.333334h128a42.666667 42.666667 0 1 1 0 85.333333H256a256 256 0 1 1 0-512h128a42.666667 42.666667 0 0 1 0 85.333333zM341.333333 341.333333h341.333334a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="life-buoy" unicode="" d="M272.298667 83.96799999999996A382.378667 382.378667 0 0 1 512 0a382.378667 382.378667 0 0 1 239.701333 83.968l-122.026666 122.026667A212.352 212.352 0 0 0 512 170.66666699999996a212.352 212.352 0 0 0-117.632 35.328l-122.026667-122.026667z m-60.330667 60.330667l122.026667 122.026666A212.352 212.352 0 0 0 298.666667 384c0 43.52 13.013333 83.925333 35.328 117.632l-122.026667 122.026667A382.378667 382.378667 0 0 1 128 384c0-90.666667 31.402667-173.994667 83.968-239.701333z m60.330667 539.733333l122.026666-122.026667A212.352 212.352 0 0 0 512 597.333333c43.52 0 83.925333-13.013333 117.632-35.328l122.026667 122.026667A382.378667 382.378667 0 0 1 512 768a382.378667 382.378667 0 0 1-239.701333-83.968z m539.733333-60.330667l-122.026667-122.026666c22.314667-33.749333 35.328-74.197333 35.328-117.674667 0-43.52-13.013333-83.925333-35.328-117.632l122.026667-122.026667A382.378667 382.378667 0 0 1 896 384a382.378667 382.378667 0 0 1-83.968 239.701333zM512-85.33333300000004C252.8-85.33333300000004 42.666667 124.79999999999995 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 341.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="link" unicode="" d="M392.533333 315.776a256 256 0 0 1 386.005334-27.648l128 128a256 256 0 1 1-361.472 362.496l-73.813334-73.386667a42.666667 42.666667 0 1 1 60.16-60.501333l73.386667 72.96a170.538667 170.538667 0 0 0 238.805333-2.517333 170.666667 170.666667 0 0 0 2.090667-239.232l-127.488-127.488A170.666667 170.666667 0 0 0 460.8 366.933333a42.666667 42.666667 0 1 1-68.352-51.114666zM631.466667 452.224a256 256 0 0 1-386.005334 27.648l-128-128a256 256 0 1 1 361.472-362.496l73.472 73.472a42.666667 42.666667 0 0 1-60.330666 60.330667l-72.96-72.96c-66.432-64.170667-172.885333-63.232-238.72 2.602666a170.666667 170.666667 0 0 0-2.090667 239.232l127.488 127.488a170.666667 170.666667 0 0 0 257.365333-18.432 42.666667 42.666667 0 1 1 68.352 51.114667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="log-in" unicode="" d="M597.333333-85.333333h213.333334a128 128 0 0 1 128 128V725.333333a128 128 0 0 1-128 128h-213.333334a42.666667 42.666667 0 0 1 0-85.333333h213.333334a42.666667 42.666667 0 0 0 42.666666-42.666667v-682.666666a42.666667 42.666667 0 0 0-42.666666-42.666667h-213.333334a42.666667 42.666667 0 0 1 0-85.333333zM439.168 243.498667a42.666667 42.666667 0 0 1 60.330667-60.330667l170.666666 170.666667a42.666667 42.666667 0 0 1 0 60.330666l-170.666666 170.666667a42.666667 42.666667 0 0 1-60.330667-60.330667L579.669333 384l-140.501333-140.501333zM640 426.666667H128a42.666667 42.666667 0 0 1 0-85.333334h512a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="list" unicode="" d="M341.333333 597.333333h554.666667a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 1 1 0-85.333334zM341.333333 341.333333h554.666667a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334zM341.333333 85.333333h554.666667a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334zM128 640m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM128 384m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM128 128m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="lock" unicode="" d="M256 469.333333V597.333333a256 256 0 1 0 512 0v-128h42.88A127.957333 127.957333 0 0 0 938.666667 341.674667v-299.349334A127.701333 127.701333 0 0 0 810.88-85.33333300000004H213.12A127.957333 127.957333 0 0 0 85.333333 42.325333v299.349334A127.701333 127.701333 0 0 0 213.12 469.333333H256z m-85.333333-127.658666v-299.349334c0-23.168 19.157333-42.325333 42.453333-42.325333h597.76c23.466667 0 42.453333 18.944 42.453333 42.325333v299.349334a42.624 42.624 0 0 1-42.453333 42.325333H213.12a42.368 42.368 0 0 1-42.453333-42.325333z m170.666666 127.786666h341.333334V597.333333a170.666667 170.666667 0 1 1-341.333334 0v-127.872z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="log-out" unicode="" d="M426.666667 0H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667V725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667h213.333334a42.666667 42.666667 0 0 1 0 85.333333H213.333333a128 128 0 0 1-128-128v-682.666666a128 128 0 0 1 128-128h213.333334a42.666667 42.666667 0 0 1 0 85.333333zM695.168 243.498667a42.666667 42.666667 0 0 1 60.330667-60.330667l170.666666 170.666667a42.666667 42.666667 0 0 1 0 60.330666l-170.666666 170.666667a42.666667 42.666667 0 0 1-60.330667-60.330667L835.669333 384l-140.501333-140.501333zM896 426.666667H384a42.666667 42.666667 0 0 1 0-85.333334h512a42.666667 42.666667 0 0 1 0 85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="loader" unicode="" d="M469.333333 810.666667v-170.666667a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0zM469.333333 128v-170.666667a42.666667 42.666667 0 0 1 85.333334 0v170.666667a42.666667 42.666667 0 0 1-85.333334 0zM180.181333 655.488l120.746667-120.746667a42.666667 42.666667 0 0 1 60.330667 60.330667l-120.746667 120.746667a42.666667 42.666667 0 0 1-60.330667-60.330667zM662.741333 172.928l120.746667-120.746667a42.666667 42.666667 0 0 1 60.330667 60.330667l-120.746667 120.746667a42.666667 42.666667 0 0 1-60.330667-60.330667zM85.333333 341.333333h170.666667a42.666667 42.666667 0 0 1 0 85.333334H85.333333a42.666667 42.666667 0 0 1 0-85.333334zM768 341.333333h170.666667a42.666667 42.666667 0 0 1 0 85.333334h-170.666667a42.666667 42.666667 0 0 1 0-85.333334zM240.512 52.181333l120.746667 120.746667a42.666667 42.666667 0 0 1-60.330667 60.330667l-120.746667-120.746667a42.666667 42.666667 0 0 1 60.330667-60.330667zM723.072 534.741333l120.746667 120.746667a42.666667 42.666667 0 1 1-60.330667 60.330667l-120.746667-120.746667a42.666667 42.666667 0 1 1 60.330667-60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="mail" unicode="" d="M170.666667 768h682.666666c70.485333 0 128-57.514667 128-128v-512c0-70.485333-57.514667-128-128-128H170.666667c-70.485333 0-128 57.514667-128 128V640c0 70.485333 57.514667 128 128 128z m720.768-108.970667A42.922667 42.922667 0 0 1 853.333333 682.666667H170.666667c-16.554667 0-31.061333-9.685333-38.101334-23.637334L512 393.429333l379.434667 265.6zM896 558.0799999999999l-359.552-251.648a42.666667 42.666667 0 0 0-48.896 0L128 558.037333V128c0-23.381333 19.285333-42.666667 42.666667-42.666667h682.666666c23.381333 0 42.666667 19.285333 42.666667 42.666667V558.0799999999999z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="maximize-" unicode="" d="M853.333333 725.333333v-213.333333a42.666667 42.666667 0 0 1 85.333334 0V768a42.666667 42.666667 0 0 1-42.666667 42.666667h-256a42.666667 42.666667 0 0 1 0-85.333334h213.333333zM170.666667 256a42.666667 42.666667 0 0 1-85.333334 0v-256a42.666667 42.666667 0 0 1 42.666667-42.666667h256a42.666667 42.666667 0 0 1 0 85.333334H170.666667v213.333333zM865.834667 798.165333l-298.666667-298.666666a42.666667 42.666667 0 1 1 60.330667-60.330667l298.666666 298.666667a42.666667 42.666667 0 1 1-60.330666 60.330666zM158.165333-30.165333l298.666667 298.666666a42.666667 42.666667 0 0 1-60.330667 60.330667l-298.666666-298.666667a42.666667 42.666667 0 0 1 60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="map" unicode="" d="M640 613.6320000000001l-256 128v-587.264l256-128V613.6320000000001z m85.333333 1.621333v-584.405333l213.333334 121.898667V737.152L725.333333 615.253333zM319.530667 847.36a42.453333 42.453333 0 0 0 41.642666 1.066667l320.170667-160.042667 278.826667 159.317333A42.666667 42.666667 0 0 0 1024 810.666667v-682.666667a42.666667 42.666667 0 0 0-21.504-37.034667l-298.666667-170.666666a42.666667 42.666667 0 0 0-40.234666-1.109334l-320.938667 160.426667-278.826667-159.317333A42.666667 42.666667 0 0 0 0-42.66666699999996V640a42.666667 42.666667 0 0 0 21.504 37.034667L319.530667 847.36zM298.666667 737.152L85.333333 615.253333v-584.405333l213.333334 121.898667V737.152z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="map-pin" unicode="" d="M545.408-7.253333a1243.946667 1243.946667 0 0 1 127.616 116.608C786.218667 228.821333 853.333333 351.872 853.333333 469.333333a341.333333 341.333333 0 0 1-682.666666 0c0-117.461333 67.114667-240.469333 180.309333-359.978666A1243.946667 1243.946667 0 0 1 512-32.981333c10.325333 7.68 21.546667 16.213333 33.408 25.685333z m189.568 57.898666a1328.298667 1328.298667 0 0 0-181.504-158.976c-8.32-6.016-14.378667-10.24-17.792-12.501333a42.666667 42.666667 0 0 0-47.36 0c-3.413333 2.304-9.472 6.485333-17.792 12.501333a1328.298667 1328.298667 0 0 0-181.504 158.976C162.218667 184.490667 85.333333 325.461333 85.333333 469.333333 85.333333 704.981333 276.352 896 512 896s426.666667-191.018667 426.666667-426.666667c0-143.872-76.885333-284.842667-203.690667-418.688zM512 298.666667a170.666667 170.666667 0 1 0 0 341.333333 170.666667 170.666667 0 0 0 0-341.333333z m0 85.333333a85.333333 85.333333 0 1 1 0 170.666667 85.333333 85.333333 0 0 1 0-170.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="menu" unicode="" d="M128 341.333333h768a42.666667 42.666667 0 0 1 0 85.333334H128a42.666667 42.666667 0 0 1 0-85.333334zM128 597.333333h768a42.666667 42.666667 0 0 1 0 85.333334H128a42.666667 42.666667 0 1 1 0-85.333334zM128 85.333333h768a42.666667 42.666667 0 0 1 0 85.333334H128a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="message-circle" unicode="" d="M357.717333 121.557333a42.666667 42.666667 0 0 0 32.725334-2.389333A314.965333 314.965333 0 0 1 533.333333 85.333333a320.085333 320.085333 0 0 1 286.165334 177.066667A314.88 314.88 0 0 1 853.333333 405.248l0.085334 19.114667C844.458667 586.752 714.752 716.373333 554.666667 725.333333h-21.461334a315.093333 315.093333 0 0 1-142.933333-33.92A320 320 0 0 1 213.333333 405.205333a314.88 314.88 0 0 1 33.834667-142.762666 42.666667 42.666667 0 0 0 2.389333-32.725334L195.413333 67.413333l162.261334 54.101334z m538.026667 102.570667A405.333333 405.333333 0 0 0 533.504 0a400.213333 400.213333 0 0 0-165.12 35.157333L141.525333-40.533333a42.666667 42.666667 0 0 0-53.973333 53.973333l75.648 226.901333A405.248 405.248 0 0 0 351.957333 767.701333 400.085333 400.085333 0 0 0 533.333333 810.666667l23.68-0.085334C762.88 799.232 927.232 634.88 938.666667 426.666667v-21.290667a400.042667 400.042667 0 0 0-42.88-181.248z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="message-square" unicode="" d="M268.501333 200.832A42.666667 42.666667 0 0 0 298.666667 213.333333h512a42.666667 42.666667 0 0 1 42.666666 42.666667V682.666667a42.666667 42.666667 0 0 1-42.666666 42.666666H213.333333a42.666667 42.666667 0 0 1-42.666666-42.666666v-579.669334l97.834666 97.834667z m-110.336-230.997333C131.285333-57.045333 85.333333-38.016 85.333333 0V682.666667a128 128 0 0 0 128 128h597.333334a128 128 0 0 0 128-128v-426.666667a128 128 0 0 0-128-128H316.330667l-158.165334-158.165333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="minimize-" unicode="" d="M384 42.666667a42.666667 42.666667 0 0 1 85.333333 0v256a42.666667 42.666667 0 0 1-42.666666 42.666666H170.666667a42.666667 42.666667 0 0 1 0-85.333333h213.333333v-213.333333zM640 725.333333a42.666667 42.666667 0 0 1-85.333333 0v-256a42.666667 42.666667 0 0 1 42.666666-42.666666h256a42.666667 42.666667 0 0 1 0 85.333333h-213.333333V725.333333zM627.498667 439.168l298.666666 298.666667a42.666667 42.666667 0 1 1-60.330666 60.330666l-298.666667-298.666666a42.666667 42.666667 0 1 1 60.330667-60.330667zM158.165333-30.165333l298.666667 298.666666a42.666667 42.666667 0 0 1-60.330667 60.330667l-298.666666-298.666667a42.666667 42.666667 0 0 1 60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="mic-off" unicode="" d="M13.020151 822.68038l938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738 60.299738l-938.185456 938.185456A42.644793 42.644793 0 1 1 13.020151 822.68038zM341.683574 511.671902v-127.934381a170.579174 170.579174 0 0 1 291.136005-120.599476 42.644793 42.644793 0 0 1-60.299738 60.385028A85.289587 85.289587 0 0 0 426.973161 383.737521V511.671902a42.644793 42.644793 0 1 1-85.289587 0z m341.158348-14.49923V724.895869a170.579174 170.579174 0 0 1-337.746764 34.07319 42.644793 42.644793 0 0 1 83.583795-16.972628 85.289587 85.289587 0 0 0 168.873382-17.057917V497.172672a42.644793 42.644793 0 0 1 85.289587 0zM695.038333 202.497149A255.868761 255.868761 0 0 0 256.393987 383.439008V469.027108a42.644793 42.644793 0 1 1-85.289587 0v-85.289587a341.158348 341.158348 0 0 1 584.830698-240.943083 42.644793 42.644793 0 0 1-60.896765 59.702711zM768.131509 469.027108v-85.289587c0-15.053612-1.364633-30.064579-4.008611-44.904967a42.644793 42.644793 0 1 1 83.924954-15.096257A341.158348 341.158348 0 0 1 853.421095 383.737521V469.027108a42.644793 42.644793 0 0 1-85.289586 0zM469.617955 85.223967v-170.579174a42.644793 42.644793 0 0 1 85.289586 0v170.579174a42.644793 42.644793 0 0 1-85.289586 0zM341.683574-128h341.158348a42.644793 42.644793 0 0 1 0 85.289587H341.683574a42.644793 42.644793 0 0 1 0-85.289587z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="minus-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM341.333333 341.333333h341.333334a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="mic" unicode="" d="M682.666667 725.333333v-341.333333a170.666667 170.666667 0 1 0-341.333334 0V725.333333a170.666667 170.666667 0 1 0 341.333334 0z m-170.666667 85.333334a85.333333 85.333333 0 0 1-85.333333-85.333334v-341.333333a85.333333 85.333333 0 1 1 170.666666 0V725.333333a85.333333 85.333333 0 0 1-85.333333 85.333334zM768 469.333333v-85.333333a256 256 0 0 0-512 0v85.333333a42.666667 42.666667 0 1 1-85.333333 0v-85.333333a341.333333 341.333333 0 0 1 682.666666 0v85.333333a42.666667 42.666667 0 0 1-85.333333 0zM469.333333 85.333333v-170.666666a42.666667 42.666667 0 0 1 85.333334 0v170.666666a42.666667 42.666667 0 0 1-85.333334 0zM341.333333-128h341.333334a42.666667 42.666667 0 0 1 0 85.333333H341.333333a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="minus-square" unicode="" d="M170.666667 682.88v-597.76c0-23.466667 18.986667-42.453333 42.453333-42.453333h597.76c23.466667 0 42.453333 18.986667 42.453333 42.453333V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88z m-85.333334 0A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.666667H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88zM341.333333 341.333333h341.333334a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="minus" unicode="" d="M213.333333 341.333333h597.333334a42.666667 42.666667 0 0 1 0 85.333334H213.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="moon" unicode="" d="M476.202667 348.245333a342.058667 342.058667 0 0 0-79.786667 358.229334 341.973333 341.973333 0 1 1 438.016-438.016 342.058667 342.058667 0 0 0-358.229333 79.786666z m462.250666-2.773333A427.477333 427.477333 0 1 0 473.472 810.496c36.778667 3.413333 60.245333-38.272 38.314667-67.968a256.469333 256.469333 0 0 1 358.698666-358.741333c29.696 21.973333 71.381333-1.536 67.968-38.314667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="monitor" unicode="" d="M554.666667 128v-85.333333h128a42.666667 42.666667 0 0 0 0-85.333334H341.333333a42.666667 42.666667 0 0 0 0 85.333334h128v85.333333H170.24A127.914667 127.914667 0 0 0 42.666667 256.256V682.410667A128.128 128.128 0 0 0 170.24 810.666667h683.52A127.914667 127.914667 0 0 0 981.333333 682.410667v-426.154667A128.128 128.128 0 0 0 853.76 128H554.666667zM128 682.410667v-426.154667A42.581333 42.581333 0 0 1 170.24 213.33333300000004h683.52c23.04 0 42.24 19.285333 42.24 42.922667V682.410667A42.581333 42.581333 0 0 1 853.76 725.333333H170.24C147.2 725.333333 128 706.048 128 682.410667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="more-vertical" unicode="" d="M512 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM512 597.333333a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333334a42.666667 42.666667 0 1 1 0 85.333333 42.666667 42.666667 0 0 1 0-85.333333zM512-85.333333a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333333 42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="more-horizontal" unicode="" d="M512 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM853.333333 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM170.666667 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="move" unicode="" d="M243.498667 481.834667a42.666667 42.666667 0 0 1-60.330667 60.330666l-128-128a42.666667 42.666667 0 0 1 0-60.330666l128-128a42.666667 42.666667 0 1 1 60.330667 60.330666L145.664 384l97.834667 97.834667zM609.834667 652.501333a42.666667 42.666667 0 0 1 60.330666 60.330667l-128 128a42.666667 42.666667 0 0 1-60.330666 0l-128-128a42.666667 42.666667 0 0 1 60.330666-60.330667L512 750.336l97.834667-97.834667zM414.165333 115.498667a42.666667 42.666667 0 1 1-60.330666-60.330667l128-128a42.666667 42.666667 0 0 1 60.330666 0l128 128a42.666667 42.666667 0 0 1-60.330666 60.330667L512 17.664l-97.834667 97.834667zM780.501333 286.165333a42.666667 42.666667 0 0 1 60.330667-60.330666l128 128a42.666667 42.666667 0 0 1 0 60.330666l-128 128a42.666667 42.666667 0 0 1-60.330667-60.330666L878.336 384l-97.834667-97.834667zM85.333333 341.333333h853.333334a42.666667 42.666667 0 0 1 0 85.333334H85.333333a42.666667 42.666667 0 0 1 0-85.333334zM469.333333 810.666667v-853.333334a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="music" unicode="" d="M426.666667 646.528V85.33333300000004a128 128 0 0 0-128-128H213.333333a128 128 0 0 0 0 256h128V682.666667a42.666667 42.666667 0 0 0 35.669334 42.069333l512 85.333333A42.666667 42.666667 0 0 0 938.666667 768v-597.333333a128 128 0 0 0-128-128h-85.333334a128 128 0 0 0 0 256h128V717.653333L426.666667 646.528zM341.333333 128H213.333333a42.666667 42.666667 0 0 1 0-85.333333h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666666v42.666667z m512 85.333333h-128a42.666667 42.666667 0 0 1 0-85.333333h85.333334a42.666667 42.666667 0 0 1 42.666666 42.666667v42.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="navigation-" unicode="" d="M293.845333 52.48l196.992 112.554667a42.666667 42.666667 0 0 0 42.325334 0l196.992-112.554667L512 644.565333 293.845333 52.48z m-59.306666-132.181333c-34.602667-19.754667-75.008 14.421333-61.226667 51.797333l298.666667 810.666667c13.696 37.205333 66.346667 37.205333 80.042666 0l298.666667-810.666667c13.781333-37.376-26.624-71.552-61.184-51.797333L512 78.848l-277.504-158.549333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="navigation" unicode="" d="M479.701333 340.053333a42.666667 42.666667 0 0 0 31.018667-31.018666l56.405333-225.706667 281.856 594.986667-594.986666-281.856 225.706666-56.405334z m-362.069333 2.56c-38.826667 9.728-44.117333 62.805333-7.893333 79.957334l810.666666 384c36.266667 17.152 73.984-20.608 56.832-56.832l-384-810.666667c-17.152-36.224-70.229333-30.933333-79.957333 7.893333l-79.146667 316.501334-316.501333 79.146666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="octagon" unicode="" d="M128 542.976v-317.952L353.024 0h317.952L896 225.024V542.976L670.976 768H353.024L128 542.976zM335.36 853.333333h353.28a42.666667 42.666667 0 0 0 30.165333-12.501333l250.026667-250.026667A42.666667 42.666667 0 0 0 981.333333 560.64v-353.28a42.666667 42.666667 0 0 0-12.501333-30.165333l-250.026667-250.026667a42.666667 42.666667 0 0 0-30.165333-12.501333H335.36a42.666667 42.666667 0 0 0-30.165333 12.501333l-250.026667 250.026667A42.666667 42.666667 0 0 0 42.666667 207.36V560.64a42.666667 42.666667 0 0 0 12.501333 30.165333l250.026667 250.026667A42.666667 42.666667 0 0 0 335.36 853.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="package" unicode="" d="M568.96 872.362667l341.333333-170.666667A128 128 0 0 0 981.333333 587.093333v-406.613333a128 128 0 0 0-70.954666-114.517333l-341.333334-170.666667a128 128 0 0 0-114.517333 0L113.066667 66.00533299999995A127.872 127.872 0 0 0 42.666667 180.90666699999997V587.093333a128 128 0 0 0 70.954666 114.517334L455.04 872.362667a128 128 0 0 0 113.92 0z m274.090667-232.490667l-312.106667 156.074667a42.624 42.624 0 0 1-37.845333 0L394.282667 746.624 725.333333 581.034667l117.717334 58.88zM896 570.965333l-151.125333-75.562666a42.837333 42.837333 0 0 0-0.853334-0.426667L554.666667 400.298667v-416.810667l317.653333 158.848A42.666667 42.666667 0 0 1 896 180.48000000000002V570.965333zM469.333333-16.725332999999978v417.024l-341.333333 170.666666v-390.357333c-0.128-16.213333 8.96-31.104 23.338667-38.357333L469.333333-16.725332999999978zM298.88 698.837333L180.949333 639.914667 512 474.368 629.930667 533.333333 298.88 698.88z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="pause-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM469.333333 256V512a42.666667 42.666667 0 0 1-85.333333 0v-256a42.666667 42.666667 0 0 1 85.333333 0zM640 256V512a42.666667 42.666667 0 0 1-85.333333 0v-256a42.666667 42.666667 0 0 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="pause" unicode="" d="M298.666667 682.666667v-597.333334h85.333333V682.666667H298.666667zM256 768h170.666667a42.666667 42.666667 0 0 0 42.666666-42.666667v-682.666666a42.666667 42.666667 0 0 0-42.666666-42.666667H256a42.666667 42.666667 0 0 0-42.666667 42.666667V725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667zM640 682.666667v-597.333334h85.333333V682.666667h-85.333333z m-42.666667 85.333333h170.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-682.666666a42.666667 42.666667 0 0 0-42.666667-42.666667h-170.666667a42.666667 42.666667 0 0 0-42.666666 42.666667V725.333333a42.666667 42.666667 0 0 0 42.666666 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="percent" unicode="" d="M780.501333 712.832l-597.333333-597.333333a42.666667 42.666667 0 0 1 60.330667-60.330667l597.333333 597.333333a42.666667 42.666667 0 1 1-60.330667 60.330667zM277.333333 469.333333a149.333333 149.333333 0 1 0 0 298.666667 149.333333 149.333333 0 0 0 0-298.666667z m0 85.333334a64 64 0 1 1 0 128 64 64 0 0 1 0-128zM746.666667 0a149.333333 149.333333 0 1 0 0 298.666667 149.333333 149.333333 0 0 0 0-298.666667z m0 85.333333a64 64 0 1 1 0 128 64 64 0 0 1 0-128z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-call" unicode="" d="M981.333333 3.5839999999999463a128 128 0 0 0-140.288-127.914667 886.613333 886.613333 0 0 0-386.474666 137.386667 874.24 874.24 0 0 0-268.928 268.8A887.68 887.68 0 0 0 47.957333 671.146667 128 128 0 0 0 175.36 810.666667H302.933333a128.085333 128.085333 0 0 0 128.042667-110.506667c4.992-37.76 14.250667-74.837333 27.52-110.421333a128.085333 128.085333 0 0 0-28.970667-135.253334l-30.592-30.549333a640 640 0 0 1 195.669334-195.669333l30.72 30.762666a128 128 0 0 0 134.997333 28.842667c35.669333-13.312 72.746667-22.570667 110.933333-27.605333A128 128 0 0 0 981.333333 130.986667v-127.36z m-85.333333 127.829333c0.554667 22.698667-15.232 41.344-36.266667 44.330667a590.72 590.72 0 0 0-129.365333 32.213333 42.581333 42.581333 0 0 1-44.8-9.429333l-54.186667-54.186667a42.666667 42.666667 0 0 0-51.285333-6.912 725.333333 725.333333 0 0 0-272 272 42.666667 42.666667 0 0 0 6.912 51.242667l54.016 54.016a42.666667 42.666667 0 0 1 9.557333 45.098667 589.525333 589.525333 0 0 0-32.128 128.853333A42.794667 42.794667 0 0 1 303.36 725.333333h-128a42.666667 42.666667 0 0 1-42.496-45.824 802.133333 802.133333 0 0 1 124.586667-351.573333 789.802667 789.802667 0 0 1 243.2-243.072 801.024 801.024 0 0 1 348.842666-124.288A42.666667 42.666667 0 0 1 896 3.413332999999966v128zM633.984 598.144a42.666667 42.666667 0 0 0 16.298667 83.712 256 256 0 0 0 202.24-202.24 42.666667 42.666667 0 1 0-83.712-16.298667 170.666667 170.666667 0 0 1-134.826667 134.826667z m3.413333 170.112a42.666667 42.666667 0 1 0 9.429334 84.821333 426.666667 426.666667 0 0 0 376.917333-376.405333 42.666667 42.666667 0 0 0-84.821333-9.557333 341.333333 341.333333 0 0 1-301.482667 301.141333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-forwarded" unicode="" d="M780.478569 499.510231a42.665422 42.665422 0 1 1 60.328907-60.328907l170.661689 170.661689a42.665422 42.665422 0 0 1 0 60.328907l-170.661689 170.661689a42.665422 42.665422 0 0 1-60.328907-60.328907L920.975805 640.007466l-140.497236-140.497235zM639.981334 597.342044h341.323378a42.665422 42.665422 0 0 1 0 85.330845h-341.323378a42.665422 42.665422 0 0 1 0-85.330845zM895.547213 129.174366a43.049411 43.049411 0 0 1-36.521601 44.45737 594.542659 594.542659 0 0 0-129.958876 32.34039 42.836084 42.836084 0 0 1-45.097352-9.471724l-54.441079-54.355748c-13.652935-13.652935-34.729654-16.468853-51.53983-6.954464a728.384089 728.384089 0 0 0-273.35736 272.888041 42.750753 42.750753 0 0 0 6.954464 51.454499l54.270417 54.185087c11.775657 11.903653 15.572879 29.524472 9.642385 45.225347-15.65821 41.897445-26.495227 85.37351-32.297724 129.27623-3.029245 21.332711-21.375377 37.033587-43.305404 36.820259H171.259005a42.921415 42.921415 0 0 1-42.708088-45.993325 803.859221 803.859221 0 0 1 125.180349-352.672381 793.107534 793.107534 0 0 1 244.47287-243.918219 805.949826 805.949826 0 0 1 350.581774-124.668363 42.921415 42.921415 0 0 1 46.718638 42.96408v128.422921zM981.304712 0.922106a128.33759 128.33759 0 0 0-41.684118-95.143891 129.190899 129.190899 0 0 0-99.325103-33.193699 892.091314 892.091314 0 0 0-388.468669 137.809314 877.883728 877.883728 0 0 0-270.28545 269.7308 889.574054 889.574054 0 0 0-138.363965 390.559275 128.294925 128.294925 0 0 0 33.108368 98.130471A128.764244 128.764244 0 0 0 171.21634 810.669155H299.511264a128.678914 128.678914 0 0 0 128.678914-110.844767c5.03452-37.886895 14.292916-75.091143 27.647193-110.802101 17.663485-46.931964 6.399813-99.837088-29.097818-135.676043l-30.761769-30.719104a642.669255 642.669255 0 0 1 196.687597-196.260942l30.889765 30.8471a128.764244 128.764244 0 0 0 135.633378 28.927156c35.838955-13.354277 73.128534-22.612674 111.484748-27.689859a128.508252 128.508252 0 0 0 110.63144-129.745549v-127.78294z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-missed" unicode="" d="M951.140258 840.833609l-255.992533-255.992534a42.665422 42.665422 0 1 1 60.328907-60.328907l255.992533 255.992534a42.665422 42.665422 0 1 1-60.328907 60.328907zM695.147725 780.504702l255.992533-255.992534a42.665422 42.665422 0 0 1 60.328907 60.328907l-255.992533 255.992534a42.665422 42.665422 0 0 1-60.328907-60.328907zM895.547213 129.174366a43.049411 43.049411 0 0 1-36.521601 44.45737 594.542659 594.542659 0 0 0-129.958876 32.34039 42.836084 42.836084 0 0 1-45.097352-9.471724l-54.441079-54.355748c-13.652935-13.652935-34.729654-16.468853-51.53983-6.954464a728.384089 728.384089 0 0 0-273.35736 272.888041 42.750753 42.750753 0 0 0 6.954464 51.454499l54.270417 54.185087c11.775657 11.903653 15.572879 29.524472 9.642385 45.225347-15.65821 41.897445-26.495227 85.37351-32.297724 129.27623-3.029245 21.332711-21.375377 37.033587-43.305404 36.820259H171.259005a42.921415 42.921415 0 0 1-42.708088-45.993325 803.859221 803.859221 0 0 1 125.180349-352.672381 793.107534 793.107534 0 0 1 244.47287-243.918219 805.949826 805.949826 0 0 1 350.581774-124.668363 42.921415 42.921415 0 0 1 46.718638 42.96408v128.422921zM981.304712 0.922106a128.33759 128.33759 0 0 0-41.684118-95.143891 129.190899 129.190899 0 0 0-99.325103-33.193699 892.091314 892.091314 0 0 0-388.468669 137.809314 877.883728 877.883728 0 0 0-270.28545 269.7308 889.574054 889.574054 0 0 0-138.363965 390.559275 128.294925 128.294925 0 0 0 33.108368 98.130471A128.764244 128.764244 0 0 0 171.21634 810.669155H299.511264a128.678914 128.678914 0 0 0 128.678914-110.844767c5.03452-37.886895 14.292916-75.091143 27.647193-110.802101 17.663485-46.931964 6.399813-99.837088-29.097818-135.676043l-30.761769-30.719104a642.669255 642.669255 0 0 1 196.687597-196.260942l30.889765 30.8471a128.764244 128.764244 0 0 0 135.633378 28.927156c35.838955-13.354277 73.128534-22.612674 111.484748-27.689859a128.508252 128.508252 0 0 0 110.63144-129.745549v-127.78294z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-off" unicode="" d="M485.596263 358.022711a42.644793 42.644793 0 1 1-60.299738-60.299738 724.961489 724.961489 0 0 1 154.502087-117.78492 42.644793 42.644793 0 0 1 51.216397 6.908457l54.158887 54.158888a42.559504 42.559504 0 0 0 44.819678 9.381854c41.791898-15.56535 85.119008-26.354482 129.256369-32.154174a42.559504 42.559504 0 0 0 36.290719-42.644793v-128.318184a42.730083 42.730083 0 0 0-46.482825-42.815373 800.613352 800.613352 0 0 0-348.663831 124.224284c-49.041512 31.130699-94.244994 67.378774-135.056061 108.232485a42.644793 42.644793 0 1 1-60.299738-60.299737 870.806682 870.806682 0 0 1 149.299422-119.703936 886.158808 886.158808 0 0 1 386.276539-137.316235A127.93438 127.93438 0 0 1 980.830249 47.44068V175.204481a127.806446 127.806446 0 0 1-110.023567 127.507933 505.895185 505.895185 0 0 0-110.876463 27.591181 127.93438 127.93438 0 0 1-134.885482-28.82788l-30.746896-30.746896a639.671902 639.671902 0 0 0-108.701578 87.293892z m-228.490803 14.371295a801.295669 801.295669 0 0 0-124.309573 349.431438A42.644793 42.644793 0 0 0 175.312746 767.540662H303.204481c21.834134 0.213224 40.086106-15.48006 43.071242-36.674522 5.757047-43.753558 16.54618-87.080668 32.111529-128.787276a42.644793 42.644793 0 0 0-9.552434-45.075547L314.84651 503.015009a42.644793 42.644793 0 0 1 60.299738-60.299738l54.158888 54.158887a128.01967 128.01967 0 0 1 28.998459 135.141351A504.70113 504.70113 0 0 0 430.712414 742.380234a128.01967 128.01967 0 0 1-127.934381 110.40737H175.270101a127.93438 127.93438 0 0 1-127.337353-139.448474 887.310217 887.310217 0 0 1 137.614748-387.385304 42.644793 42.644793 0 0 1 71.557964 46.397535zM950.68038 882.980118l-938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738-60.299738l938.185456 938.185456A42.644793 42.644793 0 1 1 950.68038 882.980118z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-incoming" unicode="" d="M725.312178 768.003733a42.665422 42.665422 0 0 1-85.330844 0v-255.992533a42.665422 42.665422 0 0 1 42.665422-42.665423h255.992534a42.665422 42.665422 0 0 1 0 85.330845h-213.327112V768.003733zM951.140258 840.833609l-298.657955-298.657956a42.665422 42.665422 0 1 1 60.328907-60.328907l298.657955 298.657956a42.665422 42.665422 0 1 1-60.328907 60.328907zM895.547213 129.174366a43.049411 43.049411 0 0 1-36.521601 44.45737 594.542659 594.542659 0 0 0-129.958876 32.34039 42.836084 42.836084 0 0 1-45.097352-9.471724l-54.441079-54.355748c-13.652935-13.652935-34.729654-16.468853-51.53983-6.954464a728.384089 728.384089 0 0 0-273.35736 272.888041 42.750753 42.750753 0 0 0 6.954464 51.454499l54.270417 54.185087c11.775657 11.903653 15.572879 29.524472 9.642385 45.225347-15.65821 41.897445-26.495227 85.37351-32.297724 129.27623-3.029245 21.332711-21.375377 37.033587-43.305404 36.820259H171.259005a42.921415 42.921415 0 0 1-42.708088-45.993325 803.859221 803.859221 0 0 1 125.180349-352.672381 793.107534 793.107534 0 0 1 244.47287-243.918219 805.949826 805.949826 0 0 1 350.581774-124.668363 42.921415 42.921415 0 0 1 46.718638 42.96408v128.422921zM981.304712 0.922106a128.33759 128.33759 0 0 0-41.684118-95.143891 129.190899 129.190899 0 0 0-99.325103-33.193699 892.091314 892.091314 0 0 0-388.468669 137.809314 877.883728 877.883728 0 0 0-270.28545 269.7308 889.574054 889.574054 0 0 0-138.363965 390.559275 128.294925 128.294925 0 0 0 33.108368 98.130471A128.764244 128.764244 0 0 0 171.21634 810.669155H299.511264a128.678914 128.678914 0 0 0 128.678914-110.844767c5.03452-37.886895 14.292916-75.091143 27.647193-110.802101 17.663485-46.931964 6.399813-99.837088-29.097818-135.676043l-30.761769-30.719104a642.669255 642.669255 0 0 1 196.687597-196.260942l30.889765 30.8471a128.764244 128.764244 0 0 0 135.633378 28.927156c35.838955-13.354277 73.128534-22.612674 111.484748-27.689859a128.508252 128.508252 0 0 0 110.63144-129.745549v-127.78294z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone" unicode="" d="M895.573333 171.818667a43.050667 43.050667 0 0 1-36.522666 44.458666 594.56 594.56 0 0 0-129.962667 32.341334 42.837333 42.837333 0 0 1-45.098667-9.472l-54.442666-54.357334c-13.653333-13.653333-34.730667-16.469333-51.541334-6.954666a728.405333 728.405333 0 0 0-273.365333 272.896 42.752 42.752 0 0 0 6.954667 51.456l54.272 54.186666c11.776 11.904 15.573333 29.525333 9.642666 45.226667-15.658667 41.898667-26.496 85.376-32.298666 129.28-3.029333 21.333333-21.376 37.034667-43.306667 36.821333H171.264a42.922667 42.922667 0 0 1-42.709333-45.994666 803.882667 803.882667 0 0 1 125.184-352.682667 793.130667 793.130667 0 0 1 244.48-243.925333 805.973333 805.973333 0 0 1 350.592-124.672 42.922667 42.922667 0 0 1 46.72 42.965333v128.426667zM981.333333 43.562667a128.341333 128.341333 0 0 0-41.685333-95.146667 129.194667 129.194667 0 0 0-99.328-33.194667 892.117333 892.117333 0 0 0-388.48 137.813334 877.909333 877.909333 0 0 0-270.293333 269.738666 889.6 889.6 0 0 0-138.368 390.570667 128.298667 128.298667 0 0 0 33.109333 98.133333A128.768 128.768 0 0 0 171.221333 853.333333H299.52a128.682667 128.682667 0 0 0 128.682667-110.848c5.034667-37.888 14.293333-75.093333 27.648-110.805333 17.664-46.933333 6.4-99.84-29.098667-135.68l-30.762667-30.72a642.688 642.688 0 0 1 196.693334-196.266667l30.890666 30.848a128.768 128.768 0 0 0 135.637334 28.928c35.84-13.354667 73.130667-22.613333 111.488-27.690666a128.512 128.512 0 0 0 110.634666-129.749334v-127.786666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="phone-outgoing" unicode="" d="M938.63929 768.003733v-213.327111a42.665422 42.665422 0 0 1 85.330844 0V810.669155a42.665422 42.665422 0 0 1-42.665422 42.665423h-255.992534a42.665422 42.665422 0 0 1 0-85.330845h213.327112zM712.81121 481.846746l298.657955 298.657956a42.665422 42.665422 0 1 1-60.328907 60.328907l-298.657955-298.657956a42.665422 42.665422 0 1 1 60.328907-60.328907zM895.973867 131.435633c0.55465 22.698005-15.231556 41.342794-36.265608 44.329374a590.702771 590.702771 0 0 0-129.361561 32.212394 42.580091 42.580091 0 0 1-44.798693-9.429059l-54.185086-54.185086a42.665422 42.665422 0 0 0-51.283838-6.911798 725.312178 725.312178 0 0 0-271.992067 271.992067 42.665422 42.665422 0 0 0 6.911799 51.241172l54.014424 54.014424a42.665422 42.665422 0 0 1 9.557055 45.097352 589.508139 589.508139 0 0 0-32.127063 128.849575A42.793419 42.793419 0 0 1 303.351152 725.338311h-127.996267a42.665422 42.665422 0 0 1-42.49476-45.822664 802.109938 802.109938 0 0 1 124.583033-351.563079 789.779631 789.779631 0 0 1 243.192907-243.064911 801.000637 801.000637 0 0 1 348.832492-124.284375A42.665422 42.665422 0 0 1 895.973867 3.439366v127.996267z m85.330845-130.513527a128.33759 128.33759 0 0 0-41.684118-95.143891 129.190899 129.190899 0 0 0-99.325103-33.193699 892.091314 892.091314 0 0 0-388.468669 137.809314 877.883728 877.883728 0 0 0-270.28545 269.7308 889.574054 889.574054 0 0 0-138.363965 390.559275 128.294925 128.294925 0 0 0 33.108368 98.130471A128.764244 128.764244 0 0 0 171.21634 810.669155H299.511264a128.678914 128.678914 0 0 0 128.678914-110.844767c5.03452-37.886895 14.292916-75.091143 27.647193-110.802101 17.663485-46.931964 6.399813-99.837088-29.097818-135.676043l-30.761769-30.719104a642.669255 642.669255 0 0 1 196.687597-196.260942l30.889765 30.8471a128.764244 128.764244 0 0 0 135.633378 28.927156c35.838955-13.354277 73.128534-22.612674 111.484748-27.689859a128.508252 128.508252 0 0 0 110.63144-129.745549v-127.78294z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="pie-chart" unicode="" d="M865.664 234.666667A384 384 0 1 0 358.4 736.085333a42.666667 42.666667 0 0 1-34.133333 78.250667A469.333333 469.333333 0 1 1 944.256 201.386667a42.666667 42.666667 0 0 1-78.592 33.194666zM981.333333 384a42.666667 42.666667 0 0 0-42.666666-42.666667H512a42.666667 42.666667 0 0 0-42.666667 42.666667V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666 469.333333 469.333333 0 0 0 469.333333-469.333333z m-197.802666 271.530667A384 384 0 0 1 554.666667 765.610667V426.666667h338.944a384 384 0 0 1-110.08 228.864z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="play-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM469.333333 293.077333L605.738667 384 469.333333 474.922667v-181.845334z m-18.986666 297.088l256-170.666666a42.666667 42.666667 0 0 0 0-70.997334l-256-170.666666A42.666667 42.666667 0 0 0 384 213.333333V554.666667a42.666667 42.666667 0 0 0 66.346667 35.498666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="play" unicode="" d="M256 78.165333L731.733333 384 256 689.834667v-611.669334z m-19.626667 725.76l597.333334-384a42.666667 42.666667 0 0 0 0-71.808l-597.333334-384A42.666667 42.666667 0 0 0 170.666667 0V768a42.666667 42.666667 0 0 0 65.706666 35.882667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="plus-square" unicode="" d="M170.666667 682.88v-597.76c0-23.466667 18.986667-42.453333 42.453333-42.453333h597.76c23.466667 0 42.453333 18.986667 42.453333 42.453333V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88z m-85.333334 0A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.666667H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88zM469.333333 554.666667v-341.333334a42.666667 42.666667 0 0 1 85.333334 0V554.666667a42.666667 42.666667 0 0 1-85.333334 0zM341.333333 341.333333h341.333334a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="plus-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM469.333333 554.666667v-341.333334a42.666667 42.666667 0 0 1 85.333334 0V554.666667a42.666667 42.666667 0 0 1-85.333334 0zM341.333333 341.333333h341.333334a42.666667 42.666667 0 0 1 0 85.333334H341.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="plus" unicode="" d="M469.333333 682.666667v-597.333334a42.666667 42.666667 0 0 1 85.333334 0V682.666667a42.666667 42.666667 0 0 1-85.333334 0zM213.333333 341.333333h597.333334a42.666667 42.666667 0 0 1 0 85.333334H213.333333a42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="pocket" unicode="" d="M128 682.666667v-256a384 384 0 1 1 768 0V682.666667a42.666667 42.666667 0 0 1-42.666667 42.666666H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666666z m725.333333 128a128 128 0 0 0 128-128v-256c0-259.2-210.133333-469.333333-469.333333-469.333334A469.333333 469.333333 0 0 0 42.666667 426.666667V682.666667a128 128 0 0 0 128 128h682.666666zM371.498667 499.498667a42.666667 42.666667 0 1 1-60.330667-60.330667l170.666667-170.666667a42.666667 42.666667 0 0 1 60.330666 0l170.666667 170.666667a42.666667 42.666667 0 1 1-60.330667 60.330667L512 358.997333 371.498667 499.498667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="printer" unicode="" d="M298.666667 554.666667h426.666666V768H298.666667v-213.333333zM213.333333 554.666667V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666h512a42.666667 42.666667 0 0 0 42.666667-42.666666v-256h42.666666a128 128 0 0 0 128-128v-213.333334a128 128 0 0 0-128-128h-85.333333a42.666667 42.666667 0 0 0 0 85.333334h85.333333a42.666667 42.666667 0 0 1 42.666667 42.666666v213.333334a42.666667 42.666667 0 0 1-42.666667 42.666666H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666666v-213.333334a42.666667 42.666667 0 0 1 42.666667-42.666666h85.333333a42.666667 42.666667 0 0 0 0-85.333334H170.666667a128 128 0 0 0-128 128v213.333334a128 128 0 0 0 128 128h42.666666zM298.666667 0h426.666666v256H298.666667v-256z m-42.666667 341.333333h512a42.666667 42.666667 0 0 0 42.666667-42.666666v-341.333334a42.666667 42.666667 0 0 0-42.666667-42.666666H256a42.666667 42.666667 0 0 0-42.666667 42.666666v341.333334a42.666667 42.666667 0 0 0 42.666667 42.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="power" unicode="" d="M753.408 582.528a341.333333 341.333333 0 1 0-482.816 0 42.666667 42.666667 0 1 1-60.330667 60.330667c-166.613333-166.656-166.570667-436.778667 0.042667-603.349334 166.613333-166.613333 436.778667-166.613333 603.392 0 166.613333 166.570667 166.656 436.693333 0.042667 603.349334a42.666667 42.666667 0 1 1-60.330667-60.330667zM469.333333 810.666667v-426.666667a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="radio" unicode="" d="M512 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM662.741333 534.741333a213.333333 213.333333 0 0 0 0-301.866666 42.666667 42.666667 0 1 1 60.330667-60.373334 298.666667 298.666667 0 0 1 0 422.613334 42.666667 42.666667 0 1 1-60.330667-60.373334z m-301.482666-301.482666a213.333333 213.333333 0 0 0 0 301.866666 42.666667 42.666667 0 0 1-60.330667 60.373334 298.666667 298.666667 0 0 1 0-422.613334 42.666667 42.666667 0 1 1 60.330667 60.373334z m422.229333 422.229333a384 384 0 0 0 0-542.976 42.666667 42.666667 0 1 1 60.330667-60.330667c183.210667 183.253333 183.210667 480.384 0 663.637334a42.666667 42.666667 0 1 1-60.330667-60.330667zM240.512 112.512a384 384 0 0 0 0 542.976 42.666667 42.666667 0 0 1-60.330667 60.330667c-183.210667-183.253333-183.210667-480.384 0-663.637334a42.666667 42.666667 0 1 1 60.330667 60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="repeat" unicode="" d="M695.074022 542.346875a42.644776 42.644776 0 1 1 60.299713-60.299713l170.579103 170.579103a42.644776 42.644776 0 0 1 0 60.299713l-170.579103 170.579103a42.644776 42.644776 0 0 1-60.299713-60.299713L835.503268 682.776122l-140.429246-140.429247zM170.841795 426.907468V512.197019a127.934327 127.934327 0 0 0 127.934327 127.934327h597.026859a42.644776 42.644776 0 0 1 0 85.289551H298.776122a213.223878 213.223878 0 0 1-213.223879-213.223878v-85.289551a42.644776 42.644776 0 0 1 85.289552 0zM328.925978 226.178508a42.644776 42.644776 0 0 1-60.299713 60.299713l-170.579103-170.579103a42.644776 42.644776 0 0 1 0-60.299712l170.579103-170.579103a42.644776 42.644776 0 1 1 60.299713 60.299713L188.496732 85.749262l140.429246 140.429246zM853.158205 341.617916v-85.289551a127.934327 127.934327 0 0 0-127.934327-127.934327H128.197019a42.644776 42.644776 0 0 1 0-85.289552h597.026859a213.223878 213.223878 0 0 1 213.223879 213.223879v85.289551a42.644776 42.644776 0 0 1-85.289552 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="refresh-ccw" unicode="" d="M85.378987 725.333333a42.666667 42.666667 0 1 1-85.333334 0v-256a42.666667 42.666667 0 0 1 42.666667-42.666666h256a42.666667 42.666667 0 0 1 0 85.333333H85.378987V725.333333zM938.71232 42.666667a42.666667 42.666667 0 0 1 85.333333 0v256a42.666667 42.666667 0 0 1-42.666666 42.666666h-256a42.666667 42.666667 0 0 1 0-85.333333h213.333333v-213.333333zM914.52032 526.250667a426.666667 426.666667 0 0 1-704 159.274666L13.485653 500.437333a42.666667 42.666667 0 0 1 58.453334-62.208l197.973333 186.026667a341.973333 341.973333 0 0 0 317.696 92.586667 341.333333 341.333333 0 0 0 246.442667-219.093334 42.666667 42.666667 0 0 1 80.469333 28.501334z m37.632-196.48l-197.973333-186.026667a341.973333 341.973333 0 0 0-317.696-92.586667 341.333333 341.333333 0 0 0-246.442667 219.093334 42.666667 42.666667 0 0 1-80.469333-28.501334 426.666667 426.666667 0 0 1 704-159.274666l197.034666 185.088a42.666667 42.666667 0 0 1-58.453333 62.208z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="rewind" unicode="" d="M426.666667 595.413333L154.837333 384 426.666667 172.586667v422.826666z m16.469333-543.744l-384 298.666667a42.666667 42.666667 0 0 0 0 67.328l384 298.666667A42.666667 42.666667 0 0 0 512 682.666667v-597.333334a42.666667 42.666667 0 0 0-68.864-33.706666zM896 172.586667v422.826666L624.170667 384 896 172.586667z m16.469333-120.917334l-384 298.666667a42.666667 42.666667 0 0 0 0 67.328l384 298.666667A42.666667 42.666667 0 0 0 981.333333 682.666667v-597.333334a42.666667 42.666667 0 0 0-68.864-33.706666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="rotate-ccw" unicode="" d="M128 725.333333a42.666667 42.666667 0 1 1-85.333333 0v-256a42.666667 42.666667 0 0 1 42.666666-42.666666h256a42.666667 42.666667 0 0 1 0 85.333333H128V725.333333zM152.192 242.218667a426.666667 426.666667 0 1 1 100.949333 443.733333l-197.034666-185.173333a42.666667 42.666667 0 1 1 58.453333-62.165334l197.973333 186.026667a341.76 341.76 0 0 0 407.210667 58.069333 341.333333 341.333333 0 1 0-487.082667-412.16 42.666667 42.666667 0 1 1-80.469333-28.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="refresh-cw" unicode="" d="M725.378987 512a42.666667 42.666667 0 0 1 0-85.333333h256a42.666667 42.666667 0 0 1 42.666666 42.666666V725.333333a42.666667 42.666667 0 0 1-85.333333 0v-213.333333h-213.333333zM85.378987 256h213.333333a42.666667 42.666667 0 0 1 0 85.333333H42.71232a42.666667 42.666667 0 0 1-42.666667-42.666666v-256a42.666667 42.666667 0 0 1 85.333334 0v213.333333zM190.04032 497.749333a341.333333 341.333333 0 0 0 563.2 127.445334l198.912-186.965334a42.666667 42.666667 0 0 1 58.453333 62.208l-197.973333 186.026667A426.026667 426.026667 0 0 1 417.666987 800a426.666667 426.666667 0 0 1-308.053334-273.792 42.666667 42.666667 0 0 1 80.426667-28.501333zM13.485653 267.562667l197.973334-186.026667a426.026667 426.026667 0 0 1 394.965333-113.578667 426.666667 426.666667 0 0 1 308.053333 273.792 42.666667 42.666667 0 0 1-80.426666 28.501334 341.333333 341.333333 0 0 0-563.2-127.445334l-198.912 186.965334a42.666667 42.666667 0 0 1-58.453334-62.208z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="rotate-cw" unicode="" d="M682.666667 512a42.666667 42.666667 0 0 1 0-85.333333h256a42.666667 42.666667 0 0 1 42.666666 42.666666V725.333333a42.666667 42.666667 0 0 1-85.333333 0v-213.333333h-213.333333zM791.338667 270.208a341.333333 341.333333 0 1 0-80.384 354.986667l198.442666-186.922667a42.666667 42.666667 0 0 1 58.538667 62.122667l-197.546667 186.026666c-133.418667 133.546667-340.48 162.901333-506.88 71.125334a426.666667 426.666667 0 1 1 608.298667-515.754667 42.666667 42.666667 0 1 1-80.469333 28.416z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="save" unicode="" d="M341.333333 725.333333v-128h298.666667a42.666667 42.666667 0 0 0 0-85.333333H298.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V725.333333H213.333333a42.666667 42.666667 0 0 1-42.666666-42.666666v-597.333334a42.666667 42.666667 0 0 1 42.666666-42.666666h42.666667v298.666666a42.666667 42.666667 0 0 0 42.666667 42.666667h426.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-298.666666h42.666667a42.666667 42.666667 0 0 1 42.666666 42.666666V537.002667L665.002667 725.333333H341.333333z m341.333334-682.666666v256H341.333333v-256h341.333334z m128-85.333334H213.333333a128 128 0 0 0-128 128V682.666667a128 128 0 0 0 128 128h469.333334a42.666667 42.666667 0 0 0 30.165333-12.501334l213.333333-213.333333A42.666667 42.666667 0 0 0 938.666667 554.666667v-469.333334a128 128 0 0 0-128-128z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="search" unicode="" d="M732.842667 223.48800000000006l193.322666-193.28a42.666667 42.666667 0 0 0-60.330666-60.373333l-193.322667 193.322666a362.666667 362.666667 0 1 0 60.330667 60.330667zM448 170.66666699999996a277.333333 277.333333 0 1 1 0 554.666666 277.333333 277.333333 0 0 1 0-554.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="server" unicode="" d="M128 725.418667v-170.837334A42.24 42.24 0 0 1 170.24 512h683.52c23.125333 0 42.24 19.157333 42.24 42.581333V725.418667A42.24 42.24 0 0 1 853.76 768H170.24a42.624 42.624 0 0 1-42.24-42.581333z m-85.333333 0A127.957333 127.957333 0 0 0 170.24 853.333333h683.52A127.573333 127.573333 0 0 0 981.333333 725.418667v-170.837334A127.957333 127.957333 0 0 0 853.76 426.666667H170.24A127.573333 127.573333 0 0 0 42.666667 554.581333V725.418667zM128 213.418667v-170.837334a42.24 42.24 0 0 1 42.24-42.581333h683.52c23.125333 0 42.24 19.157333 42.24 42.581333v170.837334a42.24 42.24 0 0 1-42.24 42.581333H170.24a42.624 42.624 0 0 1-42.24-42.581333z m-85.333333 0A127.957333 127.957333 0 0 0 170.24 341.333333h683.52A127.573333 127.573333 0 0 0 981.333333 213.418667v-170.837334A127.957333 127.957333 0 0 0 853.76-85.333333H170.24A127.573333 127.573333 0 0 0 42.666667 42.581333v170.837334zM298.666667 128m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0ZM298.666667 640m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="scissors" unicode="" d="M298.666667 469.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666zM298.666667-42.66666699999996a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666zM494.336 384L358.954667 519.381333a42.666667 42.666667 0 0 0 60.330666 60.330667L554.666667 444.330667l311.168 311.168a42.666667 42.666667 0 0 0 60.330666-60.330667l-506.88-506.88a42.666667 42.666667 0 1 0-60.330666 60.330667L494.336 384z m135.594667-136.021333a42.666667 42.666667 0 0 0 60.245333 60.416l235.946667-235.52a42.666667 42.666667 0 0 0-60.245334-60.416l-235.946666 235.52z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="share-" unicode="" d="M768 512a170.666667 170.666667 0 1 0 0 341.333333 170.666667 170.666667 0 0 0 0-341.333333z m0 85.333333a85.333333 85.333333 0 1 1 0 170.666667 85.333333 85.333333 0 0 1 0-170.666667zM256 213.33333300000004a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666zM768-85.33333300000004a170.666667 170.666667 0 1 0 0 341.333333 170.666667 170.666667 0 0 0 0-341.333333z m0 85.333333a85.333333 85.333333 0 1 1 0 170.666667 85.333333 85.333333 0 0 1 0-170.666667zM345.045333 282.709333a42.666667 42.666667 0 1 0 42.922667 73.728l291.413333-169.813333a42.666667 42.666667 0 1 0-42.922666-73.728l-291.413334 169.813333z m290.944 372.394667a42.666667 42.666667 0 1 0 43.008-73.728l-290.986666-169.813333a42.666667 42.666667 0 1 0-43.008 73.728l290.986666 169.813333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="share" unicode="" d="M128 384v-341.333333a128 128 0 0 1 128-128h512a128 128 0 0 1 128 128v341.333333a42.666667 42.666667 0 0 1-85.333333 0v-341.333333a42.666667 42.666667 0 0 0-42.666667-42.666667H256a42.666667 42.666667 0 0 0-42.666667 42.666667v341.333333a42.666667 42.666667 0 0 1-85.333333 0zM652.501333 609.834667a42.666667 42.666667 0 0 1 60.330667 60.330666l-170.666667 170.666667a42.666667 42.666667 0 0 1-60.330666 0l-170.666667-170.666667a42.666667 42.666667 0 0 1 60.330667-60.330666L512 750.336l140.501333-140.501333zM469.333333 810.666667v-554.666667a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="shield" unicode="" d="M531.072-80.810667a42.666667 42.666667 0 0 0-38.144 0 738.56 738.56 0 0 0-56.874667 32.64 958.506667 958.506667 0 0 0-122.837333 90.709334C198.229333 143.232 128 257.322667 128 384V725.333333a42.666667 42.666667 0 0 0 32.298667 41.386667l341.333333 85.333333a42.666667 42.666667 0 0 0 20.736 0l341.333333-85.333333A42.666667 42.666667 0 0 0 896 725.333333v-341.333333c0-126.72-70.229333-240.810667-185.216-341.461333a958.506667 958.506667 0 0 0-122.837333-90.709334 738.56 738.56 0 0 0-56.874667-32.64z m11.648 104.96a874.24 874.24 0 0 1 111.829333 82.645334C752.896 192.810667 810.666667 286.677333 810.666667 384V692.053333l-298.666667 74.666667-298.666667-74.666667V384c0-97.28 57.770667-191.189333 156.117334-277.205333A874.24 874.24 0 0 1 512 5.845333c9.429333 5.376 19.754667 11.477333 30.72 18.346667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="settings" unicode="" d="M512 213.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666zM272.768 680.234667l-2.56 2.56a42.666667 42.666667 0 1 1-60.416-60.416l2.901333-2.901334c32.298667-33.024 41.258667-82.432 23.637334-122.197333A113.365333 113.365333 0 0 0 131.84 423.253333H128a42.666667 42.666667 0 0 1 0-85.333333h7.424a112.896 112.896 0 0 0 103.253333-68.096 113.237333 113.237333 0 0 0-22.912-125.056l-2.56-2.56a42.666667 42.666667 0 1 1 60.416-60.416l2.901334 2.901333c33.024 32.298667 82.432 41.258667 122.197333 23.637334a113.365333 113.365333 0 0 0 74.026667-104.490667V0a42.666667 42.666667 0 0 1 85.333333 0v7.424a112.896 112.896 0 0 0 68.096 103.253333 113.237333 113.237333 0 0 0 125.056-22.912l2.56-2.56a42.666667 42.666667 0 1 1 60.416 60.416l-2.901333 2.901334a113.066667 113.066667 0 0 0-22.613334 124.714666A112.981333 112.981333 0 0 0 892.16 341.333333H896a42.666667 42.666667 0 0 1 0 85.333334h-7.424a113.066667 113.066667 0 0 0-103.466667 68.522666l-3.456 8.064v4.266667a113.28 113.28 0 0 0 26.581334 115.712l2.56 2.56a42.666667 42.666667 0 1 1-60.416 60.416l-2.901334-2.901333a112.896 112.896 0 0 0-124.288-22.784A113.109333 113.109333 0 0 0 554.666667 764.16V768a42.666667 42.666667 0 0 1-85.333334 0v-7.424a113.066667 113.066667 0 0 0-68.522666-103.466667l-8.064-3.456h-4.266667a113.28 113.28 0 0 0-115.712 26.581334zM888.746667 512H896a128 128 0 0 0 0-256h-3.669333a27.733333 27.733333 0 0 1-25.386667-16.810667 27.733333 27.733333 0 0 1 5.034667-30.677333l2.56-2.56a128 128 0 1 0-181.077334-181.077333l-2.218666 2.218666a27.904 27.904 0 0 1-31.018667 5.376 27.690667 27.690667 0 0 1-16.810667-25.216V0a128 128 0 0 0-256 0 29.781333 29.781333 0 0 1-20.650666 29.226667 27.605333 27.605333 0 0 1-30.250667-5.205334l-2.56-2.56a128 128 0 1 0-181.077333 181.077334l2.218666 2.218666a27.904 27.904 0 0 1 5.376 31.018667 27.690667 27.690667 0 0 1-25.216 16.810667H128a128 128 0 0 0 0 256 29.781333 29.781333 0 0 1 29.226667 20.650666 27.605333 27.605333 0 0 1-5.205334 30.250667l-2.56 2.56a128 128 0 1 0 181.077334 181.077333l2.218666-2.218666a27.733333 27.733333 0 0 1 30.592-5.546667l8.234667 3.626667A25.258667 25.258667 0 0 1 384 760.746667V768a128 128 0 0 0 256 0v-3.669333c0.042667-11.093333 6.656-21.034667 17.237333-25.6a27.605333 27.605333 0 0 1 30.250667 5.248l2.56 2.56a128 128 0 1 0 181.077333-181.077334l-2.218666-2.218666a27.733333 27.733333 0 0 1-5.546667-30.592l3.626667-8.234667c5.12-8.277333 13.184-12.373333 21.76-12.416z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="skip-back" unicode="" d="M768 636.586667L452.266667 384l315.733333-252.586667V636.586667z m16-627.2l-426.666667 341.333333a42.666667 42.666667 0 0 0 0 66.56l426.666667 341.333333A42.666667 42.666667 0 0 0 853.333333 725.333333v-682.666666a42.666667 42.666667 0 0 0-69.333333-33.28zM256 85.333333V682.666667a42.666667 42.666667 0 1 1-85.333333 0v-597.333334a42.666667 42.666667 0 0 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="shuffle" unicode="" d="M853.333333 554.666667a42.666667 42.666667 0 0 1 85.333334 0V768a42.666667 42.666667 0 0 1-42.666667 42.666667h-213.333333a42.666667 42.666667 0 0 1 0-85.333334h170.666666v-170.666666zM200.832 12.501333l725.333333 725.333334a42.666667 42.666667 0 1 1-60.330666 60.330666l-725.333334-725.333333a42.666667 42.666667 0 0 1 60.330667-60.330667zM682.666667 42.666667a42.666667 42.666667 0 0 1 0-85.333334h213.333333a42.666667 42.666667 0 0 1 42.666667 42.666667v213.333333a42.666667 42.666667 0 0 1-85.333334 0v-170.666666h-170.666666zM609.834667 225.834667l256-256a42.666667 42.666667 0 0 1 60.330666 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666-60.330666zM140.501333 695.168l213.333334-213.333333a42.666667 42.666667 0 1 1 60.330666 60.330666l-213.333333 213.333334a42.666667 42.666667 0 0 1-60.330667-60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="sidebar" unicode="" d="M85.333333 682.88A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.66666699999996H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88zM341.333333 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88v-597.76c0-23.466667 18.986667-42.453333 42.453333-42.453333H341.333333V725.333333z m85.333334 0v-682.666666h384.213333c23.466667 0 42.453333 18.986667 42.453333 42.453333V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H426.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="skip-forward" unicode="" d="M256 131.413333L571.733333 384 256 636.586667v-505.173334z m-16 627.2l426.666667-341.333333a42.666667 42.666667 0 0 0 0-66.56l-426.666667-341.333333A42.666667 42.666667 0 0 0 170.666667 42.666667V725.333333a42.666667 42.666667 0 0 0 69.333333 33.28zM768 682.666667v-597.333334a42.666667 42.666667 0 0 1 85.333333 0V682.666667a42.666667 42.666667 0 0 1-85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="slack" unicode="" d="M983.271856 524.026176c103.487213-343.691869 14.332936-507.582171-330.937263-611.495959C308.514752-190.956996 144.539135-101.845377 40.540031 243.339506-62.947182 587.074033 26.207095 750.964335 371.477293 854.920781 716.320916 958.706596 879.614012 870.405471 983.271856 524.026176z m-81.817179-24.52809C811.361935 800.788351 695.632005 863.324318 396.090699 773.14626 95.781557 682.754914 32.477754 566.342464 122.357209 267.952912c90.434003-300.223827 206.889111-363.527629 505.363979-273.690832 300.309142 90.434003 363.612944 206.803796 273.733489 505.236006zM495.866705 626.61758l152.714024-443.638505a42.657549 42.657549 0 0 1 80.708082 27.727406l-152.714024 443.638506a42.657549 42.657549 0 0 1-80.708082-27.727407zM294.523076 557.512352l152.714024-443.638506a42.657549 42.657549 0 0 1 80.708082 27.727407l-152.714024 443.638505a42.657549 42.657549 0 0 1-80.708082-27.727406zM685.308879 601.492284l-443.638506-152.714024a42.657549 42.657549 0 0 1 27.727407-80.708082l443.638505 152.714024a42.657549 42.657549 0 0 1-27.727406 80.708082zM754.414107 400.148655l-443.638505-152.714024a42.657549 42.657549 0 0 1 27.727406-80.708082l443.638506 152.714024a42.657549 42.657549 0 0 1-27.727407 80.708082z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="slash" unicode="" d="M512-85.33333300000004C252.8-85.33333300000004 42.666667 124.79999999999995 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m239.701333 169.301333L211.968 623.701333a384 384 0 0 1 539.733333-539.733333zM272.298667 684.032l539.733333-539.733333a384 384 0 0 1-539.733333 539.733333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="smartphone" unicode="" d="M256 725.76v-683.52c0-23.04 19.285333-42.24 42.922667-42.24h426.154666A42.581333 42.581333 0 0 1 768 42.24V725.76c0 23.04-19.285333 42.24-42.922667 42.24H298.922667A42.581333 42.581333 0 0 1 256 725.76z m-85.333333 0A127.914667 127.914667 0 0 0 298.922667 853.333333h426.154666A128.128 128.128 0 0 0 853.333333 725.76v-683.52A127.914667 127.914667 0 0 0 725.077333-85.333333H298.922667A128.128 128.128 0 0 0 170.666667 42.24V725.76zM512 128m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="square" unicode="" d="M170.666667 682.88v-597.76c0-23.466667 18.986667-42.453333 42.453333-42.453333h597.76c23.466667 0 42.453333 18.986667 42.453333 42.453333V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88z m-85.333334 0A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.666667H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="speaker" unicode="" d="M213.333333 725.76v-683.52c0-23.125333 19.114667-42.24 42.410667-42.24h512.512a42.410667 42.410667 0 0 1 42.410667 42.24V725.76a42.538667 42.538667 0 0 1-42.410667 42.24H255.744A42.410667 42.410667 0 0 1 213.333333 725.76z m-85.333333 0A127.744 127.744 0 0 0 255.744 853.333333h512.512A127.872 127.872 0 0 0 896 725.76v-683.52A127.744 127.744 0 0 0 768.256-85.333333H255.744A127.872 127.872 0 0 0 128 42.24V725.76zM512 640m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0ZM512 85.333333a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333334a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="star" unicode="" d="M418.432 525.056a42.666667 42.666667 0 0 0-32.085333-23.338667l-209.365334-30.549333 151.466667-147.370667c10.069333-9.770667 14.634667-23.893333 12.288-37.717333l-35.754667-208.085333 187.178667 98.304a42.666667 42.666667 0 0 0 39.68 0l187.178667-98.304-35.754667 208.085333a42.581333 42.581333 0 0 0 12.288 37.717333l151.466667 147.370667-209.365334 30.549333a42.666667 42.666667 0 0 0-32.085333 23.338667L512 714.410667 418.432 525.056z m55.296 304.512a42.666667 42.666667 0 0 0 76.544 0l121.898667-246.741333 272.64-39.808a42.624 42.624 0 0 0 23.637333-72.704l-197.248-191.914667 46.506667-271.146667a42.666667 42.666667 0 0 0-61.866667-44.928L512 90.410667l-243.84-128.085334a42.666667 42.666667 0 0 0-61.866667 44.928l46.506667 271.146667-197.248 191.914667a42.624 42.624 0 0 0 23.594667 72.704l272.64 39.808 121.941333 246.741333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="stop-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM426.666667 469.333333v-170.666666h170.666666v170.666666h-170.666666zM384 554.666667h256a42.666667 42.666667 0 0 0 42.666667-42.666667v-256a42.666667 42.666667 0 0 0-42.666667-42.666667H384a42.666667 42.666667 0 0 0-42.666667 42.666667V512a42.666667 42.666667 0 0 0 42.666667 42.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="sun" unicode="" d="M512 128a256 256 0 1 0 0 512 256 256 0 0 0 0-512z m0 85.333333a170.666667 170.666667 0 1 1 0 341.333334 170.666667 170.666667 0 0 1 0-341.333334zM469.333333 853.333333v-85.333333a42.666667 42.666667 0 0 1 85.333334 0V853.333333a42.666667 42.666667 0 0 1-85.333334 0zM469.333333 0v-85.333333a42.666667 42.666667 0 0 1 85.333334 0v85.333333a42.666667 42.666667 0 0 1-85.333334 0zM149.888 685.781333l60.586667-60.586666a42.666667 42.666667 0 0 1 60.330666 60.330666l-60.586666 60.586667a42.666667 42.666667 0 0 1-60.330667-60.330667zM753.194667 82.474667l60.586666-60.586667a42.666667 42.666667 0 0 1 60.330667 60.330667l-60.586667 60.586666a42.666667 42.666667 0 0 1-60.330666-60.330666zM42.666667 341.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334H42.666667a42.666667 42.666667 0 0 1 0-85.333334zM896 341.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334h-85.333333a42.666667 42.666667 0 0 1 0-85.333334zM210.218667 21.888l60.586666 60.586667a42.666667 42.666667 0 0 1-60.330666 60.330666l-60.586667-60.586666a42.666667 42.666667 0 0 1 60.330667-60.330667zM813.525333 625.194667l60.586667 60.586666a42.666667 42.666667 0 1 1-60.330667 60.330667l-60.586666-60.586667a42.666667 42.666667 0 1 1 60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="sunrise" unicode="" d="M768 128a256 256 0 1 1-512 0 42.666667 42.666667 0 0 1 85.333333 0 170.666667 170.666667 0 1 0 341.333334 0 42.666667 42.666667 0 0 1 85.333333 0zM469.333333 810.666667v-298.666667a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0zM149.888 429.781333l60.586667-60.586666a42.666667 42.666667 0 1 1 60.330666 60.330666l-60.586666 60.586667a42.666667 42.666667 0 1 1-60.330667-60.330667zM42.666667 85.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334H42.666667a42.666667 42.666667 0 0 1 0-85.333334zM896 85.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334h-85.333333a42.666667 42.666667 0 0 1 0-85.333334zM813.525333 369.194667l60.586667 60.586666a42.666667 42.666667 0 1 1-60.330667 60.330667l-60.586666-60.586667a42.666667 42.666667 0 0 1 60.330666-60.330666zM981.333333 0H42.666667a42.666667 42.666667 0 0 1 0-85.333333h938.666666a42.666667 42.666667 0 0 1 0 85.333333zM652.501333 609.834667a42.666667 42.666667 0 0 1 60.330667 60.330666l-170.666667 170.666667a42.666667 42.666667 0 0 1-60.330666 0l-170.666667-170.666667a42.666667 42.666667 0 0 1 60.330667-60.330666L512 750.336l140.501333-140.501333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="tablet" unicode="" d="M810.666667 42.24V725.76a42.538667 42.538667 0 0 1-42.410667 42.24H255.744A42.410667 42.410667 0 0 1 213.333333 725.76v-683.52c0-23.125333 19.114667-42.24 42.410667-42.24h512.512a42.410667 42.410667 0 0 1 42.410667 42.24z m85.333333 0A127.744 127.744 0 0 0 768.256-85.333333H255.744A127.872 127.872 0 0 0 128 42.24V725.76A127.744 127.744 0 0 0 255.744 853.333333h512.512A127.872 127.872 0 0 0 896 725.76v-683.52zM512 128m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="tag" unicode="" d="M841.813333 417.536l-351.146666 351.146667H127.317333V405.333333l351.146667-350.72a42.325333 42.325333 0 0 1 59.904 0l303.36 303.36a42.282667 42.282667 0 0 1 0.085333 59.562667z m59.946667-119.253333l-303.530667-303.488a126.976 126.976 0 0 0-179.626666-0.042667l-363.52 363.093333A42.325333 42.325333 0 0 0 42.666667 387.84V811.093333C42.666667 834.346667 61.610667 853.333333 84.992 853.333333h423.210667c11.221333 0 21.973333-4.48 29.909333-12.373333l363.648-363.648a126.976 126.976 0 0 0 0-179.029333zM298.666667 597.333333m-42.666667 0a42.666667 42.666667 0 1 1 85.333333 0 42.666667 42.666667 0 1 1-85.333333 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="sunset" unicode="" d="M768 128a256 256 0 1 1-512 0 42.666667 42.666667 0 0 1 85.333333 0 170.666667 170.666667 0 1 0 341.333334 0 42.666667 42.666667 0 0 1 85.333333 0zM554.666667 512V810.666667a42.666667 42.666667 0 0 1-85.333334 0v-298.666667a42.666667 42.666667 0 0 1 85.333334 0zM149.888 429.781333l60.586667-60.586666a42.666667 42.666667 0 1 1 60.330666 60.330666l-60.586666 60.586667a42.666667 42.666667 0 1 1-60.330667-60.330667zM42.666667 85.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334H42.666667a42.666667 42.666667 0 0 1 0-85.333334zM896 85.333333h85.333333a42.666667 42.666667 0 0 1 0 85.333334h-85.333333a42.666667 42.666667 0 0 1 0-85.333334zM813.525333 369.194667l60.586667 60.586666a42.666667 42.666667 0 1 1-60.330667 60.330667l-60.586666-60.586667a42.666667 42.666667 0 0 1 60.330666-60.330666zM981.333333 0H42.666667a42.666667 42.666667 0 0 1 0-85.333333h938.666666a42.666667 42.666667 0 0 1 0 85.333333zM371.498667 712.832a42.666667 42.666667 0 0 1-60.330667-60.330667l170.666667-170.666666a42.666667 42.666667 0 0 1 60.330666 0l170.666667 170.666666a42.666667 42.666667 0 1 1-60.330667 60.330667L512 572.330667 371.498667 712.832z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="target" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM512 85.333333a298.666667 298.666667 0 1 0 0 597.333334 298.666667 298.666667 0 0 0 0-597.333334z m0 85.333334a213.333333 213.333333 0 1 1 0 426.666666 213.333333 213.333333 0 0 1 0-426.666666zM512 256a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="thermometer" unicode="" d="M715.216338 38.506413A234.660898 234.660898 0 1 0 341.337529 287.673621V746.670338a149.329662 149.329662 0 0 0 298.659324 0v-459.039382a234.660898 234.660898 0 0 0 75.219485-249.167208zM554.665618 266.255481V746.670338a63.998427 63.998427 0 0 1-127.996854 0v-480.414857a42.665618 42.665618 0 0 0-18.943534-35.455128 149.329662 149.329662 0 1 1 165.883922 0A42.665618 42.665618 0 0 0 554.665618 266.255481z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="thumbs-up" unicode="" d="M850.005333 554.666667a130.730667 130.730667 0 0 0 99.456-43.989334c25.002667-28.373333 36.138667-66.090667 30.378667-103.253333l-59.733333-383.957333c-9.685333-63.146667-65.109333-109.525333-129.365334-108.8H302.421333c-23.893333 0-43.306667 19.114667-43.306666 42.666666V426.666667c0 5.973333 1.28 11.861333 3.754666 17.322666l173.141334 384c6.954667 15.402667 22.442667 25.344 39.552 25.344 95.658667 0 173.184-76.373333 173.184-170.666666v-128h201.258666z m-58.794666-554.666667a43.093333 43.093333 0 0 1 43.306666 36.352l59.733334 383.914667a42.24 42.24 0 0 1-10.112 34.389333 43.861333 43.861333 0 0 1-33.664 14.677333H605.44c-23.893333 0-43.264 19.114667-43.264 42.666667V682.666667c0 38.101333-25.344 70.357333-60.288 81.322666l-156.16-346.368V0h445.482667z m-532.053334 384h-86.613333c-23.893333 0-43.306667-19.114667-43.306667-42.666667v-298.666666c0-23.552 19.413333-42.666667 43.306667-42.666667h86.613333v384z m43.264-469.333333H172.544C100.821333-85.333333 42.666667-28.032 42.666667 42.666667v298.666666c0 70.698667 58.154667 128 129.877333 128h129.877333c23.893333 0 43.264-19.114667 43.264-42.666666v-469.333334c0-23.552-19.370667-42.666667-43.264-42.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="thumbs-down" unicode="" d="M173.482667 213.333333a130.005333 130.005333 0 0 0-98.858667 43.733334 127.104 127.104 0 0 0-30.464 103.637333l59.818667 383.914667A128.853333 128.853333 0 0 0 232.832 853.333333H721.92a42.666667 42.666667 0 0 0 42.666667-42.666666v-469.333334a42.666667 42.666667 0 0 0-3.754667-17.578666l-173.44-384a42.666667 42.666667 0 0 0-38.869333-25.088c-95.274667 0-172.757333 76.245333-172.757334 170.666666v128h-202.24z m58.88 554.666667a43.648 43.648 0 0 1-44.074667-36.437333l-59.818667-383.957334a41.728 41.728 0 0 1 10.112-34.048c8.448-9.557333 20.906667-15.061333 34.432-14.933333h245.376a42.666667 42.666667 0 0 0 42.666667-42.666667v-170.666666c0-38.229333 25.856-70.741333 61.568-81.493334l156.586667 346.709334V768H232.362667z m532.181333-384h73.813333A57.088 57.088 0 0 1 896 429.952V721.92c-4.906667 26.794667-29.141333 46.506667-58.325333 46.037333H764.586667v-384z m-42.666667 469.333333h115.029334C909.184 854.528 971.178667 802.133333 980.906667 731.093333L981.333333 725.333333v-298.666666l-0.426666-5.845334A142.122667 142.122667 0 0 0 837.674667 298.666667H721.92a42.666667 42.666667 0 0 0-42.666667 42.666666V810.666667a42.666667 42.666667 0 0 0 42.666667 42.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="toggle-left" unicode="" d="M85.333333 384c0-141.184 114.602667-256 255.616-256h342.101334A255.786667 255.786667 0 0 1 938.666667 384c0 141.184-114.602667 256-255.616 256H340.906667A255.786667 255.786667 0 0 1 85.333333 384z m-85.333333 0c0 188.501333 152.618667 341.333333 340.949333 341.333333h342.101334C871.253333 725.333333 1024 572.288 1024 384c0-188.501333-152.618667-341.333333-340.949333-341.333333H340.906667C152.746667 42.666667 0 195.712 0 384zM341.333333 213.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="toggle-right" unicode="" d="M85.333333 384c0-141.184 114.602667-256 255.616-256h342.101334A255.786667 255.786667 0 0 1 938.666667 384c0 141.184-114.602667 256-255.616 256H340.906667A255.786667 255.786667 0 0 1 85.333333 384z m-85.333333 0c0 188.501333 152.618667 341.333333 340.949333 341.333333h342.101334C871.253333 725.333333 1024 572.288 1024 384c0-188.501333-152.618667-341.333333-340.949333-341.333333H340.906667C152.746667 42.666667 0 195.712 0 384zM682.666667 213.333333a170.666667 170.666667 0 1 0 0 341.333334 170.666667 170.666667 0 0 0 0-341.333334z m0 85.333334a85.333333 85.333333 0 1 1 0 170.666666 85.333333 85.333333 0 0 1 0-170.666666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="trash-" unicode="" d="M128 597.333333h768a42.666667 42.666667 0 0 1 0 85.333334H128a42.666667 42.666667 0 1 1 0-85.333334zM768 640v-597.333333a42.666667 42.666667 0 0 0-42.666667-42.666667H298.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V640a42.666667 42.666667 0 1 1-85.333333 0v-597.333333a128 128 0 0 1 128-128h426.666666a128 128 0 0 1 128 128V640a42.666667 42.666667 0 0 1-85.333333 0zM384 640V725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h170.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-85.333333a42.666667 42.666667 0 0 1 85.333333 0V725.333333a128 128 0 0 1-128 128h-170.666666a128 128 0 0 1-128-128v-85.333333a42.666667 42.666667 0 1 1 85.333333 0zM384 426.666667v-256a42.666667 42.666667 0 0 1 85.333333 0v256a42.666667 42.666667 0 0 1-85.333333 0zM554.666667 426.666667v-256a42.666667 42.666667 0 0 1 85.333333 0v256a42.666667 42.666667 0 0 1-85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="trash" unicode="" d="M128 597.333333h768a42.666667 42.666667 0 0 1 0 85.333334H128a42.666667 42.666667 0 1 1 0-85.333334zM768 640v-597.333333a42.666667 42.666667 0 0 0-42.666667-42.666667H298.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V640a42.666667 42.666667 0 1 1-85.333333 0v-597.333333a128 128 0 0 1 128-128h426.666666a128 128 0 0 1 128 128V640a42.666667 42.666667 0 0 1-85.333333 0zM384 640V725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h170.666666a42.666667 42.666667 0 0 0 42.666667-42.666667v-85.333333a42.666667 42.666667 0 0 1 85.333333 0V725.333333a128 128 0 0 1-128 128h-170.666666a128 128 0 0 1-128-128v-85.333333a42.666667 42.666667 0 1 1 85.333333 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="trending-up" unicode="" d="M392.832 478.165333a42.666667 42.666667 0 0 1-60.330667 0l-320-320a42.666667 42.666667 0 0 1 60.330667-60.330666L362.666667 387.669333l183.168-183.168a42.666667 42.666667 0 0 1 60.330666 0l405.333334 405.333334a42.666667 42.666667 0 1 1-60.330667 60.330666L576 294.997333 392.832 478.165333zM938.666667 384a42.666667 42.666667 0 0 1 85.333333 0V640a42.666667 42.666667 0 0 1-42.666667 42.666667h-256a42.666667 42.666667 0 0 1 0-85.333334h213.333334v-213.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="trending-down" unicode="" d="M72.832 670.165333A42.666667 42.666667 0 0 1 12.501333 609.834667l320-320a42.666667 42.666667 0 0 1 60.330667 0L576 473.002667l375.168-375.168a42.666667 42.666667 0 0 1 60.330667 60.330666l-405.333334 405.333334a42.666667 42.666667 0 0 1-60.330666 0L362.666667 380.330667 72.832 670.165333zM725.333333 170.666667a42.666667 42.666667 0 0 1 0-85.333334h256a42.666667 42.666667 0 0 1 42.666667 42.666667v256a42.666667 42.666667 0 0 1-85.333333 0v-213.333333h-213.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="triangle" unicode="" d="M402.517333 753.237333a128.128 128.128 0 0 0 219.136 0L983.466667 149.333333a128 128 0 0 0-109.909334-192H150.144a128 128 0 0 0-109.098667 192.597334l361.472 603.306666zM114.602667 106.666667A42.666667 42.666667 0 0 1 150.613333 42.666667h722.474667a42.666667 42.666667 0 0 1 36.821333 63.402666L548.565333 709.162667a42.666667 42.666667 0 0 1-72.96 0.085333L114.645333 106.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="type" unicode="" d="M213.333333 682.666667h597.333334v-85.333334a42.666667 42.666667 0 0 1 85.333333 0V725.333333a42.666667 42.666667 0 0 1-42.666667 42.666667H170.666667a42.666667 42.666667 0 0 1-42.666667-42.666667v-128a42.666667 42.666667 0 1 1 85.333333 0V682.666667zM384 0h256a42.666667 42.666667 0 0 1 0 85.333333H384a42.666667 42.666667 0 0 1 0-85.333333zM469.333333 725.333333v-682.666666a42.666667 42.666667 0 0 1 85.333334 0V725.333333a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="twitter" unicode="" d="M956.843947 759.765333c32.213333 22.741333 75.349333-6.698667 66.048-45.056a372.736 372.736 0 0 0-85.12-161.28c0.64-6.869333 0.938667-13.866667 0.981333-20.864C938.75328 25.856 450.390613-234.410667 22.01728 3.925333c-39.253333 21.802667-22.485333 81.792 22.442667 80a453.461333 453.461333 0 0 1 207.957333 41.344c-94.037333 58.026667-154.24 134.869333-183.808 225.792-30.506667 93.866667-26.026667 192.512-0.597333 286.208 7.68 28.330667 15.530667 49.664 21.077333 62.208a42.666667 42.666667 0 0 0 73.898667 7.168A412.032 412.032 0 0 1 469.419947 532.309333a234.112 234.112 0 0 0 150.4 220.288 233.6 233.6 0 0 0 239.274666-44.202666 422.186667 422.186667 0 0 1 97.706667 51.370666z m-97.109334-141.226666a42.666667 42.666667 0 0 0-44.501333 12.885333 148.352 148.352 0 0 1-164.949333 41.344A148.906667 148.906667 0 0 1 554.75328 531.285333v-42.709333a42.666667 42.666667 0 0 0-41.557333-42.709333c-139.093333-3.626667-272.042667 51.2-367.872 148.906666-16.853333-72.832-18.090667-147.925333 4.437333-217.258666 28.885333-88.917333 95.018667-161.066667 208.981333-211.797334 30.592-13.610667 34.346667-55.552 6.656-74.368a538.922667 538.922667 0 0 0-154.88-72.490666C538.198613-67.456 853.419947 150.357333 853.419947 532.437333a149.76 149.76 0 0 1-2.645334 27.562667 42.752 42.752 0 0 0 11.946667 38.442667c9.514667 9.386667 18.304 19.413333 26.368 29.952-9.685333-3.584-19.456-6.826667-29.354667-9.813334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="upload" unicode="" d="M85.333333 170.666667v-128a128 128 0 0 1 128-128h597.333334a128 128 0 0 1 128 128v128a42.666667 42.666667 0 0 1-85.333334 0v-128a42.666667 42.666667 0 0 0-42.666666-42.666667H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666667v128a42.666667 42.666667 0 0 1-85.333334 0zM652.501333 609.834667a42.666667 42.666667 0 0 1 60.330667 60.330666l-170.666667 170.666667a42.666667 42.666667 0 0 1-60.330666 0l-170.666667-170.666667a42.666667 42.666667 0 0 1 60.330667-60.330666L512 750.336l140.501333-140.501333zM469.333333 810.666667v-597.333334a42.666667 42.666667 0 0 1 85.333334 0V810.666667a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="umbrella" unicode="" d="M554.666667 341.33333300000004v-256a85.333333 85.333333 0 1 1 170.666666 0 42.666667 42.666667 0 0 0 85.333334 0 170.666667 170.666667 0 1 0-341.333334 0v256H42.666667a42.666667 42.666667 0 0 0-42.453334 46.72 514.133333 514.133333 0 0 0 1023.573334 0A42.666667 42.666667 0 0 0 981.333333 341.33333300000004H554.666667zM512 768c-205.098667 0-379.008-144.64-419.84-341.333333h839.68c-40.832 196.693333-214.741333 341.333333-419.84 341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="upload-cloud" unicode="" d="M652.517547 183.168a42.666667 42.666667 0 0 1 60.330666 60.330667l-170.666666 170.666666a42.666667 42.666667 0 0 1-60.330667 0l-170.666667-170.666666a42.666667 42.666667 0 0 1 60.330667-60.330667L512.016213 323.669333l140.501334-140.501333zM469.349547 384v-384a42.666667 42.666667 0 0 1 85.333333 0v384a42.666667 42.666667 0 0 1-85.333333 0zM427.57888 808.106667a384 384 0 0 1-331.306667-635.818667 42.666667 42.666667 0 1 1 63.957334 56.490667A298.666667 298.666667 0 1 0 673.168213 501.333333a42.666667 42.666667 0 0 1 41.301334-32h53.76a170.666667 170.666667 0 0 0 81.578666-320.512 42.666667 42.666667 0 1 1 40.832-74.922666A256 256 0 0 1 768.272213 554.666667h-22.229333a384 384 0 0 1-318.464 253.44zM652.517547 183.168a42.666667 42.666667 0 0 1 60.330666 60.330667l-170.666666 170.666666a42.666667 42.666667 0 0 1-60.330667 0l-170.666667-170.666666a42.666667 42.666667 0 0 1 60.330667-60.330667L512.016213 323.669333l140.501334-140.501333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="unlock" unicode="" d="M256 469.333333V597.205333c-0.128 131.413333 100.266667 241.621333 232.192 254.805334 131.968 13.226667 252.586667-74.837333 278.954667-203.648a42.666667 42.666667 0 0 0-33.621334-50.261334 43.008 43.008 0 0 0-50.730666 33.28c-17.578667 85.888-97.962667 144.597333-185.941334 135.765334-87.978667-8.789333-154.88-82.218667-154.794666-169.898667V469.333333h468.821333A127.957333 127.957333 0 0 0 938.666667 341.674667v-299.349334A127.701333 127.701333 0 0 0 810.88-85.33333300000004H213.12A127.957333 127.957333 0 0 0 85.333333 42.325333v299.349334A127.701333 127.701333 0 0 0 213.12 469.333333H256z m-85.333333-127.658666v-299.349334c0-23.168 19.157333-42.325333 42.453333-42.325333h597.76c23.466667 0 42.453333 18.944 42.453333 42.325333v299.349334a42.624 42.624 0 0 1-42.453333 42.325333H213.12a42.368 42.368 0 0 1-42.453333-42.325333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="user-check" unicode="" d="M725.333333 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H213.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h298.666667a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM362.666667 384a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256zM755.498667 456.832a42.666667 42.666667 0 0 1-60.330667-60.330667l85.333333-85.333333a42.666667 42.666667 0 0 1 60.330667 0l170.666667 170.666667a42.666667 42.666667 0 1 1-60.330667 60.330666L810.666667 401.664l-55.168 55.168z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="user-minus" unicode="" d="M725.333333 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H213.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h298.666667a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM362.666667 384a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256zM981.333333 469.333333h-256a42.666667 42.666667 0 0 1 0-85.333333h256a42.666667 42.666667 0 0 1 0 85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="user-plus" unicode="" d="M725.333333 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H213.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h298.666667a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM362.666667 384a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256zM810.666667 554.666667v-256a42.666667 42.666667 0 0 1 85.333333 0V554.666667a42.666667 42.666667 0 0 1-85.333333 0zM981.333333 469.333333h-256a42.666667 42.666667 0 0 1 0-85.333333h256a42.666667 42.666667 0 0 1 0 85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="user-x" unicode="" d="M725.333333 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H213.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h298.666667a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM362.666667 384a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256zM737.834667 524.501333l213.333333-213.333333a42.666667 42.666667 0 0 1 60.330667 60.330667l-213.333334 213.333333a42.666667 42.666667 0 0 1-60.330666-60.330667zM951.168 584.832l-213.333333-213.333333a42.666667 42.666667 0 0 1 60.330666-60.330667l213.333334 213.333333a42.666667 42.666667 0 1 1-60.330667 60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="user" unicode="" d="M896 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H341.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h341.333334a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM512 384a213.333333 213.333333 0 1 0 0 426.666667 213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="users" unicode="" d="M768 0v85.333333a213.333333 213.333333 0 0 1-213.333333 213.333334H213.333333a213.333333 213.333333 0 0 1-213.333333-213.333334v-85.333333a42.666667 42.666667 0 0 1 85.333333 0v85.333333a128 128 0 0 0 128 128h341.333334a128 128 0 0 0 128-128v-85.333333a42.666667 42.666667 0 0 1 85.333333 0zM384 384A213.333333 213.333333 0 1 0 384 810.666667a213.333333 213.333333 0 0 0 0-426.666667z m0 85.333333a128 128 0 1 1 0 256 128 128 0 0 1 0-256zM1024 0v85.333333a213.333333 213.333333 0 0 1-160 206.421334 42.666667 42.666667 0 1 1-21.333333-82.602667A128 128 0 0 0 938.666667 85.290667V0a42.666667 42.666667 0 0 1 85.333333 0zM672.085333 721.109333a128 128 0 0 0 0-247.978666 42.666667 42.666667 0 0 1 21.162667-82.688 213.333333 213.333333 0 0 1 0 413.354666 42.666667 42.666667 0 0 1-21.162667-82.688z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="video-off" unicode="" d="M640.197128 213.158348v-42.644794a42.644793 42.644793 0 0 0-42.644793-42.644793H128.459607a42.644793 42.644793 0 0 0-42.644793 42.644793V596.961489a42.644793 42.644793 0 0 0 42.644793 42.644793h85.289587a42.644793 42.644793 0 1 1 0 85.289587H128.459607a127.93438 127.93438 0 0 1-127.93438-127.93438v-426.447935a127.93438 127.93438 0 0 1 127.93438-127.93438h469.092728a127.93438 127.93438 0 0 1 127.93438 127.93438v42.644794a42.644793 42.644793 0 0 1-85.289587 0z m316.168499 418.345423l-255.868761-185.078403 55.139718-4.392414-42.644793 42.644793 12.494924-30.149869V596.961489a127.93438 127.93438 0 0 1-127.93438 127.93438h-142.43361a42.644793 42.644793 0 0 1 0-85.289587H597.552335a42.644793 42.644793 0 0 0 42.644793-42.644793v-142.433611a42.644793 42.644793 0 0 1 12.494925-30.149869l42.644793-42.644793a42.644793 42.644793 0 0 1 55.139718-4.392414L938.710682 513.462983V170.513554a42.644793 42.644793 0 0 1 85.289587 0V596.961489a42.644793 42.644793 0 0 1-67.634642 34.542282zM13.020151 822.68038l938.185456-938.185456a42.644793 42.644793 0 0 1 60.299738 60.299738l-938.185456 938.185456A42.644793 42.644793 0 1 1 13.020151 822.68038z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="video" unicode="" d="M956.544427 632.064A42.666667 42.666667 0 0 0 1024.000427 597.333333v-426.666666a42.666667 42.666667 0 0 0-67.456-34.730667l-298.666667 213.333333a42.666667 42.666667 0 0 0 0 69.461334l298.666667 213.333333zM756.05376 384L938.667093 253.568V514.432L756.05376 384zM85.33376 597.077333v-426.154666A42.581333 42.581333 0 0 1 127.659093 128h470.016A42.666667 42.666667 0 0 1 640.000427 170.922667V597.077333A42.581333 42.581333 0 0 1 597.675093 640H127.659093A42.666667 42.666667 0 0 1 85.33376 597.077333z m-85.333333 0A128 128 0 0 0 127.659093 725.333333h470.016A127.914667 127.914667 0 0 0 725.33376 597.077333v-426.154666A128 128 0 0 0 597.675093 42.666667H127.659093A127.914667 127.914667 0 0 0 0.000427 170.922667V597.077333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="voicemail" unicode="" d="M608.306773 256a234.666667 234.666667 0 1 0 181.034667-85.333333h-554.666667a234.666667 234.666667 0 1 0 181.034667 85.333333h192.597333zM234.674773 256a149.333333 149.333333 0 1 1 0 298.666667 149.333333 149.333333 0 0 1 0-298.666667z m554.666667 0a149.333333 149.333333 0 1 1 0 298.666667 149.333333 149.333333 0 0 1 0-298.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="volume-x" unicode="" d="M442.666667 715.946667A42.666667 42.666667 0 0 0 512 682.666667v-597.333334a42.666667 42.666667 0 0 0-69.333333-33.28L241.066667 213.333333H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667V512a42.666667 42.666667 0 0 0 42.666666 42.666667h155.733334l201.6 161.28z m-160-237.226667A42.666667 42.666667 0 0 0 256 469.333333H128v-170.666666h128a42.666667 42.666667 0 0 0 26.666667-9.386667L426.666667 174.08V593.92l-144-115.2zM951.168 542.165333l-256-256a42.666667 42.666667 0 0 1 60.330667-60.330666l256 256a42.666667 42.666667 0 1 1-60.330667 60.330666zM695.168 481.834667l256-256a42.666667 42.666667 0 0 1 60.330667 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330667-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="volume-" unicode="" d="M528 715.946667A42.666667 42.666667 0 0 0 597.333333 682.666667v-597.333334a42.666667 42.666667 0 0 0-69.333333-33.28L326.4 213.333333H170.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V512a42.666667 42.666667 0 0 0 42.666667 42.666667h155.733333l201.6 161.28z m-160-237.226667A42.666667 42.666667 0 0 0 341.333333 469.333333H213.333333v-170.666666h128a42.666667 42.666667 0 0 0 26.666667-9.386667L512 174.08V593.92l-144-115.2zM718.208 504.874667a170.666667 170.666667 0 0 0 0-241.322667 42.666667 42.666667 0 1 1 60.330667-60.330667 256 256 0 0 1 0 361.984 42.666667 42.666667 0 1 1-60.330667-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="volume-1" unicode="" d="M442.666667 715.946667A42.666667 42.666667 0 0 0 512 682.666667v-597.333334a42.666667 42.666667 0 0 0-69.333333-33.28L241.066667 213.333333H85.333333a42.666667 42.666667 0 0 0-42.666666 42.666667V512a42.666667 42.666667 0 0 0 42.666666 42.666667h155.733334l201.6 161.28z m-160-237.226667A42.666667 42.666667 0 0 0 256 469.333333H128v-170.666666h128a42.666667 42.666667 0 0 0 26.666667-9.386667L426.666667 174.08V593.92l-144-115.2zM783.488 655.488a384 384 0 0 0 0-542.976 42.666667 42.666667 0 1 1 60.330667-60.330667c183.210667 183.253333 183.210667 480.384 0 663.637334a42.666667 42.666667 0 1 1-60.330667-60.330667z m-150.613333-150.613333a170.666667 170.666667 0 0 0 0-241.322667 42.666667 42.666667 0 1 1 60.330666-60.330667 256 256 0 0 1 0 361.984 42.666667 42.666667 0 1 1-60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="volume" unicode="" d="M656 715.946667A42.666667 42.666667 0 0 0 725.333333 682.666667v-597.333334a42.666667 42.666667 0 0 0-69.333333-33.28L454.4 213.333333H298.666667a42.666667 42.666667 0 0 0-42.666667 42.666667V512a42.666667 42.666667 0 0 0 42.666667 42.666667h155.733333l201.6 161.28z m-160-237.226667A42.666667 42.666667 0 0 0 469.333333 469.333333H341.333333v-170.666666h128a42.666667 42.666667 0 0 0 26.666667-9.386667L640 174.08V593.92l-144-115.2z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="watch" unicode="" d="M512 42.66716399999996a341.332622 341.332622 0 1 0 0 682.665245 341.332622 341.332622 0 0 0 0-682.665245z m0 85.333156a255.999467 255.999467 0 1 1 0 511.998933 255.999467 255.999467 0 0 1 0-511.998933zM554.666578 511.99952v-110.33577l51.498559-51.49856a42.666578 42.666578 0 0 0-60.330541-60.33054l-63.999866 63.999866A42.666578 42.666578 0 0 0 469.333422 383.99978699999997V511.99952a42.666578 42.666578 0 0 0 85.333156 0z m107.263776-352.383266a42.666578 42.666578 0 0 0 84.991823-7.765317l-14.933302-163.370326A127.957067 127.957067 0 0 0 604.159808-127.999147H419.584193a127.999733 127.999733 0 0 0-127.999734 116.437091l-14.933302 163.412993a42.666578 42.666578 0 0 0 84.991823 7.765317l14.933302-163.455659c2.005329-22.058621 20.522624-38.911919 42.837244-38.826586h184.916948a42.666578 42.666578 0 0 1 42.666578 38.869252l14.933302 163.412993zM362.069646 608.383319a42.666578 42.666578 0 1 0-84.991823 7.765317l14.933302 163.370327A127.999733 127.999733 0 0 0 419.413526 895.99872h185.428947a127.999733 127.999733 0 0 0 127.999734-116.437091l14.933302-163.412993a42.666578 42.666578 0 0 0-84.991823-7.765317l-14.933302 163.45566A42.709244 42.709244 0 0 1 605.01314 810.665564H419.413526a42.666578 42.666578 0 0 1-42.410578-38.869252l-14.933302-163.412993z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="wifi" unicode="" d="M240.64 327.765333a426.666667 426.666667 0 0 0 546.133333 0 42.666667 42.666667 0 0 1 54.613334 65.536 512 512 0 0 1-655.36 0 42.666667 42.666667 0 1 1 54.613333-65.536zM88.405333 480a640 640 0 0 0 846.762667 0 42.666667 42.666667 0 0 1 56.490667 64C717.44 785.877333 306.133333 785.877333 31.914667 544a42.666667 42.666667 0 1 1 56.490666-64z m299.818667-306.133333a213.333333 213.333333 0 0 0 247.125333 0 42.666667 42.666667 0 0 1 49.408 69.546666 298.666667 298.666667 0 0 1-345.941333 0 42.666667 42.666667 0 0 1 49.408-69.546666zM512 42.666667m-42.666667 0a42.666667 42.666667 0 1 1 85.333334 0 42.666667 42.666667 0 1 1-85.333334 0Z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="x-square" unicode="" d="M170.666667 682.88v-597.76c0-23.466667 18.986667-42.453333 42.453333-42.453333h597.76c23.466667 0 42.453333 18.986667 42.453333 42.453333V682.88A42.453333 42.453333 0 0 1 810.88 725.333333H213.12A42.453333 42.453333 0 0 1 170.666667 682.88z m-85.333334 0A127.786667 127.786667 0 0 0 213.12 810.666667h597.76A127.786667 127.786667 0 0 0 938.666667 682.88v-597.76A127.786667 127.786667 0 0 0 810.88-42.666667H213.12A127.786667 127.786667 0 0 0 85.333333 85.12V682.88zM353.834667 481.834667l256-256a42.666667 42.666667 0 0 1 60.330666 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666-60.330666zM609.834667 542.165333l-256-256a42.666667 42.666667 0 0 1 60.330666-60.330666l256 256a42.666667 42.666667 0 1 1-60.330666 60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="wind" unicode="" d="M439.466667 670.08A42.666667 42.666667 0 1 0 469.461333 597.333333H85.333333a42.666667 42.666667 0 1 1 0-85.333333h384a127.957333 127.957333 0 0 1 64.298667 238.890667 128 128 0 0 1-154.709333-20.650667 42.666667 42.666667 0 0 1 60.501333-60.16z m67.413333-632.32A128 128 0 1 1 597.205333 256H85.333333a42.666667 42.666667 0 0 1 0-85.333333h512a42.709333 42.709333 0 0 0 21.674667-79.616 42.666667 42.666667 0 0 0-51.584 6.869333 42.666667 42.666667 0 1 1-60.501333-60.16z m279.722667 498.218667A64 64 0 1 0 831.914667 426.666667H85.333333a42.666667 42.666667 0 0 1 0-85.333334h746.666667a149.333333 149.333333 0 1 1-105.642667 255.061334 42.666667 42.666667 0 1 1 60.245334-60.416z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="x" unicode="" d="M737.834667 670.165333l-512-512a42.666667 42.666667 0 0 1 60.330666-60.330666l512 512a42.666667 42.666667 0 1 1-60.330666 60.330666zM225.834667 609.834667l512-512a42.666667 42.666667 0 0 1 60.330666 60.330666l-512 512a42.666667 42.666667 0 0 1-60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="x-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768zM609.834667 542.165333l-256-256a42.666667 42.666667 0 0 1 60.330666-60.330666l256 256a42.666667 42.666667 0 1 1-60.330666 60.330666zM353.834667 481.834667l256-256a42.666667 42.666667 0 0 1 60.330666 60.330666l-256 256a42.666667 42.666667 0 0 1-60.330666-60.330666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="zap" unicode="" d="M128 256a42.666667 42.666667 0 0 0-32.768 69.973333l426.666667 512c27.264 32.725333 80.384 9.685333 75.093333-32.597333L560.298667 512H896a42.666667 42.666667 0 0 0 32.768-69.973333l-426.666667-512c-27.264-32.725333-80.384-9.685333-75.093333 32.597333l36.693333 293.376H128z m91.093333 85.333333H512a42.666667 42.666667 0 0 0 42.325333-47.957333l-24.618666-196.949333 275.2 330.24H512a42.666667 42.666667 0 0 0-42.325333 47.957333l24.618666 196.949333L219.093333 341.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="zoom-in" unicode="" d="M469.333333 42.666667a384 384 0 1 0 300.032 144.298666l156.8-156.8a42.666667 42.666667 0 0 0-60.330666-60.330666l-156.8 156.8A382.378667 382.378667 0 0 0 469.333333 42.666667z m0 85.333333a298.666667 298.666667 0 1 1 0 597.333333 298.666667 298.666667 0 0 1 0-597.333333zM426.666667 554.666667v-256a42.666667 42.666667 0 0 1 85.333333 0V554.666667a42.666667 42.666667 0 0 1-85.333333 0zM341.333333 384h256a42.666667 42.666667 0 0 1 0 85.333333H341.333333a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="zoom-out" unicode="" d="M709.034667 126.634667a384 384 0 1 0 60.330666 60.330666l156.8-156.8a42.666667 42.666667 0 0 0-60.330666-60.330666l-156.8 156.8zM469.333333 128a298.666667 298.666667 0 1 1 0 597.333333 298.666667 298.666667 0 0 1 0-597.333333zM341.333333 384h256a42.666667 42.666667 0 0 1 0 85.333333H341.333333a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="command" unicode="" d="M597.333333 213.33333300000004h-170.666666v-85.333333a170.666667 170.666667 0 1 0-170.666667 170.666667h85.333333v170.666666H256a170.666667 170.666667 0 1 0 170.666667 170.666667v-85.333333h170.666666V640a170.666667 170.666667 0 1 0 170.666667-170.666667h-85.333333v-170.666666h85.333333a170.666667 170.666667 0 1 0-170.666667-170.666667v85.333333z m-170.666666 256v-170.666666h170.666666v170.666666h-170.666666zM341.333333 554.666667V640a85.333333 85.333333 0 1 1-85.333333-85.333333h85.333333z m0-341.333334H256a85.333333 85.333333 0 1 1 85.333333-85.333333v85.333333z m341.333334-85.333333a85.333333 85.333333 0 1 1 85.333333 85.333333h-85.333333v-85.333333z m85.333333 426.666667a85.333333 85.333333 0 1 1-85.333333 85.333333v-85.333333h85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="cloud" unicode="" d="M768.190293 512a255.914667 255.914667 0 1 0 0-511.829333H384.318293A383.914667 383.914667 0 1 0 745.918293 512h22.186667z m0-85.290667h-53.76a42.666667 42.666667 0 0 0-41.301333 32 298.581333 298.581333 0 1 1-288.853333-373.248h383.914666a170.624 170.624 0 1 1 0 341.248z" horiz-adv-x="1044" />
|
||||
|
||||
|
||||
<glyph glyph-name="hash" unicode="" d="M170.666667 469.333333h682.666666a42.666667 42.666667 0 0 1 0 85.333334H170.666667a42.666667 42.666667 0 1 1 0-85.333334zM170.666667 213.33333300000004h682.666666a42.666667 42.666667 0 0 1 0 85.333334H170.666667a42.666667 42.666667 0 0 1 0-85.333334zM384.256 772.6933329999999l-85.333333-768a42.666667 42.666667 0 1 1 84.821333-9.386666l85.333333 768a42.666667 42.666667 0 1 1-84.821333 9.386666zM640.256 772.6933329999999l-85.333333-768a42.666667 42.666667 0 1 1 84.821333-9.386666l85.333333 768a42.666667 42.666667 0 1 1-84.821333 9.386666z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="headphones" unicode="" d="M85.333333 213.33333300000004v170.666667C85.333333 619.648 276.352 810.666667 512 810.666667s426.666667-191.018667 426.666667-426.666667v-298.666667a128 128 0 0 0-128-128h-42.666667a128 128 0 0 0-128 128v128a128 128 0 0 0 128 128h85.333333v42.666667a341.333333 341.333333 0 0 1-682.666666 0v-42.666667h85.333333a128 128 0 0 0 128-128v-128a128 128 0 0 0-128-128H213.333333a128 128 0 0 0-128 128v128z m768 42.666667h-85.333333a42.666667 42.666667 0 0 1-42.666667-42.666667v-128a42.666667 42.666667 0 0 1 42.666667-42.666666h42.666667a42.666667 42.666667 0 0 1 42.666666 42.666666v170.666667zM170.666667 256v-170.666667a42.666667 42.666667 0 0 1 42.666666-42.666666h42.666667a42.666667 42.666667 0 0 1 42.666667 42.666666v128a42.666667 42.666667 0 0 1-42.666667 42.666667H170.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="underline" unicode="" d="M213.333333 768a42.666667 42.666667 0 1 0 85.333334 0v-298.666667a213.333333 213.333333 0 0 1 426.666666 0V768a42.666667 42.666667 0 0 0 85.333334 0v-298.666667a298.666667 298.666667 0 0 0-597.333334 0V768zM170.666667-42.666667a42.666667 42.666667 0 0 0 0 85.333334h682.666666a42.666667 42.666667 0 0 0 0-85.333334H170.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="italic" unicode="" d="M669.568 682.666667H810.666667a42.666667 42.666667 0 0 1 0 85.333333h-384a42.666667 42.666667 0 1 1 0-85.333333h151.765333l-224-597.333334H213.333333a42.666667 42.666667 0 0 1 0-85.333333h384a42.666667 42.666667 0 0 1 0 85.333333h-151.765333l224 597.333334z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="bold" unicode="" d="M743.936 399.701333A213.333333 213.333333 0 0 0 640 0H256a42.666667 42.666667 0 0 0-42.666667 42.666667V725.333333a42.666667 42.666667 0 0 0 42.666667 42.666667h341.333333a213.333333 213.333333 0 0 0 146.602667-368.298667zM298.666667 682.666667v-256h298.666666a128 128 0 0 1 0 256H298.666667z m0-597.333334h341.333333a128 128 0 0 1 0 256H298.666667v-256z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="crop" unicode="" d="M302.764169 679.379651L682.710044 682.664978a127.999733 127.999733 0 0 0 127.999734-127.999734v-383.9992h170.666311a42.666578 42.666578 0 0 0 0-85.333155h-170.666311v-170.666311a42.666578 42.666578 0 0 0-85.333156 0v170.666311H341.377422c-70.698519 0-127.999733 57.301214-127.999733 128.383732l3.285326 379.561876-173.567638-1.49333a42.666578 42.666578 0 0 0-0.767998 85.333156l175.060968 1.535996L218.924344 853.715288a42.666578 42.666578 0 0 0 85.333156-0.767998l-1.493331-173.567639z m-0.767998-85.333155L298.710844 213.332622a42.666578 42.666578 0 0 1 42.666578-42.666578h383.9992V554.665244c0 23.551951-19.114627 42.666578-42.282578 42.666578l-381.097873-3.285326z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="help-circle" unicode="" d="M512-85.333333C252.8-85.333333 42.666667 124.8 42.666667 384S252.8 853.333333 512 853.333333s469.333333-210.133333 469.333333-469.333333-210.133333-469.333333-469.333333-469.333333z m0 85.333333a384 384 0 1 1 0 768 384 384 0 0 1 0-768z m0 128a42.666667 42.666667 0 1 0 0 85.333333 42.666667 42.666667 0 0 0 0-85.333333z m-83.925333 369.834667a42.666667 42.666667 0 1 0-80.469334 28.330666A170.666667 170.666667 0 0 0 679.253333 469.333333c0-54.613333-32.341333-97.706667-82.986666-131.498666a323.2 323.2 0 0 0-74.197334-36.992 42.666667 42.666667 0 1 0-26.965333 80.981333 255.488 255.488 0 0 1 53.845333 27.008c29.312 19.584 44.970667 40.448 44.970667 60.586667a85.333333 85.333333 0 0 1-165.845333 28.416z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="paperclip" unicode="" d="M884.522667 428.586667l-392.106667-392.106667a213.461333 213.461333 0 0 0-301.909333 301.909333l392.106666 392.106667a128.085333 128.085333 0 1 0 181.162667-181.162667l-392.533333-392.106666a42.709333 42.709333 0 1 0-60.416 60.416l362.24 361.813333a42.666667 42.666667 0 0 1-60.288 60.330667l-362.24-361.813334a128.042667 128.042667 0 1 1 181.034666-181.077333l392.533334 392.106667a213.418667 213.418667 0 1 1-301.824 301.824l-392.106667-392.106667a298.794667 298.794667 0 1 1 422.570667-422.570667l392.106666 392.106667a42.666667 42.666667 0 0 1-60.330666 60.330667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="shopping-cart" unicode="" d="M341.333333-128a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334z m512-85.333333a128 128 0 1 0 0 256 128 128 0 0 0 0-256z m0 85.333333a42.666667 42.666667 0 1 1 0 85.333334 42.666667 42.666667 0 0 1 0-85.333334zM278.912 682.666667H981.333333a42.666667 42.666667 0 0 0 41.813334-51.029334l-71.68-357.930666A127.786667 127.786667 0 0 0 824.32 170.666667H373.674667a128 128 0 0 0-128 111.36L180.864 773.546667A42.666667 42.666667 0 0 1 138.666667 810.666667H42.666667a42.666667 42.666667 0 1 0 0 85.333333h96.085333a128 128 0 0 0 126.72-111.36L278.869333 682.666667z m51.413333-389.546667c2.773333-21.376 21.077333-37.290667 43.008-37.12h451.84a42.666667 42.666667 0 0 1 42.666667 34.389333L929.28 597.333333H290.133333l40.192-304.213333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="tv" unicode="" d="M409.002667 640L268.501333 780.501333a42.666667 42.666667 0 0 0 60.330667 60.330667L512 657.664l183.168 183.168a42.666667 42.666667 0 0 0 60.330667-60.330667L614.997333 640H853.333333a128 128 0 0 0 128-128v-469.333333a128 128 0 0 0-128-128H170.666667a128 128 0 0 0-128 128V512a128 128 0 0 0 128 128h238.336zM170.666667 554.666667a42.666667 42.666667 0 0 1-42.666667-42.666667v-469.333333a42.666667 42.666667 0 0 1 42.666667-42.666667h682.666666a42.666667 42.666667 0 0 1 42.666667 42.666667V512a42.666667 42.666667 0 0 1-42.666667 42.666667H170.666667z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="wifi-off" unicode="" d="M669.172142 166.544518a42.497957 42.497957 0 0 0-33.035022 7.033262 213.129173 213.129173 0 0 1-246.888835 0 42.625835 42.625835 0 1 0-49.360716 69.480111 298.167714 298.167714 0 0 0 198.721641 54.006932l-118.542446 118.585072a423.700797 423.700797 0 0 1-178.602247-88.278103 42.625835 42.625835 0 1 0-54.688946 65.38803A508.952466 508.952466 0 0 0 350.92766 484.703748L251.950472 583.766187a635.551195 635.551195 0 0 1-162.233927-104.305417A42.625835 42.625835 0 1 0 33.27994 543.31427 720.802864 720.802864 0 0 0 187.926468 647.790191L13.458927 822.300358A42.625835 42.625835 0 1 0 73.731857 882.530663l217.604886-217.562261 1.321401-1.364026 168.158918-168.158918c2.685428-2.131292 5.1151-4.518338 7.246392-7.203766l543.479392-543.479392a42.625835 42.625835 0 0 0-60.315556-60.27293l-282.055148 282.012522zM512.479573-0.122496a42.625835 42.625835 0 1 0 0 85.251669 42.625835 42.625835 0 0 0 0-85.251669z m182.481199 385.380171a42.625835 42.625835 0 0 0 37.425482 76.641251c38.107496-18.62749 73.742694-41.901195 106.053077-69.352233a42.625835 42.625835 0 1 0-55.15783-64.961772 423.700797 423.700797 0 0 1-88.320729 57.672754z m-234.015833 252.003935a42.625835 42.625835 0 0 0-6.862759 84.995914A724.639189 724.639189 0 0 0 991.679207 543.356896a42.625835 42.625835 0 0 0-56.436605-63.938752A639.38752 639.38752 0 0 1 460.902313 637.26161z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="minimize" unicode="" d="M298.666667 768v-128a42.666667 42.666667 0 0 0-42.666667-42.666667H128a42.666667 42.666667 0 1 1 0-85.333333h128a128 128 0 0 1 128 128V768a42.666667 42.666667 0 1 1-85.333333 0z m597.333333-170.666667h-128a42.666667 42.666667 0 0 0-42.666667 42.666667V768a42.666667 42.666667 0 0 1-85.333333 0v-128a128 128 0 0 1 128-128h128a42.666667 42.666667 0 0 1 0 85.333333z m-170.666667-597.333333v128a42.666667 42.666667 0 0 0 42.666667 42.666667h128a42.666667 42.666667 0 0 1 0 85.333333h-128a128 128 0 0 1-128-128v-128a42.666667 42.666667 0 0 1 85.333333 0zM128 170.666667h128a42.666667 42.666667 0 0 0 42.666667-42.666667v-128a42.666667 42.666667 0 0 1 85.333333 0v128a128 128 0 0 1-128 128H128a42.666667 42.666667 0 0 1 0-85.333333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="maximize" unicode="" d="M341.333333 810.666667H213.333333a128 128 0 0 1-128-128v-128a42.666667 42.666667 0 1 1 85.333334 0V682.666667a42.666667 42.666667 0 0 0 42.666666 42.666666h128a42.666667 42.666667 0 1 1 0 85.333334z m597.333334-256V682.666667a128 128 0 0 1-128 128h-128a42.666667 42.666667 0 0 1 0-85.333334h128a42.666667 42.666667 0 0 0 42.666666-42.666666v-128a42.666667 42.666667 0 0 1 85.333334 0z m-256-597.333334h128a128 128 0 0 1 128 128v128a42.666667 42.666667 0 0 1-85.333334 0v-128a42.666667 42.666667 0 0 0-42.666666-42.666666h-128a42.666667 42.666667 0 0 1 0-85.333334zM85.333333 213.333333v-128a128 128 0 0 1 128-128h128a42.666667 42.666667 0 0 1 0 85.333334H213.333333a42.666667 42.666667 0 0 0-42.666666 42.666666v128a42.666667 42.666667 0 0 1-85.333334 0z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="gitlab" unicode="" d="M537.088-82.730667a42.666667 42.666667 0 0 0-50.176 0L32.341333 247.637333a78.421333 78.421333 0 0 0-28.16 87.594667l52.053334 161.365333 104.96 322.645334c3.413333 8.832 8.832 16.725333 15.701333 22.912a61.269333 61.269333 0 0 0 83.157333-0.810667 59.733333 59.733333 0 0 0 15.616-25.173333L370.176 526.08h283.648l95.274667 293.12c3.413333 8.874667 8.874667 16.768 15.744 22.954667a61.269333 61.269333 0 0 0 83.157333-0.810667 59.733333 59.733333 0 0 0 15.616-25.173333l103.936-319.957334 54.741333-162.133333a78.506667 78.506667 0 0 0-31.573333-87.125333l-453.632-329.685334zM86.869333 313.514667L512 4.522667l426.794667 310.186666-52.224 154.666667-80.725334 248.490667L725.333333 470.229333a42.666667 42.666667 0 0 0-40.576-29.482666h-345.6A42.666667 42.666667 0 0 0 298.666667 470.186667L217.898667 717.952 137.472 470.314667l-50.602667-156.8z" horiz-adv-x="1025" />
|
||||
|
||||
|
||||
<glyph glyph-name="sliders" unicode="" d="M810.666667 170.666667v-170.666667a42.666667 42.666667 0 0 1 85.333333 0v170.666667h85.333333a42.666667 42.666667 0 0 1 0 85.333333h-256a42.666667 42.666667 0 0 1 0-85.333333h85.333334zM469.333333 597.333333V768a42.666667 42.666667 0 0 0 85.333334 0v-170.666667h85.333333a42.666667 42.666667 0 0 0 0-85.333333H384a42.666667 42.666667 0 1 0 0 85.333333h85.333333z m-341.333333-341.333333v-256a42.666667 42.666667 0 0 1 85.333333 0v256h85.333334a42.666667 42.666667 0 0 1 0 85.333333H42.666667a42.666667 42.666667 0 0 1 0-85.333333h85.333333z m85.333333 213.333333a42.666667 42.666667 0 0 0-85.333333 0V768a42.666667 42.666667 0 1 0 85.333333 0v-298.666667z m341.333334-469.333333a42.666667 42.666667 0 0 0-85.333334 0v384a42.666667 42.666667 0 0 0 85.333334 0v-384z m341.333333 384a42.666667 42.666667 0 0 0-85.333333 0V768a42.666667 42.666667 0 0 0 85.333333 0v-384z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="star-on" unicode="" d="M473.728 829.568a42.666667 42.666667 0 0 0 76.544 0l121.898667-246.741333 272.64-39.808a42.624 42.624 0 0 0 23.637333-72.704l-197.248-191.914667 46.506667-271.146667a42.666667 42.666667 0 0 0-61.866667-44.928L512 90.410667l-243.84-128.085334a42.666667 42.666667 0 0 0-61.866667 44.928l46.506667 271.146667-197.248 191.914667a42.624 42.624 0 0 0 23.594667 72.704l272.64 39.808 121.941333 246.741333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
<glyph glyph-name="heart-on" unicode="" d="M527.061333 729.472A277.333333 277.333333 0 0 0 1000.618667 533.333333a277.333333 277.333333 0 0 0-81.28-196.138666l-377.173334-377.173334a42.666667 42.666667 0 0 0-60.330666 0l-377.173334 377.173334a277.376 277.376 0 0 0 392.277334 392.277333l15.061333-15.061333 15.061333 15.061333z" horiz-adv-x="1024" />
|
||||
|
||||
|
||||
|
||||
|
||||
</font>
|
||||
</defs></svg>
|
||||
|
After Width: | Height: | Size: 187 KiB |
BIN
public/assets/fonts/feather/fonts/feather.ttf
Normal file
BIN
public/assets/fonts/feather/fonts/feather.ttf
Normal file
Binary file not shown.
BIN
public/assets/fonts/feather/fonts/feather.woff
Normal file
BIN
public/assets/fonts/feather/fonts/feather.woff
Normal file
Binary file not shown.
992
public/assets/fonts/feather/icon-font.css
Normal file
992
public/assets/fonts/feather/icon-font.css
Normal file
@ -0,0 +1,992 @@
|
||||
@font-face {
|
||||
font-family: 'feather';
|
||||
src: url('fonts/feather.eot?t=1501841394106');
|
||||
/* IE9*/
|
||||
src:
|
||||
url('fonts/feather.eot?t=1501841394106#iefix') format('embedded-opentype'),
|
||||
url('fonts/feather.ttf?t=1501841394106') format('truetype'),
|
||||
url('fonts/feather.svg?t=1501841394106#feather') format('svg');
|
||||
/* iOS 4.1- */
|
||||
}
|
||||
|
||||
.feather {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'feather' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-alert-octagon:before {
|
||||
content: '\e81b';
|
||||
}
|
||||
|
||||
.icon-alert-circle:before {
|
||||
content: '\e81c';
|
||||
}
|
||||
|
||||
.icon-activity:before {
|
||||
content: '\e81d';
|
||||
}
|
||||
|
||||
.icon-alert-triangle:before {
|
||||
content: '\e81e';
|
||||
}
|
||||
|
||||
.icon-align-center:before {
|
||||
content: '\e81f';
|
||||
}
|
||||
|
||||
.icon-airplay:before {
|
||||
content: '\e820';
|
||||
}
|
||||
|
||||
.icon-align-justify:before {
|
||||
content: '\e821';
|
||||
}
|
||||
|
||||
.icon-align-left:before {
|
||||
content: '\e822';
|
||||
}
|
||||
|
||||
.icon-align-right:before {
|
||||
content: '\e823';
|
||||
}
|
||||
|
||||
.icon-arrow-down-left:before {
|
||||
content: '\e824';
|
||||
}
|
||||
|
||||
.icon-arrow-down-right:before {
|
||||
content: '\e825';
|
||||
}
|
||||
|
||||
.icon-anchor:before {
|
||||
content: '\e826';
|
||||
}
|
||||
|
||||
.icon-aperture:before {
|
||||
content: '\e827';
|
||||
}
|
||||
|
||||
.icon-arrow-left:before {
|
||||
content: '\e828';
|
||||
}
|
||||
|
||||
.icon-arrow-right:before {
|
||||
content: '\e829';
|
||||
}
|
||||
|
||||
.icon-arrow-down:before {
|
||||
content: '\e82a';
|
||||
}
|
||||
|
||||
.icon-arrow-up-left:before {
|
||||
content: '\e82b';
|
||||
}
|
||||
|
||||
.icon-arrow-up-right:before {
|
||||
content: '\e82c';
|
||||
}
|
||||
|
||||
.icon-arrow-up:before {
|
||||
content: '\e82d';
|
||||
}
|
||||
|
||||
.icon-award:before {
|
||||
content: '\e82e';
|
||||
}
|
||||
|
||||
.icon-bar-chart:before {
|
||||
content: '\e82f';
|
||||
}
|
||||
|
||||
.icon-at-sign:before {
|
||||
content: '\e830';
|
||||
}
|
||||
|
||||
.icon-bar-chart-2:before {
|
||||
content: '\e831';
|
||||
}
|
||||
|
||||
.icon-battery-charging:before {
|
||||
content: '\e832';
|
||||
}
|
||||
|
||||
.icon-bell-off:before {
|
||||
content: '\e833';
|
||||
}
|
||||
|
||||
.icon-battery:before {
|
||||
content: '\e834';
|
||||
}
|
||||
|
||||
.icon-bluetooth:before {
|
||||
content: '\e835';
|
||||
}
|
||||
|
||||
.icon-bell:before {
|
||||
content: '\e836';
|
||||
}
|
||||
|
||||
.icon-book:before {
|
||||
content: '\e837';
|
||||
}
|
||||
|
||||
.icon-briefcase:before {
|
||||
content: '\e838';
|
||||
}
|
||||
|
||||
.icon-camera-off:before {
|
||||
content: '\e839';
|
||||
}
|
||||
|
||||
.icon-calendar:before {
|
||||
content: '\e83a';
|
||||
}
|
||||
|
||||
.icon-bookmark:before {
|
||||
content: '\e83b';
|
||||
}
|
||||
|
||||
.icon-box:before {
|
||||
content: '\e83c';
|
||||
}
|
||||
|
||||
.icon-camera:before {
|
||||
content: '\e83d';
|
||||
}
|
||||
|
||||
.icon-check-circle:before {
|
||||
content: '\e83e';
|
||||
}
|
||||
|
||||
.icon-check:before {
|
||||
content: '\e83f';
|
||||
}
|
||||
|
||||
.icon-check-square:before {
|
||||
content: '\e840';
|
||||
}
|
||||
|
||||
.icon-cast:before {
|
||||
content: '\e841';
|
||||
}
|
||||
|
||||
.icon-chevron-down:before {
|
||||
content: '\e842';
|
||||
}
|
||||
|
||||
.icon-chevron-left:before {
|
||||
content: '\e843';
|
||||
}
|
||||
|
||||
.icon-chevron-right:before {
|
||||
content: '\e844';
|
||||
}
|
||||
|
||||
.icon-chevron-up:before {
|
||||
content: '\e845';
|
||||
}
|
||||
|
||||
.icon-chevrons-down:before {
|
||||
content: '\e846';
|
||||
}
|
||||
|
||||
.icon-chevrons-right:before {
|
||||
content: '\e847';
|
||||
}
|
||||
|
||||
.icon-chevrons-up:before {
|
||||
content: '\e848';
|
||||
}
|
||||
|
||||
.icon-chevrons-left:before {
|
||||
content: '\e849';
|
||||
}
|
||||
|
||||
.icon-circle:before {
|
||||
content: '\e84a';
|
||||
}
|
||||
|
||||
.icon-clipboard:before {
|
||||
content: '\e84b';
|
||||
}
|
||||
|
||||
.icon-chrome:before {
|
||||
content: '\e84c';
|
||||
}
|
||||
|
||||
.icon-clock:before {
|
||||
content: '\e84d';
|
||||
}
|
||||
|
||||
.icon-cloud-lightning:before {
|
||||
content: '\e84e';
|
||||
}
|
||||
|
||||
.icon-cloud-drizzle:before {
|
||||
content: '\e84f';
|
||||
}
|
||||
|
||||
.icon-cloud-rain:before {
|
||||
content: '\e850';
|
||||
}
|
||||
|
||||
.icon-cloud-off:before {
|
||||
content: '\e851';
|
||||
}
|
||||
|
||||
.icon-codepen:before {
|
||||
content: '\e852';
|
||||
}
|
||||
|
||||
.icon-cloud-snow:before {
|
||||
content: '\e853';
|
||||
}
|
||||
|
||||
.icon-compass:before {
|
||||
content: '\e854';
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: '\e855';
|
||||
}
|
||||
|
||||
.icon-corner-down-right:before {
|
||||
content: '\e856';
|
||||
}
|
||||
|
||||
.icon-corner-down-left:before {
|
||||
content: '\e857';
|
||||
}
|
||||
|
||||
.icon-corner-left-down:before {
|
||||
content: '\e858';
|
||||
}
|
||||
|
||||
.icon-corner-left-up:before {
|
||||
content: '\e859';
|
||||
}
|
||||
|
||||
.icon-corner-up-left:before {
|
||||
content: '\e85a';
|
||||
}
|
||||
|
||||
.icon-corner-up-right:before {
|
||||
content: '\e85b';
|
||||
}
|
||||
|
||||
.icon-corner-right-down:before {
|
||||
content: '\e85c';
|
||||
}
|
||||
|
||||
.icon-corner-right-up:before {
|
||||
content: '\e85d';
|
||||
}
|
||||
|
||||
.icon-cpu:before {
|
||||
content: '\e85e';
|
||||
}
|
||||
|
||||
.icon-credit-card:before {
|
||||
content: '\e85f';
|
||||
}
|
||||
|
||||
.icon-crosshair:before {
|
||||
content: '\e860';
|
||||
}
|
||||
|
||||
.icon-disc:before {
|
||||
content: '\e861';
|
||||
}
|
||||
|
||||
.icon-delete:before {
|
||||
content: '\e862';
|
||||
}
|
||||
|
||||
.icon-download-cloud:before {
|
||||
content: '\e863';
|
||||
}
|
||||
|
||||
.icon-download:before {
|
||||
content: '\e864';
|
||||
}
|
||||
|
||||
.icon-droplet:before {
|
||||
content: '\e865';
|
||||
}
|
||||
|
||||
.icon-edit-2:before {
|
||||
content: '\e866';
|
||||
}
|
||||
|
||||
.icon-edit:before {
|
||||
content: '\e867';
|
||||
}
|
||||
|
||||
.icon-edit-1:before {
|
||||
content: '\e868';
|
||||
}
|
||||
|
||||
.icon-external-link:before {
|
||||
content: '\e869';
|
||||
}
|
||||
|
||||
.icon-eye:before {
|
||||
content: '\e86a';
|
||||
}
|
||||
|
||||
.icon-feather:before {
|
||||
content: '\e86b';
|
||||
}
|
||||
|
||||
.icon-facebook:before {
|
||||
content: '\e86c';
|
||||
}
|
||||
|
||||
.icon-file-minus:before {
|
||||
content: '\e86d';
|
||||
}
|
||||
|
||||
.icon-eye-off:before {
|
||||
content: '\e86e';
|
||||
}
|
||||
|
||||
.icon-fast-forward:before {
|
||||
content: '\e86f';
|
||||
}
|
||||
|
||||
.icon-file-text:before {
|
||||
content: '\e870';
|
||||
}
|
||||
|
||||
.icon-film:before {
|
||||
content: '\e871';
|
||||
}
|
||||
|
||||
.icon-file:before {
|
||||
content: '\e872';
|
||||
}
|
||||
|
||||
.icon-file-plus:before {
|
||||
content: '\e873';
|
||||
}
|
||||
|
||||
.icon-folder:before {
|
||||
content: '\e874';
|
||||
}
|
||||
|
||||
.icon-filter:before {
|
||||
content: '\e875';
|
||||
}
|
||||
|
||||
.icon-flag:before {
|
||||
content: '\e876';
|
||||
}
|
||||
|
||||
.icon-globe:before {
|
||||
content: '\e877';
|
||||
}
|
||||
|
||||
.icon-grid:before {
|
||||
content: '\e878';
|
||||
}
|
||||
|
||||
.icon-heart:before {
|
||||
content: '\e879';
|
||||
}
|
||||
|
||||
.icon-home:before {
|
||||
content: '\e87a';
|
||||
}
|
||||
|
||||
.icon-github:before {
|
||||
content: '\e87b';
|
||||
}
|
||||
|
||||
.icon-image:before {
|
||||
content: '\e87c';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: '\e87d';
|
||||
}
|
||||
|
||||
.icon-layers:before {
|
||||
content: '\e87e';
|
||||
}
|
||||
|
||||
.icon-info:before {
|
||||
content: '\e87f';
|
||||
}
|
||||
|
||||
.icon-instagram:before {
|
||||
content: '\e880';
|
||||
}
|
||||
|
||||
.icon-layout:before {
|
||||
content: '\e881';
|
||||
}
|
||||
|
||||
.icon-link-2:before {
|
||||
content: '\e882';
|
||||
}
|
||||
|
||||
.icon-life-buoy:before {
|
||||
content: '\e883';
|
||||
}
|
||||
|
||||
.icon-link:before {
|
||||
content: '\e884';
|
||||
}
|
||||
|
||||
.icon-log-in:before {
|
||||
content: '\e885';
|
||||
}
|
||||
|
||||
.icon-list:before {
|
||||
content: '\e886';
|
||||
}
|
||||
|
||||
.icon-lock:before {
|
||||
content: '\e887';
|
||||
}
|
||||
|
||||
.icon-log-out:before {
|
||||
content: '\e888';
|
||||
}
|
||||
|
||||
.icon-loader:before {
|
||||
content: '\e889';
|
||||
}
|
||||
|
||||
.icon-mail:before {
|
||||
content: '\e88a';
|
||||
}
|
||||
|
||||
.icon-maximize-2:before {
|
||||
content: '\e88b';
|
||||
}
|
||||
|
||||
.icon-map:before {
|
||||
content: '\e88c';
|
||||
}
|
||||
|
||||
.icon-map-pin:before {
|
||||
content: '\e88e';
|
||||
}
|
||||
|
||||
.icon-menu:before {
|
||||
content: '\e88f';
|
||||
}
|
||||
|
||||
.icon-message-circle:before {
|
||||
content: '\e890';
|
||||
}
|
||||
|
||||
.icon-message-square:before {
|
||||
content: '\e891';
|
||||
}
|
||||
|
||||
.icon-minimize-2:before {
|
||||
content: '\e892';
|
||||
}
|
||||
|
||||
.icon-mic-off:before {
|
||||
content: '\e893';
|
||||
}
|
||||
|
||||
.icon-minus-circle:before {
|
||||
content: '\e894';
|
||||
}
|
||||
|
||||
.icon-mic:before {
|
||||
content: '\e895';
|
||||
}
|
||||
|
||||
.icon-minus-square:before {
|
||||
content: '\e896';
|
||||
}
|
||||
|
||||
.icon-minus:before {
|
||||
content: '\e897';
|
||||
}
|
||||
|
||||
.icon-moon:before {
|
||||
content: '\e898';
|
||||
}
|
||||
|
||||
.icon-monitor:before {
|
||||
content: '\e899';
|
||||
}
|
||||
|
||||
.icon-more-vertical:before {
|
||||
content: '\e89a';
|
||||
}
|
||||
|
||||
.icon-more-horizontal:before {
|
||||
content: '\e89b';
|
||||
}
|
||||
|
||||
.icon-move:before {
|
||||
content: '\e89c';
|
||||
}
|
||||
|
||||
.icon-music:before {
|
||||
content: '\e89d';
|
||||
}
|
||||
|
||||
.icon-navigation-2:before {
|
||||
content: '\e89e';
|
||||
}
|
||||
|
||||
.icon-navigation:before {
|
||||
content: '\e89f';
|
||||
}
|
||||
|
||||
.icon-octagon:before {
|
||||
content: '\e8a0';
|
||||
}
|
||||
|
||||
.icon-package:before {
|
||||
content: '\e8a1';
|
||||
}
|
||||
|
||||
.icon-pause-circle:before {
|
||||
content: '\e8a2';
|
||||
}
|
||||
|
||||
.icon-pause:before {
|
||||
content: '\e8a3';
|
||||
}
|
||||
|
||||
.icon-percent:before {
|
||||
content: '\e8a4';
|
||||
}
|
||||
|
||||
.icon-phone-call:before {
|
||||
content: '\e8a5';
|
||||
}
|
||||
|
||||
.icon-phone-forwarded:before {
|
||||
content: '\e8a6';
|
||||
}
|
||||
|
||||
.icon-phone-missed:before {
|
||||
content: '\e8a7';
|
||||
}
|
||||
|
||||
.icon-phone-off:before {
|
||||
content: '\e8a8';
|
||||
}
|
||||
|
||||
.icon-phone-incoming:before {
|
||||
content: '\e8a9';
|
||||
}
|
||||
|
||||
.icon-phone:before {
|
||||
content: '\e8aa';
|
||||
}
|
||||
|
||||
.icon-phone-outgoing:before {
|
||||
content: '\e8ab';
|
||||
}
|
||||
|
||||
.icon-pie-chart:before {
|
||||
content: '\e8ac';
|
||||
}
|
||||
|
||||
.icon-play-circle:before {
|
||||
content: '\e8ad';
|
||||
}
|
||||
|
||||
.icon-play:before {
|
||||
content: '\e8ae';
|
||||
}
|
||||
|
||||
.icon-plus-square:before {
|
||||
content: '\e8af';
|
||||
}
|
||||
|
||||
.icon-plus-circle:before {
|
||||
content: '\e8b0';
|
||||
}
|
||||
|
||||
.icon-plus:before {
|
||||
content: '\e8b1';
|
||||
}
|
||||
|
||||
.icon-pocket:before {
|
||||
content: '\e8b2';
|
||||
}
|
||||
|
||||
.icon-printer:before {
|
||||
content: '\e8b3';
|
||||
}
|
||||
|
||||
.icon-power:before {
|
||||
content: '\e8b4';
|
||||
}
|
||||
|
||||
.icon-radio:before {
|
||||
content: '\e8b5';
|
||||
}
|
||||
|
||||
.icon-repeat:before {
|
||||
content: '\e8b6';
|
||||
}
|
||||
|
||||
.icon-refresh-ccw:before {
|
||||
content: '\e8b7';
|
||||
}
|
||||
|
||||
.icon-rewind:before {
|
||||
content: '\e8b8';
|
||||
}
|
||||
|
||||
.icon-rotate-ccw:before {
|
||||
content: '\e8b9';
|
||||
}
|
||||
|
||||
.icon-refresh-cw:before {
|
||||
content: '\e8ba';
|
||||
}
|
||||
|
||||
.icon-rotate-cw:before {
|
||||
content: '\e8bb';
|
||||
}
|
||||
|
||||
.icon-save:before {
|
||||
content: '\e8bc';
|
||||
}
|
||||
|
||||
.icon-search:before {
|
||||
content: '\e8bd';
|
||||
}
|
||||
|
||||
.icon-server:before {
|
||||
content: '\e8be';
|
||||
}
|
||||
|
||||
.icon-scissors:before {
|
||||
content: '\e8bf';
|
||||
}
|
||||
|
||||
.icon-share-2:before {
|
||||
content: '\e8c0';
|
||||
}
|
||||
|
||||
.icon-share:before {
|
||||
content: '\e8c1';
|
||||
}
|
||||
|
||||
.icon-shield:before {
|
||||
content: '\e8c2';
|
||||
}
|
||||
|
||||
.icon-settings:before {
|
||||
content: '\e8c3';
|
||||
}
|
||||
|
||||
.icon-skip-back:before {
|
||||
content: '\e8c4';
|
||||
}
|
||||
|
||||
.icon-shuffle:before {
|
||||
content: '\e8c5';
|
||||
}
|
||||
|
||||
.icon-sidebar:before {
|
||||
content: '\e8c6';
|
||||
}
|
||||
|
||||
.icon-skip-forward:before {
|
||||
content: '\e8c7';
|
||||
}
|
||||
|
||||
.icon-slack:before {
|
||||
content: '\e8c8';
|
||||
}
|
||||
|
||||
.icon-slash:before {
|
||||
content: '\e8c9';
|
||||
}
|
||||
|
||||
.icon-smartphone:before {
|
||||
content: '\e8ca';
|
||||
}
|
||||
|
||||
.icon-square:before {
|
||||
content: '\e8cb';
|
||||
}
|
||||
|
||||
.icon-speaker:before {
|
||||
content: '\e8cc';
|
||||
}
|
||||
|
||||
.icon-star:before {
|
||||
content: '\e8cd';
|
||||
}
|
||||
|
||||
.icon-stop-circle:before {
|
||||
content: '\e8ce';
|
||||
}
|
||||
|
||||
.icon-sun:before {
|
||||
content: '\e8cf';
|
||||
}
|
||||
|
||||
.icon-sunrise:before {
|
||||
content: '\e8d0';
|
||||
}
|
||||
|
||||
.icon-tablet:before {
|
||||
content: '\e8d1';
|
||||
}
|
||||
|
||||
.icon-tag:before {
|
||||
content: '\e8d2';
|
||||
}
|
||||
|
||||
.icon-sunset:before {
|
||||
content: '\e8d3';
|
||||
}
|
||||
|
||||
.icon-target:before {
|
||||
content: '\e8d4';
|
||||
}
|
||||
|
||||
.icon-thermometer:before {
|
||||
content: '\e8d5';
|
||||
}
|
||||
|
||||
.icon-thumbs-up:before {
|
||||
content: '\e8d6';
|
||||
}
|
||||
|
||||
.icon-thumbs-down:before {
|
||||
content: '\e8d7';
|
||||
}
|
||||
|
||||
.icon-toggle-left:before {
|
||||
content: '\e8d8';
|
||||
}
|
||||
|
||||
.icon-toggle-right:before {
|
||||
content: '\e8d9';
|
||||
}
|
||||
|
||||
.icon-trash-2:before {
|
||||
content: '\e8da';
|
||||
}
|
||||
|
||||
.icon-trash:before {
|
||||
content: '\e8db';
|
||||
}
|
||||
|
||||
.icon-trending-up:before {
|
||||
content: '\e8dc';
|
||||
}
|
||||
|
||||
.icon-trending-down:before {
|
||||
content: '\e8dd';
|
||||
}
|
||||
|
||||
.icon-triangle:before {
|
||||
content: '\e8de';
|
||||
}
|
||||
|
||||
.icon-type:before {
|
||||
content: '\e8df';
|
||||
}
|
||||
|
||||
.icon-twitter:before {
|
||||
content: '\e8e0';
|
||||
}
|
||||
|
||||
.icon-upload:before {
|
||||
content: '\e8e1';
|
||||
}
|
||||
|
||||
.icon-umbrella:before {
|
||||
content: '\e8e2';
|
||||
}
|
||||
|
||||
.icon-upload-cloud:before {
|
||||
content: '\e8e3';
|
||||
}
|
||||
|
||||
.icon-unlock:before {
|
||||
content: '\e8e4';
|
||||
}
|
||||
|
||||
.icon-user-check:before {
|
||||
content: '\e8e5';
|
||||
}
|
||||
|
||||
.icon-user-minus:before {
|
||||
content: '\e8e6';
|
||||
}
|
||||
|
||||
.icon-user-plus:before {
|
||||
content: '\e8e7';
|
||||
}
|
||||
|
||||
.icon-user-x:before {
|
||||
content: '\e8e8';
|
||||
}
|
||||
|
||||
.icon-user:before {
|
||||
content: '\e8e9';
|
||||
}
|
||||
|
||||
.icon-users:before {
|
||||
content: '\e8ea';
|
||||
}
|
||||
|
||||
.icon-video-off:before {
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-video:before {
|
||||
content: '\e8ec';
|
||||
}
|
||||
|
||||
.icon-voicemail:before {
|
||||
content: '\e8ed';
|
||||
}
|
||||
|
||||
.icon-volume-x:before {
|
||||
content: '\e8ee';
|
||||
}
|
||||
|
||||
.icon-volume-2:before {
|
||||
content: '\e8ef';
|
||||
}
|
||||
|
||||
.icon-volume-1:before {
|
||||
content: '\e8f0';
|
||||
}
|
||||
|
||||
.icon-volume:before {
|
||||
content: '\e8f1';
|
||||
}
|
||||
|
||||
.icon-watch:before {
|
||||
content: '\e8f2';
|
||||
}
|
||||
|
||||
.icon-wifi:before {
|
||||
content: '\e8f3';
|
||||
}
|
||||
|
||||
.icon-x-square:before {
|
||||
content: '\e8f4';
|
||||
}
|
||||
|
||||
.icon-wind:before {
|
||||
content: '\e8f5';
|
||||
}
|
||||
|
||||
.icon-x:before {
|
||||
content: '\e8f6';
|
||||
}
|
||||
|
||||
.icon-x-circle:before {
|
||||
content: '\e8f7';
|
||||
}
|
||||
|
||||
.icon-zap:before {
|
||||
content: '\e8f8';
|
||||
}
|
||||
|
||||
.icon-zoom-in:before {
|
||||
content: '\e8f9';
|
||||
}
|
||||
|
||||
.icon-zoom-out:before {
|
||||
content: '\e8fa';
|
||||
}
|
||||
|
||||
.icon-command:before {
|
||||
content: '\e8fb';
|
||||
}
|
||||
|
||||
.icon-cloud:before {
|
||||
content: '\e8fc';
|
||||
}
|
||||
|
||||
.icon-hash:before {
|
||||
content: '\e8fd';
|
||||
}
|
||||
|
||||
.icon-headphones:before {
|
||||
content: '\e8fe';
|
||||
}
|
||||
|
||||
.icon-underline:before {
|
||||
content: '\e8ff';
|
||||
}
|
||||
|
||||
.icon-italic:before {
|
||||
content: '\e900';
|
||||
}
|
||||
|
||||
.icon-bold:before {
|
||||
content: '\e901';
|
||||
}
|
||||
|
||||
.icon-crop:before {
|
||||
content: '\e902';
|
||||
}
|
||||
|
||||
.icon-help-circle:before {
|
||||
content: '\e903';
|
||||
}
|
||||
|
||||
.icon-paperclip:before {
|
||||
content: '\e904';
|
||||
}
|
||||
|
||||
.icon-shopping-cart:before {
|
||||
content: '\e905';
|
||||
}
|
||||
|
||||
.icon-tv:before {
|
||||
content: '\e906';
|
||||
}
|
||||
|
||||
.icon-wifi-off:before {
|
||||
content: '\e907';
|
||||
}
|
||||
|
||||
.icon-minimize:before {
|
||||
content: '\e88d';
|
||||
}
|
||||
|
||||
.icon-maximize:before {
|
||||
content: '\e908';
|
||||
}
|
||||
|
||||
.icon-gitlab:before {
|
||||
content: '\e909';
|
||||
}
|
||||
|
||||
.icon-sliders:before {
|
||||
content: '\e90a';
|
||||
}
|
||||
|
||||
.icon-star-on:before {
|
||||
content: '\e90b';
|
||||
}
|
||||
|
||||
.icon-heart-on:before {
|
||||
content: '\e90c';
|
||||
}
|
||||
3
public/favicon.svg
Normal file
3
public/favicon.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="36" height="34" viewBox="0 0 36 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 0C3.9459 0 0 3.51277 0 17C0 30.4872 3.9459 34 18 34C32.0541 34 36 30.4872 36 17C36 3.51277 32.0541 0 18 0ZM23.6724 25.7864L23.618 18.7247L19.412 25.7864H16.5335L12.3276 19.0262V25.7864H6.37997V8.21054H11.747L18.0544 18.5016L24.1955 8.21054H29.5626L29.617 25.7834H23.6694L23.6724 25.7864Z" fill="#FFDD00"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
27
public/index.html
Normal file
27
public/index.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<!-- Title -->
|
||||
<title>AngkorLife Emoticions CMS</title>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<!-- Favicon & Manifest -->
|
||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.svg" />
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<!-- Google Fonts Style -->
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,600" rel="stylesheet" />
|
||||
<link href="%PUBLIC_URL%/assets/fonts/datta/datta-icons.css" rel="stylesheet" />
|
||||
<link href="%PUBLIC_URL%/assets/fonts/feather/icon-font.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
9
src/App.test.tsx
Normal file
9
src/App.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
56
src/App.tsx
Normal file
56
src/App.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { toast, ToastContainer } from 'react-toastify';
|
||||
import { MutationCache, QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import axios from 'axios';
|
||||
import RootLayout from '@layout/RootLayout';
|
||||
|
||||
const App = () => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false
|
||||
},
|
||||
mutations: {
|
||||
retry: false
|
||||
}
|
||||
},
|
||||
queryCache: new QueryCache({
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
toast.error(`${error.response?.data?.errorMessage || error.message || 'An unexpected error occurred.'}`);
|
||||
} else {
|
||||
toast.error('An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
}),
|
||||
mutationCache: new MutationCache({
|
||||
onError: (error) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
toast.error(`${error.response?.data?.errorMessage || error.message || 'An unexpected error occurred.'}`);
|
||||
} else {
|
||||
toast.error('An unexpected error occurred.');
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ToastContainer
|
||||
position="top-center"
|
||||
autoClose={3500}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>
|
||||
<RootLayout />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
67
src/components/errorboundary/ErrorBoundary.tsx
Normal file
67
src/components/errorboundary/ErrorBoundary.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { JSX } from 'react';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: JSX.Element;
|
||||
onReset?: () => void;
|
||||
navigate: (path: string) => void;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
isModalOpen: boolean;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
state: ErrorBoundaryState = { hasError: false, error: null, isModalOpen: false };
|
||||
|
||||
/* 에러 발생시 fallback UI 표시하도록 상태 업데이트 */
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error, isModalOpen: true };
|
||||
}
|
||||
|
||||
/* 에러 내용과 발생 위치에 대한 log */
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.error('Uncaught error:', error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
handleCloseBtn = () => {
|
||||
this.setState({ hasError: false, error: null, isModalOpen: false });
|
||||
this.props.navigate('/');
|
||||
};
|
||||
|
||||
// confirm 버튼 활성화시 -> reset 동작
|
||||
handleConfirmBtn = () => {
|
||||
this.setState({ hasError: false, error: null, isModalOpen: false });
|
||||
if (this.props.onReset) {
|
||||
this.props.onReset();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { hasError, isModalOpen, error } = this.state;
|
||||
if (hasError) {
|
||||
const modalBtn = {
|
||||
closeTxt: 'Close',
|
||||
isCancelBtn: true // 필요에 따라 설정
|
||||
};
|
||||
const modalTxt = error || 'An unexpected error occurred.';
|
||||
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={isModalOpen}
|
||||
closeBtn={{ onClose: this.handleCloseBtn, buttonName: modalBtn.closeTxt }}
|
||||
{...(modalBtn.isCancelBtn ? { confirmBtn: { onClose: this.handleConfirmBtn, buttonName: 'Retry' } } : {})}
|
||||
>
|
||||
{'Error: ' + (modalTxt || 'An unexpected error occurred.')}
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
18
src/components/scrolltotop/ScrollToTop.tsx
Normal file
18
src/components/scrolltotop/ScrollToTop.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ScrollToTop = ({ children }: Props): ReactElement => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [location]);
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default ScrollToTop;
|
||||
104
src/components/ui/button/Button.module.scss
Normal file
104
src/components/ui/button/Button.module.scss
Normal file
@ -0,0 +1,104 @@
|
||||
@mixin button-styles(
|
||||
$background-color,
|
||||
$color,
|
||||
$border-radius: 4px,
|
||||
$border-color: transparent,
|
||||
$apply-hover: true,
|
||||
$hover-background: null,
|
||||
$hover-color: $color
|
||||
) {
|
||||
background-color: $background-color;
|
||||
color: $color;
|
||||
border-radius: $border-radius;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@if $apply-hover {
|
||||
&:hover {
|
||||
background-color: $hover-background;
|
||||
color: $hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-family: pretendard;
|
||||
line-height: 1px;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease;
|
||||
|
||||
// [action] 공통 스타일
|
||||
@mixin base-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
// ------------------------------------- SIZE --------------------------------------
|
||||
|
||||
// Delete(icon)
|
||||
&--xs {
|
||||
@include base-action;
|
||||
}
|
||||
|
||||
// Details, Edit
|
||||
&--sm {
|
||||
height: 30px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
// Close, Confirm, Process, Cancel
|
||||
&--md {
|
||||
height: 33px;
|
||||
min-width: 100px;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
// Add, Delete, Change, Reset
|
||||
&--lg {
|
||||
@include base-action;
|
||||
min-width: 74px;
|
||||
}
|
||||
|
||||
// ------------------------------------- SIZE --------------------------------------
|
||||
|
||||
// ------------------------------------- STYLE -------------------------------------
|
||||
|
||||
// Details, Edit
|
||||
&--rounded {
|
||||
@include button-styles(#fff, #7e84a3, 4px, #a1a7c4, #fff, #6c757d, #fff);
|
||||
}
|
||||
|
||||
// Confirm
|
||||
&--confirm {
|
||||
@include button-styles(#fec702, #fff, 50px, null, true, #fdde6d, #fff);
|
||||
}
|
||||
|
||||
// Process
|
||||
&--process {
|
||||
@include button-styles(#fec702, #fff, 4px, null, true, #fdde6d, #fff);
|
||||
}
|
||||
|
||||
// Close, Cancel
|
||||
&--close {
|
||||
@include button-styles(#fff, #858c97, 50px, #c2cad4, false);
|
||||
}
|
||||
|
||||
// Add, Reset, Change, Delete
|
||||
&--action {
|
||||
@include button-styles(#fff, #7e84a3, 4px, #d5d7e3, false);
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------- STYLE -------------------------------------
|
||||
37
src/components/ui/button/Button.tsx
Normal file
37
src/components/ui/button/Button.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import styles from './Button.module.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface ButtonProps {
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
color?: 'rounded' | 'confirm' | 'process' | 'close' | 'action' | 'disabled';
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
value?: string | number;
|
||||
name?: string;
|
||||
children: React.ReactNode;
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Button: React.FC<ButtonProps> = ({
|
||||
size = 'sm',
|
||||
color = 'rounded',
|
||||
type = 'button',
|
||||
value,
|
||||
name,
|
||||
children,
|
||||
onClick,
|
||||
disabled
|
||||
}) => {
|
||||
const classNames = cn('button', `button--${size}`, `button--${color}`);
|
||||
|
||||
return (
|
||||
<button type={type} className={classNames} value={value} name={name} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Button;
|
||||
20
src/components/ui/button/SortButton.module.scss
Normal file
20
src/components/ui/button/SortButton.module.scss
Normal file
@ -0,0 +1,20 @@
|
||||
.btn_sort {
|
||||
background: none;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.default_arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
}
|
||||
43
src/components/ui/button/SortButton.tsx
Normal file
43
src/components/ui/button/SortButton.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import styles from './SortButton.module.scss';
|
||||
import SortDown from '@styles/images/icons/ic-arrow-down.svg';
|
||||
import SortUp from '@styles/images/icons/ic-arrow-up.svg';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
column: string;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
dataList: {
|
||||
content: object[];
|
||||
} | null;
|
||||
sortRequest: {
|
||||
sort: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const SortButton = ({ label, column, onClick, dataList, sortRequest }: Props) => {
|
||||
const isSortActive = sortRequest.sort[0] && sortRequest.sort[0].startsWith(column);
|
||||
const isAscending = sortRequest.sort[0] === `${column},asc`;
|
||||
|
||||
return (
|
||||
<button className={cn('btn_sort')} onClick={onClick} disabled={dataList?.content.length === 0}>
|
||||
{label}
|
||||
{isSortActive ? (
|
||||
<i className={cn('arrow')}>
|
||||
<img src={isAscending ? SortUp : SortDown} alt="arrow_icon" />
|
||||
</i>
|
||||
) : (
|
||||
<i className={cn('arrow', 'default_arrow')}>
|
||||
<img src={SortUp} alt="arrow_icon" />
|
||||
<img src={SortDown} alt="arrow_icon" />
|
||||
</i>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortButton;
|
||||
13
src/components/ui/loader/Loader.tsx
Normal file
13
src/components/ui/loader/Loader.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
const Loader: React.FC = () => {
|
||||
return (
|
||||
<div className="loader-bg">
|
||||
<div className="loader-tarck">
|
||||
<div className="loader-fill" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loader;
|
||||
71
src/components/ui/modal/AlertModal.module.scss
Normal file
71
src/components/ui/modal/AlertModal.module.scss
Normal file
@ -0,0 +1,71 @@
|
||||
.container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1001;
|
||||
background: rgba(0, 0, 0, 0.5); // 어두운 배경
|
||||
overflow-y: auto;
|
||||
|
||||
.modal-wrap {
|
||||
position: relative;
|
||||
width: 500px; // 모달의 기본 너비
|
||||
max-width: 90%; // 작은 화면에서 반응형
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.2);
|
||||
padding: 36px 40px 30px;
|
||||
font-family: Pretendard, sans-serif;
|
||||
|
||||
.body {
|
||||
color: #505050;
|
||||
font-family: Pretendard;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
white-space: pre-line;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
.btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.confirm {
|
||||
background-color: #fec702;
|
||||
border-radius: 50px;
|
||||
color: #fff;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: darken(#fec702, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
background-color: #fff;
|
||||
border: 1px solid #c2cad4;
|
||||
border-radius: 50px;
|
||||
color: #858c97;
|
||||
|
||||
&:hover {
|
||||
background-color: darken(#fff, 3%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/components/ui/modal/AlertModal.tsx
Normal file
64
src/components/ui/modal/AlertModal.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Button from '../button/Button';
|
||||
|
||||
interface Button {
|
||||
onClose: () => void;
|
||||
buttonName: string;
|
||||
}
|
||||
|
||||
interface AlertModalProps {
|
||||
isOpen: boolean;
|
||||
closeBtn?: Button;
|
||||
confirmBtn?: Button;
|
||||
children: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const AlertModal = ({ isOpen, closeBtn, confirmBtn, children, disabled }: AlertModalProps) => {
|
||||
// 모달 열린 상태에선 스크롤 제어
|
||||
useEffect(() => {
|
||||
if (isOpen && document.body.style.overflow !== 'hidden') {
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (!document.querySelector('.modal_wrapper')) {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal_wrapper" onClick={(e) => handleClickOutside(e)}>
|
||||
<div className="black_bg"></div>
|
||||
<div className="alert_modal" onClick={(e) => e.stopPropagation()}>
|
||||
<p className="modal_txt">{children}</p>
|
||||
<div className="btn_alert">
|
||||
{closeBtn && (
|
||||
<Button size="md" color="close" onClick={closeBtn.onClose}>
|
||||
{closeBtn.buttonName}
|
||||
</Button>
|
||||
)}
|
||||
{confirmBtn && (
|
||||
<Button size="md" color="confirm" onClick={confirmBtn.onClose} disabled={disabled}>
|
||||
{confirmBtn.buttonName}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default AlertModal;
|
||||
30
src/components/ui/modal/Modal.tsx
Normal file
30
src/components/ui/modal/Modal.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: (isOpen: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Modal: React.FC<Props> = ({ isOpen, onClose, children }) => {
|
||||
const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if ((event.target as Element).className === 'black_bg') {
|
||||
onClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="modal_wrapper" onClick={(e) => handleClickOutside(e)}>
|
||||
<div className="black_bg"></div>
|
||||
<div className="modal_content">{children}</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
55
src/components/ui/pagination/Pagination.tsx
Normal file
55
src/components/ui/pagination/Pagination.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import prevImg from '@icons/chevron-left.svg';
|
||||
import nextImg from '@icons/chevron-right.svg';
|
||||
|
||||
interface Props {
|
||||
totalItems: number;
|
||||
itemsPerPage: number;
|
||||
currentPage: number;
|
||||
onPaginate: (pageNumber: number) => void;
|
||||
}
|
||||
|
||||
const Pagination = ({ totalItems, itemsPerPage, currentPage, onPaginate }: Props) => {
|
||||
const totalPage = Math.ceil(totalItems / itemsPerPage);
|
||||
const pagesToShow = 10;
|
||||
|
||||
return (
|
||||
<div className="pagination_jc">
|
||||
<button
|
||||
className={currentPage === 1 ? 'disabled' : 'prev'}
|
||||
onClick={() => {
|
||||
onPaginate(currentPage - 1);
|
||||
}}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<img src={prevImg} alt="prev Button" />
|
||||
</button>
|
||||
{Array.from(
|
||||
{ length: pagesToShow },
|
||||
(_, index) => Math.floor((currentPage - 1) / pagesToShow) * pagesToShow + 1 + index
|
||||
).map((num) => {
|
||||
if (num <= totalPage) {
|
||||
return (
|
||||
<button key={num} onClick={() => onPaginate(num)} className={currentPage === num ? 'active' : ''}>
|
||||
{num}
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})}
|
||||
<button
|
||||
className={currentPage !== totalPage && totalItems !== 0 ? 'next' : 'disabled'}
|
||||
onClick={() => {
|
||||
onPaginate(currentPage + 1);
|
||||
}}
|
||||
disabled={currentPage === totalPage || totalItems === 0}
|
||||
>
|
||||
<img src={nextImg} alt="next Button" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
45
src/components/ui/radio/Radio.module.scss
Normal file
45
src/components/ui/radio/Radio.module.scss
Normal file
@ -0,0 +1,45 @@
|
||||
.radio {
|
||||
margin: 0 16px 0 0;
|
||||
padding-left: 23px;
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('~@icons/btn-radio-default.svg');
|
||||
background-position: left;
|
||||
}
|
||||
|
||||
.radio {
|
||||
&.checked {
|
||||
cursor: default;
|
||||
background-image: url('~@icons/btn-radio-active.svg');
|
||||
}
|
||||
}
|
||||
|
||||
input[type='radio'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
|
||||
.chart_radio_label {
|
||||
font-family: 'Pretendard';
|
||||
padding: 0 12px;
|
||||
display: inline-block;
|
||||
line-height: 36px;
|
||||
color: #131523;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
background: none;
|
||||
margin: 0;
|
||||
width: 70px;
|
||||
text-align: center;
|
||||
|
||||
&.checked {
|
||||
background: #fec702;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.input_name {
|
||||
color: #fff;
|
||||
}
|
||||
52
src/components/ui/radio/Radio.tsx
Normal file
52
src/components/ui/radio/Radio.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
import styles from './Radio.module.scss';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
export interface RadioOption {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface RadioGroupProps {
|
||||
className?: 'radio' | 'chart_radio_label';
|
||||
options: RadioOption[];
|
||||
name: string;
|
||||
value: string;
|
||||
onChange: (option: RadioOption) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Radio: React.FC<RadioGroupProps> = ({
|
||||
className = 'radio',
|
||||
options,
|
||||
name,
|
||||
value,
|
||||
onChange,
|
||||
disabled = false
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.id}
|
||||
className={cn(className, {
|
||||
['checked']: value === option.label
|
||||
})}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={name}
|
||||
checked={value === option.id}
|
||||
onChange={() => onChange(option)}
|
||||
disabled={disabled}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Radio;
|
||||
37
src/components/ui/searchBar/SearchBar.module.scss
Normal file
37
src/components/ui/searchBar/SearchBar.module.scss
Normal file
@ -0,0 +1,37 @@
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #d5d7e3;
|
||||
border-radius: 18px;
|
||||
width: 300px;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
width: 200px; // 작은 화면에서 중간 크기
|
||||
}
|
||||
|
||||
button {
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
padding-left: 10px;
|
||||
|
||||
&.disabled {
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
border: 0;
|
||||
padding: 6px 12px;
|
||||
background-color: #fff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #131523;
|
||||
appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
}
|
||||
28
src/components/ui/searchBar/SearchBar.tsx
Normal file
28
src/components/ui/searchBar/SearchBar.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import styles from './SearchBar.module.scss';
|
||||
import SearchIcon from '@images/icons/ic-search.svg';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface Props {
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLInputElement>;
|
||||
placeholderText: string;
|
||||
}
|
||||
|
||||
const SearchBar = forwardRef<HTMLInputElement, Props>(({ onClick, onKeyDown, placeholderText }, ref) => {
|
||||
return (
|
||||
<form name="searchForm" className={cn('search-form')}>
|
||||
<button onClick={onClick}>
|
||||
<img src={SearchIcon} alt="SearchIcon" />
|
||||
</button>
|
||||
<input type="search" onKeyDown={onKeyDown} placeholder={placeholderText} ref={ref} defaultValue="" />
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
||||
SearchBar.displayName = 'SearchBar';
|
||||
|
||||
export default SearchBar;
|
||||
19
src/hooks/useModal.ts
Normal file
19
src/hooks/useModal.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ModalState<T> {
|
||||
isOpen: boolean;
|
||||
modalData?: T;
|
||||
}
|
||||
|
||||
export const useModal = <T>() => {
|
||||
const [modal, setModal] = useState<ModalState<T>>({ isOpen: false });
|
||||
|
||||
const handleToggleModal = (isOpen: boolean, modalData?: T) => {
|
||||
setModal({
|
||||
isOpen,
|
||||
modalData: isOpen ? modalData : undefined
|
||||
});
|
||||
};
|
||||
|
||||
return { ...modal, handleToggleModal };
|
||||
};
|
||||
25
src/index.tsx
Normal file
25
src/index.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { Provider } from 'react-redux';
|
||||
import { PersistGate } from 'redux-persist/integration/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
|
||||
import { persistor, store } from './redux';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
import './styles/custom/custom.scss';
|
||||
import './styles/styles.css';
|
||||
import './styles/common.css';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<App />
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
reportWebVitals();
|
||||
31
src/layout/AuthLayout.tsx
Normal file
31
src/layout/AuthLayout.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import SignIn from '@pages/authentication/SignIn';
|
||||
import IntegrationProcess from '@pages/authentication/IntegrationProcess';
|
||||
import SecondaryCertification from '@pages/authentication/SecondaryCertification';
|
||||
|
||||
import loginLogo from '@images/logo/login-logo.png';
|
||||
|
||||
const AuthLayout = () => {
|
||||
return (
|
||||
<div className="auth-wrapper">
|
||||
<div className="auth-content">
|
||||
<div className="card">
|
||||
<div className="card-body text-center">
|
||||
<div className="login_logo">
|
||||
<img src={loginLogo} alt="login_logo" />
|
||||
</div>
|
||||
<Routes>
|
||||
<Route path="/" element={<SignIn />} />
|
||||
<Route path="/integration" element={<IntegrationProcess />} />
|
||||
<Route path="/secondCert" element={<SecondaryCertification />} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthLayout;
|
||||
96
src/layout/MasterLayout.tsx
Normal file
96
src/layout/MasterLayout.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { RootState } from '@redux/index';
|
||||
import ErrorBoundary from '@components/errorboundary/ErrorBoundary';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useMenuPrivacyQuery } from '@api/menuAPI';
|
||||
import Loader from '@components/ui/loader/Loader';
|
||||
import Header from './components/header/Header';
|
||||
import Sidebar from './components/Sidebar';
|
||||
|
||||
const MasterLayout = () => {
|
||||
const authLv = {
|
||||
level: useSelector((state: RootState) => state.auth.userData?.auth as string)
|
||||
};
|
||||
const { data: menuItems = [], isLoading } = useMenuPrivacyQuery(authLv);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && menuItems.length > 0 && location.pathname === '/') {
|
||||
navigate(menuItems[0]?.menuLink);
|
||||
}
|
||||
}, [isLoading, menuItems, location]);
|
||||
|
||||
interface DynamicRouteCompnentProps {
|
||||
path: string;
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
const DynamicRouteComponent: React.FC<DynamicRouteCompnentProps> = ({ path, componentName }) => {
|
||||
const menuPath = path.replace(/\s/g, '');
|
||||
const menuName = componentName.replace(/\s/g, '');
|
||||
|
||||
const Component = React.lazy(() =>
|
||||
import(`../pages${menuPath}/${menuName}`).catch(() => ({ default: () => <>error</> }))
|
||||
);
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<Loader />}>
|
||||
<Component />
|
||||
</React.Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* <Header pagePath={menuItems ? menuItems[0]?.menuLink : ''} /> */}
|
||||
<Header pagePath={'/emoticons/manage'} />
|
||||
<div className="pcoded-main-container">
|
||||
<Sidebar menuInfo={menuItems} />
|
||||
<div className="pcoded-wrapper">
|
||||
<div className="pcoded-content">
|
||||
<div className="pcoded-inner-content">
|
||||
<div className="main-body">
|
||||
<div className="page-wrapper">
|
||||
<ErrorBoundary navigate={navigate}>
|
||||
<Routes>
|
||||
{menuItems?.map((item, index) =>
|
||||
item.children[0] ? (
|
||||
item.children?.map((cItem, cIndex) => (
|
||||
<Route
|
||||
key={cIndex}
|
||||
path={cItem.menuLink}
|
||||
element={<DynamicRouteComponent path={cItem.menuFolder} componentName={cItem.menuFile} />}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Route
|
||||
key={index}
|
||||
path={item.menuLink}
|
||||
element={<DynamicRouteComponent path={item.menuFolder} componentName={item.menuFile} />}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasterLayout;
|
||||
29
src/layout/RootLayout.tsx
Normal file
29
src/layout/RootLayout.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../redux';
|
||||
|
||||
import AuthLayout from './AuthLayout';
|
||||
import MasterLayout from './MasterLayout';
|
||||
import ScrollToTop from '@components/scrolltotop/ScrollToTop';
|
||||
|
||||
const RootLayout = () => {
|
||||
const isAuthenticated = useSelector((state: RootState) => state.auth.isAuthenticated);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const handleLogout = (): void => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
window.addEventListener('logout', handleLogout);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('logout', handleLogout);
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
return <ScrollToTop>{isAuthenticated ? <MasterLayout /> : <AuthLayout />}</ScrollToTop>;
|
||||
};
|
||||
|
||||
export default RootLayout;
|
||||
20
src/layout/TitleLayout.tsx
Normal file
20
src/layout/TitleLayout.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Row, Col } from 'react-bootstrap';
|
||||
|
||||
interface TitleLaytoutProps {
|
||||
title: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const TitleLayout: React.FC<TitleLaytoutProps> = ({ title, children }) => {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="page-title">{title}</h1>
|
||||
<Row>
|
||||
<Col sm={12}>{children}</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleLayout;
|
||||
85
src/layout/components/Sidebar.tsx
Normal file
85
src/layout/components/Sidebar.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
|
||||
import { MenuPrivacyResponse } from '@api/menuAPI';
|
||||
|
||||
import ScrollBar from 'react-perfect-scrollbar';
|
||||
|
||||
interface Props {
|
||||
menuInfo: MenuPrivacyResponse[];
|
||||
}
|
||||
|
||||
const Sidebar = ({ menuInfo }: Props) => {
|
||||
const location = useLocation();
|
||||
const naviLinkClass = ['nav-link'];
|
||||
|
||||
const navItem = (menuInfo: MenuPrivacyResponse, index: number) => {
|
||||
// 🔹 menuFolder에 "/emoticons/" 또는 "/manage/"가 포함되지 않으면 렌더링하지 않음
|
||||
if (!menuInfo.menuFolder.includes('/emoticons/') && !menuInfo.menuFolder.includes('/manage/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ([menuInfo.menuLink].concat(menuInfo.children.map((item) => item.menuLink)).includes(location.pathname)) {
|
||||
if (!naviLinkClass.includes('active')) {
|
||||
naviLinkClass.push('active');
|
||||
}
|
||||
} else {
|
||||
const activeIndex = naviLinkClass.findIndex((item) => item === 'active');
|
||||
|
||||
if (activeIndex > 0) {
|
||||
naviLinkClass.splice(activeIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuInfo.children[0]?.id) {
|
||||
return (
|
||||
<li key={index} className={naviLinkClass.join(' ')}>
|
||||
<Link to={menuInfo.children[0].menuLink} className={naviLinkClass.join(' ')}>
|
||||
<span className="pcoded-micon">
|
||||
<img src={menuInfo.menuIcon} />
|
||||
</span>
|
||||
<span className="pcoded-mtext" style={{ whiteSpace: 'pre-wrap', padding: '0 5px 0 0' }}>
|
||||
{menuInfo.menuName}
|
||||
</span>
|
||||
</Link>
|
||||
<ul className="pcoded-submenu">{menuInfo.children.map((item, index) => navItem(item, index))}</ul>
|
||||
</li>
|
||||
);
|
||||
} else if (menuInfo.parentId === 0) {
|
||||
return (
|
||||
<li key={index} className={naviLinkClass.join(' ')}>
|
||||
<Link to={menuInfo.menuLink} className={naviLinkClass.join(' ')}>
|
||||
<span className="pcoded-micon">
|
||||
<img src={menuInfo.menuIcon} />
|
||||
</span>
|
||||
<span className="pcoded-mtext" style={{ whiteSpace: 'pre-wrap', padding: '0 5px 0 0' }}>
|
||||
{menuInfo.menuName}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<li key={index}>
|
||||
<Link to={menuInfo.menuLink} className={naviLinkClass.join(' ')}>
|
||||
{menuInfo.menuName}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="pcoded-navbar">
|
||||
<div className="navbar-wrapper">
|
||||
<div className="navbar-content datta-scroll">
|
||||
<ScrollBar>
|
||||
<ul className="nav pcoded-inner-navbar">{menuInfo.map((item, index) => navItem(item, index))}</ul>
|
||||
</ScrollBar>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
114
src/layout/components/header/Header.tsx
Normal file
114
src/layout/components/header/Header.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Dropdown } from 'react-bootstrap';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { RootState } from '@redux/index';
|
||||
import { logoutSuccess } from '@redux/auth';
|
||||
import { aesDecrypt } from '@utils/cryption';
|
||||
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
|
||||
import headerLogo from '@images/logo/logo-main.png';
|
||||
import imgBell from '@icons/ic-bell.svg';
|
||||
import downImg from '@icons/ic-chevron-down.svg';
|
||||
import userInfoImg from '@icons/ic-user.svg';
|
||||
import logoutImg from '@icons/ic-logout.svg';
|
||||
import AdminInfo from './my_info/AdminInfoModal';
|
||||
|
||||
interface Props {
|
||||
pagePath: string;
|
||||
}
|
||||
|
||||
const Header = ({ pagePath }: Props) => {
|
||||
const version = process.env.REACT_APP_VERSION;
|
||||
const userName = aesDecrypt(useSelector((state: RootState) => state.auth.userData?.userName as string));
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const headerClass = ['navbar', 'pcoded-header', 'navbar-expand-lg'];
|
||||
|
||||
const openModal = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logoutSuccess());
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
<AdminInfo onClose={() => setIsModalOpen(false)} />
|
||||
</Modal>
|
||||
<header className={headerClass.join(' ')}>
|
||||
<div className="navbar-brand header-logo">
|
||||
<Link to={pagePath}>
|
||||
<div style={{ display: 'flex', alignItems: 'end' }}>
|
||||
<img src={headerLogo} alt="angkor chat" />
|
||||
<span style={{ fontSize: '14px', marginLeft: '5px' }}>{version}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="collapse navbar-collapse">
|
||||
<ul className="navbar-nav ms-auto">
|
||||
<li>
|
||||
<Dropdown align="end">
|
||||
<Dropdown.Toggle variant={'link'} id="dropdown-basic">
|
||||
<span className="pcoded-micon">
|
||||
<img src={imgBell} alt="bell" />
|
||||
<div className="alarm">0</div>
|
||||
</span>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu align="end" className="notification">
|
||||
<div className="notification_box new_report">
|
||||
<span className="circle" />
|
||||
<Link to={'/'}>There is a new report</Link>
|
||||
</div>
|
||||
<div className="notification_box new_inquiry">
|
||||
<span className="circle" />
|
||||
<Link to={'/'}>There is a new inquiry</Link>
|
||||
</div>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</li>
|
||||
<li>
|
||||
<Dropdown align="end" className="drp-user">
|
||||
<Dropdown.Toggle variant={'link'} id="dropdown-basic" style={{ textDecoration: 'none' }}>
|
||||
<span className="userinfoname">{userName}</span>
|
||||
<img src={downImg} alt="ic_chevron_down" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="profile-notification">
|
||||
<ul className="pro-body">
|
||||
<li>
|
||||
<a id="userInfopop" className="dropdown-item" onClick={(e) => openModal(e)}>
|
||||
<img src={userInfoImg} alt="ic-user" />
|
||||
<span>My Info.</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a title="Logout" onClick={handleLogout}>
|
||||
<img src={logoutImg} alt="ic-logout" />
|
||||
<span>Logout</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
213
src/layout/components/header/my_info/AdminInfoModal.tsx
Normal file
213
src/layout/components/header/my_info/AdminInfoModal.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, { useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { InfoResponse, useInfoQuery } from '@api/authAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import { useCheckPasswordMutation, useUpdatePasswordMutation } from '@api/adminAPI';
|
||||
import { sha256Encrypt } from '@utils/cryption';
|
||||
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AUTH_CODE = '008';
|
||||
|
||||
const AdminInfo = ({ onClose }: Props) => {
|
||||
const [toggleChangePassword, setToggleChangePassword] = useState(true);
|
||||
|
||||
const { data: myInfo = {} as InfoResponse, isLoading: infoLoading, isError: infoError } = useInfoQuery();
|
||||
const {
|
||||
data: authCodeList = [],
|
||||
isLoading: authCodeLoading,
|
||||
isError: authCodeError
|
||||
} = useSimpleCodeListQuery(AUTH_CODE);
|
||||
|
||||
const Password: React.FC = () => {
|
||||
return (
|
||||
<div className="info-pw">
|
||||
<span>********</span>
|
||||
<Button size="sm" color="rounded" onClick={() => setToggleChangePassword(false)}>
|
||||
Change Password
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChangePassword: React.FC = () => {
|
||||
const [modal, setModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
|
||||
const [changePassword, setChangePassword] = useState({
|
||||
currentPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
isPasswordCheck: false
|
||||
});
|
||||
|
||||
const { mutate: checkPassword } = useCheckPasswordMutation();
|
||||
const { mutate: updataPassword } = useUpdatePasswordMutation();
|
||||
|
||||
const handleModalClose = (toggle: boolean) => {
|
||||
setModal((prevState) => ({ ...prevState, isOpen: false }));
|
||||
|
||||
if (toggle) {
|
||||
setToggleChangePassword(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCurrentPasswordCheck = () => {
|
||||
const request = {
|
||||
userId: myInfo.adminId,
|
||||
password: sha256Encrypt(changePassword.currentPassword)
|
||||
};
|
||||
|
||||
checkPassword(request, {
|
||||
onSuccess: () => {
|
||||
setChangePassword((prevState) => ({ ...prevState, isPasswordCheck: true }));
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'You can change your password.'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordChange = () => {
|
||||
const passwordPattern = /^(?=.*[0-9])(?=.*[!@#$%^&*()])(?=.{8,16}).*$/;
|
||||
const isValidPassword = passwordPattern.test(changePassword.newPassword);
|
||||
|
||||
if (!isValidPassword) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'New Password must be at least 8 characters and not more than 16 characters, including at least one digit/special character (!@#$%^&*()).'
|
||||
});
|
||||
} else if (changePassword.currentPassword === changePassword.newPassword) {
|
||||
setModal({ isOpen: true, txt: 'You cannot set the same password as your previous password.' });
|
||||
} else if (changePassword.newPassword !== changePassword.confirmPassword) {
|
||||
setModal({ isOpen: true, txt: 'Password is different.' });
|
||||
} else {
|
||||
const request = {
|
||||
userId: myInfo.adminId,
|
||||
password: sha256Encrypt(changePassword.newPassword)
|
||||
};
|
||||
|
||||
updataPassword(request, {
|
||||
onSuccess: () => {
|
||||
toast.success('Your password has been changed.');
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={modal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () => handleModalClose(false),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{modal.txt}
|
||||
</AlertModal>
|
||||
<div className="info-pw-wrap">
|
||||
{!changePassword.isPasswordCheck && (
|
||||
<div className="password_flex">
|
||||
<input
|
||||
type="password"
|
||||
className="new_password_form_control"
|
||||
placeholder="Enter Current Password"
|
||||
onChange={(e) => setChangePassword((prevState) => ({ ...prevState, currentPassword: e.target.value }))}
|
||||
/>
|
||||
<Button size="sm" color="rounded" onClick={handleCurrentPasswordCheck}>
|
||||
Confirm
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{changePassword.isPasswordCheck && (
|
||||
<>
|
||||
<input
|
||||
type="password"
|
||||
className="new_password_form_control"
|
||||
placeholder="Enter new Password"
|
||||
onChange={(e) => setChangePassword((prevState) => ({ ...prevState, newPassword: e.target.value }))}
|
||||
/>
|
||||
<div className="password_flex">
|
||||
<input
|
||||
type="password"
|
||||
className="new_password_form_control"
|
||||
placeholder="Re-enter new Password"
|
||||
onChange={(e) =>
|
||||
setChangePassword((prevState) => ({ ...prevState, confirmPassword: e.target.value }))
|
||||
}
|
||||
/>
|
||||
<Button color="rounded" onClick={handlePasswordChange} disabled={!changePassword.isPasswordCheck}>
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
if (infoLoading && authCodeLoading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (infoError || authCodeError) {
|
||||
return <span>데이터 로딩에 실패했습니다. 다시 시도해 주세요.</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="title">
|
||||
<span className="modal_title">My Info</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table my_info">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Administrator Name</th>
|
||||
<td>{myInfo.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>{myInfo.email}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Password</th>
|
||||
<td>{toggleChangePassword ? <Password /> : <ChangePassword />}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Phone Number</th>
|
||||
<td>{myInfo.phone}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Permission</th>
|
||||
<td>{authCodeLoading ? '' : authCodeList.find((item) => item.code === myInfo.auth)?.codeName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<td>{myInfo.groupName}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminInfo;
|
||||
113
src/pages/authentication/IntegrationProcess.tsx
Normal file
113
src/pages/authentication/IntegrationProcess.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FaUserCircle, FaLock } from 'react-icons/fa';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { integrationSuccess } from '@redux/auth';
|
||||
import { aesEncrypt } from '@utils/cryption';
|
||||
import { useCheckIdMutation } from '@api/authAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
|
||||
const FaUserCircleIcon = FaUserCircle as unknown as React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
const FaLockIcon = FaLock as unknown as React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
const IntegrationProcess: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalTxt, setModalTxt] = useState('');
|
||||
const [modalBtn, setModalBtn] = useState({
|
||||
isCancelBtn: false,
|
||||
closeTxt: 'Close'
|
||||
});
|
||||
const [angkorId, setAngkorId] = useState('');
|
||||
|
||||
const { mutate: checkId } = useCheckIdMutation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const request = {
|
||||
angkorId: angkorId
|
||||
};
|
||||
|
||||
checkId(request, {
|
||||
onSuccess: () => {
|
||||
setModalTxt(
|
||||
`Your AngkorLife User ID is ${angkorId}.
|
||||
Do you want to continue logging in?`
|
||||
);
|
||||
setModalBtn({ isCancelBtn: true, closeTxt: 'Cancel' });
|
||||
setIsModalOpen(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.response?.data.errorCode === 2000) {
|
||||
setModalTxt(
|
||||
`This AngkorLife User ID does not exist.
|
||||
Set the userID in the AngkorLife app.`
|
||||
);
|
||||
setIsModalOpen(true);
|
||||
} else {
|
||||
setModalTxt(error.response?.data.errorMessage as string);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCloseBtn = () => {
|
||||
if (modalBtn.isCancelBtn === true) {
|
||||
setModalBtn({ isCancelBtn: false, closeTxt: 'Close' });
|
||||
}
|
||||
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleConfirmBtn = () => {
|
||||
dispatch(integrationSuccess(aesEncrypt(angkorId)));
|
||||
|
||||
navigate('/secondCert');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={isModalOpen}
|
||||
closeBtn={{ onClose: () => handleCloseBtn(), buttonName: modalBtn.closeTxt }}
|
||||
{...(modalBtn.isCancelBtn ? { confirmBtn: { onClose: () => handleConfirmBtn(), buttonName: 'Confirm' } } : {})}
|
||||
>
|
||||
{modalTxt}
|
||||
</AlertModal>
|
||||
<div className="login_form">
|
||||
<span style={{ margin: '20px 0', display: 'block' }}>
|
||||
AngkorLife ID is not linked.
|
||||
<br />
|
||||
You need to link your AngkorLife ID to continue logging in.
|
||||
</span>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="input-group mb-3">
|
||||
<i>
|
||||
<FaUserCircleIcon />
|
||||
</i>
|
||||
<input
|
||||
type="text"
|
||||
id="angkorId"
|
||||
className="form-control"
|
||||
placeholder="Angkor User ID"
|
||||
value={angkorId}
|
||||
onChange={(e) => setAngkorId(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={angkorId === '' ? 'btn btn-primary shadow-2 mb-4 disabled' : 'btn btn-primary shadow-2 mb-4'}
|
||||
>
|
||||
Check
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IntegrationProcess;
|
||||
158
src/pages/authentication/SecondaryCertification.tsx
Normal file
158
src/pages/authentication/SecondaryCertification.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { RootState } from '@redux/index';
|
||||
import { loginAuthenticate } from '@redux/auth';
|
||||
import { aesDecrypt } from '@utils/cryption';
|
||||
|
||||
import Stopwatch from './components/Stopwatch';
|
||||
import { useAuthCheckMutation, useAuthCodeMutation } from '@api/authAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
|
||||
const SecondaryCertification: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalTxt, setModalTxt] = useState('');
|
||||
const [authCodeInfo, setAuthCodeInfo] = useState({ authKey: '', authNumber: '', userId: '' });
|
||||
const [authCodeMap, setAuthCodeMap] = useState(new Array(6).fill(''));
|
||||
|
||||
const authCodeMapRefs = useRef<(HTMLInputElement | null)[]>([]);
|
||||
|
||||
const { mutate: authCode } = useAuthCodeMutation();
|
||||
const { mutate: authCheck } = useAuthCheckMutation();
|
||||
const angkorId = useSelector((state: RootState) => state.auth.userData?.angkorUserId as string);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handelReset = useCallback(() => {
|
||||
const request = {
|
||||
id: aesDecrypt(angkorId)
|
||||
};
|
||||
|
||||
authCode(request, {
|
||||
onSuccess: (data) => {
|
||||
setAuthCodeInfo((prevState) => ({
|
||||
...prevState,
|
||||
authKey: data.tokenType + ' ' + data.authKey,
|
||||
userId: data.userId
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
setAuthCodeMap(new Array(6).fill(''));
|
||||
}, [angkorId, authCode]);
|
||||
|
||||
useEffect(() => {
|
||||
handelReset();
|
||||
authCodeMapRefs.current[0]?.focus();
|
||||
}, [handelReset]);
|
||||
|
||||
const submitAuthCheck = () => {
|
||||
authCheck(authCodeInfo, {
|
||||
onSuccess: (data) => {
|
||||
dispatch(loginAuthenticate(data.accessToken));
|
||||
|
||||
navigate('/');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
submitAuthCheck();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAuthCodeInfo((prevState) => ({
|
||||
...prevState,
|
||||
authNumber: authCodeMap.join('')
|
||||
}));
|
||||
}, [authCodeMap]);
|
||||
|
||||
const handleKeydown = (e: React.KeyboardEvent<HTMLElement>, index: number) => {
|
||||
const key = e.key;
|
||||
if (/^\d$/.test(key)) {
|
||||
setAuthCodeMap((codeMap) => codeMap.map((num, i) => (i === index ? key : num)));
|
||||
|
||||
authCodeMapRefs.current[index + 1]?.focus();
|
||||
} else if (['Backspace', 'Delete'].includes(key)) {
|
||||
setAuthCodeMap((codeMap) => {
|
||||
const updated = [...codeMap.slice(0, index), ...codeMap.slice(index + 1)];
|
||||
|
||||
updated.push('');
|
||||
|
||||
return updated;
|
||||
});
|
||||
|
||||
if (authCodeMap[index + 1] === '' || index === 5) {
|
||||
authCodeMapRefs.current[index - 1]?.focus();
|
||||
}
|
||||
} else if (key === 'ArrowLeft') {
|
||||
authCodeMapRefs.current[index - 1]?.focus();
|
||||
} else if (key === 'ArrowRight') {
|
||||
if (!authCodeMap[index]) {
|
||||
return;
|
||||
}
|
||||
authCodeMapRefs.current[index + 1]?.focus();
|
||||
} else if (key === 'Enter') {
|
||||
e.preventDefault();
|
||||
|
||||
submitAuthCheck();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = (index: number) => {
|
||||
if (authCodeMap[index] === '') {
|
||||
const currentTarget: number = authCodeMap.findIndex((value) => value === '');
|
||||
|
||||
authCodeMapRefs.current[currentTarget]?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal isOpen={isModalOpen} closeBtn={{ onClose: () => setIsModalOpen(false), buttonName: 'Close' }}>
|
||||
{modalTxt}
|
||||
</AlertModal>
|
||||
<div className="login_form secondcertification_content">
|
||||
<span style={{ margin: '20px 0', display: 'block', color: '#000', cursor: 'pointer' }}>
|
||||
Check the authentication number on AngkorLife.
|
||||
</span>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="second_certification">
|
||||
{authCodeMap.map((input, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
className="form-control"
|
||||
value={input}
|
||||
readOnly
|
||||
maxLength={1}
|
||||
onKeyDown={(e) => handleKeydown(e, index)}
|
||||
onClick={() => handleClick(index)}
|
||||
ref={(item) => {
|
||||
authCodeMapRefs.current[index] = item;
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<Stopwatch initialTime={180} onReset={() => handelReset()} />
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={authCodeMap.every((e: string) => e) ? 'btn btn-primary' : 'btn btn-primary disabled'}
|
||||
>
|
||||
Check
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondaryCertification;
|
||||
134
src/pages/authentication/SignIn.tsx
Normal file
134
src/pages/authentication/SignIn.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { FaUserCircle, FaLock } from 'react-icons/fa';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import '@styles/style.scss';
|
||||
import { sha256Encrypt, aesDecrypt } from '@utils/cryption';
|
||||
import { loginSuccess } from '@redux/auth';
|
||||
import { useLoginMutation } from '@api/authAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
|
||||
const FaUserCircleIcon = FaUserCircle as unknown as React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
const FaLockIcon = FaLock as unknown as React.FC<React.SVGProps<SVGSVGElement>>;
|
||||
|
||||
const SignIn: React.FC = () => {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [modalTxt, setModalTxt] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [rememberEmail, setRememberEmail] = useState(false);
|
||||
|
||||
const { mutate: login } = useLoginMutation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const emailInfo = window.localStorage.getItem('EmailSave');
|
||||
|
||||
if (emailInfo) {
|
||||
setRememberEmail(true);
|
||||
setEmail(emailInfo);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const request = {
|
||||
id: formData.get('email') as string,
|
||||
password: sha256Encrypt(formData.get('password') as string)
|
||||
};
|
||||
|
||||
login(request, {
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
// redux state update
|
||||
dispatch(loginSuccess(data));
|
||||
|
||||
// Remeber Email Check
|
||||
if (rememberEmail) {
|
||||
window.localStorage.setItem('EmailSave', email);
|
||||
} else {
|
||||
window.localStorage.removeItem('EmailSave');
|
||||
}
|
||||
|
||||
// Account Integration Check
|
||||
if (aesDecrypt(data.angkorUserId).length === 0) {
|
||||
navigate('/integration');
|
||||
} else {
|
||||
navigate('/secondCert');
|
||||
}
|
||||
} else {
|
||||
setModalTxt('An unknown error has occurred. Please retry again.');
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRememberEmailChange = () => {
|
||||
setRememberEmail(!rememberEmail);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal isOpen={isModalOpen} closeBtn={{ onClose: () => setIsModalOpen(false), buttonName: 'Close' }}>
|
||||
{modalTxt}
|
||||
</AlertModal>
|
||||
<div className="login_form">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="input-group mb-3">
|
||||
<i>
|
||||
<FaUserCircleIcon />
|
||||
</i>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
name="email"
|
||||
className="form-control"
|
||||
placeholder="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => {
|
||||
setEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="input-group mb-4">
|
||||
<i>
|
||||
<FaLockIcon />
|
||||
</i>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
className="form-control"
|
||||
placeholder="password"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group text-left">
|
||||
<div className="checkbox checkbox-fill d-inline saveemail">
|
||||
<input
|
||||
className="form-check-label"
|
||||
type="checkbox"
|
||||
name="checkbox-fill-a2"
|
||||
id="checkbox-fill-a2"
|
||||
checked={rememberEmail}
|
||||
onChange={handleRememberEmailChange}
|
||||
/>
|
||||
<label htmlFor="checkbox-fill-a2" className="cr form-check-label">
|
||||
<span>Remember Email</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn btn-primary shadow-2 mb-4">LOGIN</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
54
src/pages/authentication/components/Stopwatch.tsx
Normal file
54
src/pages/authentication/components/Stopwatch.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface StopwatchProps {
|
||||
initialTime: number;
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
const Stopwatch = ({ initialTime, onReset }: StopwatchProps) => {
|
||||
const [timeLeft, setTimeLeft] = useState(initialTime);
|
||||
|
||||
useEffect(() => {
|
||||
// 타이머가 0이 되면 카운트 정지
|
||||
if (timeLeft === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 매 초마다 timeLeft 1 씩 감소
|
||||
const intervalId = setInterval(() => {
|
||||
setTimeLeft((prevTimeLeft) => prevTimeLeft - 1);
|
||||
}, 1000);
|
||||
|
||||
// 컴포넌트가 언마운트 되거나 timeLeft 가 변경될때 마다 인터벌을 정리합니다.
|
||||
return () => clearInterval(intervalId);
|
||||
}, [timeLeft]);
|
||||
|
||||
// 타이머 리셋
|
||||
const resetTimer = () => {
|
||||
setTimeLeft(initialTime);
|
||||
onReset();
|
||||
};
|
||||
|
||||
// 시간 표기
|
||||
const formatTimeLeft = () => {
|
||||
const minutes = Math.floor(timeLeft / 60);
|
||||
const seconds = timeLeft % 60;
|
||||
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{formatTimeLeft()}</p>
|
||||
<span
|
||||
className="send_btn"
|
||||
style={{ margin: '20px 0', display: 'block', color: '#000', cursor: 'pointer' }}
|
||||
onClick={resetTimer}
|
||||
>
|
||||
Send code again
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stopwatch;
|
||||
42
src/pages/emoticons/givingEmoticons/GivingEmoticons.tsx
Normal file
42
src/pages/emoticons/givingEmoticons/GivingEmoticons.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import GivingEmoticonsList from './components/GivingEmoticonList';
|
||||
import EmoticonOwnerList from './components/EmoticonOwnerList';
|
||||
|
||||
type ComponentType = 'List' | 'Details';
|
||||
|
||||
export interface GivingEmoticonState {
|
||||
guid: number;
|
||||
emoticonName: string;
|
||||
}
|
||||
|
||||
const GivingEmoticons = () => {
|
||||
const [currentComponent, setCurrentComponent] = useState<ComponentType>('List');
|
||||
const [emoticonState, setEmoticonState] = useState<GivingEmoticonState>({ guid: 0, emoticonName: '' });
|
||||
|
||||
const handleDetailClick = () => {
|
||||
setCurrentComponent('Details');
|
||||
};
|
||||
|
||||
const handleBackClick = () => {
|
||||
setCurrentComponent('List');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<h1 className="page-title">Giving Emoticons</h1>
|
||||
{currentComponent === 'List' && (
|
||||
<div className="top-wrap">
|
||||
<GivingEmoticonsList onDetailClick={handleDetailClick} setEmoticonState={setEmoticonState} />
|
||||
</div>
|
||||
)}
|
||||
{currentComponent === 'Details' && (
|
||||
<div className="sub-wrap">
|
||||
<EmoticonOwnerList onBackClick={handleBackClick} emoticonState={emoticonState} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GivingEmoticons;
|
||||
@ -0,0 +1,293 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Card, Col, Row } from 'react-bootstrap';
|
||||
import { format, parse } from 'date-fns';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { handleSearchSubmit } from '@utils/searchBoxUtils';
|
||||
|
||||
import { GivingEmoticonState } from '../GivingEmoticons';
|
||||
import OwnerDetailModal from './OwnerDetailModal';
|
||||
import NonOwnerAddModal from './NonOwnerAddModal';
|
||||
|
||||
import styles from '../../manageEmoticons/ManageEmoticons.module.scss';
|
||||
import PrevIcon from '@icons/chevron-left.svg';
|
||||
import DropDownIcon from '@icons/ic-chevron-down.svg';
|
||||
import PlusIcon from '@images/icons/ic-plus.svg';
|
||||
import { EmoticonOwnerListRequest, useEmoticonOwnerListQuery } from '@api/emoticonAPI';
|
||||
import { setNewSortType } from '@utils/sortUtils';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import SortButton from '@components/ui/button/SortButton';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface EmoticonModalInfo {
|
||||
isOpen: boolean;
|
||||
modalName: string | null;
|
||||
}
|
||||
|
||||
export interface OwnerDetailData {
|
||||
angkorId: string | undefined;
|
||||
userId: string | undefined;
|
||||
emoticonCode: number;
|
||||
emoticonName: string;
|
||||
saveDate: string | undefined;
|
||||
expiredDate: string | undefined;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onBackClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
emoticonState: GivingEmoticonState;
|
||||
}
|
||||
|
||||
const EmoticonOwnerList = ({ onBackClick, emoticonState }: Props) => {
|
||||
const [modalInfo, setModalInfo] = useState<EmoticonModalInfo>({
|
||||
isOpen: false,
|
||||
modalName: null
|
||||
});
|
||||
const [emoticonOwnerListRequest, setEmoticonOwnerListRequest] = useState<EmoticonOwnerListRequest>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: '',
|
||||
status: ''
|
||||
});
|
||||
const [ownerDetailData, setOwnerDetailData] = useState<OwnerDetailData>({
|
||||
angkorId: '',
|
||||
userId: '',
|
||||
emoticonCode: emoticonState.guid,
|
||||
emoticonName: emoticonState.emoticonName,
|
||||
saveDate: '',
|
||||
expiredDate: ''
|
||||
});
|
||||
const [isDropBoxOpen, setIsDropBoxOpen] = useState(false);
|
||||
|
||||
// Ref
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const { data: emoticonOwnerListData = null } = useEmoticonOwnerListQuery(
|
||||
emoticonState.guid,
|
||||
emoticonOwnerListRequest
|
||||
);
|
||||
|
||||
// Func
|
||||
// Date 포맷팅 함수
|
||||
const formatDate = (dateNum: number) => {
|
||||
const dateStr = dateNum.toString();
|
||||
const parsedDate = parse(dateStr, 'yyyyMMdd', new Date());
|
||||
|
||||
if (dateStr.startsWith('9999')) return 'indefinitely';
|
||||
|
||||
return format(parsedDate, 'yyyy-MM-dd');
|
||||
};
|
||||
|
||||
// Sort 정렬 변경 핸들러
|
||||
const handleSortChange = (column: 'saveDt') => {
|
||||
setEmoticonOwnerListRequest((prev) => ({
|
||||
...prev,
|
||||
sort: setNewSortType(column, prev.sort)
|
||||
}));
|
||||
};
|
||||
|
||||
const handleDropBoxClick = () => {
|
||||
setIsDropBoxOpen(!isDropBoxOpen);
|
||||
};
|
||||
|
||||
// Status 종류 변경 핸들러
|
||||
const handleStatusChange = (status: string) => {
|
||||
setEmoticonOwnerListRequest((prev) => ({
|
||||
...prev,
|
||||
status: status
|
||||
}));
|
||||
setIsDropBoxOpen(false);
|
||||
};
|
||||
|
||||
// Detail Modal Open 핸들러
|
||||
const handleDetailModalOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const modalType = e.currentTarget.textContent;
|
||||
const selectedUserNo = Number(e.currentTarget.value);
|
||||
const userData = emoticonOwnerListData?.content.find((user) => user.userNo === selectedUserNo);
|
||||
|
||||
setModalInfo(() => ({
|
||||
isOpen: true,
|
||||
modalName: modalType
|
||||
}));
|
||||
|
||||
setOwnerDetailData(() => ({
|
||||
angkorId: userData?.angkorId,
|
||||
userId: userData?.userId,
|
||||
emoticonCode: emoticonState.guid,
|
||||
emoticonName: emoticonState.emoticonName,
|
||||
saveDate: userData?.saveDt,
|
||||
expiredDate: formatDate(userData?.expiredDate as number)
|
||||
}));
|
||||
};
|
||||
|
||||
// Add Modal Open 핸들러
|
||||
const handleAddModalOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const modalType = e.currentTarget.textContent;
|
||||
|
||||
setModalInfo(() => ({
|
||||
isOpen: true,
|
||||
modalName: modalType
|
||||
}));
|
||||
};
|
||||
|
||||
// Modal close 핸들러
|
||||
const handleModalClose = () => {
|
||||
setModalInfo(() => ({
|
||||
isOpen: false,
|
||||
modalName: null
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={modalInfo.isOpen} onClose={handleModalClose}>
|
||||
{modalInfo.modalName === 'Add' && (
|
||||
<NonOwnerAddModal emoticonGuid={emoticonState.guid} onCloseModal={handleModalClose} />
|
||||
)}
|
||||
{modalInfo.modalName === 'Details' && (
|
||||
<OwnerDetailModal ownerDetailData={ownerDetailData} onCloseModal={handleModalClose} />
|
||||
)}
|
||||
</Modal>
|
||||
<div className={cn('manage_emoticons')}>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<Card>
|
||||
<Card.Header className={cn('card_header')}>
|
||||
<Card.Title as="h5">
|
||||
<div>
|
||||
<button className="none_button" onClick={onBackClick}>
|
||||
<img src={PrevIcon} alt="prev" />
|
||||
</button>
|
||||
{`Gift Emoticon (${emoticonState.emoticonName})`}{' '}
|
||||
<span style={{ color: 'rgb(126 132 163 / 61%)', padding: '0 10px' }}>></span> Emoticon Owners
|
||||
List
|
||||
</div>
|
||||
</Card.Title>
|
||||
|
||||
<div className={cn('header_box')}>
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onKeyDown={(event) => handleSearchSubmit(event, searchRef, setEmoticonOwnerListRequest)}
|
||||
onClick={(event) => handleSearchSubmit(event, searchRef, setEmoticonOwnerListRequest)}
|
||||
placeholderText="Angkor Id, User Id"
|
||||
/>
|
||||
<Button size="lg" color="action" onClick={handleAddModalOpen}>
|
||||
<img src={PlusIcon} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className={cn('emoticon_list_table')}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-5">No</th>
|
||||
<th className="width-15">Angkor Id</th>
|
||||
<th className="width-15">User Id</th>
|
||||
<th className="width-15">Expired Date</th>
|
||||
<th className="width-15">
|
||||
<SortButton
|
||||
label="Save Date"
|
||||
column="saveDt"
|
||||
onClick={() => handleSortChange('saveDt')}
|
||||
dataList={emoticonOwnerListData}
|
||||
sortRequest={emoticonOwnerListRequest}
|
||||
/>
|
||||
</th>
|
||||
<th className={cn('status', 'width-15')}>
|
||||
<button className={cn('btn_sort')} onClick={handleDropBoxClick}>
|
||||
Status
|
||||
<i className={cn(`${isDropBoxOpen ? 'open' : 'status_img'}`)}>
|
||||
<img src={DropDownIcon} alt="icon" />
|
||||
</i>
|
||||
</button>
|
||||
{isDropBoxOpen && (
|
||||
<ul className={cn('status_dropdown_menu')}>
|
||||
<li>
|
||||
<button onClick={() => handleStatusChange('')}>
|
||||
<i className={cn('all')} />
|
||||
All
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleStatusChange('U')}>
|
||||
<i className={cn('normal')} />
|
||||
Using
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleStatusChange('H')}>
|
||||
<i className={cn('block')} />
|
||||
Hiding
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => handleStatusChange('D')}>
|
||||
<i className={cn('report')} />
|
||||
Deleted
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{emoticonOwnerListData?.content.map((item, idx: number) => (
|
||||
<tr key={idx}>
|
||||
<td className="width-5">{item.userNo}</td>
|
||||
<td className="width-15">{item.angkorId}</td>
|
||||
<td className="width-15">{item.userId}</td>
|
||||
<td className="width-15"> {formatDate(item.expiredDate)}</td>
|
||||
<td className="width-15">{item.saveDt}</td>
|
||||
<td className="width-15">
|
||||
<i
|
||||
style={
|
||||
item.status === 'U'
|
||||
? { background: '#21d59b' }
|
||||
: item.status === 'H'
|
||||
? { background: '#fec702' }
|
||||
: item.status === 'D'
|
||||
? { background: '#f0142f' }
|
||||
: { background: 'none' }
|
||||
}
|
||||
/>
|
||||
{['Using', 'Hiding', 'Deleted'].find((constants) => constants.includes(item.status))}
|
||||
</td>
|
||||
<td style={{ width: '15%' }}>
|
||||
<Button size="sm" color="rounded" value={item.userNo} onClick={handleDetailModalOpen}>
|
||||
Details
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<Pagination
|
||||
totalItems={emoticonOwnerListData?.totalElements ?? 0}
|
||||
itemsPerPage={emoticonOwnerListRequest.size}
|
||||
currentPage={emoticonOwnerListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setEmoticonOwnerListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmoticonOwnerList;
|
||||
@ -0,0 +1,57 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Col, Row } from 'react-bootstrap';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { GivingEmoticonState } from '../GivingEmoticons';
|
||||
import EmoticonTable from '../../manageEmoticons/components/EmoticonTable';
|
||||
|
||||
import styles from '../../manageEmoticons/ManageEmoticons.module.scss';
|
||||
import { EmoticonListRequest, useEmoticonListQuery } from '@api/emoticonAPI';
|
||||
import { SEARCH_FILTER_OPTIONS_EMOTICONS } from '@constants/constants';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface Props {
|
||||
onDetailClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
setEmoticonState: React.Dispatch<React.SetStateAction<GivingEmoticonState>>;
|
||||
}
|
||||
|
||||
const GivingEmoticonsList = ({ onDetailClick, setEmoticonState }: Props) => {
|
||||
const [emoticonListRequest, setEmoticonListRequest] = useState<EmoticonListRequest>({
|
||||
page: 1,
|
||||
size: 9,
|
||||
sort: [],
|
||||
emotiType: SEARCH_FILTER_OPTIONS_EMOTICONS[3].value
|
||||
});
|
||||
|
||||
// API
|
||||
const { data: emoticonListData } = useEmoticonListQuery(emoticonListRequest);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('manage_emoticons')}>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<Card>
|
||||
<Card.Header className={cn('card_header')}>
|
||||
<Card.Title as="h5">Gift Emoticon List</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<EmoticonTable
|
||||
btnName="Gift"
|
||||
emoticonListData={emoticonListData}
|
||||
emoticonListRequest={emoticonListRequest}
|
||||
onModalClick={onDetailClick}
|
||||
setEmoticonListRequest={setEmoticonListRequest}
|
||||
setEmoticonState={setEmoticonState}
|
||||
/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GivingEmoticonsList;
|
||||
@ -0,0 +1,276 @@
|
||||
.giving_emoticon_table {
|
||||
padding-bottom: 40px;
|
||||
font-family: pretendard;
|
||||
|
||||
h5 {
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
tr {
|
||||
color: #131523;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
th {
|
||||
width: 140px;
|
||||
font-weight: 600;
|
||||
padding: 1.0625rem 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
td {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
border-left: 1px solid #e6e9f4;
|
||||
padding: 15px 0.5rem;
|
||||
width: 540px;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
td {
|
||||
height: 70px; /* 첫 번째 td의 높이 */
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
td {
|
||||
height: 150px; /* 두 번째 td의 높이 */
|
||||
overflow-y: scroll;
|
||||
background-color: #dbdbdb24;
|
||||
}
|
||||
}
|
||||
}
|
||||
.date_picker {
|
||||
display: flex;
|
||||
width: 140px;
|
||||
align-items: center;
|
||||
color: #131523;
|
||||
background-color: #f5f6fa;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
border: 1px solid #c2cad4;
|
||||
border-radius: 4px;
|
||||
font-family: 'Poppins';
|
||||
letter-spacing: -0.65px;
|
||||
padding: 8px 34px 7px 9px;
|
||||
gap: 3px;
|
||||
font-weight: 500;
|
||||
.sendDate {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.selected_user_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 90%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.selected_user_box {
|
||||
margin: 8px auto;
|
||||
padding: 15px 20px;
|
||||
height: 45px;
|
||||
width: 90%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); /* 더 부드러운 그림자 효과 */
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
box-shadow 0.2s ease-in-out; /* 전환 효과 추가 */
|
||||
|
||||
&:hover {
|
||||
background-color: #f1f1f1; /* 호버 시 배경색 변화 */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); /* 호버 시 그림자 강화 */
|
||||
|
||||
button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 10px;
|
||||
font-size: 16px; /* 폰트 크기 조정 */
|
||||
color: #333; /* 텍스트 색상 변경 */
|
||||
}
|
||||
|
||||
button {
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #d9534f; /* 더 부드러운 색상 */
|
||||
margin-left: 15px;
|
||||
font-size: 16px; /* 폰트 크기 조정 */
|
||||
font-weight: 600;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.2s ease-in-out,
|
||||
transform 0.2s ease-in-out; /* 전환 효과 조정 */
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userList_table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: #131523;
|
||||
font-family: pretendard;
|
||||
|
||||
.userList_table_header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
padding-bottom: 20px;
|
||||
|
||||
h5 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
thead {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
tr {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
padding: 1rem 0;
|
||||
|
||||
.form-check {
|
||||
padding-left: 0;
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
input[type='checkbox'] + label {
|
||||
height: 23px;
|
||||
cursor: pointer;
|
||||
padding-left: 23px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('~@icons/btn-check-box-default.svg');
|
||||
background-position: left;
|
||||
}
|
||||
input[type='checkbox']:checked + label {
|
||||
background-image: url('~@icons/btn-check-box-active.svg');
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
&.status {
|
||||
position: relative;
|
||||
.status_img {
|
||||
img {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
&.status_dropdown_menu {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0px auto auto 0px;
|
||||
margin: 0px;
|
||||
transform: translate(0px, 40px);
|
||||
box-shadow: 0 1px 4px 0 rgba(21, 34, 50, 0.08);
|
||||
background: #fff;
|
||||
border-radius: 0.25rem;
|
||||
z-index: 900;
|
||||
&.show {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
width: 100%;
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: middle;
|
||||
justify-content: center;
|
||||
padding: 1rem 3px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
.form-check {
|
||||
padding-left: 0;
|
||||
input[type='checkbox'] {
|
||||
display: none;
|
||||
}
|
||||
input[type='checkbox'] + label {
|
||||
height: 23px;
|
||||
cursor: pointer;
|
||||
padding-left: 23px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url('~@icons/btn-check-box-default.svg');
|
||||
background-position: left;
|
||||
}
|
||||
input[type='checkbox']:checked + label {
|
||||
background-image: url('~@icons/btn-check-box-active.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// select style
|
||||
.select_custom {
|
||||
width: 100%;
|
||||
}
|
||||
@ -0,0 +1,260 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import Select, { SingleValue } from 'react-select';
|
||||
import { addMonths, format } from 'date-fns';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { handleSearchSubmit } from '@utils/searchBoxUtils';
|
||||
|
||||
import styles from './NonOwnerAddModal.module.scss';
|
||||
import CalendarIcon from '@icons/ic-calendar.svg';
|
||||
import {
|
||||
EmoticonNonOwnerListRequest,
|
||||
useEmoticonNonOwnerListQuery,
|
||||
usePostAdminGiftEmoticonMutation
|
||||
} from '@api/emoticonAPI';
|
||||
import { EXPIRE_DATE_OPTIONS_EMOTICONS, SearchFilterOption } from '@constants/constants';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface Props {
|
||||
emoticonGuid: number;
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const NonOwnerAddModal = ({ emoticonGuid, onCloseModal }: Props) => {
|
||||
const [emoticonNonOwnerListRequest, setEmoticonNonOwnerListRequest] = useState<EmoticonNonOwnerListRequest>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: ''
|
||||
});
|
||||
const [changedExpiredDate, setChangedExpireDate] = useState<Date>(new Date());
|
||||
const [checkedItems, setCheckedItems] = useState<string[]>([]);
|
||||
|
||||
// Ref
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: NonOwnerListData } = useEmoticonNonOwnerListQuery(emoticonGuid, emoticonNonOwnerListRequest);
|
||||
const { mutate: postAdminGiftEmoticon, isPending: isPostingAdminGiftedEmoticon } = usePostAdminGiftEmoticonMutation();
|
||||
|
||||
// Func
|
||||
// Datepicker date 변경 핸들러
|
||||
const handleExpireDateChange = (date: Date) => {
|
||||
setChangedExpireDate(date);
|
||||
};
|
||||
|
||||
// Datepicker 옆의 날짜 select option 핸들러
|
||||
const handleExpireOptionChange = (option: SingleValue<SearchFilterOption>) => {
|
||||
const now = new Date();
|
||||
const newDate = Number(option?.value);
|
||||
|
||||
switch (option?.label) {
|
||||
case EXPIRE_DATE_OPTIONS_EMOTICONS[0].label:
|
||||
return setChangedExpireDate(now);
|
||||
case EXPIRE_DATE_OPTIONS_EMOTICONS[1].label:
|
||||
return setChangedExpireDate(addMonths(now, newDate));
|
||||
case EXPIRE_DATE_OPTIONS_EMOTICONS[2].label:
|
||||
return setChangedExpireDate(addMonths(now, newDate));
|
||||
case EXPIRE_DATE_OPTIONS_EMOTICONS[3].label:
|
||||
return setChangedExpireDate(addMonths(now, newDate));
|
||||
case EXPIRE_DATE_OPTIONS_EMOTICONS[4].label:
|
||||
return setChangedExpireDate(new Date(9999, 0, 1));
|
||||
default:
|
||||
return setChangedExpireDate(now);
|
||||
}
|
||||
};
|
||||
|
||||
// user 개개인 체크 핸들러
|
||||
const handleUserSelect = (checked: boolean, userAngkorId: string) => {
|
||||
if (checked) {
|
||||
setCheckedItems((prev) => [...prev, userAngkorId]);
|
||||
} else {
|
||||
setCheckedItems((prev) => prev.filter((item) => item !== userAngkorId));
|
||||
}
|
||||
};
|
||||
|
||||
// user 전체 체크 핸들러
|
||||
const handleAllUsersSelect = (checked: boolean) => {
|
||||
const allUserAngkorIdArr = NonOwnerListData?.content.map((user) => user.angkorId as string);
|
||||
|
||||
if (checked) {
|
||||
setCheckedItems((prev) => {
|
||||
const newSet = new Set([...prev, ...(allUserAngkorIdArr as string[])]);
|
||||
return Array.from(newSet);
|
||||
});
|
||||
} else {
|
||||
const newUserArr = [...checkedItems].filter((user) => !allUserAngkorIdArr?.includes(user));
|
||||
|
||||
setCheckedItems(newUserArr);
|
||||
}
|
||||
};
|
||||
|
||||
// 선택된 user 체크 해제 핸들러
|
||||
const handleSelectedUserDelete = (userId: string) => {
|
||||
const newSelectedUsers = checkedItems.filter((user) => user !== userId);
|
||||
|
||||
setCheckedItems(newSelectedUsers);
|
||||
};
|
||||
|
||||
// user에게 이모티콘 선물하기, Process Btn 핸들러
|
||||
const handleProcess = () => {
|
||||
const request = {
|
||||
guid: emoticonGuid,
|
||||
angkorId: checkedItems,
|
||||
expiredDate: Number(format(changedExpiredDate, 'yyyyMMdd')),
|
||||
status: 'U'
|
||||
};
|
||||
|
||||
postAdminGiftEmoticon(request, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonOwnerList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonNonOwnerList'] });
|
||||
onCloseModal();
|
||||
toast.success('Giving emoticons as a gift was successful.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={cn('giving_emoticon_table')}>
|
||||
<h5>Giving emoticons as a Gift</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Expired Date</th>
|
||||
<td>
|
||||
<div className={cn('date_picker')}>
|
||||
<div>
|
||||
<img src={CalendarIcon} alt="CalendarIcon" />
|
||||
</div>
|
||||
<DatePicker
|
||||
className={cn('sendDate')}
|
||||
dateFormat="yyyy-MM-dd"
|
||||
shouldCloseOnSelect
|
||||
selected={changedExpiredDate}
|
||||
onChange={handleExpireDateChange}
|
||||
minDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
<Select
|
||||
className={cn('select_custom')}
|
||||
options={EXPIRE_DATE_OPTIONS_EMOTICONS}
|
||||
defaultValue={EXPIRE_DATE_OPTIONS_EMOTICONS[0]}
|
||||
onChange={handleExpireOptionChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Selected User</th>
|
||||
<td>
|
||||
<div className={cn('selected_user_container')}>
|
||||
{checkedItems.length > 0
|
||||
? checkedItems.map((user) => (
|
||||
<div
|
||||
className={cn('selected_user_box')}
|
||||
key={user}
|
||||
onClick={() => handleSelectedUserDelete(user)}
|
||||
>
|
||||
<span>{user}</span>
|
||||
<button>X</button>
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className={cn('userList_table')}>
|
||||
<div className={cn('userList_table_header')}>
|
||||
<h5>User Search</h5>
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onKeyDown={(event) => handleSearchSubmit(event, searchRef, setEmoticonNonOwnerListRequest)}
|
||||
onClick={(event) => handleSearchSubmit(event, searchRef, setEmoticonNonOwnerListRequest)}
|
||||
placeholderText="Angkor Id, User Id"
|
||||
/>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-5">
|
||||
<div className={cn('form-check')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mainCheckbox"
|
||||
name="select-all"
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => handleAllUsersSelect(e.target.checked)}
|
||||
checked={NonOwnerListData?.content
|
||||
.map((item) => item.angkorId)
|
||||
.every((angkorId) => checkedItems.includes(angkorId))}
|
||||
/>
|
||||
<label htmlFor="mainCheckbox">
|
||||
<span className="hidden"></span>
|
||||
</label>
|
||||
</div>
|
||||
</th>
|
||||
<th className="width-15">No</th>
|
||||
<th className="width-30">User Id</th>
|
||||
<th className="width-50">Angkor Id</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{NonOwnerListData?.content.map((user, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="width-5">
|
||||
<div className={cn('form-check')}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={'select-' + user.angkorId}
|
||||
name={'select-' + user.angkorId}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
handleUserSelect(e.target.checked, user.angkorId)
|
||||
}
|
||||
checked={checkedItems.includes(user.angkorId)}
|
||||
/>
|
||||
|
||||
<label htmlFor={'select-' + user.angkorId}></label>
|
||||
</div>
|
||||
</td>
|
||||
<td className="width-15">{user.userNo}</td>
|
||||
<td className="width-30">{user.userId}</td>
|
||||
<td className="width-50">{user.angkorId}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="footer">
|
||||
<Pagination
|
||||
totalItems={NonOwnerListData?.totalElements ?? 0}
|
||||
itemsPerPage={emoticonNonOwnerListRequest.size}
|
||||
currentPage={emoticonNonOwnerListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setEmoticonNonOwnerListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleProcess} disabled={isPostingAdminGiftedEmoticon}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NonOwnerAddModal;
|
||||
@ -0,0 +1,48 @@
|
||||
.owner_detail_modal {
|
||||
padding: 0 20px;
|
||||
h5 {
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
color: #131523;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
th {
|
||||
height: 50px;
|
||||
font-weight: 600;
|
||||
padding: 1.0625rem 0.5rem;
|
||||
width: 140px;
|
||||
display: block;
|
||||
}
|
||||
td {
|
||||
border-left: 1px solid #e6e9f4;
|
||||
padding: 1.25rem 1rem;
|
||||
width: 493px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&:last-child {
|
||||
td {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
import { useState } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import { OwnerDetailData } from './EmoticonOwnerList';
|
||||
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import styles from './OwnerDetailModal.module.scss';
|
||||
import CalendarIcon from '@icons/ic-calendar.svg';
|
||||
import { useDeleteEmoticonOwnershipMutation, useUpdateExpiredDateMutation } from '@api/emoticonAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface ButtonInfo {
|
||||
onClose: () => void; // 함수 타입 정의
|
||||
buttonName: string; // 문자열 타입 정의
|
||||
}
|
||||
|
||||
interface AlertModalInfo {
|
||||
isOpen: boolean;
|
||||
txt: string;
|
||||
closeBtn: ButtonInfo;
|
||||
confirmBtn?: ButtonInfo;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
ownerDetailData: OwnerDetailData;
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const OwnerDetailModal = ({ ownerDetailData, onCloseModal }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState<AlertModalInfo>({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
closeBtn: { onClose: onCloseModal, buttonName: 'Cancel' },
|
||||
confirmBtn: { onClose: onCloseModal, buttonName: 'Confirm' }
|
||||
});
|
||||
const [changedExpiredDate, setChangedExpireDate] = useState<string>(ownerDetailData.expiredDate as string);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: patchExpireDate } = useUpdateExpiredDateMutation();
|
||||
const { mutate: deleteOwnership } = useDeleteEmoticonOwnershipMutation();
|
||||
|
||||
// Func
|
||||
// Datepicker의 날짜 변경 핸들러
|
||||
const handleExpireDateChange = (date: Date) => {
|
||||
const newDate = format(date, 'yyyy-MM-dd');
|
||||
|
||||
setChangedExpireDate(newDate);
|
||||
};
|
||||
|
||||
const handleChangeBtnClick = () => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: "Are you sure you want to extend the validity of this user's emoji?",
|
||||
closeBtn: {
|
||||
onClose: handleAlertClose,
|
||||
buttonName: 'Cancel'
|
||||
},
|
||||
confirmBtn: {
|
||||
onClose: handleExpireDateConfirm,
|
||||
buttonName: 'Confirm'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Change 버튼 활성화 이후, Alert Modal confirm 버튼 핸들러
|
||||
const handleExpireDateConfirm = () => {
|
||||
const request = {
|
||||
guid: ownerDetailData.emoticonCode,
|
||||
angkorId: ownerDetailData.angkorId as string,
|
||||
expiredDate: Number(format(parseISO(changedExpiredDate), 'yyyyMMdd'))
|
||||
};
|
||||
|
||||
patchExpireDate(request, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonOwnerList'] });
|
||||
setAlertModal((prev) => ({ ...prev, txt: '', isOpen: false }));
|
||||
onCloseModal();
|
||||
toast.success('Expiration date updated.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteBtnClick = () => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: "Are you sure you want to delete that user's emoji?",
|
||||
closeBtn: {
|
||||
onClose: handleAlertClose,
|
||||
buttonName: 'Cancel'
|
||||
},
|
||||
confirmBtn: {
|
||||
onClose: handleDeleteConfirm,
|
||||
buttonName: 'Confirm'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Delete 버튼 활성화 이후, Alert Modal close 버튼 핸들러
|
||||
const handleAlertClose = () => {
|
||||
setAlertModal((prev) => ({ ...prev, txt: '', isOpen: false }));
|
||||
};
|
||||
|
||||
// Delete 버튼 활성화 이후, Alert Modal confirm 버튼 핸들러
|
||||
const handleDeleteConfirm = () => {
|
||||
const request = {
|
||||
guid: ownerDetailData.emoticonCode,
|
||||
angkorId: ownerDetailData.angkorId as string
|
||||
};
|
||||
|
||||
deleteOwnership(request, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonOwnerList'] });
|
||||
setAlertModal((prev) => ({ ...prev, txt: '', isOpen: false }));
|
||||
onCloseModal();
|
||||
toast.success('Emoticon owner removed');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('owner_detail_modal')}>
|
||||
<AlertModal isOpen={alertModal.isOpen} closeBtn={alertModal.closeBtn} confirmBtn={alertModal.confirmBtn}>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<h5>Emoticon Owners Details</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Angkor Id</th>
|
||||
<td>{ownerDetailData.angkorId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>User Id</th>
|
||||
<td>{ownerDetailData.userId}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Emoticon Code</th>
|
||||
<td>{ownerDetailData.emoticonCode}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Emoticon Name</th>
|
||||
<td>{ownerDetailData.emoticonName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Save Date</th>
|
||||
<td>{ownerDetailData.saveDate}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Expired Date</th>
|
||||
<td>
|
||||
<div className="datepicker-group">
|
||||
<div className={cn('calendar_img')}>
|
||||
<img src={CalendarIcon} alt="ic_chevron_down" />
|
||||
</div>
|
||||
<DatePicker
|
||||
className="startDate"
|
||||
dateFormat="yyyy-MM-dd"
|
||||
shouldCloseOnSelect
|
||||
selected={new Date(changedExpiredDate === 'indefinitely' ? '9999-12-31' : changedExpiredDate)}
|
||||
onChange={handleExpireDateChange}
|
||||
placeholderText="YYYY-MM-DD"
|
||||
minDate={new Date()}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="md"
|
||||
color="action"
|
||||
onClick={handleChangeBtnClick}
|
||||
disabled={changedExpiredDate === ownerDetailData.expiredDate}
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className={cn('footer')}>
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleDeleteBtnClick}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OwnerDetailModal;
|
||||
175
src/pages/emoticons/manageEmoticons/ManageEmoticons.module.scss
Normal file
175
src/pages/emoticons/manageEmoticons/ManageEmoticons.module.scss
Normal file
@ -0,0 +1,175 @@
|
||||
.manage_emoticons {
|
||||
.card_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header_box {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoticon_list_table {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e6e9f4;
|
||||
font-size: 12px;
|
||||
color: #131523;
|
||||
font-family: pretendard;
|
||||
table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
thead {
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
tr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
th {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
font-weight: 500;
|
||||
&.status {
|
||||
position: relative;
|
||||
|
||||
.status_img,
|
||||
.open {
|
||||
img {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
.open img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
ul {
|
||||
&.status_dropdown_menu {
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin: 0px;
|
||||
box-shadow: 0 1px 4px 0 rgba(21, 34, 50, 0.1);
|
||||
background: #fff;
|
||||
border-radius: 0.25rem;
|
||||
z-index: 10;
|
||||
|
||||
li {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.25rem 1rem;
|
||||
clear: both;
|
||||
font-weight: 400;
|
||||
color: #131523;
|
||||
text-align: initial;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
||||
button {
|
||||
color: #131523;
|
||||
border: none;
|
||||
background-color: #fff;
|
||||
|
||||
i {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
transform: translateY(-1px);
|
||||
background: #fec702;
|
||||
|
||||
&.all {
|
||||
background: linear-gradient(126deg, red, orange, yellow, green, blue, navy, purple);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.normal,
|
||||
&.my_status_1 {
|
||||
background: #21d59b;
|
||||
}
|
||||
|
||||
&.block,
|
||||
&.my_status_0 {
|
||||
background: #f0142f;
|
||||
}
|
||||
&.report,
|
||||
&.my_status_2 {
|
||||
background: #fec702;
|
||||
}
|
||||
|
||||
&.withdraw {
|
||||
background: #c2cad4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn_sort {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
i {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 9px;
|
||||
background-color: #a5a7ab;
|
||||
}
|
||||
tr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15px 0.5rem;
|
||||
|
||||
i {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
transform: translateY(-1px);
|
||||
background: #fec702;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/pages/emoticons/manageEmoticons/ManageEmoticons.tsx
Normal file
117
src/pages/emoticons/manageEmoticons/ManageEmoticons.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Col, Row } from 'react-bootstrap';
|
||||
import Select, { SingleValue } from 'react-select';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import EmoticonTable from './components/EmoticonTable';
|
||||
import EmoticonAddModal from './components/modal/EmoticonAddModal';
|
||||
import EmoticonDetailModal from './components/modal/EmoticonDetailModal';
|
||||
|
||||
import styles from './ManageEmoticons.module.scss';
|
||||
import PlusIcon from '@images/icons/ic-plus.svg';
|
||||
import { EmoticonListRequest, useEmoticonListQuery } from '@api/emoticonAPI';
|
||||
import { SEARCH_FILTER_OPTIONS_EMOTICONS, SearchFilterOption } from '@constants/constants';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface EmoticonModalInfo {
|
||||
isOpen: boolean;
|
||||
modalName: string | null;
|
||||
emoticonGuid: number;
|
||||
}
|
||||
|
||||
const ManageEmoticons = () => {
|
||||
const [modalInfo, setModalInfo] = useState<EmoticonModalInfo>({
|
||||
isOpen: false,
|
||||
modalName: null,
|
||||
emoticonGuid: 0
|
||||
});
|
||||
const [emoticonListRequest, setEmoticonListRequest] = useState<EmoticonListRequest>({
|
||||
page: 1,
|
||||
size: 9,
|
||||
sort: [],
|
||||
emotiType: SEARCH_FILTER_OPTIONS_EMOTICONS[1].value
|
||||
});
|
||||
|
||||
// API
|
||||
const { data: emoticonListData } = useEmoticonListQuery(emoticonListRequest);
|
||||
console.log(emoticonListData);
|
||||
// Func
|
||||
// selectBox 옵션 이벤트
|
||||
const handleSelectChange = (selectedOption: SingleValue<SearchFilterOption>) => {
|
||||
if (selectedOption) {
|
||||
setEmoticonListRequest((prev) => ({ ...prev, emotiType: selectedOption.value, page: 1 }));
|
||||
}
|
||||
};
|
||||
|
||||
// modal open 이벤트
|
||||
const handleModalOpen = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const modalType = e.currentTarget.textContent;
|
||||
const selectedEmoticonGuid = Number(e.currentTarget.value);
|
||||
|
||||
setModalInfo(() => ({
|
||||
isOpen: true,
|
||||
modalName: modalType,
|
||||
emoticonGuid: selectedEmoticonGuid
|
||||
}));
|
||||
};
|
||||
|
||||
// modal close 이벤트
|
||||
const handleModalClose = () => {
|
||||
setModalInfo(() => ({
|
||||
isOpen: false,
|
||||
modalName: null,
|
||||
emoticonGuid: 0
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal isOpen={modalInfo.isOpen} onClose={handleModalClose}>
|
||||
{modalInfo.modalName === 'Add' && <EmoticonAddModal onCloseModal={handleModalClose} />}
|
||||
{modalInfo.modalName === 'Edit' && (
|
||||
<EmoticonDetailModal emoticonGuid={modalInfo.emoticonGuid} onCloseModal={handleModalClose} />
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<div className={cn('manage_emoticons')}>
|
||||
<h1 className="page-title">Manage Emoticons</h1>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<Card>
|
||||
<Card.Header className={cn('card_header')}>
|
||||
<Card.Title as="h5">Emoticon List</Card.Title>
|
||||
<div className={cn('header_box')}>
|
||||
<Select
|
||||
options={SEARCH_FILTER_OPTIONS_EMOTICONS}
|
||||
onChange={handleSelectChange}
|
||||
defaultValue={SEARCH_FILTER_OPTIONS_EMOTICONS[0]}
|
||||
value={SEARCH_FILTER_OPTIONS_EMOTICONS.find(
|
||||
(option) => option.value === emoticonListRequest.emotiType
|
||||
)}
|
||||
/>
|
||||
<Button color="action" size="lg" onClick={handleModalOpen}>
|
||||
<img src={PlusIcon} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<EmoticonTable
|
||||
emoticonListData={emoticonListData}
|
||||
emoticonListRequest={emoticonListRequest}
|
||||
onModalClick={handleModalOpen}
|
||||
setEmoticonListRequest={setEmoticonListRequest}
|
||||
/>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageEmoticons;
|
||||
@ -0,0 +1,73 @@
|
||||
.emoticon_list_table {
|
||||
width: 100%;
|
||||
border-top: 1px solid #e6e9f4;
|
||||
font-size: 12px;
|
||||
color: #131523;
|
||||
font-family: pretendard;
|
||||
table {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
thead {
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
tr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
th {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
overflow-y: scroll;
|
||||
width: 100%;
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 9px;
|
||||
background-color: #a5a7ab;
|
||||
}
|
||||
tr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
|
||||
td {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 15px 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gift_emoticon_box {
|
||||
position: relative;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background: #f6f6f8;
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
border-radius: 10%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-webkit-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
|
||||
import { GivingEmoticonState } from '@pages/emoticons/givingEmoticons/GivingEmoticons';
|
||||
|
||||
import styles from './EmoticonTable.module.scss';
|
||||
import { EmoticonListRequest, EmoticonListResponse } from '@api/emoticonAPI';
|
||||
import { Page } from 'src/services/commonResponse/PageResponse';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface Props {
|
||||
emoticonListData: Page<EmoticonListResponse> | undefined;
|
||||
emoticonListRequest: EmoticonListRequest;
|
||||
btnName?: 'Edit' | 'Gift';
|
||||
onModalClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
setEmoticonListRequest: React.Dispatch<React.SetStateAction<EmoticonListRequest>>;
|
||||
setEmoticonState?: React.Dispatch<React.SetStateAction<GivingEmoticonState>>;
|
||||
}
|
||||
|
||||
const EmoticonTable = ({
|
||||
emoticonListData,
|
||||
onModalClick,
|
||||
btnName = 'Edit',
|
||||
emoticonListRequest,
|
||||
setEmoticonListRequest,
|
||||
setEmoticonState
|
||||
}: Props) => {
|
||||
if (!emoticonListData || emoticonListData.content.length === 0) {
|
||||
return <div>No emoticonListData available</div>;
|
||||
}
|
||||
return (
|
||||
<div className={cn('emoticon_list_table')}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-18">Emoticon</th>
|
||||
<th className="width-18">Emoticon Name</th>
|
||||
<th className="width-18">Guid</th>
|
||||
<th className="width-18">Price</th>
|
||||
<th className="width-18">Registration Date</th>
|
||||
<th className="width-18"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{emoticonListData?.content.map((item, idx: number) => (
|
||||
<tr key={idx}>
|
||||
<td className="width-18">
|
||||
<div className={cn('gift_emoticon_box')}>
|
||||
<img src={item.absFilePath} alt={item.emotiName} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="width-18">{item.emotiName}</td>
|
||||
<td className="width-18">{item.guid}</td>
|
||||
<td className="width-18">{item.price}</td>
|
||||
<td className="width-18">{item.regDt}</td>
|
||||
<td className="width-18">
|
||||
<Button
|
||||
size="sm"
|
||||
color="rounded"
|
||||
value={item.guid}
|
||||
name={item.emotiName}
|
||||
onClick={(e) => {
|
||||
onModalClick(e);
|
||||
setEmoticonState?.({ guid: item.guid, emoticonName: item.emotiName });
|
||||
}}
|
||||
>
|
||||
{btnName === 'Edit' ? 'Edit' : 'Gift'}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<Pagination
|
||||
totalItems={emoticonListData?.totalElements ?? 0}
|
||||
itemsPerPage={emoticonListRequest.size}
|
||||
currentPage={emoticonListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setEmoticonListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmoticonTable;
|
||||
@ -0,0 +1,200 @@
|
||||
.emoticon_add_modal {
|
||||
.modal_title {
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
font-family: pretendard;
|
||||
color: #131523;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
th {
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
font-weight: 600;
|
||||
padding: 1.0625rem 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
td {
|
||||
border-left: 1px solid #e6e9f4;
|
||||
padding: 15px 0.5rem;
|
||||
width: 440px;
|
||||
display: block;
|
||||
.form_control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #131523;
|
||||
background-color: #f5f6fa;
|
||||
background-clip: padding-box;
|
||||
border: 0 solid #ced4da;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Short cut
|
||||
.short_cut_section {
|
||||
padding-top: 60px;
|
||||
.section_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
padding-bottom: 20px;
|
||||
h5 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
}
|
||||
img {
|
||||
cursor: pointer;
|
||||
filter: invert(81%) sepia(30%) saturate(5422%) hue-rotate(2deg) brightness(150%) contrast(106%);
|
||||
}
|
||||
}
|
||||
|
||||
.section_upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
-moz-column-gap: 15px;
|
||||
column-gap: 15px;
|
||||
row-gap: 10px;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
label {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
P {
|
||||
font-family: theme-font-family;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
padding-top: 0.5rem;
|
||||
color: #858c97;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File Upload
|
||||
.file_upload_section {
|
||||
.section_header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
padding-bottom: 20px;
|
||||
h5 {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
border: 1px solid #d5d7e3;
|
||||
border-radius: 4px;
|
||||
min-width: 74px;
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
padding: 0 10px 0 3px;
|
||||
background-color: #fff;
|
||||
font-size: 14px;
|
||||
color: #131523;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section_upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
-moz-column-gap: 15px;
|
||||
column-gap: 15px;
|
||||
row-gap: 10px;
|
||||
padding-top: 20px;
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
label {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
P {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
padding-top: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #858c97;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// footer
|
||||
.modal_footer {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
font-family: pretendard;
|
||||
}
|
||||
|
||||
/* 그외 input에 이미지 파일이 없는 경우 */
|
||||
.img_no_file {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
/* 파일이 있는 경우 */
|
||||
.img_has_file {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.lable_img_no_file {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: block;
|
||||
background: #f6f6f8;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #707070;
|
||||
}
|
||||
@ -0,0 +1,689 @@
|
||||
import React, { Children, useRef, useState } from 'react';
|
||||
import Select, { SingleValue } from 'react-select';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import classNames from 'classnames/bind';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import styles from './EmoticonAddModal.module.scss';
|
||||
import PlusIcon from '@icons/ic-plus.svg';
|
||||
import HelpIcon from '@icons/ic-help.svg';
|
||||
import { EmoticonPostFormData, EmoticonPostJsonData, FileDetail, usePostEmoticonMutation } from '@api/emoticonAPI';
|
||||
import { DEFAULT_TYPE, IMAGE_UPLOAD_GUIDELINES, SearchFilterOption } from '@constants/constants';
|
||||
import Radio, { RadioOption } from '@components/ui/radio/Radio';
|
||||
import { validateImageFileProperties, ValidateImageResponse } from '@utils/fileUtils';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface EmoticonAddState {
|
||||
emoticonName: string;
|
||||
imageType: string[];
|
||||
status: string;
|
||||
statusNm: string;
|
||||
isDefault: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const EmoticonAddModal = ({ onCloseModal }: Props) => {
|
||||
const [modal, setModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [emoticonAddState, setEmoticonAddState] = useState<EmoticonAddState>({
|
||||
emoticonName: '',
|
||||
imageType: ['PNG'],
|
||||
status: '1',
|
||||
statusNm: 'ON',
|
||||
isDefault: 'D'
|
||||
});
|
||||
const [shortCutImageUrl, setShortCutImageUrl] = useState<string[]>(); // 이미지 URL 상태
|
||||
const [shortCutImageFile, setShortCutImageFile] = useState<Array<File | null>>([null]);
|
||||
const [emoticonDetailFilesUrl, setEmoticonDetailFilesUrl] = useState<FileDetail[]>([]); // 맨 아래 files들의 이미지 URL
|
||||
const [emoticonDetailFiles, setEmoticonDetailFiles] = useState<Array<File | null>>([null]);
|
||||
|
||||
const EMOTICON_COUNT_OPTIONS = Array.from({ length: 32 }, (_, index) => ({
|
||||
label: String(index + 1),
|
||||
value: String(index + 1)
|
||||
}));
|
||||
|
||||
// Ref
|
||||
const dropIndexRef = useRef<number | null>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: postEmoticon, isPending: isPostingEmoticon } = usePostEmoticonMutation();
|
||||
|
||||
// Func
|
||||
// requestBody에 쓰일 데이터 타입 체크하는 함수
|
||||
const convertToFormData = (requestBody: EmoticonPostJsonData, formDataBody: EmoticonPostFormData): FormData => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'request',
|
||||
new Blob([JSON.stringify(requestBody)], {
|
||||
type: 'application/json'
|
||||
})
|
||||
);
|
||||
|
||||
formDataBody.files.forEach((file) => formData.append('files', file as File));
|
||||
formDataBody.shortCutFiles.forEach((file) => formData.append('shortCutFiles', file as File));
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModal((prevState) => ({ ...prevState, isOpen: false }));
|
||||
};
|
||||
|
||||
const handleEmoticonNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newEmoticonName = e.target.value;
|
||||
|
||||
setEmoticonAddState((prev) => ({ ...prev, emoticonName: newEmoticonName }));
|
||||
};
|
||||
|
||||
const handleEmoticonCountOptionChange = (selectedOption: SingleValue<SearchFilterOption>) => {
|
||||
if (selectedOption) {
|
||||
setEmoticonDetailFiles(Array.from({ length: Number(selectedOption.value) }, () => null));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value, checked } = e.target;
|
||||
|
||||
setEmoticonAddState((prev) => {
|
||||
const newImageType = checked ? [...prev.imageType, value] : prev.imageType.filter((imgType) => imgType !== value);
|
||||
|
||||
return {
|
||||
...prev,
|
||||
imageType: newImageType
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleStatusChange = (option: RadioOption) => {
|
||||
const status = option.id;
|
||||
setEmoticonAddState((prev) => ({ ...prev, status: status, statusNm: option.id === '0' ? 'OFF' : 'ON' }));
|
||||
};
|
||||
|
||||
const handleIsDefaultChange = (option: RadioOption) => {
|
||||
const selecteDefaultType = option.id;
|
||||
setEmoticonAddState((prev) => ({ ...prev, isDefault: selecteDefaultType }));
|
||||
};
|
||||
|
||||
const handleHelpIconClick = () => {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: IMAGE_UPLOAD_GUIDELINES
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent<HTMLLIElement>, dropIndex: number) => {
|
||||
dropIndexRef.current = dropIndex;
|
||||
};
|
||||
|
||||
const handelDragOver = (e: React.DragEvent<HTMLLIElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent<HTMLLIElement>, dragStartIndex: number) => {
|
||||
e.preventDefault();
|
||||
const dropIndex = dropIndexRef.current;
|
||||
|
||||
if (dropIndex === dragStartIndex || dropIndex === null || dropIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedEmoticonDetailFilesUrl = JSON.parse(JSON.stringify(emoticonDetailFilesUrl));
|
||||
const updatedEmoticonDetailFiles = [...emoticonDetailFiles];
|
||||
|
||||
// emoticonDetailFilesUrl 배열을 drag, drop에 맞게 업데이트
|
||||
[updatedEmoticonDetailFilesUrl[dragStartIndex], updatedEmoticonDetailFilesUrl[dropIndex]] = [
|
||||
updatedEmoticonDetailFilesUrl[dropIndex],
|
||||
updatedEmoticonDetailFilesUrl[dragStartIndex]
|
||||
];
|
||||
|
||||
// dispOrd를 drag, drop 인덱스에 맞게 업데이트
|
||||
[updatedEmoticonDetailFilesUrl[dragStartIndex].dispOrd, updatedEmoticonDetailFilesUrl[dropIndex].dispOrd] = [
|
||||
updatedEmoticonDetailFilesUrl[dropIndex].dispOrd,
|
||||
updatedEmoticonDetailFilesUrl[dragStartIndex].dispOrd
|
||||
];
|
||||
|
||||
// API로 보내는 파일들 인덱스 변경
|
||||
[updatedEmoticonDetailFiles[dragStartIndex], updatedEmoticonDetailFiles[dropIndex]] = [
|
||||
updatedEmoticonDetailFiles[dropIndex],
|
||||
updatedEmoticonDetailFiles[dragStartIndex]
|
||||
];
|
||||
|
||||
dropIndexRef.current = null; // 끝났음을 의미하며, 혹여나 계속 남아 있을 참조값을 위해 명시적으로 null 넣어준다.
|
||||
|
||||
setEmoticonDetailFilesUrl(updatedEmoticonDetailFilesUrl);
|
||||
setEmoticonDetailFiles(updatedEmoticonDetailFiles);
|
||||
};
|
||||
|
||||
const handleShortcutUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (!file) {
|
||||
// 0. image 유무, 조건
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. image png 형식, 조건
|
||||
if (file.type !== 'image/png') {
|
||||
alert('Select the correct file format. (PNG)');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. image 용량 및 크기 검사
|
||||
const imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 96, 96);
|
||||
|
||||
if (!imageValidationResult.isValid) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: imageValidationResult.message || 'Unknown error'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일을 읽어 Base64 인코딩된 데이터 URL로 변환
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onloadend = () => {
|
||||
const ShortCutImageUrl = reader.result as string;
|
||||
|
||||
if (ShortCutImageUrl) {
|
||||
setShortCutImageUrl([ShortCutImageUrl]);
|
||||
}
|
||||
};
|
||||
|
||||
// API requestBody의 shortCut file 형태
|
||||
setShortCutImageFile([file]);
|
||||
};
|
||||
|
||||
const handleDetailFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const dispOrd = Number(e.target.dataset.displayOrder); // dataset을 사용하여 data-disp-ord 속성 접근
|
||||
|
||||
let imageValidationResult: ValidateImageResponse = { isValid: false, message: '' };
|
||||
const KOREAN_PATTERN = /[ㄱ-ㅎㅏ-ㅣ가-힣]/;
|
||||
const isSelectedPng = emoticonAddState.imageType.includes('PNG');
|
||||
const isSelectedGif = emoticonAddState.imageType.includes('GIF');
|
||||
|
||||
// 0. image file 유무, 조건
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (KOREAN_PATTERN.test(file.name)) {
|
||||
// 1. 파일명 한글인지 확인, 조건
|
||||
alert('File names should not contain Korean characters.');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
} else if (!isSelectedPng && !isSelectedGif) {
|
||||
// 2. imageType 둘 다 선택 안 했을 때, 조건
|
||||
alert('Please select the image type first.');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
} else if (isSelectedPng && !isSelectedGif) {
|
||||
// 3. imageType PNG만 선택 시, 조건
|
||||
if (file.type === 'image/png') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 360, 360);
|
||||
} else {
|
||||
alert('Please check the image type, Expected type PNG.');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
} else if (!isSelectedPng && isSelectedGif) {
|
||||
// 4. imageType GIF만 선택 시, 조건
|
||||
if (file.type === 'image/gif') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 2 * 1024 * 1024, 360, 360);
|
||||
} else {
|
||||
alert('Please check the image type, Expected type GIF.');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
} else if (isSelectedPng && isSelectedGif) {
|
||||
// 5. imageType PNG,GIF 둘 다 선택 시 , 조건
|
||||
if (file.type === 'image/gif') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 2 * 1024 * 1024, 360, 360);
|
||||
} else if (file.type === 'image/png') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 360, 360);
|
||||
} else {
|
||||
alert('Please check the image type, Expected type PNG, GIF.');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageValidationResult.isValid) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: imageValidationResult.message || 'Unknown error'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 위의 모든 이미지 업로드 조건을 통과하면, 이미지 파일의 url을 위의 url state에 동기화한다.
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onloadend = () => {
|
||||
const uploadedEmoticonDetailImagesUrl = reader.result as string;
|
||||
|
||||
if (uploadedEmoticonDetailImagesUrl) {
|
||||
const changeUploadDetailFileUrl = {
|
||||
guid: 1,
|
||||
dispOrd: dispOrd,
|
||||
absFilePath: uploadedEmoticonDetailImagesUrl
|
||||
};
|
||||
|
||||
// 이미지 미리보기 업데이트용
|
||||
const changeUploadDetailFileUrlArr = [...emoticonDetailFilesUrl];
|
||||
changeUploadDetailFileUrlArr[dispOrd - 1] = changeUploadDetailFileUrl; // 서버에는 첫 번째 사진이 인덱스 1로 저장되기에
|
||||
|
||||
setEmoticonDetailFilesUrl([...changeUploadDetailFileUrlArr]);
|
||||
|
||||
// API 업로드용
|
||||
|
||||
const updatedEmoticonUploadDetailFiles = [...emoticonDetailFiles];
|
||||
updatedEmoticonUploadDetailFiles[dispOrd - 1] = file; // 파일로 업데이트
|
||||
|
||||
setEmoticonDetailFiles(updatedEmoticonUploadDetailFiles as (File | null)[]);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleMultipleDetailFilesUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
|
||||
let imageValidationResult: ValidateImageResponse = { isValid: false, message: '' };
|
||||
const KOREAN_PATTERN = /[ㄱ-ㅎㅏ-ㅣ가-힣]/;
|
||||
const isSelectedPng = emoticonAddState.imageType.includes('PNG');
|
||||
const isSelectedGif = emoticonAddState.imageType.includes('GIF');
|
||||
|
||||
// 0. image file 유무, 조건
|
||||
if (!files) {
|
||||
return;
|
||||
} else if (emoticonDetailFiles.length !== files.length) {
|
||||
// 1. Emoticon Count와 upload한 files의 갯수 동일한 지, 조건
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please match Emoticon Count with the number of files.'
|
||||
});
|
||||
e.target.value = '';
|
||||
return;
|
||||
} else if (!isSelectedPng && !isSelectedGif) {
|
||||
// 2. imageType 둘 다 선택 안 했을 때, 조건
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please select the image type first.'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
} else {
|
||||
// 3.files 내부의 각각의 image file들의 형식이 옳은지 파악한다, 조건
|
||||
const filesArray = Array.from(files);
|
||||
for (const file of filesArray) {
|
||||
// 1. 파일명 한글인지 확인, 조건
|
||||
if (KOREAN_PATTERN.test(file.name)) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'File names should not contain Korean characters.'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
} else if (isSelectedPng && !isSelectedGif) {
|
||||
// 2. imageType PNG만 선택 시, 조건
|
||||
if (file.type === 'image/png') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 360, 360);
|
||||
} else {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please check the image type, Expected type PNG.'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
} else if (!isSelectedPng && isSelectedGif) {
|
||||
// 3. imageType GIF만 선택 시, 조건
|
||||
if (file.type === 'image/gif') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 2 * 1024 * 1024, 360, 360);
|
||||
} else {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please check the image type, Expected type GIF.'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
} else if (isSelectedPng && isSelectedGif) {
|
||||
// 4. imageType PNG,GIF 둘 다 선택 시 , 조건
|
||||
if (file.type === 'image/gif') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 2 * 1024 * 1024, 360, 360);
|
||||
} else if (file.type === 'image/png') {
|
||||
imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 360, 360);
|
||||
} else {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please check the image type, Expected type PNG, GIF.'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageValidationResult.isValid) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: imageValidationResult.message || 'Unknown error'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const filesArray = Array.from(files);
|
||||
|
||||
filesArray.forEach((item, index) => {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(item);
|
||||
|
||||
reader.onloadend = () => {
|
||||
const uploadedEmoticonDetailImagesUrl = reader.result as string;
|
||||
|
||||
if (uploadedEmoticonDetailImagesUrl) {
|
||||
const changeUploadDetailFileUrl = {
|
||||
absFilePath: uploadedEmoticonDetailImagesUrl,
|
||||
dispOrd: index + 1
|
||||
};
|
||||
|
||||
// 이미지 미리보기 업데이트용
|
||||
setEmoticonDetailFilesUrl((prev) => {
|
||||
const newState = [...prev];
|
||||
newState[index] = changeUploadDetailFileUrl;
|
||||
return newState;
|
||||
});
|
||||
|
||||
// API 업로드용
|
||||
setEmoticonDetailFiles((prev) => {
|
||||
const newState = [...prev];
|
||||
newState[index] = item as File; // 파일로 업데이트
|
||||
return newState;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const requestBody = {
|
||||
emotiName: emoticonAddState.emoticonName,
|
||||
status: emoticonAddState.status,
|
||||
isDefault: emoticonAddState.isDefault
|
||||
};
|
||||
|
||||
const formDataBody = {
|
||||
shortCutFiles: shortCutImageFile,
|
||||
files: emoticonDetailFiles
|
||||
};
|
||||
|
||||
if (requestBody.emotiName === '') {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Title is required.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formDataBody.shortCutFiles[0] === null) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please register a short cut image.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (formDataBody.files.some((item) => item === null)) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: 'Please check that the file count matches the Emoticon Count.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const typeCheckedFormData = convertToFormData(requestBody, formDataBody);
|
||||
|
||||
postEmoticon(typeCheckedFormData, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
toast.success('The emoticon has been added.');
|
||||
onCloseModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={modal.isOpen}
|
||||
confirmBtn={{
|
||||
onClose: handleModalClose,
|
||||
buttonName: 'Confirm'
|
||||
}}
|
||||
>
|
||||
{modal.txt}
|
||||
</AlertModal>
|
||||
<form className={cn('emoticon_add_modal')} onSubmit={handleConfirmSubmit}>
|
||||
<h5 className={cn('modal_title')}>Emoticon Add</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Emoticon Name</th>
|
||||
<td>
|
||||
<input
|
||||
className={cn('form_control')}
|
||||
type="text"
|
||||
placeholder="Enter a Emoticon Name..."
|
||||
value={emoticonAddState.emoticonName}
|
||||
onChange={handleEmoticonNameChange}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Emoticon Count</th>
|
||||
<td>
|
||||
<Select
|
||||
options={EMOTICON_COUNT_OPTIONS}
|
||||
defaultValue={EMOTICON_COUNT_OPTIONS[0]}
|
||||
value={EMOTICON_COUNT_OPTIONS.find((option) => emoticonDetailFiles.length === Number(option.value))}
|
||||
onChange={handleEmoticonCountOptionChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Emoticon image type</th>
|
||||
<td>
|
||||
<div className="form-group">
|
||||
<div className="checkbox checkbox-fill d-inline ">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="PNG"
|
||||
id="checkbox-fill-a2"
|
||||
checked={emoticonAddState.imageType.includes('PNG')}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="checkbox-fill-a2" className="cr">
|
||||
<span>PNG</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="checkbox checkbox-fill d-inline ">
|
||||
<input
|
||||
type="checkbox"
|
||||
value="GIF"
|
||||
id="checkbox-fill-a1"
|
||||
checked={emoticonAddState.imageType.includes('GIF')}
|
||||
onChange={handleCheckboxChange}
|
||||
/>
|
||||
<label htmlFor="checkbox-fill-a1" className="cr">
|
||||
<span>GIF</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>
|
||||
<Radio
|
||||
options={[
|
||||
{ id: '1', label: 'ON' },
|
||||
{ id: '0', label: 'OFF' }
|
||||
]}
|
||||
name="itemStatus"
|
||||
value={emoticonAddState.statusNm}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>isDefault</th>
|
||||
<td>
|
||||
<Radio
|
||||
options={DEFAULT_TYPE}
|
||||
name="itemIsDefault"
|
||||
value={
|
||||
emoticonAddState?.isDefault === 'D'
|
||||
? 'Defaults'
|
||||
: emoticonAddState?.isDefault === 'B'
|
||||
? 'Sale'
|
||||
: 'Gift'
|
||||
}
|
||||
onChange={handleIsDefaultChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={cn('short_cut_section')}>
|
||||
<div className={cn('section_header')}>
|
||||
<h5>Short cut</h5>
|
||||
<img src={HelpIcon} alt="help" onClick={handleHelpIconClick} />
|
||||
</div>
|
||||
<ul className={cn('section_upload')}>
|
||||
<li>
|
||||
<label
|
||||
htmlFor="short-cut-image-change"
|
||||
className={cn({
|
||||
img_has_file: shortCutImageUrl,
|
||||
lable_img_no_file: !shortCutImageUrl
|
||||
})}
|
||||
>
|
||||
<img
|
||||
alt="shortCut"
|
||||
className={cn('img_no_file', { img_has_file: shortCutImageUrl })}
|
||||
src={shortCutImageUrl ? shortCutImageUrl[0] : PlusIcon}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="short-cut-image-change"
|
||||
data-role="shortCutImg"
|
||||
onChange={handleShortcutUpload}
|
||||
accept="image/png" // png만 허용
|
||||
name="shortCut"
|
||||
type="file"
|
||||
/>
|
||||
|
||||
<p>Active</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={cn('file_upload_section')}>
|
||||
<div className={cn('section_header')}>
|
||||
<h5>File Upload</h5>
|
||||
<label htmlFor="filesInput">
|
||||
<img src={PlusIcon} alt="filesInput" />
|
||||
Select Multiple Files
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
id="filesInput"
|
||||
name="files"
|
||||
multiple
|
||||
accept="image/png, image/gif"
|
||||
onChange={handleMultipleDetailFilesUpload}
|
||||
/>
|
||||
</div>
|
||||
<ul className={cn('section_upload')}>
|
||||
{Children.toArray(
|
||||
emoticonDetailFiles?.map((item, index) => {
|
||||
return (
|
||||
<li
|
||||
onDragEnter={(e) => handleDragStart(e, index)}
|
||||
onDragEnd={(e) => handleDragEnd(e, index)}
|
||||
onDragOver={handelDragOver}
|
||||
draggable
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={'files_image' + index}
|
||||
className={cn({
|
||||
img_has_file: emoticonDetailFilesUrl[index]?.absFilePath,
|
||||
lable_img_no_file: !emoticonDetailFilesUrl[index]?.absFilePath
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
emoticonDetailFilesUrl[index]?.absFilePath
|
||||
? emoticonDetailFilesUrl[index]?.absFilePath
|
||||
: PlusIcon
|
||||
}
|
||||
alt="file_upload_img"
|
||||
className={cn({
|
||||
img_has_file: emoticonDetailFilesUrl[index]?.absFilePath,
|
||||
img_no_file: !emoticonDetailFilesUrl[index]?.absFilePath
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id={'files_image' + index}
|
||||
name={'files_image_change' + index}
|
||||
data-display-order={index + 1}
|
||||
type="file"
|
||||
accept="image/png, image/gif"
|
||||
onChange={handleDetailFileUpload}
|
||||
/>
|
||||
<p>{index + 1}</p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<footer className={cn('modal_footer')}>
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="md" color="confirm" type="submit" disabled={isPostingEmoticon}>
|
||||
Process
|
||||
</Button>
|
||||
</footer>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmoticonAddModal;
|
||||
@ -0,0 +1,165 @@
|
||||
//Emoticons modal
|
||||
.emoticon_detail_modal {
|
||||
.modal_title {
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
font-family: pretendard;
|
||||
color: #131523;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
th {
|
||||
width: 240px;
|
||||
height: 50px;
|
||||
font-weight: 600;
|
||||
padding: 1.0625rem 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
td {
|
||||
border-left: 1px solid #e6e9f4;
|
||||
padding: 15px 0.5rem;
|
||||
width: 440px;
|
||||
display: block;
|
||||
.form_control {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #131523;
|
||||
background-color: #f5f6fa;
|
||||
background-clip: padding-box;
|
||||
border: 0 solid #ced4da;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Short cut
|
||||
.short_cut_section {
|
||||
padding-top: 60px;
|
||||
.section_header {
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
}
|
||||
|
||||
.section_upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
-moz-column-gap: 15px;
|
||||
column-gap: 15px;
|
||||
row-gap: 10px;
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
label {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
P {
|
||||
font-family: theme-font-family;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #858c97;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// File Upload
|
||||
.file_upload_section {
|
||||
.section_header {
|
||||
border-bottom: 1px solid #e6e9f4;
|
||||
padding-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
font-family: pretendard;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section_upload {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
-moz-column-gap: 15px;
|
||||
column-gap: 15px;
|
||||
row-gap: 10px;
|
||||
padding-top: 20px;
|
||||
input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
label {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
P {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
color: #858c97;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// footer
|
||||
.modal_footer {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
font-family: pretendard;
|
||||
}
|
||||
|
||||
/* 그외 */
|
||||
.img_no_file {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.img_has_file {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.lable_img_no_file {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
display: block;
|
||||
background: #f6f6f8;
|
||||
border-radius: 4px;
|
||||
border: 1px dashed #707070;
|
||||
}
|
||||
@ -0,0 +1,488 @@
|
||||
import React, { Children, useEffect, useRef, useState } from 'react';
|
||||
import classNames from 'classnames/bind';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import styles from './EmoticonDetailModal.module.scss';
|
||||
import PlusIcon from '@icons/ic-plus.svg';
|
||||
import {
|
||||
EmoticonPatchJsonData,
|
||||
FileDetail,
|
||||
ReorderFile,
|
||||
useEmoticonDetailsQuery,
|
||||
useUpdateEmoticonFileMutation,
|
||||
useUpdateEmoticonMutation
|
||||
} from '@api/emoticonAPI';
|
||||
import Radio, { RadioOption } from '@components/ui/radio/Radio';
|
||||
import { validateImageFileProperties, ValidateImageResponse } from '@utils/fileUtils';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import { DEFAULT_TYPE } from '@constants/constants';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
const cn = classNames.bind(styles);
|
||||
|
||||
interface EmoticonState {
|
||||
emoticonName: string;
|
||||
status: number;
|
||||
statusNm: string;
|
||||
isDefault: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
emoticonGuid: number;
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const EmoticonDetailsDataModal = ({ emoticonGuid, onCloseModal }: Props) => {
|
||||
const [modal, setModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [emoticonState, setEmoticonState] = useState<EmoticonState>({
|
||||
emoticonName: '',
|
||||
status: 0,
|
||||
statusNm: '',
|
||||
isDefault: ''
|
||||
});
|
||||
const [shortCutImageUrl, setShortCutImageUrl] = useState<string>(); // shortCut image URL
|
||||
const [reorderFiles, setReorderFiles] = useState<ReorderFile[]>([]);
|
||||
const [emoticonDetailFilesUrl, setEmoticonDetailFilesUrl] = useState<FileDetail[]>([]); // files image URL
|
||||
const [isDataChanged, setIsDataChanged] = useState(false);
|
||||
const [isApiCalled, setIsApiCalled] = useState(false);
|
||||
|
||||
// Ref
|
||||
const dropIndexRef = useRef<number | null>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: emoticonDetailsData } = useEmoticonDetailsQuery(emoticonGuid);
|
||||
const { mutate: updateEmoticon, isPending: isUpdatingEmoticon } = useUpdateEmoticonMutation();
|
||||
const { mutate: updateEmoticonFile } = useUpdateEmoticonFileMutation();
|
||||
|
||||
// emoticonDetailsData 변경에 따른 emoticonState 초기 값 셋팅. - useEffect
|
||||
useEffect(() => {
|
||||
if (emoticonDetailsData) {
|
||||
setEmoticonState({
|
||||
emoticonName: emoticonDetailsData.emoticonName,
|
||||
status: emoticonDetailsData.status,
|
||||
statusNm: emoticonDetailsData.status === 0 ? 'OFF' : 'ON',
|
||||
isDefault: emoticonDetailsData.isDefault
|
||||
});
|
||||
setEmoticonDetailFilesUrl(emoticonDetailsData.detailFile.sort((a, b) => a.dispOrd - b.dispOrd));
|
||||
setShortCutImageUrl(emoticonDetailsData.shortCutFile[0].absFilePath);
|
||||
}
|
||||
}, [emoticonDetailsData]);
|
||||
|
||||
// detail modal의 detail 이미지를 수정하여 사진 변경 API가 호출 된 상황에, confirm을 누르지 않고 모달 외부를 눌러 언마운트 시켰을 때, query 무효화를 시킨다. - useEffect
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleModalOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleModalOutsideClick);
|
||||
};
|
||||
}, [isApiCalled]);
|
||||
|
||||
// Func
|
||||
// api requestBody에 쓰일 데이터 타입 체크하는 함수
|
||||
const convertToFormData = (requestBody: EmoticonPatchJsonData, formDataBody: File): FormData => {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'request',
|
||||
new Blob([JSON.stringify(requestBody)], {
|
||||
type: 'application/json'
|
||||
})
|
||||
);
|
||||
|
||||
formData.append('file', formDataBody);
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
setModal((prevState) => ({ ...prevState, isOpen: false }));
|
||||
};
|
||||
|
||||
const handleModalOutsideClick = (event: MouseEvent) => {
|
||||
if ((event.target as Element).className === 'black_bg') {
|
||||
if (isApiCalled) {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonDetails'] });
|
||||
}
|
||||
onCloseModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmoticonNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEmoticonState((prev) => ({ ...prev, emoticonName: e.target.value }));
|
||||
setIsDataChanged(true);
|
||||
};
|
||||
|
||||
const handleStatusChange = (option: RadioOption) => {
|
||||
const status = Number(option.id);
|
||||
setEmoticonState((prev) => ({ ...prev, status: status, statusNm: option.id === '0' ? 'OFF' : 'ON' }));
|
||||
setIsDataChanged(true);
|
||||
};
|
||||
|
||||
const handleIsDefaultChange = (option: RadioOption) => {
|
||||
const selecteDefaultType = option.id;
|
||||
setEmoticonState((prev) => ({ ...prev, isDefault: selecteDefaultType }));
|
||||
setIsDataChanged(true);
|
||||
};
|
||||
|
||||
const handleDragStart = (e: React.DragEvent<HTMLLIElement>, dropIndex: number) => {
|
||||
dropIndexRef.current = dropIndex;
|
||||
};
|
||||
|
||||
const handelDragOver = (e: React.DragEvent<HTMLLIElement>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: React.DragEvent<HTMLLIElement>, dragStartIndex: number) => {
|
||||
e.preventDefault();
|
||||
const dropIndex = dropIndexRef.current;
|
||||
|
||||
if (dropIndex === dragStartIndex || dropIndex === null || dropIndex === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedEmoticonDetailFiles: FileDetail[] = JSON.parse(JSON.stringify(emoticonDetailFilesUrl));
|
||||
|
||||
// emoticonDetailFilesUrl 배열을 drag, drop에 맞게 업데이트
|
||||
[updatedEmoticonDetailFiles[dragStartIndex], updatedEmoticonDetailFiles[dropIndex]] = [
|
||||
updatedEmoticonDetailFiles[dropIndex],
|
||||
updatedEmoticonDetailFiles[dragStartIndex]
|
||||
];
|
||||
|
||||
const newReorderFiles = updatedEmoticonDetailFiles
|
||||
.filter((file) => file.id !== undefined && file.dispOrd !== undefined)
|
||||
.map((file, index) => ({
|
||||
id: file.id as number,
|
||||
dispOrd: (index + 1) as number
|
||||
}))
|
||||
.sort((a, b) => a.id - b.id);
|
||||
|
||||
setReorderFiles(newReorderFiles);
|
||||
setEmoticonDetailFilesUrl(updatedEmoticonDetailFiles);
|
||||
setIsDataChanged(true);
|
||||
|
||||
dropIndexRef.current = null; // 참조 초기화
|
||||
};
|
||||
|
||||
const handleShortcutUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
// 0. image 유무, 조건
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. image 형식, 조건
|
||||
if (file.type !== 'image/png') {
|
||||
alert('Select the correct file format. (PNG)');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. image 용량 및 크기 검사
|
||||
const imageValidationResult: ValidateImageResponse = await validateImageFileProperties(file, 150 * 1024, 96, 96);
|
||||
|
||||
if (!imageValidationResult.isValid) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: imageValidationResult.message || 'Unknown error'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 파일을 읽어 Base64 인코딩된 데이터 URL로 변환
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onloadend = () => {
|
||||
const ShortCutImageUrl = reader.result as string;
|
||||
|
||||
if (ShortCutImageUrl) {
|
||||
const requestBody = {
|
||||
guid: emoticonGuid,
|
||||
dispOrd: 0
|
||||
};
|
||||
const typeCheckedFormData = convertToFormData(requestBody, file);
|
||||
|
||||
setShortCutImageUrl(ShortCutImageUrl);
|
||||
setIsDataChanged(true);
|
||||
updateEmoticonFile(typeCheckedFormData, {
|
||||
onSuccess: () => {
|
||||
setIsApiCalled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleDetailFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
const selectedImageDispOrd = Number(e.target.dataset.displayOrder); // e.target으로 값에 접근하면 string으로 잡히기에, Number로 변환
|
||||
const selectedImageId = Number(e.target.dataset.id);
|
||||
const selectedImageIndex = Number(e.target.dataset.index);
|
||||
|
||||
let imageValidationResult: ValidateImageResponse = { isValid: false, message: '' };
|
||||
const validGifCount = emoticonDetailFilesUrl.length !== 12;
|
||||
const validPngCount = emoticonDetailFilesUrl.length !== 12;
|
||||
|
||||
// 0. image file 유무, 조건
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. GIF와 PNG가 아닌 경우, 조건
|
||||
if (file.type !== 'image/gif' && file.type !== 'image/png') {
|
||||
alert('Select the correct file format. (GIF or PNG)');
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
} else if (file.type === 'image/gif' && validGifCount) {
|
||||
// 2. gif 형식 + 사진 갯수 + 그외 이미지 유효 조건들 확인, 조건
|
||||
imageValidationResult = await validateImageFileProperties(file, 2 * 1024 * 1024, 360, 360);
|
||||
} else if (file.type === 'image/png' && validPngCount) {
|
||||
// 3. png 형식 + 사진 갯수 + 그외 이미지 유효 조건들 확인, 조건
|
||||
imageValidationResult = await validateImageFileProperties(file, 150 * 1024, 360, 360);
|
||||
}
|
||||
|
||||
if (!imageValidationResult.isValid) {
|
||||
setModal({
|
||||
isOpen: true,
|
||||
txt: imageValidationResult.message || 'Unknown error'
|
||||
});
|
||||
e.target.value = ''; // 파일 선택 취소
|
||||
return;
|
||||
}
|
||||
|
||||
// 위의 모든 이미지 업로드 조건을 통과하면, 이미지 파일의 url을 위의 url state에 동기화한다.
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
reader.onloadend = () => {
|
||||
const uploadedEmoticonDetailImagesUrl = reader.result as string;
|
||||
|
||||
if (uploadedEmoticonDetailImagesUrl) {
|
||||
const requestBody = {
|
||||
guid: emoticonGuid,
|
||||
dispOrd: selectedImageDispOrd
|
||||
};
|
||||
|
||||
const typeCheckedFormData = convertToFormData(requestBody, file);
|
||||
|
||||
const changeUploadDetailFileUrl = {
|
||||
id: selectedImageId,
|
||||
dispOrd: selectedImageDispOrd,
|
||||
absFilePath: uploadedEmoticonDetailImagesUrl
|
||||
};
|
||||
|
||||
// 이미지 미리보기 업데이트용
|
||||
setEmoticonDetailFilesUrl((prev) => {
|
||||
const newState = [...prev];
|
||||
newState[selectedImageIndex] = changeUploadDetailFileUrl;
|
||||
return newState;
|
||||
});
|
||||
|
||||
setIsDataChanged(true);
|
||||
updateEmoticonFile(typeCheckedFormData, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
setIsApiCalled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const handleCancelClick = () => {
|
||||
if (isApiCalled) {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonDetails'] });
|
||||
onCloseModal();
|
||||
} else {
|
||||
onCloseModal();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
// shortCut과 file image만 변경됐을 경우, 조건 ( 이미지 순서 변경 + 그외 이름 등의 사항의 변경은 해당 안 된다.)
|
||||
if (reorderFiles.length === 0 && !isDataChanged) {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonDetails'] });
|
||||
onCloseModal();
|
||||
return;
|
||||
}
|
||||
|
||||
const request = {
|
||||
guid: emoticonGuid,
|
||||
emotiName: emoticonState.emoticonName,
|
||||
status: emoticonState.status.toString(),
|
||||
isDefault: emoticonState.isDefault,
|
||||
reorderFiles: reorderFiles
|
||||
};
|
||||
|
||||
updateEmoticon(request, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['emoticonDetails'] });
|
||||
onCloseModal();
|
||||
toast.success('The emoticon has been edited.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={modal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: handleModalClose,
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{modal.txt}
|
||||
</AlertModal>
|
||||
<div className={cn('emoticon_detail_modal')}>
|
||||
<h5 className={cn('modal_title')}>Register a default Emoticon</h5>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Emoticon Name</th>
|
||||
<td>
|
||||
<input
|
||||
className={cn('form_control')}
|
||||
type="text"
|
||||
name="emoticonNameInput"
|
||||
value={emoticonState.emoticonName}
|
||||
onChange={handleEmoticonNameChange}
|
||||
placeholder="Enter a item Name..."
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>
|
||||
<Radio
|
||||
options={[
|
||||
{ id: '1', label: 'ON' },
|
||||
{ id: '0', label: 'OFF' }
|
||||
]}
|
||||
name="itemStatus"
|
||||
value={emoticonState.statusNm}
|
||||
onChange={handleStatusChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>isDefault</th>
|
||||
<td>
|
||||
<Radio
|
||||
options={DEFAULT_TYPE}
|
||||
name="itemIsDefault"
|
||||
value={
|
||||
emoticonState?.isDefault === 'D' ? 'Defaults' : emoticonState?.isDefault === 'B' ? 'Sale' : 'Gift'
|
||||
}
|
||||
onChange={handleIsDefaultChange}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className={cn('short_cut_section')}>
|
||||
<h5 className={cn('section_header')}>Short cut</h5>
|
||||
<ul className={cn('section_upload')}>
|
||||
<li>
|
||||
<label
|
||||
htmlFor="short-cut-image-change"
|
||||
className={cn({
|
||||
img_has_file: shortCutImageUrl,
|
||||
lable_img_no_file: !shortCutImageUrl
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={shortCutImageUrl ? shortCutImageUrl : PlusIcon}
|
||||
alt="shortCut"
|
||||
className={cn({
|
||||
img_has_file: shortCutImageUrl,
|
||||
img_no_file: !shortCutImageUrl
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id="short-cut-image-change"
|
||||
data-role="shortCutImg"
|
||||
onChange={handleShortcutUpload}
|
||||
accept="image/png"
|
||||
name="shortCut"
|
||||
type="file"
|
||||
/>
|
||||
<p>Active</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={cn('file_upload_section')}>
|
||||
<h5 className={cn('section_header')}>File Upload</h5>
|
||||
<ul className={cn('section_upload')}>
|
||||
{Children.toArray(
|
||||
emoticonDetailFilesUrl?.map((item, index) => (
|
||||
<li
|
||||
onDragEnter={(e) => handleDragStart(e, index)}
|
||||
onDragEnd={(e) => handleDragEnd(e, index)}
|
||||
onDragOver={handelDragOver}
|
||||
draggable
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
htmlFor={'files_image' + item.dispOrd}
|
||||
className={cn({
|
||||
img_has_file: item.absFilePath,
|
||||
lable_img_no_file: !item.absFilePath
|
||||
})}
|
||||
>
|
||||
<img
|
||||
src={item.absFilePath ? item.absFilePath : PlusIcon}
|
||||
alt="file_upload_img"
|
||||
className={cn({
|
||||
img_has_file: item.absFilePath,
|
||||
img_no_file: !item.absFilePath
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
id={'files_image' + item.dispOrd}
|
||||
name={'files_image_change' + item.dispOrd}
|
||||
data-display-order={item.dispOrd}
|
||||
data-index={index}
|
||||
data-id={item.id}
|
||||
type="file"
|
||||
accept="image/png, image/gif"
|
||||
onChange={handleDetailFileUpload}
|
||||
/>
|
||||
<p>{index + 1}</p>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<footer className={cn('modal_footer')}>
|
||||
<Button size="md" color="close" onClick={handleCancelClick}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
color="confirm"
|
||||
disabled={!isDataChanged || isUpdatingEmoticon}
|
||||
onClick={handleConfirmClick}
|
||||
>
|
||||
Process
|
||||
</Button>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmoticonDetailsDataModal;
|
||||
256
src/pages/manage/administrator/AdministratorList.tsx
Normal file
256
src/pages/manage/administrator/AdministratorList.tsx
Normal file
@ -0,0 +1,256 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import AdministratorDetailModal from './components/AdministratorDetailModal';
|
||||
import AdministratorAddModal from './components/AdministratorAddModal';
|
||||
|
||||
import PlusImg from '@icons/ic-plus.svg';
|
||||
import DeleteImg from '@icons/ic-bin.svg';
|
||||
import { useModal } from '@hooks/useModal';
|
||||
import { AdminListRequest, useAdminListQuery, useDeleteAdminMutation } from '@api/adminAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import TitleLayout from '@layout/TitleLayout';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import { handleSearchSubmit } from '@utils/searchBoxUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
const AUTH_CODE = '008';
|
||||
|
||||
const AdministratorList = () => {
|
||||
const { isOpen, handleToggleModal } = useModal();
|
||||
const [checkItems, setCheckItems] = useState({ id: [] as number[] });
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 전체선택 핸들러
|
||||
const handleAllCheckClick = (checked: HTMLInputElement['checked']) => {
|
||||
if (checked) {
|
||||
setCheckItems({ id: adminListData?.content.map((item) => item.id) ?? [] });
|
||||
} else {
|
||||
setCheckItems({ id: [] });
|
||||
}
|
||||
};
|
||||
|
||||
// 개별선택 핸들러
|
||||
const handleSingleCheckClick = (checked: HTMLInputElement['checked'], id: number) => {
|
||||
if (checked) {
|
||||
setCheckItems((prevState) => ({
|
||||
...prevState,
|
||||
id: [...prevState.id, id]
|
||||
}));
|
||||
} else {
|
||||
setCheckItems((prevState) => ({
|
||||
...prevState,
|
||||
id: checkItems.id.filter((item) => item !== id)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const [id, setId] = useState(0);
|
||||
const [adminListRequest, setAdminListRequest] = useState<AdminListRequest>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: ''
|
||||
});
|
||||
|
||||
// Ref
|
||||
const searchRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: authCodeList = [], isLoading: authCodeLoading } = useSimpleCodeListQuery(AUTH_CODE);
|
||||
const { data: adminListData = null, isLoading: adminListLoading } = useAdminListQuery(adminListRequest);
|
||||
const { mutate: deleteAdmin, isPending: isDeletingAdmin } = useDeleteAdminMutation();
|
||||
|
||||
// Func
|
||||
// Modal event
|
||||
const handleModalBtnClick = (id?: number) => {
|
||||
id ? setId(id) : setId(0);
|
||||
handleToggleModal(true);
|
||||
};
|
||||
|
||||
// 삭제 핸들러
|
||||
const handleDeleteClick = () => {
|
||||
deleteAdmin(checkItems, {
|
||||
onSuccess: () => {
|
||||
toast.success('Admin has been deleted');
|
||||
// setAlertModal((prev) => ({
|
||||
// ...prev,
|
||||
// isOpen: false
|
||||
// }));
|
||||
queryClient.invalidateQueries({ queryKey: ['adminList'] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
disabled={isDeletingAdmin}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Process' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal isOpen={isOpen} onClose={() => handleToggleModal(false)}>
|
||||
{id > 0 ? (
|
||||
<AdministratorDetailModal id={id} onCloseModal={() => handleToggleModal(false)} />
|
||||
) : (
|
||||
<AdministratorAddModal onCloseModal={() => handleToggleModal(false)} />
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<TitleLayout title="Administrator">
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">Administrator List</Card.Title>
|
||||
<div className="header_box">
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onClick={(event) => handleSearchSubmit(event, searchRef, setAdminListRequest)}
|
||||
onKeyDown={(event) => handleSearchSubmit(event, searchRef, setAdminListRequest)}
|
||||
placeholderText="Name, Email, Contact"
|
||||
/>
|
||||
<Button size="lg" color="action" onClick={() => handleModalBtnClick()}>
|
||||
<img src={PlusImg} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '5%' }}>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mainCheckbox"
|
||||
name="select-all"
|
||||
onChange={(e) => {
|
||||
handleAllCheckClick(e.target.checked);
|
||||
}}
|
||||
checked={checkItems.id.length === adminListData?.content.length}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="mainCheckbox">
|
||||
<span className="hidden"></span>
|
||||
</label>
|
||||
</div>
|
||||
</th>
|
||||
<th style={{ width: '10%' }}>Administrator</th>
|
||||
<th style={{ width: '15%' }}>Person in charge</th>
|
||||
<th style={{ width: '8%' }}>Permission</th>
|
||||
<th style={{ width: '15%' }}>Contact</th>
|
||||
<th style={{ width: '22%' }}>Email</th>
|
||||
<th style={{ width: '15%' }}>Group</th>
|
||||
<th style={{ width: '10%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{adminListData?.content.map((admin, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ width: '5%' }}>
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={'select-' + admin.id}
|
||||
name={'select-' + admin.id}
|
||||
onChange={(e) => handleSingleCheckClick(e.target.checked, admin.id)}
|
||||
checked={checkItems.id.includes(admin.id) ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={'select-' + admin.id}>
|
||||
<span className="hidden"></span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td style={{ width: '10%' }}>{admin.userId}</td>
|
||||
<td style={{ width: '15%' }}>{admin.name}</td>
|
||||
<td style={{ width: '8%' }}>
|
||||
{authCodeList.map((item) => (item.code === admin.auth ? item.codeName : ''))}
|
||||
</td>
|
||||
<td style={{ width: '15%' }}>{admin.phone}</td>
|
||||
<td style={{ width: '22%' }}>{admin.email}</td>
|
||||
<td style={{ width: '15%' }}>{admin.groupName}</td>
|
||||
<td style={{ width: '10%' }}>
|
||||
<Button
|
||||
size="sm"
|
||||
color="rounded"
|
||||
value={admin.id}
|
||||
onClick={(e) => handleModalBtnClick(parseInt(e.currentTarget.value))}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<div className="left">
|
||||
<Button size="xs" color="action" disabled={checkItems.id.length < 1}>
|
||||
<img
|
||||
src={DeleteImg}
|
||||
alt="deleteBtn"
|
||||
onClick={() => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'Are you sure you want to delete it?',
|
||||
confirmBtn: { isConfirm: true, onConfirmClick: () => handleDeleteClick() }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<Pagination
|
||||
totalItems={adminListData?.totalElements ?? 0}
|
||||
itemsPerPage={adminListRequest.size}
|
||||
currentPage={adminListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setAdminListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</TitleLayout>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdministratorList;
|
||||
@ -0,0 +1,184 @@
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import { useAdminGroupCodeQuery } from '@api/adminGroupAPI';
|
||||
import { AddAdminRequest, useAddAdminMutation } from '@api/adminAPI';
|
||||
import { isEmpty } from '@utils/objectUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const AUTH_CODE = '008';
|
||||
|
||||
const AdministratorAddModal = ({ onCloseModal }: Props) => {
|
||||
const [addAdminRequest, setAddAdminRequest] = useState<AddAdminRequest>({
|
||||
email: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
grp: '',
|
||||
auth: ''
|
||||
});
|
||||
|
||||
// Ref
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const contactRef = useRef<HTMLInputElement>(null);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: authCodeList } = useSimpleCodeListQuery(AUTH_CODE);
|
||||
const { data: groupCodeList } = useAdminGroupCodeQuery();
|
||||
const { mutate: addAdmin, isPending: isPostingAdmin } = useAddAdminMutation();
|
||||
|
||||
const groupCodeOptions =
|
||||
groupCodeList?.map((group) => ({
|
||||
label: group.groupName,
|
||||
value: group.groupCd
|
||||
})) ?? [];
|
||||
|
||||
const authCodeOptions =
|
||||
authCodeList?.map((auth) => ({
|
||||
label: auth.codeName,
|
||||
value: auth.code
|
||||
})) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
if (authCodeList && authCodeList.length > 0) {
|
||||
setAddAdminRequest((prevState) => ({ ...prevState, auth: authCodeList[0].code }));
|
||||
}
|
||||
}, [authCodeList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (groupCodeList && groupCodeList.length > 0) {
|
||||
setAddAdminRequest((prevState) => ({ ...prevState, grp: groupCodeList[0].groupCd }));
|
||||
}
|
||||
}, [groupCodeList]);
|
||||
|
||||
// Func
|
||||
const handleConfirmClick = () => {
|
||||
if (addAdminRequest.name === '') {
|
||||
toast.error('Please enter your name.');
|
||||
nameRef.current?.focus();
|
||||
|
||||
return;
|
||||
} else if (!/^[\d-]+$/.test(addAdminRequest.phone)) {
|
||||
toast.error('Please enter your contact.');
|
||||
contactRef.current?.focus();
|
||||
|
||||
return;
|
||||
} else if (!/^[\w.-]+\w@[\w.-]+\w$/.test(addAdminRequest.email)) {
|
||||
toast.error('Please enter your email in the correct format.');
|
||||
emailRef.current?.focus();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
addAdmin(addAdminRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('A new Admin has been created.');
|
||||
onCloseModal();
|
||||
queryClient.invalidateQueries({ queryKey: ['adminList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['adminInfo'] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="title">
|
||||
<span className="modal_title">Add Administrator</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Administrator</th>
|
||||
<td>Admin + Number (Auto Create)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Person in charge</th>
|
||||
<td>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddAdminRequest((prevState) => ({ ...prevState, name: e.target.value }))}
|
||||
placeholder="Name"
|
||||
disabled={isEmpty(addAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Contact</th>
|
||||
<td>
|
||||
<input
|
||||
ref={contactRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddAdminRequest((prevState) => ({ ...prevState, phone: e.target.value }))}
|
||||
placeholder="000-0000-0000"
|
||||
disabled={isEmpty(addAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddAdminRequest((prevState) => ({ ...prevState, email: e.target.value }))}
|
||||
placeholder="AngkorLife@email.com"
|
||||
disabled={isEmpty(addAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<td>
|
||||
<Select
|
||||
menuPortalTarget={document.getElementsByClassName('modal_wrapper')[0] as HTMLElement}
|
||||
options={groupCodeOptions}
|
||||
value={groupCodeOptions.find((item) => item.value === addAdminRequest.grp)}
|
||||
onChange={(newValue) =>
|
||||
setAddAdminRequest((prevState) => ({ ...prevState, grp: newValue?.value ?? '' }))
|
||||
}
|
||||
isDisabled={isEmpty(addAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Permission</th>
|
||||
<td>
|
||||
<Select
|
||||
menuPortalTarget={document.getElementsByClassName('modal_wrapper')[0] as HTMLElement}
|
||||
options={authCodeOptions}
|
||||
value={authCodeOptions.find((item) => item.value === addAdminRequest.auth)}
|
||||
onChange={(newValue) =>
|
||||
setAddAdminRequest((prevState) => ({ ...prevState, auth: newValue?.value ?? '' }))
|
||||
}
|
||||
isDisabled={isEmpty(addAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isPostingAdmin}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdministratorAddModal;
|
||||
@ -0,0 +1,232 @@
|
||||
import Select from 'react-select';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { UpdateAdminRequest, useAdminInfoQuery, useResetPasswordMutation, useUpdateAdminMutation } from '@api/adminAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import { useAdminGroupCodeQuery } from '@api/adminGroupAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import { isEmpty } from '@utils/objectUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
onCloseModal: () => void;
|
||||
}
|
||||
|
||||
const AUTH_CODE = '008';
|
||||
|
||||
const AdministratorDetailModal = ({ id, onCloseModal }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
isConfirm: false
|
||||
});
|
||||
|
||||
const [updateAdminRequest, setUpdateAdminRequest] = useState<UpdateAdminRequest>({
|
||||
email: '',
|
||||
name: '',
|
||||
phone: '',
|
||||
grp: '',
|
||||
auth: '',
|
||||
id: 0
|
||||
});
|
||||
|
||||
// Ref
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
const contactRef = useRef<HTMLInputElement>(null);
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: authCodeList } = useSimpleCodeListQuery(AUTH_CODE);
|
||||
const { data: groupCodeList } = useAdminGroupCodeQuery();
|
||||
const { data: adminInfo } = useAdminInfoQuery(id);
|
||||
const { mutate: updateAdmin, isPending: isUpdatingAdmin } = useUpdateAdminMutation();
|
||||
const { mutate: resetPassword, isPending: isResettingPassword } = useResetPasswordMutation();
|
||||
|
||||
// 변수
|
||||
const groupCodeOptions =
|
||||
groupCodeList?.map((group) => ({
|
||||
label: group.groupName,
|
||||
value: group.groupCd
|
||||
})) ?? [];
|
||||
|
||||
const authCodeOptions =
|
||||
authCodeList?.map((auth) => ({
|
||||
label: auth.codeName,
|
||||
value: auth.code
|
||||
})) ?? [];
|
||||
|
||||
// Func
|
||||
const handlePasswordReset = () => {
|
||||
const resetAccount = {
|
||||
userId: adminInfo?.userId || '',
|
||||
email: adminInfo?.email
|
||||
};
|
||||
|
||||
resetPassword(resetAccount, {
|
||||
onSuccess: () => {
|
||||
toast.success('A temporary password has been sent to your e-mail.');
|
||||
onCloseModal();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (updateAdminRequest.name === '') {
|
||||
toast.error('Please enter your name.');
|
||||
nameRef.current?.focus();
|
||||
|
||||
return;
|
||||
} else if (!/^[\d-]+$/.test(updateAdminRequest.phone)) {
|
||||
toast.error('Please enter your contact.');
|
||||
contactRef.current?.focus();
|
||||
|
||||
return;
|
||||
} else if (!/^[\w.-]+\w@[\w.-]+\w$/.test(updateAdminRequest.email)) {
|
||||
toast.error('Please enter your email in the correct format.');
|
||||
emailRef.current?.focus();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
updateAdmin(updateAdminRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('Admin has been updated.');
|
||||
onCloseModal();
|
||||
queryClient.invalidateQueries({ queryKey: ['adminList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['adminInfo'] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (adminInfo && adminInfo !== undefined) {
|
||||
setUpdateAdminRequest(adminInfo);
|
||||
}
|
||||
}, [adminInfo]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
confirmBtn={alertModal.isConfirm ? { onClose: handlePasswordReset, buttonName: 'Confirm' } : undefined}
|
||||
closeBtn={{ onClose: () => onCloseModal(), buttonName: 'Close' }}
|
||||
disabled={isResettingPassword}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Edit Administrator</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Administrator</th>
|
||||
<td>Admin(임시)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Person in charge</th>
|
||||
<td>
|
||||
<input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
value={updateAdminRequest.name}
|
||||
onChange={(e) => setUpdateAdminRequest((prevState) => ({ ...prevState, name: e.target.value }))}
|
||||
placeholder="Name"
|
||||
disabled={isEmpty(updateAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Contact</th>
|
||||
<td>
|
||||
<input
|
||||
ref={contactRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
value={updateAdminRequest.phone}
|
||||
onChange={(e) => setUpdateAdminRequest((prevState) => ({ ...prevState, phone: e.target.value }))}
|
||||
placeholder="000-0000-0000"
|
||||
disabled={isEmpty(updateAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<td>
|
||||
<input
|
||||
ref={emailRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
value={updateAdminRequest.email}
|
||||
onChange={(e) => setUpdateAdminRequest((prevState) => ({ ...prevState, email: e.target.value }))}
|
||||
placeholder="AngkorLife@email.com"
|
||||
disabled={isEmpty(updateAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Group</th>
|
||||
<td>
|
||||
<Select
|
||||
menuPortalTarget={document.getElementsByClassName('modal_wrapper')[0] as HTMLElement}
|
||||
options={groupCodeOptions}
|
||||
value={groupCodeOptions.find((item) => item.value === updateAdminRequest.grp)}
|
||||
onChange={(newValue) =>
|
||||
setUpdateAdminRequest((prevState) => ({ ...prevState, grp: newValue?.value ?? '' }))
|
||||
}
|
||||
isDisabled={isEmpty(updateAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Permission</th>
|
||||
<td>
|
||||
<Select
|
||||
menuPortalTarget={document.getElementsByClassName('modal_wrapper')[0] as HTMLElement}
|
||||
options={authCodeOptions}
|
||||
value={authCodeOptions.find((item) => item.value === updateAdminRequest.auth)}
|
||||
onChange={(newValue) =>
|
||||
setUpdateAdminRequest((prevState) => ({ ...prevState, auth: newValue?.value ?? '' }))
|
||||
}
|
||||
isDisabled={isEmpty(updateAdminRequest)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className="left">
|
||||
<Button
|
||||
size="lg"
|
||||
color="action"
|
||||
onClick={() =>
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'Are you sure to reset the password for this account?',
|
||||
isConfirm: true
|
||||
})
|
||||
}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isUpdatingAdmin}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdministratorDetailModal;
|
||||
36
src/pages/manage/code/Code.tsx
Normal file
36
src/pages/manage/code/Code.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import MajorCode from './components/MajorCode';
|
||||
import MinorCode from './components/MinorCode';
|
||||
|
||||
export interface Major {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const Code: React.FC = () => {
|
||||
const [major, setMajor] = useState<Major>({ code: '', name: '' });
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<h1 className="page-title">Code</h1>
|
||||
<Row style={{ flexWrap: 'nowrap' }}>
|
||||
<Col
|
||||
sm={12}
|
||||
style={{ transform: `translateX(${major.code === '' ? 0 : -100}%)`, transition: 'all 0.5s ease-in-out' }}
|
||||
>
|
||||
<MajorCode setMajorCode={setMajor} />
|
||||
</Col>
|
||||
<Col
|
||||
sm={12}
|
||||
style={{ transform: `translateX(${major.code === '' ? 100 : -100}%)`, transition: 'all 0.5s ease-in-out' }}
|
||||
>
|
||||
<MinorCode major={major} setMajor={setMajor} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Code;
|
||||
196
src/pages/manage/code/components/AddCodeModal.tsx
Normal file
196
src/pages/manage/code/components/AddCodeModal.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { AddCodeRequest, useAddCodeMutation } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
major: { code: string; name: string };
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AddCodeModal = ({ major, onClose }: Props) => {
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [addMajorRequest, setAddMajorRequest] = useState<AddCodeRequest>({
|
||||
majorName: '',
|
||||
codeEn: '',
|
||||
codeKr: '',
|
||||
etc: ''
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: addMajor, isPending: isPostingMajor } = useAddCodeMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (major.code !== '') {
|
||||
setAddMajorRequest((prevState) => ({ ...prevState, major: major.code, majorName: undefined }));
|
||||
}
|
||||
}, [major]);
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (addMajorRequest.majorName === '' || addMajorRequest.codeEn === '' || addMajorRequest.codeKr === '') {
|
||||
setAlertModal({ isOpen: true, txt: 'Please fill the empty form.' });
|
||||
return;
|
||||
}
|
||||
|
||||
addMajor(addMajorRequest, {
|
||||
onSuccess: () => {
|
||||
setIsSuccess(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['codeList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['majorCodeList'] });
|
||||
toast.success('Major has been created.');
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({ isOpen: true, txt: error.response?.data.errorMessage as string });
|
||||
setIsSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (isSuccess) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
// etc 이미지 -> base64로 변환
|
||||
const handleInsertImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onloadend = () => {
|
||||
const menuIconUrl = fileReader.result?.toString();
|
||||
|
||||
if (menuIconUrl) {
|
||||
setAddMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
etc: menuIconUrl
|
||||
}));
|
||||
}
|
||||
fileReader.onloadend = null;
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () => handleAlertModalClose(),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Add Code</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Major Name</th>
|
||||
<td>
|
||||
{major.code === '' ? (
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) =>
|
||||
setAddMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
majorName: e.target.value
|
||||
}))
|
||||
}
|
||||
placeholder="Enter a major name."
|
||||
/>
|
||||
) : (
|
||||
major.name
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code Eng</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) =>
|
||||
setAddMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
codeEn: e.target.value
|
||||
}))
|
||||
}
|
||||
placeholder="Enter a major name(eng)."
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code Kmer</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) =>
|
||||
setAddMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
codeKr: e.target.value
|
||||
}))
|
||||
}
|
||||
placeholder="Enter a major name(kmer)."
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Etc. (Optional)</th>
|
||||
<td style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={addMajorRequest.etc}
|
||||
className="form_control"
|
||||
onChange={(e) =>
|
||||
setAddMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
etc: e.target.value
|
||||
}))
|
||||
}
|
||||
placeholder="Enter additional contents."
|
||||
/>
|
||||
{major.code.includes('005') && (
|
||||
<div className="file_upload" style={{ width: '15%' }}>
|
||||
<label htmlFor="fileInput">Add</label>
|
||||
<input id="fileInput" type="file" accept="image/*" onChange={handleInsertImg} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isPostingMajor}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCodeModal;
|
||||
241
src/pages/manage/code/components/DetailCodeModal.tsx
Normal file
241
src/pages/manage/code/components/DetailCodeModal.tsx
Normal file
@ -0,0 +1,241 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import DeleteIcon from '@icons/ic-bin.svg';
|
||||
import { toast } from 'react-toastify';
|
||||
import { UpdateCodeRequest, useDeleteCodeMutation, useUpdateCodeMutation } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
code: { code: string; codeEn: string; codeKr: string; etc: string };
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const DetailCodeModal = ({ code, onClose }: Props) => {
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
isConfirm: false
|
||||
});
|
||||
const [updateMajorRequest, setUpdateMajorRequest] = useState<UpdateCodeRequest>({
|
||||
data: {
|
||||
codeEn: '',
|
||||
codeKr: '',
|
||||
etc: ''
|
||||
},
|
||||
path: ''
|
||||
});
|
||||
|
||||
// Ref
|
||||
const codeEngRef = useRef<HTMLInputElement | null>(null);
|
||||
const codeKrRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: updateCode, isPending: isUpdatingCode } = useUpdateCodeMutation();
|
||||
const { mutate: deleteCode, isPending: isDeletingCode } = useDeleteCodeMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (code) {
|
||||
setUpdateMajorRequest({
|
||||
data: {
|
||||
codeEn: code.codeEn,
|
||||
codeKr: code.codeKr,
|
||||
etc: code.etc
|
||||
},
|
||||
path: code.code
|
||||
});
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (!codeEngRef.current?.value.trim() || !codeKrRef.current?.value.trim()) {
|
||||
toast.error('Both codes are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
updateCode(updateMajorRequest, {
|
||||
onSuccess: () => {
|
||||
setIsSuccess(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['codeList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['majorCodeList'] });
|
||||
toast.success('Code has been updated.');
|
||||
onClose();
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({ isOpen: true, txt: error.response?.data.errorMessage as string, isConfirm: false });
|
||||
setIsSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
deleteCode(code.code, {
|
||||
onSuccess: () => {
|
||||
toast.success('Code has been deleted.');
|
||||
onClose();
|
||||
setIsSuccess(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['codeList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['majorCodeList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({ isOpen: true, txt: error.response?.data.errorMessage as string, isConfirm: false });
|
||||
setIsSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '', isConfirm: false });
|
||||
|
||||
if (isSuccess) {
|
||||
queryClient.invalidateQueries({ queryKey: ['codeList'] });
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsertImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onloadend = () => {
|
||||
const menuIconUrl = fileReader.result?.toString();
|
||||
|
||||
if (menuIconUrl) {
|
||||
setUpdateMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: {
|
||||
...prevState.data,
|
||||
etc: menuIconUrl
|
||||
}
|
||||
}));
|
||||
}
|
||||
fileReader.onloadend = null;
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
disabled={isDeletingCode}
|
||||
isOpen={alertModal.isOpen}
|
||||
confirmBtn={alertModal.isConfirm ? { onClose: handleDeleteClick, buttonName: 'Confirm' } : undefined}
|
||||
closeBtn={{
|
||||
onClose: handleAlertModalClose,
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Edit Code</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<td>{code.code}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code Eng</th>
|
||||
<td>
|
||||
<input
|
||||
ref={codeEngRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={code.codeEn}
|
||||
onChange={(e) => {
|
||||
setUpdateMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: {
|
||||
...prevState.data,
|
||||
codeEn: e.target.value
|
||||
}
|
||||
}));
|
||||
}}
|
||||
placeholder="Enter a code name (eng)."
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Code Kr</th>
|
||||
<td>
|
||||
<input
|
||||
ref={codeKrRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={code.codeKr}
|
||||
onChange={(e) =>
|
||||
setUpdateMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: {
|
||||
...prevState.data,
|
||||
codeKr: e.target.value
|
||||
}
|
||||
}))
|
||||
}
|
||||
placeholder="Enter a code name (kr)."
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Etc. (Optional)</th>
|
||||
<td style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={updateMajorRequest.data.etc}
|
||||
className="form_control"
|
||||
onChange={(e) =>
|
||||
setUpdateMajorRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: {
|
||||
...prevState.data,
|
||||
etc: e.target.value
|
||||
}
|
||||
}))
|
||||
}
|
||||
placeholder="Enter text or add an image."
|
||||
/>
|
||||
{code.code.startsWith('005') && (
|
||||
<div className="file_upload" style={{ width: '15%' }}>
|
||||
<label htmlFor="fileInput">Add</label>
|
||||
<input id="fileInput" type="file" accept="image/*" onChange={handleInsertImg} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<div className="left">
|
||||
<Button
|
||||
size="xs"
|
||||
color="action"
|
||||
onClick={() => setAlertModal({ isOpen: true, txt: 'Are you sure you want to delete it?', isConfirm: true })}
|
||||
>
|
||||
<img src={DeleteIcon} alt="delete icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isUpdatingCode}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DetailCodeModal;
|
||||
189
src/pages/manage/code/components/MajorCode.tsx
Normal file
189
src/pages/manage/code/components/MajorCode.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import React, { Dispatch, SetStateAction, useRef, useState } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
|
||||
import { Major } from '../Code';
|
||||
import MajorNameModal from './MajorNameModal';
|
||||
import AddCodeModal from './AddCodeModal';
|
||||
|
||||
import PlusImg from '@icons/ic-plus.svg';
|
||||
import { MajorCodeListRequest, useMajorCodeListQuery } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import { handleSearchSubmit } from '@utils/searchBoxUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
interface Props {
|
||||
setMajorCode: Dispatch<SetStateAction<Major>>;
|
||||
}
|
||||
|
||||
const MajorCode = ({ setMajorCode }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const [modalInfo, setModalInfo] = useState({
|
||||
isOpen: false,
|
||||
modalName: '',
|
||||
code: '',
|
||||
name: ''
|
||||
});
|
||||
const [codeListRequest, setCodeListRequest] = useState<MajorCodeListRequest>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: ''
|
||||
});
|
||||
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { data: majorList, isLoading: majorLoading, refetch: majorRefetch } = useMajorCodeListQuery(codeListRequest);
|
||||
|
||||
// Minors event
|
||||
const handleMinorsClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const majorCode = e.currentTarget.value;
|
||||
const majorName = e.currentTarget.name;
|
||||
|
||||
setMajorCode({ code: majorCode, name: majorName });
|
||||
};
|
||||
|
||||
if (majorLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Confirm' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal
|
||||
isOpen={modalInfo.isOpen}
|
||||
onClose={() => setModalInfo({ isOpen: false, modalName: '', code: '', name: '' })}
|
||||
>
|
||||
{modalInfo.modalName === 'major' ? (
|
||||
<MajorNameModal
|
||||
major={{ code: modalInfo.code, name: modalInfo.name }}
|
||||
onClose={() => setModalInfo({ isOpen: false, modalName: '', code: '', name: '' })}
|
||||
majorListRefetch={majorRefetch}
|
||||
/>
|
||||
) : (
|
||||
<AddCodeModal
|
||||
major={{ code: modalInfo.code, name: modalInfo.name }}
|
||||
onClose={() => setModalInfo({ isOpen: false, modalName: '', code: '', name: '' })}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">
|
||||
Code List
|
||||
<span style={{ color: 'rgb(126 132 163 / 61%)', padding: '0 10px' }}>></span>
|
||||
Major Code
|
||||
</Card.Title>
|
||||
<div className="header_box" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onClick={(event) => handleSearchSubmit(event, searchRef, setCodeListRequest)}
|
||||
onKeyDown={(event) => handleSearchSubmit(event, searchRef, setCodeListRequest)}
|
||||
placeholderText="Major Name"
|
||||
/>
|
||||
<Button
|
||||
size="lg"
|
||||
color="action"
|
||||
onClick={() => setModalInfo({ isOpen: true, modalName: 'add', code: '', name: '' })}
|
||||
>
|
||||
<img src={PlusImg} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '40%' }}>Major Code</th>
|
||||
<th style={{ width: '40%' }}>Major Name</th>
|
||||
<th style={{ width: '20%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{majorList?.content.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ width: '40%' }}>{item.major}</td>
|
||||
<td style={{ width: '40%' }}>{item.majorName}</td>
|
||||
<td style={{ width: '20%', gap: '10px' }}>
|
||||
<button
|
||||
className="list_edit_button"
|
||||
value={item.major}
|
||||
name={item.majorName}
|
||||
onClick={(e) =>
|
||||
setModalInfo({
|
||||
isOpen: true,
|
||||
modalName: 'major',
|
||||
code: e.currentTarget.value,
|
||||
name: e.currentTarget.name
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="list_edit_button"
|
||||
value={item.major}
|
||||
name={item.majorName}
|
||||
onClick={(e) => handleMinorsClick(e)}
|
||||
>
|
||||
Minors
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<Pagination
|
||||
totalItems={majorList?.totalElements ?? 0}
|
||||
itemsPerPage={codeListRequest.size}
|
||||
currentPage={codeListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setCodeListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MajorCode;
|
||||
111
src/pages/manage/code/components/MajorNameModal.tsx
Normal file
111
src/pages/manage/code/components/MajorNameModal.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import { QueryObserverResult, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { Page } from 'src/services/commonResponse/PageResponse';
|
||||
import { MajorCodeListResponse, UpdateMajorNameRequest, useUpdateMajorNameMutation } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import { isEmpty } from '@utils/objectUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
major: { code: string; name: string };
|
||||
onClose: () => void;
|
||||
majorListRefetch: () => Promise<QueryObserverResult<Page<MajorCodeListResponse>, Error>>;
|
||||
}
|
||||
|
||||
const MajorNameModal = ({ major, onClose, majorListRefetch }: Props) => {
|
||||
const [updateSuccess, setUpdateSuccess] = useState(false);
|
||||
const [modal, setModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [updateMajorNameRequest, setUpdateMajorNameRequest] = useState<UpdateMajorNameRequest>({
|
||||
data: {
|
||||
codeValue: major.name
|
||||
},
|
||||
path: major.code
|
||||
});
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: updateMajorName, isPending: isUpdatingMajorName } = useUpdateMajorNameMutation();
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
updateMajorName(updateMajorNameRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('Major name has been updated.');
|
||||
onClose();
|
||||
setUpdateSuccess(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['majorCodeList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setModal({ isOpen: true, txt: error.response?.data.errorMessage as string });
|
||||
setUpdateSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (updateSuccess) {
|
||||
majorListRefetch();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={modal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () => handleAlertModalClose(),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{modal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Major Name</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Major Code</th>
|
||||
<td>{major.code}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Major Name</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={major.name}
|
||||
onChange={(e) =>
|
||||
setUpdateMajorNameRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, codeValue: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMajorNameRequest.data)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isUpdatingMajorName}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MajorNameModal;
|
||||
236
src/pages/manage/code/components/MinorCode.tsx
Normal file
236
src/pages/manage/code/components/MinorCode.tsx
Normal file
@ -0,0 +1,236 @@
|
||||
import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
|
||||
import AddCodeModal from './AddCodeModal';
|
||||
import DetailCodeModal from './DetailCodeModal';
|
||||
import { Major } from '../Code';
|
||||
|
||||
import prevImg from '@icons/chevron-left.svg';
|
||||
import plusImg from '@icons/ic-plus.svg';
|
||||
import { CodeListRequest, useCodeListQuery } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
interface Props {
|
||||
major: Major;
|
||||
setMajor: Dispatch<SetStateAction<Major>>;
|
||||
}
|
||||
|
||||
interface ModalInfo {
|
||||
isOpen?: boolean;
|
||||
modalName: string;
|
||||
code: string;
|
||||
codeEn?: string;
|
||||
codeKr?: string;
|
||||
etc?: string;
|
||||
}
|
||||
|
||||
const MinorCode = ({ major, setMajor }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const [modalInfo, setModalInfo] = useState<ModalInfo>({
|
||||
isOpen: false,
|
||||
modalName: '',
|
||||
code: ''
|
||||
});
|
||||
const [codeListRequest, setCodeListRequest] = useState<CodeListRequest>({
|
||||
params: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: ''
|
||||
},
|
||||
path: ''
|
||||
});
|
||||
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { data: codeList, isLoading: codeLoading } = useCodeListQuery(codeListRequest);
|
||||
|
||||
useEffect(() => {
|
||||
if (major.code !== '') {
|
||||
setCodeListRequest((prevState) => ({
|
||||
...prevState,
|
||||
path: major.code
|
||||
}));
|
||||
}
|
||||
}, [major]);
|
||||
|
||||
// 검색 이벤트
|
||||
const handleSearchSubmit = (event: React.MouseEvent<HTMLButtonElement> | React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (
|
||||
event.type === 'click' ||
|
||||
(event.type === 'keydown' && (event as React.KeyboardEvent<HTMLInputElement>).key === 'Enter')
|
||||
) {
|
||||
event.preventDefault();
|
||||
setCodeListRequest((prevState) => ({
|
||||
...prevState,
|
||||
params: {
|
||||
...prevState.params,
|
||||
searchKeyword: searchRef.current?.value.trim() || '',
|
||||
page: 1
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevClick = () => {
|
||||
setMajor({ code: '', name: '' });
|
||||
};
|
||||
|
||||
if (codeLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Confirm' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal isOpen={modalInfo.isOpen === true} onClose={() => setModalInfo({ modalName: '', code: '' })}>
|
||||
{modalInfo.modalName === 'add' ? (
|
||||
<AddCodeModal
|
||||
major={{ code: modalInfo.code, name: modalInfo.codeEn ?? '' }}
|
||||
onClose={() => setModalInfo({ modalName: '', code: '' })}
|
||||
/>
|
||||
) : (
|
||||
<DetailCodeModal
|
||||
code={{
|
||||
code: modalInfo.code,
|
||||
codeEn: modalInfo.codeEn ?? '',
|
||||
codeKr: modalInfo.codeKr ?? '',
|
||||
etc: modalInfo.etc ?? ''
|
||||
}}
|
||||
onClose={() => setModalInfo({ modalName: '', code: '' })}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">
|
||||
<button className="none_button" onClick={() => handlePrevClick()}>
|
||||
<img src={prevImg} alt="prev" />
|
||||
</button>
|
||||
Major Code ({major.name})<span style={{ color: 'rgb(126 132 163 / 61%)', padding: '0 10px' }}>></span>
|
||||
Minor Code
|
||||
</Card.Title>
|
||||
<div className="header_box" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onClick={handleSearchSubmit}
|
||||
onKeyDown={handleSearchSubmit}
|
||||
placeholderText="Search"
|
||||
/>
|
||||
<Button
|
||||
size="lg"
|
||||
color="action"
|
||||
onClick={() => setModalInfo({ isOpen: true, modalName: 'add', code: major.code, codeEn: major.name })}
|
||||
>
|
||||
<img src={plusImg} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '10%' }}>Code</th>
|
||||
<th style={{ width: '30%' }}>Code Eng</th>
|
||||
<th style={{ width: '30%' }}>Code Kmer</th>
|
||||
<th style={{ width: '20%' }}>etc</th>
|
||||
<th style={{ width: '10%' }}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{codeList?.content.map((item, idx) => (
|
||||
<tr key={idx}>
|
||||
<td style={{ width: '10%' }}>{item.code}</td>
|
||||
<td style={{ width: '30%' }}>{item.codeEn}</td>
|
||||
<td style={{ width: '30%' }}>{item.codeKr}</td>
|
||||
<td style={{ width: '20%' }}>
|
||||
{item.etc.startsWith('data:image') ? (
|
||||
<img src={item.etc} alt={item.codeEn} style={{ width: '35px' }} />
|
||||
) : (
|
||||
item.etc
|
||||
)}
|
||||
</td>
|
||||
<td style={{ width: '10%' }}>
|
||||
<button
|
||||
className="list_edit_button"
|
||||
data-code={item.code}
|
||||
data-code-en={item.codeEn}
|
||||
data-code-kr={item.codeKr}
|
||||
data-etc={item.etc}
|
||||
onClick={(e) =>
|
||||
setModalInfo({
|
||||
isOpen: true,
|
||||
modalName: 'major',
|
||||
code: e.currentTarget.dataset.code ?? '',
|
||||
codeEn: e.currentTarget.dataset.codeEn ?? '',
|
||||
codeKr: e.currentTarget.dataset.codeKr ?? '',
|
||||
etc: e.currentTarget.dataset.etc ?? ''
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<Pagination
|
||||
totalItems={codeList?.totalElements ?? 0}
|
||||
itemsPerPage={codeListRequest.params.size}
|
||||
currentPage={codeListRequest.params.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setCodeListRequest((prevState) => ({
|
||||
...prevState,
|
||||
params: { ...prevState.params, page: pageNumber }
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MinorCode;
|
||||
269
src/pages/manage/group/Group.tsx
Normal file
269
src/pages/manage/group/Group.tsx
Normal file
@ -0,0 +1,269 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Card, Col, Row } from 'react-bootstrap';
|
||||
import GroupDetailModal from './components/GroupDetailModal';
|
||||
import GroupAddModal from './components/GroupAddModal';
|
||||
import plusImg from '@icons/ic-plus.svg';
|
||||
import deleteImg from '@icons/ic-bin.svg';
|
||||
import { toast } from 'react-toastify';
|
||||
import { AdminGroupListRequest, useAdminGroupListQuery, useDeleteAdminGroupMutation } from '@api/adminGroupAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import SearchBar from '@components/ui/searchBar/SearchBar';
|
||||
import Button from '@components/ui/button/Button';
|
||||
import Pagination from '@components/ui/pagination/Pagination';
|
||||
|
||||
const Group = () => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [groupCd, setGroupCd] = useState('');
|
||||
const [checkItems, setCheckItems] = useState({ groupCd: [] as string[] });
|
||||
const [groupListRequest, setGroupListRequest] = useState<AdminGroupListRequest>({
|
||||
page: 1,
|
||||
size: 10,
|
||||
sort: [],
|
||||
searchKeyword: ''
|
||||
});
|
||||
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { data: groupList = null, refetch: groupListRefetch } = useAdminGroupListQuery(groupListRequest);
|
||||
const { mutate: deleteGroup, isPending: isDeletingGroup } = useDeleteAdminGroupMutation();
|
||||
|
||||
// Modal event
|
||||
const handleModalBtnClick = (groupCd?: string) => {
|
||||
groupCd ? setGroupCd(groupCd) : setGroupCd('');
|
||||
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Search event
|
||||
const handleSearch = () => {
|
||||
setGroupListRequest((prevState) => ({
|
||||
...prevState,
|
||||
page: 0
|
||||
}));
|
||||
|
||||
groupListRefetch();
|
||||
};
|
||||
|
||||
// CheckBox check event
|
||||
const handleAllCheckClick = (checked: HTMLInputElement['checked']) => {
|
||||
if (checked) {
|
||||
setCheckItems((prevState) => ({
|
||||
...prevState,
|
||||
groupCd: [...prevState.groupCd, ...(groupList?.content.map((item) => item.groupCd) ?? [])]
|
||||
}));
|
||||
} else {
|
||||
setCheckItems({ groupCd: [] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSingleCheckClick = (checked: HTMLInputElement['checked'], groupCd: string) => {
|
||||
if (checked) {
|
||||
setCheckItems((prevState) => ({
|
||||
...prevState,
|
||||
groupCd: [...prevState.groupCd, groupCd]
|
||||
}));
|
||||
} else {
|
||||
setCheckItems((prevState) => ({
|
||||
...prevState,
|
||||
groupCd: checkItems.groupCd.filter((item) => item !== groupCd)
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// Delete event
|
||||
const handleDeleteClick = () => {
|
||||
deleteGroup(checkItems, {
|
||||
onSuccess: () => {
|
||||
toast.success('Admin Group has been deleted');
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: 'Admin Group has been deleted',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => null
|
||||
}
|
||||
});
|
||||
|
||||
groupListRefetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: error.response?.data.errorMessage as string,
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => null
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
disabled={isDeletingGroup}
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Process' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
{groupCd === '' ? (
|
||||
<GroupAddModal onCloseModal={() => setIsModalOpen(false)} groupListRefetch={groupListRefetch} />
|
||||
) : (
|
||||
<GroupDetailModal
|
||||
groupCd={groupCd}
|
||||
onCloseModal={() => setIsModalOpen(false)}
|
||||
groupListRefetch={groupListRefetch}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
<div>
|
||||
<h1 className="page-title">Group</h1>
|
||||
<Row>
|
||||
<Col sm={12}>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">Group List</Card.Title>
|
||||
<div className="header_box">
|
||||
<SearchBar
|
||||
ref={searchRef}
|
||||
onClick={() => groupListRefetch()}
|
||||
onKeyDown={(e) => {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}}
|
||||
placeholderText="Group Name"
|
||||
/>
|
||||
<div className="modal_add_btn">
|
||||
<button onClick={() => handleModalBtnClick()}>
|
||||
<img src={plusImg} alt="plus" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="mainCheckbox"
|
||||
name="select-all"
|
||||
onChange={(e) => handleAllCheckClick(e.target.checked)}
|
||||
checked={checkItems.groupCd.length === groupList?.content.length ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="mainCheckbox">
|
||||
<span className="hidden"></span>
|
||||
</label>
|
||||
</div>
|
||||
</th>
|
||||
<th className="width-40">Group Name</th>
|
||||
<th className="width-40">Members</th>
|
||||
<th className="width-10"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{groupList?.content.map((group, idx) => (
|
||||
<tr key={idx}>
|
||||
<td className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={'select-' + group.groupCd}
|
||||
name={'select-' + group.groupCd}
|
||||
onChange={(e) => handleSingleCheckClick(e.target.checked, group.groupCd)}
|
||||
checked={checkItems.groupCd.includes(group.groupCd) ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={'select-' + group.groupCd}>
|
||||
<span className="hidden"></span>
|
||||
</label>
|
||||
</div>
|
||||
</td>
|
||||
<td className="width-40">{group.groupName}</td>
|
||||
<td className="width-40">{group.members}</td>
|
||||
<td className="width-10">
|
||||
<button
|
||||
className="list_edit_button"
|
||||
value={group.groupCd}
|
||||
onClick={(e) => handleModalBtnClick(e.currentTarget.value)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer">
|
||||
<div className="left">
|
||||
<Button size="xs" color="action" disabled={checkItems.groupCd.length === 0}>
|
||||
<img
|
||||
src={deleteImg}
|
||||
alt="deleteBtn"
|
||||
onClick={() => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'Are you sure you want to delete it?',
|
||||
confirmBtn: { isConfirm: true, onConfirmClick: () => handleDeleteClick() }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<Pagination
|
||||
totalItems={groupList?.totalElements ?? 0}
|
||||
itemsPerPage={groupListRequest.size}
|
||||
currentPage={groupListRequest.page}
|
||||
onPaginate={(pageNumber: number) =>
|
||||
setGroupListRequest((prevState) => ({ ...prevState, page: pageNumber }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Group;
|
||||
99
src/pages/manage/group/components/GroupAddModal.tsx
Normal file
99
src/pages/manage/group/components/GroupAddModal.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { QueryObserverResult, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import { AddAdminGroupRequest, AdminGroupListResponse, useAddAdminGroupMutation } from '@api/adminGroupAPI';
|
||||
import { Page } from 'src/services/commonResponse/PageResponse';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
onCloseModal: () => void;
|
||||
groupListRefetch: () => Promise<QueryObserverResult<Page<AdminGroupListResponse>, Error>>;
|
||||
}
|
||||
|
||||
const GroupAddModal = ({ onCloseModal, groupListRefetch }: Props) => {
|
||||
const [addSuccess, setAddSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [addGroupRequest, setAddGroupRequest] = useState<AddAdminGroupRequest>({
|
||||
groupName: ''
|
||||
});
|
||||
|
||||
// Ref
|
||||
const groupNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: addGroup, isPending: isPostingGroup } = useAddAdminGroupMutation();
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (addGroupRequest.groupName === '') {
|
||||
toast.error('Please enter group name.');
|
||||
groupNameRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
addGroup(addGroupRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('A new admin group has been created.');
|
||||
queryClient.invalidateQueries({ queryKey: ['adminGroupList'] });
|
||||
onCloseModal();
|
||||
setAddSuccess(true);
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({ isOpen: true, txt: error.response?.data.errorMessage as string });
|
||||
setAddSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (addSuccess) {
|
||||
groupListRefetch();
|
||||
onCloseModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal isOpen={alertModal.isOpen} closeBtn={{ onClose: () => handleAlertModalClose(), buttonName: 'Close' }}>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Add Group</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<td>
|
||||
<input
|
||||
ref={groupNameRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddGroupRequest((prevState) => ({ ...prevState, groupName: e.target.value }))}
|
||||
placeholder="Group Name"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isPostingGroup}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupAddModal;
|
||||
122
src/pages/manage/group/components/GroupDetailModal.tsx
Normal file
122
src/pages/manage/group/components/GroupDetailModal.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { QueryObserverResult, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
AdminGroupListResponse,
|
||||
UpdateAdminGroupRequest,
|
||||
useAdminGroupInfoQuery,
|
||||
useUpdateAdminGroupMutation
|
||||
} from '@api/adminGroupAPI';
|
||||
import { Page } from 'src/services/commonResponse/PageResponse';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
groupCd: string;
|
||||
onCloseModal: () => void;
|
||||
groupListRefetch: () => Promise<QueryObserverResult<Page<AdminGroupListResponse>, Error>>;
|
||||
}
|
||||
|
||||
const GroupDetailModal = ({ groupCd, onCloseModal, groupListRefetch }: Props) => {
|
||||
const [addSuccess, setAddSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [updateGroupRequest, setUpdateGroupRequest] = useState<UpdateAdminGroupRequest>({
|
||||
groupCd: '',
|
||||
groupName: ''
|
||||
});
|
||||
|
||||
// Ref
|
||||
const groupNameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: groupInfo } = useAdminGroupInfoQuery(groupCd);
|
||||
const { mutate: updateGroup, isPending: isUpdatingGroup } = useUpdateAdminGroupMutation();
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
if (updateGroupRequest.groupName === '') {
|
||||
toast.error('Please enter group name.');
|
||||
groupNameRef.current?.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
updateGroup(updateGroupRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('Admin group has been updated.');
|
||||
onCloseModal();
|
||||
setAddSuccess(true);
|
||||
groupListRefetch();
|
||||
},
|
||||
onError: () => {
|
||||
setAddSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (addSuccess) {
|
||||
groupListRefetch();
|
||||
onCloseModal();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal isOpen={alertModal.isOpen} closeBtn={{ onClose: () => handleAlertModalClose(), buttonName: 'Close' }}>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Edit Group</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Group Name</th>
|
||||
<td>
|
||||
<input
|
||||
ref={groupNameRef}
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={groupInfo?.groupName}
|
||||
onChange={(e) =>
|
||||
setUpdateGroupRequest((prevState) => ({
|
||||
...prevState,
|
||||
groupCd: e.target.value,
|
||||
groupName: 'bbbbbbbbb'
|
||||
}))
|
||||
}
|
||||
placeholder="Group Name"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Members</th>
|
||||
<td>{groupInfo?.members}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Create Date</th>
|
||||
<td>{groupInfo?.createDt}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onCloseModal}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isUpdatingGroup}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GroupDetailModal;
|
||||
51
src/pages/manage/menu/Menu.tsx
Normal file
51
src/pages/manage/menu/Menu.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
|
||||
import TopMenu from './components/TopMenu';
|
||||
import SubMenu from './components/SubMenu';
|
||||
|
||||
export interface TopMenuInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const Menu: React.FC = () => {
|
||||
const [topMenuInfo, setTopMenuInfo] = useState<TopMenuInfo>({ id: 0, name: '' });
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const newId = searchParams.get('id');
|
||||
const newName = searchParams.get('name');
|
||||
setTopMenuInfo((prevState) => ({
|
||||
...prevState,
|
||||
id: newId ? parseInt(newId, 10) : 0,
|
||||
name: newName || ''
|
||||
}));
|
||||
}, [location.search]);
|
||||
|
||||
return (
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<h1 className="page-title">Menu</h1>c
|
||||
<Row style={{ flexWrap: 'nowrap' }}>
|
||||
{topMenuInfo.id === 0 && (
|
||||
<div>
|
||||
<Col className="top-wrap" sm={12}>
|
||||
<TopMenu setTopId={setTopMenuInfo} />
|
||||
</Col>
|
||||
</div>
|
||||
)}
|
||||
{topMenuInfo.id !== 0 && (
|
||||
<div>
|
||||
<Col className="sub-wrap" sm={12}>
|
||||
<SubMenu topMenuInfo={topMenuInfo} setTopMenuInfo={setTopMenuInfo} />
|
||||
</Col>
|
||||
</div>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
212
src/pages/manage/menu/components/MenuAddModal.tsx
Normal file
212
src/pages/manage/menu/components/MenuAddModal.tsx
Normal file
@ -0,0 +1,212 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { AddMenuRequest, useAddMenuMutation } from '@api/menuAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import Radio, { RadioOption } from '@components/ui/radio/Radio';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
id?: number;
|
||||
onClose: () => void;
|
||||
isTopMenu: boolean;
|
||||
}
|
||||
|
||||
const MenuAddModal = ({ id, onClose, isTopMenu }: Props) => {
|
||||
const [addSuccess, setAddSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
|
||||
const [addMenuRequest, setAddMenuRequest] = useState<AddMenuRequest>({
|
||||
topId: id ?? 0,
|
||||
menuName: '',
|
||||
menuLink: '',
|
||||
menuFolder: '',
|
||||
menuFile: '',
|
||||
menuIcon: '',
|
||||
levels: []
|
||||
});
|
||||
|
||||
const code = process.env.REACT_APP_CODE as string;
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { data: authCodeList, isLoading: isAuthCodeLoading } = useSimpleCodeListQuery('008');
|
||||
const { mutate: addMenu, isPending: isPostingMenu } = useAddMenuMutation();
|
||||
|
||||
// Func
|
||||
const handleConfirmClick = () => {
|
||||
addMenu(addMenuRequest, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
toast.success('A new menu has been created.');
|
||||
onClose();
|
||||
setAddSuccess(true);
|
||||
},
|
||||
onError: () => {
|
||||
setAddSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (addSuccess) {
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRadioCheck = (option: RadioOption, code: string) => {
|
||||
if (option.id.includes('Y')) {
|
||||
setAddMenuRequest((prevState) => ({ ...prevState, levels: [...prevState.levels, code] }));
|
||||
} else {
|
||||
setAddMenuRequest((prevState) => ({ ...prevState, levels: prevState.levels.filter((item) => item !== code) }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsertImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onloadend = () => {
|
||||
const menuIconUrl = fileReader.result?.toString();
|
||||
|
||||
if (menuIconUrl) {
|
||||
setAddMenuRequest((prevState) => ({ ...prevState, menuIcon: menuIconUrl }));
|
||||
e.target.value = '';
|
||||
}
|
||||
fileReader.onloadend = null;
|
||||
};
|
||||
};
|
||||
|
||||
if (isAuthCodeLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () => handleAlertModalClose(),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Manage Menu Add</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Menu Name</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddMenuRequest((prevState) => ({ ...prevState, menuName: e.target.value }))}
|
||||
placeholder="Enter a menu name"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu Link</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddMenuRequest((prevState) => ({ ...prevState, menuLink: e.target.value }))}
|
||||
placeholder="Enter a menu link"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu Folder</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddMenuRequest((prevState) => ({ ...prevState, menuFolder: e.target.value }))}
|
||||
placeholder="Enter a menu folder"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu File</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
onChange={(e) => setAddMenuRequest((prevState) => ({ ...prevState, menuFile: e.target.value }))}
|
||||
placeholder="Enter a menu file"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{isTopMenu && (
|
||||
<tr>
|
||||
<th>Menu Icon</th>
|
||||
<td style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
value={addMenuRequest.menuIcon}
|
||||
onChange={(e) => setAddMenuRequest((prevState) => ({ ...prevState, menuIcon: e.target.value }))}
|
||||
placeholder="Enter a menu icon"
|
||||
/>
|
||||
<div className="file_upload" style={{ width: '15%' }}>
|
||||
<label htmlFor="fileInput">Add</label>
|
||||
<input id="fileInput" type="file" accept="image/*" onChange={(e) => handleInsertImg(e)} />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{authCodeList?.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<th>{item.codeName}</th>
|
||||
<td>
|
||||
<div className="radio_wrap flex">
|
||||
<Radio
|
||||
options={[
|
||||
{ id: item.codeName + 'Y', label: 'Y' },
|
||||
{ id: item.codeName + 'N', label: 'N' }
|
||||
]}
|
||||
name={`auth${index}`}
|
||||
value={addMenuRequest.levels.includes(item.code) ? 'Y' : 'N'}
|
||||
onChange={(option) => handleRadioCheck(option, item.code)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleConfirmClick} disabled={isPostingMenu}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuAddModal;
|
||||
282
src/pages/manage/menu/components/MenuDetailModal.tsx
Normal file
282
src/pages/manage/menu/components/MenuDetailModal.tsx
Normal file
@ -0,0 +1,282 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { toast } from 'react-toastify';
|
||||
import { UpdateMenuRequest, useMenuDetailQuery, useUpdateMenuMutation } from '@api/menuAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import Radio, { RadioOption } from '@components/ui/radio/Radio';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import { isEmpty } from '@utils/objectUtils';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
id: number;
|
||||
onClose: () => void;
|
||||
isTopMenu: boolean;
|
||||
}
|
||||
|
||||
const MenuDetailModal = ({ id, onClose, isTopMenu }: Props) => {
|
||||
const [updateSuccess, setUpdateSuccess] = useState(false);
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: ''
|
||||
});
|
||||
const [updateMenuRequest, setUpdateMenuRequest] = useState<UpdateMenuRequest>({
|
||||
data: {
|
||||
menuName: '',
|
||||
menuLink: '',
|
||||
menuFolder: '',
|
||||
menuFile: '',
|
||||
menuIcon: '',
|
||||
levels: []
|
||||
},
|
||||
path: id
|
||||
});
|
||||
|
||||
const code = process.env.REACT_APP_CODE as string;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: updateMenu, isPending: isUpdateingMenu } = useUpdateMenuMutation();
|
||||
const { data: menuDetail } = useMenuDetailQuery(id);
|
||||
const { data: authCodeList } = useSimpleCodeListQuery('008');
|
||||
|
||||
useEffect(() => {
|
||||
if (menuDetail) {
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: {
|
||||
menuName: menuDetail.menuName,
|
||||
menuLink: menuDetail.menuLink,
|
||||
menuFolder: menuDetail.menuFolder,
|
||||
menuFile: menuDetail.menuFile,
|
||||
menuIcon: menuDetail.menuIcon,
|
||||
levels: menuDetail.levels
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [menuDetail]);
|
||||
|
||||
const handleProcessClick = () => {
|
||||
updateMenu(updateMenuRequest, {
|
||||
onSuccess: () => {
|
||||
toast.success('A menu has been updated.');
|
||||
onClose();
|
||||
setUpdateSuccess(true);
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({ isOpen: true, txt: error.response?.data.errorMessage as string });
|
||||
setUpdateSuccess(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAlertModalClose = () => {
|
||||
setAlertModal({ isOpen: false, txt: '' });
|
||||
|
||||
if (updateSuccess) {
|
||||
queryClient.invalidateQueries({ queryKey: ['menuPrivacy'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuDetail'] });
|
||||
|
||||
if (isTopMenu) {
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
} else {
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
}
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRadioCheck = (option: RadioOption, code: string) => {
|
||||
if (option.id.includes('Y')) {
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, levels: [...prevState.data.levels, code] }
|
||||
}));
|
||||
} else {
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, levels: prevState.data.levels.filter((item) => item !== code) }
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleInsertImg = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files ? e.target.files[0] : null;
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onloadend = () => {
|
||||
const menuIconUrl = fileReader.result?.toString();
|
||||
|
||||
if (menuIconUrl) {
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuIcon: menuIconUrl }
|
||||
}));
|
||||
}
|
||||
fileReader.onloadend = null;
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () => handleAlertModalClose(),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<div className="title">
|
||||
<span className="modal_title">Menu Info</span>
|
||||
</div>
|
||||
<div className="body">
|
||||
<table className="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Menu Name</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={updateMenuRequest.data.menuName}
|
||||
onChange={(e) =>
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuName: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu Link</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={updateMenuRequest.data.menuLink}
|
||||
onChange={(e) =>
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuLink: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu Folder</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={updateMenuRequest.data.menuFolder}
|
||||
onChange={(e) =>
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuFolder: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Menu File</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
defaultValue={updateMenuRequest.data.menuFile}
|
||||
onChange={(e) =>
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuFile: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{isTopMenu && (
|
||||
<tr>
|
||||
<th>Menu Icon</th>
|
||||
<td style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
className="form_control"
|
||||
value={updateMenuRequest.data.menuIcon}
|
||||
onChange={(e) =>
|
||||
setUpdateMenuRequest((prevState) => ({
|
||||
...prevState,
|
||||
data: { ...prevState.data, menuIcon: e.target.value }
|
||||
}))
|
||||
}
|
||||
readOnly={isEmpty(updateMenuRequest.data)}
|
||||
placeholder="Enter a menu icon"
|
||||
/>
|
||||
<div className="file_upload" style={{ width: '15%' }}>
|
||||
<label htmlFor="fileInput">Change</label>
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleInsertImg}
|
||||
disabled={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{authCodeList?.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<th>{item.codeName}</th>
|
||||
<td>
|
||||
<div className="radio_wrap flex">
|
||||
<div className="content">
|
||||
<Radio
|
||||
options={[
|
||||
{ id: item.codeName + 'Y', label: 'Y' },
|
||||
{ id: item.codeName + 'N', label: 'N' }
|
||||
]}
|
||||
name={`auth${index}`}
|
||||
value={updateMenuRequest.data.levels.includes(item.code) ? 'Y' : 'N'}
|
||||
onChange={(option) => handleRadioCheck(option, item.code)}
|
||||
disabled={isEmpty(updateMenuRequest.data)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="footer">
|
||||
<Button size="md" color="close" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button size="md" color="confirm" onClick={handleProcessClick} disabled={isUpdateingMenu}>
|
||||
Process
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MenuDetailModal.displayName = 'MenuDisplayModal';
|
||||
|
||||
export default MenuDetailModal;
|
||||
392
src/pages/manage/menu/components/SubMenu.tsx
Normal file
392
src/pages/manage/menu/components/SubMenu.tsx
Normal file
@ -0,0 +1,392 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import MenuDetailModal from './MenuDetailModal';
|
||||
import MenuAddModal from './MenuAddModal';
|
||||
import { TopMenuInfo } from '../Menu';
|
||||
|
||||
import prevImg from '@icons/chevron-left.svg';
|
||||
import plusImg from '@icons/ic-plus.svg';
|
||||
import deleteImg from '@icons/ic-bin.svg';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
MenuListResponse,
|
||||
ReorderMenuRequest,
|
||||
useDeleteMenuMutation,
|
||||
useMenuSubListQuery,
|
||||
useReorderMenuMutation
|
||||
} from '@api/menuAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
topMenuInfo: TopMenuInfo;
|
||||
setTopMenuInfo: Dispatch<SetStateAction<TopMenuInfo>>;
|
||||
}
|
||||
|
||||
const SubMenu = ({ topMenuInfo, setTopMenuInfo }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [displayOrderChange, setDisplayOrderChange] = useState(false);
|
||||
const [menuId, setMenuId] = useState(0);
|
||||
const [menuList, setMenuList] = useState<MenuListResponse[]>([]);
|
||||
const [checkItems, setCheckItems] = useState<number[]>([]);
|
||||
const [originalMenuList, setOriginalMenuList] = useState<MenuListResponse[]>([]);
|
||||
|
||||
const code = process.env.REACT_APP_CODE as string;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Ref
|
||||
const draggingMenuIndex = useRef<number>(0);
|
||||
|
||||
// API
|
||||
const queryClient = useQueryClient();
|
||||
const { mutate: deleteMenu, isPending: isDeletingMenu } = useDeleteMenuMutation();
|
||||
const { mutate: reorderMenu } = useReorderMenuMutation();
|
||||
const { data: subMenuList = [], isLoading: subMenuListLoading } = useMenuSubListQuery(topMenuInfo.id);
|
||||
const { data: authCodeList = [], isLoading: authCodeLoading } = useSimpleCodeListQuery('008');
|
||||
|
||||
useEffect(() => {
|
||||
setMenuList(subMenuList);
|
||||
}, [subMenuList]);
|
||||
|
||||
// 드래그 시작 시 원래 상태 저장
|
||||
useEffect(() => {
|
||||
setOriginalMenuList([...menuList]);
|
||||
}, [displayOrderChange]);
|
||||
|
||||
// Prev
|
||||
const handlePrevClick = () => {
|
||||
navigate(-1);
|
||||
setMenuList([]);
|
||||
setTopMenuInfo({ id: 0, name: '' });
|
||||
};
|
||||
|
||||
// Modal
|
||||
const handleModalBtnClick = (modalName: string, id: number) => {
|
||||
if (modalName === 'add') {
|
||||
setMenuId(0);
|
||||
setIsModalOpen(true);
|
||||
} else if (modalName === 'detail') {
|
||||
setMenuId(id);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
// CheckBox Check Event
|
||||
const handleAllCheckClick = (checked: HTMLInputElement['checked']) => {
|
||||
if (checked) {
|
||||
setCheckItems(subMenuList.map((item) => item.id));
|
||||
} else {
|
||||
setCheckItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSingleCheckClick = (checked: HTMLInputElement['checked'], id: number) => {
|
||||
if (checked) {
|
||||
setCheckItems((prevState) => [...prevState, id]);
|
||||
} else {
|
||||
setCheckItems(checkItems.filter((item) => item !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Drag Event
|
||||
const handleDragEnter = (idx: number) => {
|
||||
draggingMenuIndex.current = idx;
|
||||
};
|
||||
|
||||
const handleDragEnd = (idx: number) => {
|
||||
setMenuList((prevState) => {
|
||||
const newArr = [...prevState];
|
||||
|
||||
[newArr[idx], newArr[draggingMenuIndex.current]] = [prevState[draggingMenuIndex.current], prevState[idx]];
|
||||
|
||||
return newArr;
|
||||
});
|
||||
};
|
||||
|
||||
// Delete Event
|
||||
const handleDeleteClick = () => {
|
||||
deleteMenu(checkItems, {
|
||||
onSuccess: () => {
|
||||
toast.success('Menu has been deleted');
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: 'Menu has been deleted',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['menuPrivacy'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: error.response?.data.errorMessage as string,
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Display Order Event
|
||||
const handleDisplayOrderChangeProcess = () => {
|
||||
const displayOrderList: ReorderMenuRequest[] = menuList.map((item, index) => ({
|
||||
id: item.id,
|
||||
displayOrder: index
|
||||
}));
|
||||
|
||||
reorderMenu(displayOrderList, {
|
||||
onSuccess: () => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'The displayOrder has been changed',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
setDisplayOrderChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['menuPrivacy'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuSubList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: error.response?.data.errorMessage as string,
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ChangeDisplayOrder 변경 후 cancel 버튼 클릭 시 롤백
|
||||
const handleCancel = () => {
|
||||
setMenuList(originalMenuList);
|
||||
setDisplayOrderChange(false);
|
||||
};
|
||||
|
||||
if (subMenuListLoading && authCodeLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
disabled={isDeletingMenu}
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Confirm' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
{menuId > 0 ? (
|
||||
<MenuDetailModal id={menuId} onClose={() => setIsModalOpen(false)} isTopMenu={false} />
|
||||
) : (
|
||||
<MenuAddModal id={topMenuInfo.id} onClose={() => setIsModalOpen(false)} isTopMenu={false} />
|
||||
)}
|
||||
</Modal>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">
|
||||
<button className="none_button" onClick={() => handlePrevClick()}>
|
||||
<img src={prevImg} alt="prev" />
|
||||
</button>
|
||||
Top Menu ({topMenuInfo.name})
|
||||
<span style={{ color: 'rgb(126 132 163 / 61%)', padding: '0 10px' }}>></span>
|
||||
Sub Menu
|
||||
</Card.Title>
|
||||
<div className="header_box" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<div className="modal_add_btn">
|
||||
<button onClick={() => handleModalBtnClick('add', topMenuInfo.id)}>
|
||||
<img src={plusImg} alt="plus" />
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="userlist_table list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkBox"
|
||||
id="mainCheckBox"
|
||||
name="select-all"
|
||||
onChange={(e) => handleAllCheckClick(e.target.checked)}
|
||||
checked={
|
||||
checkItems.length === subMenuList.length ? (subMenuList.length === 0 ? false : true) : false
|
||||
}
|
||||
/>
|
||||
<label htmlFor="mainCheckBox" />
|
||||
</div>
|
||||
</th>
|
||||
<th className="width-15">Menu Name</th>
|
||||
<th className="width-15">Menu Link</th>
|
||||
<th className="width-15">Menu Folder</th>
|
||||
<th className="width-15">Menu File</th>
|
||||
<th className="width-5">Display Order</th>
|
||||
{authCodeList.map((item, index) => (
|
||||
<th key={index} style={{ width: 20 / authCodeList.length + '%' }}>
|
||||
{item.codeName}
|
||||
</th>
|
||||
))}
|
||||
<th className="width-5" />
|
||||
<th className="width-5" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{menuList.length !== 0 &&
|
||||
menuList?.map((item, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
{...(displayOrderChange
|
||||
? {
|
||||
onDragEnter: () => {
|
||||
handleDragEnter(idx);
|
||||
},
|
||||
onDragOver: (e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
onDragEnd: () => {
|
||||
handleDragEnd(idx);
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
: null)}
|
||||
>
|
||||
<td className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={'select-' + item.id}
|
||||
name={'select-' + item.id}
|
||||
onChange={(e) => handleSingleCheckClick(e.target.checked, item.id)}
|
||||
checked={checkItems.includes(item.id) ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={'select-' + item.id} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="width-15">{item.menuName}</td>
|
||||
<td className="width-15">{item.menuLink}</td>
|
||||
<td className="width-15">{item.menuFolder}</td>
|
||||
<td className="width-15">{item.menuFile}</td>
|
||||
<td className="width-5">{item.displayOrder}</td>
|
||||
{authCodeList.map((authItem, authIndex) => (
|
||||
<td key={authIndex} style={{ width: 20 / authCodeList.length + '%' }}>
|
||||
{item.levels[authIndex] === authItem.code ? 'Y' : 'N'}
|
||||
</td>
|
||||
))}
|
||||
<td style={{ width: '10%', gap: '10px' }}>
|
||||
<button
|
||||
className="list_edit_button"
|
||||
value={item.id}
|
||||
onClick={(e) =>
|
||||
handleModalBtnClick('detail', parseInt((e.target as HTMLButtonElement).value))
|
||||
}
|
||||
>
|
||||
Detail
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer" style={{ marginTop: '40px' }}>
|
||||
<div className="left">
|
||||
<Button size="xs" color="action">
|
||||
<img
|
||||
src={deleteImg}
|
||||
alt="deleteBtn"
|
||||
onClick={() =>
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'Are you sure you want to delete the menu?',
|
||||
confirmBtn: { isConfirm: true, onConfirmClick: () => handleDeleteClick() }
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<div className="button" style={displayOrderChange ? { display: 'flex', gap: '10px' } : {}}>
|
||||
{displayOrderChange ? (
|
||||
<>
|
||||
<button className="square_button" onClick={handleCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="square_button"
|
||||
style={{ background: '#fec702', color: '#fff', border: '1px solid #fec702' }}
|
||||
onClick={handleDisplayOrderChangeProcess}
|
||||
>
|
||||
Process
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="lg" color="action" onClick={() => setDisplayOrderChange(true)}>
|
||||
Change displayOrder
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubMenu;
|
||||
375
src/pages/manage/menu/components/TopMenu.tsx
Normal file
375
src/pages/manage/menu/components/TopMenu.tsx
Normal file
@ -0,0 +1,375 @@
|
||||
import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react';
|
||||
import { Card } from 'react-bootstrap';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { TopMenuInfo } from '../Menu';
|
||||
|
||||
import plusImg from '@icons/ic-plus.svg';
|
||||
import deleteImg from '@icons/ic-bin.svg';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'react-toastify';
|
||||
import {
|
||||
MenuListResponse,
|
||||
ReorderMenuRequest,
|
||||
useDeleteMenuMutation,
|
||||
useMenuTopListQuery,
|
||||
useReorderMenuMutation
|
||||
} from '@api/menuAPI';
|
||||
import { useSimpleCodeListQuery } from '@api/codeAPI';
|
||||
import AlertModal from '@components/ui/modal/AlertModal';
|
||||
import Modal from '@components/ui/modal/Modal';
|
||||
import MenuDetailModal from './MenuDetailModal';
|
||||
import MenuAddModal from './MenuAddModal';
|
||||
import Button from '@components/ui/button/Button';
|
||||
|
||||
interface Props {
|
||||
setTopId: Dispatch<SetStateAction<TopMenuInfo>>;
|
||||
}
|
||||
|
||||
const TopMenu = ({ setTopId }: Props) => {
|
||||
const [alertModal, setAlertModal] = useState({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [menuId, setMenuId] = useState(0);
|
||||
const [displayOrderChange, setDisplayOrderChange] = useState(false);
|
||||
const [menuList, setMenuList] = useState<MenuListResponse[]>([]);
|
||||
const [checkItems, setCheckItems] = useState<number[]>([]);
|
||||
|
||||
const code = process.env.REACT_APP_CODE as string;
|
||||
|
||||
const draggingMenuIndex = useRef<number>(0);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: topMenuList = [], isLoading: topMenuListLoading } = useMenuTopListQuery();
|
||||
const { data: authCodeList = [], isLoading: authCodeLoading } = useSimpleCodeListQuery('008');
|
||||
const { mutate: reorderMenu } = useReorderMenuMutation();
|
||||
const { mutate: deleteMenu, isPending: isDeletingMenu } = useDeleteMenuMutation();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (topMenuList.length > 0) {
|
||||
setMenuList(topMenuList);
|
||||
}
|
||||
}, [topMenuList]);
|
||||
|
||||
// Modal event
|
||||
const handleModalBtnClick = (modalName: string, id?: number) => {
|
||||
if (modalName === 'add') {
|
||||
setMenuId(0);
|
||||
setIsModalOpen(true);
|
||||
} else if (modalName === 'detail' && id) {
|
||||
setMenuId(id);
|
||||
setIsModalOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
// CheckBox Check Event
|
||||
const handleAllCheckClick = (checked: HTMLInputElement['checked']) => {
|
||||
if (checked) {
|
||||
setCheckItems(topMenuList.map((item) => item.id));
|
||||
} else {
|
||||
setCheckItems([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSingleCheckClick = (checked: HTMLInputElement['checked'], id: number) => {
|
||||
if (checked) {
|
||||
setCheckItems((prevState) => [...prevState, id]);
|
||||
} else {
|
||||
setCheckItems(checkItems.filter((item) => item !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Drag Event
|
||||
const handleDragEnter = (idx: number) => {
|
||||
draggingMenuIndex.current = idx;
|
||||
};
|
||||
|
||||
const handleDragEnd = (idx: number) => {
|
||||
setMenuList((prevState) => {
|
||||
const newArr = [...prevState];
|
||||
|
||||
[newArr[idx], newArr[draggingMenuIndex.current]] = [prevState[draggingMenuIndex.current], prevState[idx]];
|
||||
|
||||
return newArr;
|
||||
});
|
||||
};
|
||||
|
||||
// Sub Menu Event
|
||||
const handleSubMenuClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const menuId: number = parseInt(e.currentTarget.value);
|
||||
const menuName: string = menuList.find((item) => item.id === menuId)?.menuName ?? '';
|
||||
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
searchParams.set('id', String(menuId));
|
||||
searchParams.set('name', String(menuName));
|
||||
|
||||
navigate({
|
||||
pathname: location.pathname,
|
||||
search: `?${searchParams.toString()}`
|
||||
});
|
||||
|
||||
setTopId({ id: menuId, name: menuName });
|
||||
};
|
||||
|
||||
// Delete Event
|
||||
const handleDeleteClick = () => {
|
||||
deleteMenu(checkItems, {
|
||||
onSuccess: () => {
|
||||
toast.success('Menu has been deleted');
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: 'Menu has been deleted',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => null
|
||||
}
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({ queryKey: ['menuPrivacy'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Display Order Event
|
||||
const handleDisplayOrderChangeProcess = () => {
|
||||
const displayOrderList: ReorderMenuRequest[] = menuList.map((item, index) => ({
|
||||
id: item.id,
|
||||
displayOrder: index
|
||||
}));
|
||||
|
||||
reorderMenu(displayOrderList, {
|
||||
onSuccess: () => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'The displayOrder has been changed',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
setDisplayOrderChange(false);
|
||||
queryClient.invalidateQueries({ queryKey: ['menuPrivacy'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['menuTopList'] });
|
||||
},
|
||||
onError: (error) => {
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: error.response?.data.errorMessage as string,
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ChangeDisplayOrder 변경 후 cancel 버튼 클릭 시 롤백
|
||||
const handleCancel = () => {
|
||||
setMenuList(topMenuList);
|
||||
setDisplayOrderChange(false);
|
||||
};
|
||||
|
||||
if (topMenuListLoading && authCodeLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
disabled={isDeletingMenu}
|
||||
isOpen={alertModal.isOpen}
|
||||
closeBtn={{
|
||||
onClose: () =>
|
||||
setAlertModal({
|
||||
isOpen: false,
|
||||
txt: '',
|
||||
confirmBtn: {
|
||||
isConfirm: false,
|
||||
onConfirmClick: () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
buttonName: 'Close'
|
||||
}}
|
||||
{...(alertModal.confirmBtn.isConfirm
|
||||
? { confirmBtn: { onClose: () => alertModal.confirmBtn.onConfirmClick(), buttonName: 'Confirm' } }
|
||||
: {})}
|
||||
>
|
||||
{alertModal.txt}
|
||||
</AlertModal>
|
||||
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)}>
|
||||
{menuId > 0 ? (
|
||||
<MenuDetailModal id={menuId} onClose={() => setIsModalOpen(false)} isTopMenu={true} />
|
||||
) : (
|
||||
<MenuAddModal onClose={() => setIsModalOpen(false)} isTopMenu={true} />
|
||||
)}
|
||||
</Modal>
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<div className="list_header">
|
||||
<Card.Title as="h5">
|
||||
Menu List
|
||||
<span style={{ color: 'rgb(126 132 163 / 61%)', padding: '0 10px' }}>></span>
|
||||
Top Menu
|
||||
</Card.Title>
|
||||
<div className="header_box" style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
||||
<Button size="lg" color="action" onClick={() => handleModalBtnClick('add')}>
|
||||
<img src={plusImg} alt="plus" />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<div className="list_table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkBox"
|
||||
id="mainCheckBox"
|
||||
name="select-all"
|
||||
onChange={(e) => handleAllCheckClick(e.target.checked)}
|
||||
checked={checkItems.length === topMenuList.length ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor="mainCheckBox" />
|
||||
</div>
|
||||
</th>
|
||||
<th className="width-15">Menu Name</th>
|
||||
<th className="width-15">Menu Link</th>
|
||||
<th className="width-15">Menu Folder</th>
|
||||
<th className="width-15">Menu File</th>
|
||||
<th className="width-5">Display Order</th>
|
||||
{authCodeList.map((item, index) => (
|
||||
<th key={index} style={{ width: 20 / authCodeList.length + '%' }}>
|
||||
{item.codeName}
|
||||
</th>
|
||||
))}
|
||||
<th className="width-5" />
|
||||
<th className="width-5" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{menuList.length === 0
|
||||
? null
|
||||
: menuList.map((item, idx) => (
|
||||
<tr
|
||||
key={idx}
|
||||
{...(displayOrderChange
|
||||
? {
|
||||
onDragEnter: () => {
|
||||
handleDragEnter(idx);
|
||||
},
|
||||
onDragOver: (e) => {
|
||||
e.preventDefault();
|
||||
},
|
||||
onDragEnd: () => {
|
||||
handleDragEnd(idx);
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
: null)}
|
||||
>
|
||||
<td className="width-5">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={'select-' + item.id}
|
||||
name={'select-' + item.id}
|
||||
onChange={(e) => handleSingleCheckClick(e.target.checked, item.id)}
|
||||
checked={checkItems.includes(item.id) ? true : false}
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={'select-' + item.id} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="width-15">{item.menuName}</td>
|
||||
<td className="width-15">{item.menuLink}</td>
|
||||
<td className="width-15">{item.menuFolder}</td>
|
||||
<td className="width-15">{item.menuFile}</td>
|
||||
<td className="width-5">{item.displayOrder}</td>
|
||||
{authCodeList.map((authItem, authIndex) => (
|
||||
<td key={authIndex} style={{ width: 20 / authCodeList.length + '%' }}>
|
||||
{item.levels[authIndex] === authItem.code ? 'Y' : 'N'}
|
||||
</td>
|
||||
))}
|
||||
<td style={{ width: '10%', gap: '10px' }}>
|
||||
<button
|
||||
className="list_edit_button"
|
||||
value={item.id}
|
||||
onClick={(e) => handleModalBtnClick('detail', parseInt(e.currentTarget.value))}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button className="list_edit_button" value={item.id} onClick={handleSubMenuClick}>
|
||||
SubMenu
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="list_footer" style={{ marginTop: '40px' }}>
|
||||
<div className="left">
|
||||
<Button size="xs" color="action" disabled={checkItems.length < 1}>
|
||||
<img
|
||||
src={deleteImg}
|
||||
alt="deleteBtn"
|
||||
onClick={() =>
|
||||
setAlertModal({
|
||||
isOpen: true,
|
||||
txt: 'Are you sure you want to delete the menu?',
|
||||
confirmBtn: { isConfirm: true, onConfirmClick: () => handleDeleteClick() }
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="right">
|
||||
<div className="button" style={displayOrderChange ? { display: 'flex', gap: '10px' } : {}}>
|
||||
{displayOrderChange ? (
|
||||
<>
|
||||
<Button size="lg" color="action" onClick={handleCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="lg" color="process" onClick={handleDisplayOrderChangeProcess}>
|
||||
Process
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button size="lg" color="action" onClick={() => setDisplayOrderChange(true)}>
|
||||
Change displayOrder
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TopMenu;
|
||||
1
src/react-app-env.d.ts
vendored
Normal file
1
src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
55
src/redux/auth.ts
Normal file
55
src/redux/auth.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
interface UserData {
|
||||
auth: string;
|
||||
userNo: number;
|
||||
userId: string;
|
||||
userName: string;
|
||||
angkorUserId: string | null;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface AuthState {
|
||||
isAuthenticated: boolean;
|
||||
userData: UserData | null;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
isAuthenticated: false,
|
||||
userData: null
|
||||
};
|
||||
|
||||
export const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState,
|
||||
reducers: {
|
||||
loginSuccess: (state, action: PayloadAction<UserData>) => {
|
||||
state.userData = action.payload;
|
||||
},
|
||||
integrationSuccess: (state, action: PayloadAction<string>) => {
|
||||
if (state.userData) {
|
||||
state.userData.angkorUserId = action.payload;
|
||||
}
|
||||
},
|
||||
loginAuthenticate: (state, action: PayloadAction<string>) => {
|
||||
state.isAuthenticated = true;
|
||||
if (state.userData) {
|
||||
state.userData.accessToken = action.payload;
|
||||
}
|
||||
},
|
||||
tokenRefresh: (state, action: PayloadAction<string>) => {
|
||||
if (state.userData) {
|
||||
state.userData.accessToken = action.payload;
|
||||
}
|
||||
},
|
||||
logoutSuccess: (state) => {
|
||||
state.userData = null;
|
||||
state.isAuthenticated = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { loginSuccess, integrationSuccess, loginAuthenticate, tokenRefresh, logoutSuccess } = authSlice.actions;
|
||||
|
||||
export default authSlice.reducer;
|
||||
31
src/redux/index.ts
Normal file
31
src/redux/index.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PURGE, REGISTER, PERSIST } from 'redux-persist';
|
||||
import storageSession from 'redux-persist/lib/storage/session';
|
||||
|
||||
import authReducer from './auth';
|
||||
|
||||
const persistConfig = {
|
||||
key: 'root',
|
||||
storage: storageSession,
|
||||
whitelist: ['auth']
|
||||
};
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
auth: authReducer
|
||||
});
|
||||
|
||||
const persistedReducer = persistReducer(persistConfig, rootReducer);
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: persistedReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({
|
||||
serializableCheck: {
|
||||
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER]
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
|
||||
export const persistor = persistStore(store);
|
||||
15
src/reportWebVitals.ts
Normal file
15
src/reportWebVitals.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
196
src/services/api/adminAPI.ts
Normal file
196
src/services/api/adminAPI.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { UseMutationResult, UseQueryResult, useMutation, useQuery, keepPreviousData } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Page } from '../commonResponse/PageResponse';
|
||||
import { customAxios, ErrorResponse } from '@lib/customAxios';
|
||||
|
||||
// ------------------------------ GET /admin/list ------------------------------
|
||||
|
||||
export interface AdminListRequest {
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
searchKeyword: string;
|
||||
}
|
||||
|
||||
export interface AdminListResponse {
|
||||
id: number;
|
||||
userId: string;
|
||||
name: string;
|
||||
auth: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
grp: string;
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
const adminList = (request: AdminListRequest): Promise<Page<AdminListResponse>> => {
|
||||
return customAxios.get<Page<AdminListResponse>>('admin/list', { params: request }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAdminListQuery = (request: AdminListRequest): UseQueryResult<Page<AdminListResponse>, Error> => {
|
||||
return useQuery<Page<AdminListResponse>, Error>({
|
||||
queryKey: ['adminList', request],
|
||||
queryFn: () => adminList(request),
|
||||
staleTime: Infinity,
|
||||
gcTime: 1000 * 60 * 60,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
placeholderData: keepPreviousData
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /admin/list ------------------------------
|
||||
|
||||
// ------------------------------ GET /admin/{id} ------------------------------
|
||||
|
||||
export interface AdminInfoResponse {
|
||||
id: number;
|
||||
email: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
grp: string;
|
||||
groupName: string;
|
||||
auth: string;
|
||||
mngrId: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const adminInfo = (id: number): Promise<AdminInfoResponse> => {
|
||||
return customAxios.get<AdminInfoResponse>(`admin/${id}`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAdminInfoQuery = (id: number): UseQueryResult<AdminInfoResponse, Error> => {
|
||||
return useQuery<AdminInfoResponse, Error>({
|
||||
queryKey: ['adminInfo', id],
|
||||
queryFn: () => adminInfo(id),
|
||||
staleTime: Infinity,
|
||||
gcTime: 1000 * 60 * 60,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
enabled: id !== 0
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /admin/{id} ------------------------------
|
||||
|
||||
// ------------------------------ POST /admin ------------------------------
|
||||
export interface AddAdminRequest {
|
||||
email: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
grp: string;
|
||||
auth: string;
|
||||
}
|
||||
|
||||
const addAdmin = (request: AddAdminRequest): Promise<void> => {
|
||||
return customAxios.post('admin', request);
|
||||
};
|
||||
|
||||
export const useAddAdminMutation = (): UseMutationResult<void, AxiosError<ErrorResponse>, AddAdminRequest, unknown> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, AddAdminRequest, unknown>({
|
||||
mutationFn: addAdmin
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /admin ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /admin ------------------------------
|
||||
export interface UpdateAdminRequest {
|
||||
email: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
grp: string;
|
||||
auth: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
const updateAdmin = (request: UpdateAdminRequest): Promise<void> => {
|
||||
return customAxios.patch('admin', request);
|
||||
};
|
||||
|
||||
export const useUpdateAdminMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
UpdateAdminRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, UpdateAdminRequest, unknown>({
|
||||
mutationFn: updateAdmin
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /admin/{id} ------------------------------
|
||||
|
||||
// ------------------------------ DELETE /admin ------------------------------
|
||||
interface deleteAdminRequest {
|
||||
id: number[];
|
||||
}
|
||||
|
||||
const deleteAdmin = (request: deleteAdminRequest): Promise<void> => {
|
||||
return customAxios.delete('admin', { data: request });
|
||||
};
|
||||
|
||||
export const useDeleteAdminMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
deleteAdminRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, deleteAdminRequest, unknown>({
|
||||
mutationFn: deleteAdmin
|
||||
});
|
||||
};
|
||||
// ------------------------------ DELETE /admin ------------------------------
|
||||
|
||||
// ------------------------------ POST /admin/check/password ------------------------------
|
||||
interface PasswordRequest {
|
||||
userId: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
const checkPassword = (request: PasswordRequest): Promise<void> => {
|
||||
return customAxios.post('admin/check/password', request);
|
||||
};
|
||||
|
||||
export const useCheckPasswordMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
PasswordRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, PasswordRequest, unknown>({
|
||||
mutationFn: checkPassword
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /admin/check/password ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /admin/update/password ------------------------------
|
||||
const updatePassword = (request: PasswordRequest): Promise<void> => {
|
||||
return customAxios.patch('admin/update/password', request);
|
||||
};
|
||||
|
||||
export const useUpdatePasswordMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
PasswordRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, PasswordRequest, unknown>({
|
||||
mutationFn: updatePassword
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /admin/update/password ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /admin/reset/password ------------------------------
|
||||
const resetPassword = (request: PasswordRequest): Promise<void> => {
|
||||
return customAxios.patch('admin/reset/password', request);
|
||||
};
|
||||
|
||||
export const useResetPasswordMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
PasswordRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, PasswordRequest, unknown>({
|
||||
mutationFn: resetPassword
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /admin/reset/password ------------------------------
|
||||
142
src/services/api/adminGroupAPI.ts
Normal file
142
src/services/api/adminGroupAPI.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import { UseMutationResult, UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Page } from '../commonResponse/PageResponse';
|
||||
import { customAxios, ErrorResponse } from '@lib/customAxios';
|
||||
|
||||
// ------------------------------ GET /admin/group/list ------------------------------
|
||||
export interface AdminGroupListRequest {
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
searchKeyword: string;
|
||||
}
|
||||
|
||||
export interface AdminGroupListResponse {
|
||||
groupCd: string;
|
||||
groupName: string;
|
||||
members: number;
|
||||
}
|
||||
|
||||
const adminGroupList = (request: AdminGroupListRequest): Promise<Page<AdminGroupListResponse>> => {
|
||||
return customAxios.get<Page<AdminGroupListResponse>>('admin/group/list', { params: request }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAdminGroupListQuery = (
|
||||
request: AdminGroupListRequest
|
||||
): UseQueryResult<Page<AdminGroupListResponse>, Error> => {
|
||||
return useQuery<Page<AdminGroupListResponse>, Error>({
|
||||
queryKey: ['adminGroupList', request],
|
||||
queryFn: () => adminGroupList(request),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /admin/group/list ------------------------------
|
||||
|
||||
// ------------------------------ GET /admin/group/{id} ------------------------------
|
||||
interface AdminGroupInfoResponse {
|
||||
groupCd: string;
|
||||
groupName: string;
|
||||
members: number;
|
||||
createDt: string;
|
||||
}
|
||||
|
||||
const adminGroupInfo = (id: string): Promise<AdminGroupInfoResponse> => {
|
||||
return customAxios.get<AdminGroupInfoResponse>(`admin/group/${id}`).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAdminGroupInfoQuery = (id: string): UseQueryResult<AdminGroupInfoResponse, Error> => {
|
||||
return useQuery<AdminGroupInfoResponse, Error>({
|
||||
queryKey: ['adminGroupInfo', id],
|
||||
queryFn: () => adminGroupInfo(id),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 1000 * 60 * 5,
|
||||
enabled: id !== ''
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /admin/group/{id} ------------------------------
|
||||
|
||||
// ------------------------------ GET /admin/group/code ------------------------------
|
||||
interface AdminGroupCodeResponse {
|
||||
groupCd: string;
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
const adminGroupCode = (): Promise<AdminGroupCodeResponse[]> => {
|
||||
return customAxios.get<AdminGroupCodeResponse[]>('admin/group/code').then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAdminGroupCodeQuery = (): UseQueryResult<AdminGroupCodeResponse[], Error> => {
|
||||
return useQuery<AdminGroupCodeResponse[], Error>({
|
||||
queryKey: ['adminGroupCode'],
|
||||
queryFn: adminGroupCode,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /admin/group/code ------------------------------
|
||||
|
||||
// ------------------------------ POST /admin/group ------------------------------
|
||||
export interface AddAdminGroupRequest {
|
||||
groupName: string;
|
||||
}
|
||||
|
||||
const addAdminGroup = (request: AddAdminGroupRequest): Promise<void> => {
|
||||
return customAxios.post('admin/group', request);
|
||||
};
|
||||
|
||||
export const useAddAdminGroupMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
AddAdminGroupRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, AddAdminGroupRequest, unknown>({
|
||||
mutationFn: addAdminGroup
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /admin/group ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /admin/group ------------------------------
|
||||
export interface UpdateAdminGroupRequest {
|
||||
groupName: string;
|
||||
groupCd: string;
|
||||
}
|
||||
|
||||
const updateAdminGroup = (request: UpdateAdminGroupRequest): Promise<void> => {
|
||||
return customAxios.patch('admin/group', request);
|
||||
};
|
||||
|
||||
export const useUpdateAdminGroupMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
UpdateAdminGroupRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, UpdateAdminGroupRequest, unknown>({
|
||||
mutationFn: updateAdminGroup
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /admin/group ------------------------------
|
||||
|
||||
// ------------------------------ DELETE /admin/group ------------------------------
|
||||
interface DeleteAdminGroupRequest {
|
||||
groupCd: string[];
|
||||
}
|
||||
|
||||
const deleteAdminGroup = (request: DeleteAdminGroupRequest): Promise<void> => {
|
||||
return customAxios.delete('admin/group', { data: request });
|
||||
};
|
||||
|
||||
export const useDeleteAdminGroupMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
DeleteAdminGroupRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, DeleteAdminGroupRequest, unknown>({
|
||||
mutationFn: deleteAdminGroup
|
||||
});
|
||||
};
|
||||
// ------------------------------ DELETE /admin/group ------------------------------
|
||||
126
src/services/api/authAPI.ts
Normal file
126
src/services/api/authAPI.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import { customAxios, ErrorResponse } from '@lib/customAxios';
|
||||
import { UseMutationResult, UseQueryResult, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
// ------------------------------ POST /login ------------------------------
|
||||
interface LoginRequest {
|
||||
id: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface LoginResponse {
|
||||
userNo: number;
|
||||
userId: string;
|
||||
userName: string;
|
||||
angkorUserId: string;
|
||||
auth: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
const login = (request: LoginRequest): Promise<LoginResponse> => {
|
||||
return customAxios.post<LoginResponse>('login', request).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useLoginMutation = (): UseMutationResult<
|
||||
LoginResponse,
|
||||
AxiosError<ErrorResponse>,
|
||||
LoginRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<LoginResponse, AxiosError<ErrorResponse>, LoginRequest, unknown>({
|
||||
mutationFn: login
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /login ------------------------------
|
||||
|
||||
// ------------------------------ GET /check/{id} ------------------------------
|
||||
interface CheckIdRequest {
|
||||
angkorId: string;
|
||||
}
|
||||
|
||||
const checkId = (request: CheckIdRequest): Promise<void> => {
|
||||
return customAxios.get(`check/${request.angkorId}`);
|
||||
};
|
||||
|
||||
export const useCheckIdMutation = (): UseMutationResult<void, AxiosError<ErrorResponse>, CheckIdRequest, unknown> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, CheckIdRequest, unknown>({
|
||||
mutationFn: checkId
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /check/{id} ------------------------------
|
||||
|
||||
// ------------------------------ POST /auth/code ------------------------------
|
||||
interface AuthCodeRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface AuthCodeResponse {
|
||||
userId: string;
|
||||
tokenType: string;
|
||||
authKey: string;
|
||||
}
|
||||
|
||||
const authCode = (request: AuthCodeRequest): Promise<AuthCodeResponse> => {
|
||||
return customAxios.post<AuthCodeResponse>('auth/code', null, { params: request }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAuthCodeMutation = (): UseMutationResult<
|
||||
AuthCodeResponse,
|
||||
AxiosError<ErrorResponse>,
|
||||
AuthCodeRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<AuthCodeResponse, AxiosError<ErrorResponse>, AuthCodeRequest, unknown>({
|
||||
mutationFn: authCode
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /auth/code ------------------------------
|
||||
|
||||
// ------------------------------ GET /auth/check ------------------------------
|
||||
interface AuthCheckRequest {
|
||||
authKey: string;
|
||||
authNumber: string;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
const authCheck = (request: AuthCheckRequest): Promise<LoginResponse> => {
|
||||
return customAxios.get<LoginResponse>('auth/check', { params: request }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useAuthCheckMutation = (): UseMutationResult<
|
||||
LoginResponse,
|
||||
AxiosError<ErrorResponse>,
|
||||
AuthCheckRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<LoginResponse, AxiosError<ErrorResponse>, AuthCheckRequest, unknown>({
|
||||
mutationFn: authCheck
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /auth/check ------------------------------
|
||||
|
||||
// ------------------------------ GET /info ------------------------------
|
||||
export interface InfoResponse {
|
||||
adminId: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
grp: string;
|
||||
groupName: string;
|
||||
auth: string;
|
||||
angkorUserId: string;
|
||||
}
|
||||
|
||||
const info = (): Promise<InfoResponse> => {
|
||||
return customAxios.get<InfoResponse>('info').then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useInfoQuery = (): UseQueryResult<InfoResponse, Error> => {
|
||||
return useQuery<InfoResponse, Error>({
|
||||
queryKey: ['info'],
|
||||
queryFn: () => info(),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /info ------------------------------
|
||||
176
src/services/api/codeAPI.ts
Normal file
176
src/services/api/codeAPI.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { UseMutationResult, UseQueryResult, keepPreviousData, useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Page } from '../commonResponse/PageResponse';
|
||||
import { customAxios, ErrorResponse } from '@lib/customAxios';
|
||||
|
||||
// ------------------------------ GET /code/major/list ------------------------------
|
||||
export interface MajorCodeListRequest {
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
searchKeyword: string;
|
||||
}
|
||||
|
||||
export interface MajorCodeListResponse {
|
||||
major: string;
|
||||
majorName: string;
|
||||
}
|
||||
|
||||
const majorCodeList = (request: MajorCodeListRequest): Promise<Page<MajorCodeListResponse>> => {
|
||||
return customAxios.get(`code/major/list`, { params: request }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useMajorCodeListQuery = (
|
||||
request: MajorCodeListRequest
|
||||
): UseQueryResult<Page<MajorCodeListResponse>, Error> => {
|
||||
return useQuery<Page<MajorCodeListResponse>, Error>({
|
||||
queryKey: ['majorCodeList', request],
|
||||
queryFn: () => majorCodeList(request),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /code/major/list ------------------------------
|
||||
|
||||
// ------------------------------ GET /code/{major}/list ------------------------------
|
||||
|
||||
interface CodeListParams {
|
||||
page: number;
|
||||
size: number;
|
||||
sort: string[];
|
||||
searchKeyword: string;
|
||||
}
|
||||
export interface CodeListRequest {
|
||||
path: string;
|
||||
params: CodeListParams;
|
||||
}
|
||||
|
||||
export interface CodeListResponse {
|
||||
code: string;
|
||||
codeEn: string;
|
||||
codeKr: string;
|
||||
etc: string;
|
||||
}
|
||||
|
||||
const codeList = (request: CodeListRequest): Promise<Page<CodeListResponse>> => {
|
||||
return customAxios.get(`code/${request.path}/list`, { params: request.params }).then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useCodeListQuery = (request: CodeListRequest): UseQueryResult<Page<CodeListResponse>, Error> => {
|
||||
return useQuery<Page<CodeListResponse>, Error>({
|
||||
queryKey: ['codeList', request],
|
||||
queryFn: () => codeList(request),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: keepPreviousData,
|
||||
enabled: request.path !== ''
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /code/{major}/list ------------------------------
|
||||
|
||||
// ------------------------------ GET /code/{major}/list/simple ------------------------------
|
||||
interface SimpleCodeListResponse {
|
||||
code: string;
|
||||
codeName: string;
|
||||
}
|
||||
|
||||
const simpleCodeList = (request: string): Promise<SimpleCodeListResponse[]> => {
|
||||
return customAxios.get('code/' + request + '/list/simple').then((res) => res.data);
|
||||
};
|
||||
|
||||
export const useSimpleCodeListQuery = (request: string): UseQueryResult<SimpleCodeListResponse[], Error> => {
|
||||
return useQuery<SimpleCodeListResponse[], Error>({
|
||||
queryKey: ['simpleCodeList', request],
|
||||
queryFn: () => simpleCodeList(request),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false
|
||||
});
|
||||
};
|
||||
// ------------------------------ GET /code/{major}/list/simple ------------------------------
|
||||
|
||||
// ------------------------------ POST /code ------------------------------
|
||||
export interface AddCodeRequest {
|
||||
major?: string;
|
||||
majorName?: string;
|
||||
codeEn: string;
|
||||
codeKr: string;
|
||||
etc: string;
|
||||
}
|
||||
|
||||
const addCode = (request: AddCodeRequest): Promise<void> => {
|
||||
return customAxios.post('code', request);
|
||||
};
|
||||
|
||||
export const useAddCodeMutation = (): UseMutationResult<void, AxiosError<ErrorResponse>, AddCodeRequest, unknown> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, AddCodeRequest, unknown>({
|
||||
mutationFn: addCode
|
||||
});
|
||||
};
|
||||
// ------------------------------ POST /code ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /code/{code} ------------------------------
|
||||
interface UpdateCodeData {
|
||||
codeEn: string;
|
||||
codeKr: string;
|
||||
etc: string;
|
||||
}
|
||||
|
||||
export interface UpdateCodeRequest {
|
||||
data: UpdateCodeData;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const updateCode = (request: UpdateCodeRequest): Promise<void> => {
|
||||
return customAxios.patch(`code/${request.path}`, request.data);
|
||||
};
|
||||
|
||||
export const useUpdateCodeMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
UpdateCodeRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, UpdateCodeRequest, unknown>({
|
||||
mutationFn: updateCode
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /code/{code} ------------------------------
|
||||
|
||||
// ------------------------------ PATCH /code/{major}/name ------------------------------
|
||||
interface UpdateMajorNameData {
|
||||
codeValue: string;
|
||||
}
|
||||
|
||||
export interface UpdateMajorNameRequest {
|
||||
data: UpdateMajorNameData;
|
||||
path: string;
|
||||
}
|
||||
|
||||
const updateMajorName = (request: UpdateMajorNameRequest): Promise<void> => {
|
||||
return customAxios.patch(`code/${request.path}/name`, request.data);
|
||||
};
|
||||
|
||||
export const useUpdateMajorNameMutation = (): UseMutationResult<
|
||||
void,
|
||||
AxiosError<ErrorResponse>,
|
||||
UpdateMajorNameRequest,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, UpdateMajorNameRequest, unknown>({
|
||||
mutationFn: updateMajorName
|
||||
});
|
||||
};
|
||||
// ------------------------------ PATCH /code/{major}/name ------------------------------
|
||||
|
||||
// ------------------------------ DELETE /code/{code} ------------------------------
|
||||
const deleteCode = (path: string): Promise<void> => {
|
||||
return customAxios.delete(`code/${path}`);
|
||||
};
|
||||
|
||||
export const useDeleteCodeMutation = (): UseMutationResult<void, AxiosError<ErrorResponse>, string, unknown> => {
|
||||
return useMutation<void, AxiosError<ErrorResponse>, string, unknown>({
|
||||
mutationFn: deleteCode
|
||||
});
|
||||
};
|
||||
// ------------------------------ DELETE /code/{code} ------------------------------
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user