feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
@@ -1,17 +1,36 @@
 | 
			
		||||
# One API 的前端界面
 | 
			
		||||
 | 
			
		||||
> 每个文件夹代表一个主题,欢迎提交你的主题
 | 
			
		||||
 | 
			
		||||
## 提交新的主题
 | 
			
		||||
 | 
			
		||||
> 欢迎在页面底部保留你和 One API 的版权信息以及指向链接
 | 
			
		||||
 | 
			
		||||
1. 在 `web` 文件夹下新建一个文件夹,文件夹名为主题名。
 | 
			
		||||
2. 把你的主题文件放到这个文件夹下。
 | 
			
		||||
3. 修改 `package.json` 文件,把 `build` 命令改为:`"build": "react-scripts build && mv -f build ../build/default"`,其中 `default` 为你的主题名。
 | 
			
		||||
 | 
			
		||||
## 主题列表
 | 
			
		||||
 | 
			
		||||
### 主题:default
 | 
			
		||||
 | 
			
		||||
默认主题,由 [JustSong](https://github.com/songquanpeng) 开发。
 | 
			
		||||
 | 
			
		||||
预览:
 | 
			
		||||
|||
 | 
			
		||||
|:---:|:---:|
 | 
			
		||||
 | 
			
		||||
### 主题:berry
 | 
			
		||||
 | 
			
		||||
由 [MartialBE](https://github.com/MartialBE) 开发。
 | 
			
		||||
 | 
			
		||||
预览:
 | 
			
		||||
|||
 | 
			
		||||
|:---:|:---:|
 | 
			
		||||
|||
 | 
			
		||||
|||
 | 
			
		||||
|||
 | 
			
		||||
 | 
			
		||||
#### 开发说明
 | 
			
		||||
 | 
			
		||||
请查看 [web/berry/README.md](https://github.com/songquanpeng/one-api/tree/main/web/berry/README.md)
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1,2 @@
 | 
			
		||||
default
 | 
			
		||||
default
 | 
			
		||||
berry
 | 
			
		||||
							
								
								
									
										26
									
								
								web/berry/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
# 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*
 | 
			
		||||
.idea
 | 
			
		||||
package-lock.json
 | 
			
		||||
yarn.lock
 | 
			
		||||
							
								
								
									
										61
									
								
								web/berry/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
# One API 前端界面
 | 
			
		||||
 | 
			
		||||
这个项目是 One API 的前端界面,它基于 [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template) 进行开发。
 | 
			
		||||
 | 
			
		||||
## 使用的开源项目
 | 
			
		||||
 | 
			
		||||
使用了以下开源项目作为我们项目的一部分:
 | 
			
		||||
 | 
			
		||||
- [Berry Free React Admin Template](https://github.com/codedthemes/berry-free-react-admin-template)
 | 
			
		||||
- [minimal-ui-kit](minimal-ui-kit)
 | 
			
		||||
 | 
			
		||||
## 开发说明
 | 
			
		||||
 | 
			
		||||
当添加新的渠道时,需要修改以下地方:
 | 
			
		||||
 | 
			
		||||
1. `web/berry/src/constants/ChannelConstants.js`
 | 
			
		||||
 | 
			
		||||
在该文件中的 `CHANNEL_OPTIONS` 添加新的渠道
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
export const CHANNEL_OPTIONS = {
 | 
			
		||||
  //key 为渠道ID
 | 
			
		||||
  1: {
 | 
			
		||||
    key: 1, // 渠道ID
 | 
			
		||||
    text: "OpenAI", // 渠道名称
 | 
			
		||||
    value: 1, // 渠道ID
 | 
			
		||||
    color: "primary", // 渠道列表显示的颜色
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
2. `web/berry/src/views/Channel/type/Config.js`
 | 
			
		||||
 | 
			
		||||
在该文件中的`typeConfig`添加新的渠道配置, 如果无需配置,可以不添加
 | 
			
		||||
 | 
			
		||||
```js
 | 
			
		||||
const typeConfig = {
 | 
			
		||||
  // key 为渠道ID
 | 
			
		||||
  3: {
 | 
			
		||||
    inputLabel: {
 | 
			
		||||
      // 输入框名称 配置
 | 
			
		||||
      // 对应的字段名称
 | 
			
		||||
      base_url: "AZURE_OPENAI_ENDPOINT",
 | 
			
		||||
      other: "默认 API 版本",
 | 
			
		||||
    },
 | 
			
		||||
    prompt: {
 | 
			
		||||
      // 输入框提示 配置
 | 
			
		||||
      // 对应的字段名称
 | 
			
		||||
      base_url: "请填写AZURE_OPENAI_ENDPOINT",
 | 
			
		||||
 | 
			
		||||
      // 注意:通过判断 `other` 是否有值来判断是否需要显示 `other` 输入框, 默认是没有值的
 | 
			
		||||
      other: "请输入默认API版本,例如:2023-06-01-preview",
 | 
			
		||||
    },
 | 
			
		||||
    modelGroup: "openai", // 模型组名称,这个值是给 填入渠道支持模型 按钮使用的。 填入渠道支持模型 按钮会根据这个值来获取模型组,如果填写默认是 openai
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 许可证
 | 
			
		||||
 | 
			
		||||
本项目中使用的代码遵循 MIT 许可证。
 | 
			
		||||
							
								
								
									
										9
									
								
								web/berry/jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "target": "esnext",
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "baseUrl": "src"
 | 
			
		||||
  },
 | 
			
		||||
  "include": ["src/**/*"],
 | 
			
		||||
  "exclude": ["node_modules"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								web/berry/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,84 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "one_api_web",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "proxy": "http://127.0.0.1:3000",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "homepage": "",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@emotion/cache": "^11.9.3",
 | 
			
		||||
    "@emotion/react": "^11.9.3",
 | 
			
		||||
    "@emotion/styled": "^11.9.3",
 | 
			
		||||
    "@mui/icons-material": "^5.8.4",
 | 
			
		||||
    "@mui/lab": "^5.0.0-alpha.88",
 | 
			
		||||
    "@mui/material": "^5.8.6",
 | 
			
		||||
    "@mui/system": "^5.8.6",
 | 
			
		||||
    "@mui/utils": "^5.8.6",
 | 
			
		||||
    "@mui/x-date-pickers": "^6.18.5",
 | 
			
		||||
    "@tabler/icons-react": "^2.44.0",
 | 
			
		||||
    "apexcharts": "^3.35.3",
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "dayjs": "^1.11.10",
 | 
			
		||||
    "formik": "^2.2.9",
 | 
			
		||||
    "framer-motion": "^6.3.16",
 | 
			
		||||
    "history": "^5.3.0",
 | 
			
		||||
    "marked": "^4.1.1",
 | 
			
		||||
    "material-ui-popup-state": "^4.0.1",
 | 
			
		||||
    "notistack": "^3.0.1",
 | 
			
		||||
    "prop-types": "^15.8.1",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-apexcharts": "^1.4.0",
 | 
			
		||||
    "react-device-detect": "^2.2.2",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-perfect-scrollbar": "^1.5.8",
 | 
			
		||||
    "react-redux": "^8.0.2",
 | 
			
		||||
    "react-router": "6.3.0",
 | 
			
		||||
    "react-router-dom": "6.3.0",
 | 
			
		||||
    "react-scripts": "^5.0.1",
 | 
			
		||||
    "react-turnstile": "^1.1.2",
 | 
			
		||||
    "redux": "^4.2.0",
 | 
			
		||||
    "yup": "^0.32.11"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-scripts start",
 | 
			
		||||
    "build": "react-scripts build && mv -f build ../build/berry",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": [
 | 
			
		||||
      "react-app"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "babel": {
 | 
			
		||||
    "presets": [
 | 
			
		||||
      "@babel/preset-react"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": {
 | 
			
		||||
    "production": [
 | 
			
		||||
      "defaults",
 | 
			
		||||
      "not IE 11"
 | 
			
		||||
    ],
 | 
			
		||||
    "development": [
 | 
			
		||||
      "last 1 chrome version",
 | 
			
		||||
      "last 1 firefox version",
 | 
			
		||||
      "last 1 safari version"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "^7.21.4",
 | 
			
		||||
    "@babel/eslint-parser": "^7.21.3",
 | 
			
		||||
    "eslint": "^8.38.0",
 | 
			
		||||
    "eslint-config-prettier": "^8.8.0",
 | 
			
		||||
    "eslint-config-react-app": "^7.0.1",
 | 
			
		||||
    "eslint-plugin-flowtype": "^8.0.3",
 | 
			
		||||
    "eslint-plugin-import": "^2.27.5",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "^6.7.1",
 | 
			
		||||
    "eslint-plugin-prettier": "^4.2.1",
 | 
			
		||||
    "eslint-plugin-react": "^7.32.2",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^4.6.0",
 | 
			
		||||
    "immutable": "^4.3.0",
 | 
			
		||||
    "prettier": "^2.8.7",
 | 
			
		||||
    "sass": "^1.53.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								web/berry/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										26
									
								
								web/berry/public/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <title>One API</title>
 | 
			
		||||
    <link rel="icon" href="/favicon.ico" />
 | 
			
		||||
    <!-- Meta Tags-->
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
    <meta name="theme-color" content="#2296f3" />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
 | 
			
		||||
    />
 | 
			
		||||
    <link rel="preconnect" href="https://fonts.gstatic.com" />
 | 
			
		||||
    <link
 | 
			
		||||
      href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
    />
 | 
			
		||||
  </head>
 | 
			
		||||
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										43
									
								
								web/berry/src/App.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import { ThemeProvider } from '@mui/material/styles';
 | 
			
		||||
import { CssBaseline, StyledEngineProvider } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// routing
 | 
			
		||||
import Routes from 'routes';
 | 
			
		||||
 | 
			
		||||
// defaultTheme
 | 
			
		||||
import themes from 'themes';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import NavigationScroll from 'layout/NavigationScroll';
 | 
			
		||||
 | 
			
		||||
// auth
 | 
			
		||||
import UserProvider from 'contexts/UserContext';
 | 
			
		||||
import StatusProvider from 'contexts/StatusContext';
 | 
			
		||||
import { SnackbarProvider } from 'notistack';
 | 
			
		||||
 | 
			
		||||
// ==============================|| APP ||============================== //
 | 
			
		||||
 | 
			
		||||
const App = () => {
 | 
			
		||||
  const customization = useSelector((state) => state.customization);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledEngineProvider injectFirst>
 | 
			
		||||
      <ThemeProvider theme={themes(customization)}>
 | 
			
		||||
        <CssBaseline />
 | 
			
		||||
        <NavigationScroll>
 | 
			
		||||
          <SnackbarProvider autoHideDuration={5000} maxSnack={3} anchorOrigin={{ vertical: 'top', horizontal: 'right' }}>
 | 
			
		||||
            <UserProvider>
 | 
			
		||||
              <StatusProvider>
 | 
			
		||||
                <Routes />
 | 
			
		||||
              </StatusProvider>
 | 
			
		||||
            </UserProvider>
 | 
			
		||||
          </SnackbarProvider>
 | 
			
		||||
        </NavigationScroll>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </StyledEngineProvider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default App;
 | 
			
		||||
							
								
								
									
										40
									
								
								web/berry/src/assets/images/404.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,40 @@
 | 
			
		||||
<svg width="480" height="360" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
  <path fill-rule="evenodd" clip-rule="evenodd"
 | 
			
		||||
    d="M0 198.781c0 41.457 14.945 79.235 39.539 107.785 28.214 32.765 69.128 53.365 114.734 53.434a148.458 148.458 0 0056.495-11.036c9.051-3.699 19.182-3.274 27.948 1.107a75.774 75.774 0 0033.957 8.011c5.023 0 9.942-.495 14.7-1.434 13.581-2.67 25.94-8.99 36.089-17.94 6.379-5.627 14.548-8.456 22.898-8.446h.142c27.589 0 53.215-8.732 74.492-23.696 19.021-13.36 34.554-31.696 44.904-53.225C474.92 234.581 480 213.388 480 190.958c0-76.931-59.774-139.305-133.498-139.305-7.516 0-14.88.663-22.063 1.899C305.418 21.42 271.355 0 232.498 0a103.647 103.647 0 00-45.879 10.661c-13.24 6.487-25.011 15.705-34.641 26.939-32.697.544-62.93 11.69-87.675 30.291C25.351 97.155 0 144.882 0 198.781z"
 | 
			
		||||
    fill="url(#prefix__paint0_linear)" opacity=".2" />
 | 
			
		||||
  <g filter="url(#prefix__filter0_d)">
 | 
			
		||||
    <circle opacity=".15" cx="182.109" cy="97.623" r="44.623" fill="#FFC107" />
 | 
			
		||||
    <circle cx="182.109" cy="97.623" r="23.406" fill="url(#prefix__paint1_linear)" />
 | 
			
		||||
    <path fill-rule="evenodd" clip-rule="evenodd"
 | 
			
		||||
      d="M244.878 306.611c34.56 0 62.575-28.016 62.575-62.575 0-34.56-28.015-62.576-62.575-62.576-34.559 0-62.575 28.016-62.575 62.576 0 34.559 28.016 62.575 62.575 62.575zm0-23.186c21.754 0 39.389-17.635 39.389-39.389 0-21.755-17.635-39.39-39.389-39.39s-39.389 17.635-39.389 39.39c0 21.754 17.635 39.389 39.389 39.389z"
 | 
			
		||||
      fill="#061B64" />
 | 
			
		||||
    <path fill-rule="evenodd" clip-rule="evenodd"
 | 
			
		||||
      d="M174.965 264.592c0-4.133-1.492-5.625-5.637-5.625h-11.373v-66.611c0-4.476-1.492-5.637-5.638-5.637h-9.172a9.866 9.866 0 00-7.948 3.974l-55.03 68.274a11.006 11.006 0 00-1.957 6.787v5.968c0 4.145 1.492 5.637 5.625 5.637h54.676v21.707c0 4.133 1.492 5.625 5.625 5.625h8.12c4.146 0 5.638-1.492 5.638-5.625v-21.707h11.434c4.414 0 5.637-1.492 5.637-5.637v-7.13zm-72.42-5.625l35.966-44.415v44.415h-35.966zM411.607 264.592c0-4.133-1.492-5.625-5.638-5.625h-11.422v-66.611c0-4.476-1.492-5.637-5.637-5.637h-9.111a9.87 9.87 0 00-7.949 3.974l-55.03 68.274a11.011 11.011 0 00-1.981 6.787v5.968c0 4.145 1.492 5.637 5.626 5.637h54.687v21.707c0 4.133 1.492 5.625 5.626 5.625h8.12c4.145 0 5.637-1.492 5.637-5.625v-21.707h11.434c4.476 0 5.638-1.492 5.638-5.637v-7.13zm-72.42-5.625l35.965-44.415v44.415h-35.965z"
 | 
			
		||||
      fill="#2065D1" />
 | 
			
		||||
    <path opacity=".24"
 | 
			
		||||
      d="M425.621 117.222a8.267 8.267 0 00-9.599-8.157 11.129 11.129 0 00-9.784-5.87h-.403a13.23 13.23 0 00-20.365-14.078 13.23 13.23 0 00-5.316 14.078h-.403a11.153 11.153 0 100 22.293h38.68v-.073a8.279 8.279 0 007.19-8.193zM104.258 199.045a7.093 7.093 0 00-7.093-7.092c-.381.007-.761.039-1.138.097a9.552 9.552 0 00-8.425-5.026h-.343a11.348 11.348 0 10-22.012 0h-.342a9.564 9.564 0 100 19.114h33.177v-.061a7.107 7.107 0 006.176-7.032z"
 | 
			
		||||
      fill="#2065D1" />
 | 
			
		||||
  </g>
 | 
			
		||||
  <defs>
 | 
			
		||||
    <linearGradient id="prefix__paint0_linear" x1="328.81" y1="424.032" x2="505.393" y2="26.048"
 | 
			
		||||
      gradientUnits="userSpaceOnUse">
 | 
			
		||||
      <stop stop-color="#2065D1" />
 | 
			
		||||
      <stop offset="1" stop-color="#2065D1" stop-opacity=".01" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <linearGradient id="prefix__paint1_linear" x1="135.297" y1="97.623" x2="182.109" y2="144.436"
 | 
			
		||||
      gradientUnits="userSpaceOnUse">
 | 
			
		||||
      <stop stop-color="#FFE16A" />
 | 
			
		||||
      <stop offset="1" stop-color="#B78103" />
 | 
			
		||||
    </linearGradient>
 | 
			
		||||
    <filter id="prefix__filter0_d" x="51" y="49" width="394.621" height="277.611" filterUnits="userSpaceOnUse"
 | 
			
		||||
      color-interpolation-filters="sRGB">
 | 
			
		||||
      <feFlood flood-opacity="0" result="BackgroundImageFix" />
 | 
			
		||||
      <feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" />
 | 
			
		||||
      <feOffset dx="8" dy="8" />
 | 
			
		||||
      <feGaussianBlur stdDeviation="6" />
 | 
			
		||||
      <feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0" />
 | 
			
		||||
      <feBlend in2="BackgroundImageFix" result="effect1_dropShadow" />
 | 
			
		||||
      <feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape" />
 | 
			
		||||
    </filter>
 | 
			
		||||
  </defs>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										65
									
								
								web/berry/src/assets/images/auth/auth-blue-card.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										39
									
								
								web/berry/src/assets/images/auth/auth-pattern-dark.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
<svg width="670" height="903" viewBox="0 0 670 903" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="670" height="903">
 | 
			
		||||
<g opacity="0.2">
 | 
			
		||||
<path d="M0 0H670V903H0V0Z" fill="white"/>
 | 
			
		||||
</g>
 | 
			
		||||
</mask>
 | 
			
		||||
<g mask="url(#mask0)">
 | 
			
		||||
<path d="M2030.91 374.849L426.331 1300.78" stroke="#8492C4"/>
 | 
			
		||||
<path d="M426.409 -527.071L2030.72 399.311" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1919.22 310.39L314.731 1236.47" stroke="#8492C4"/>
 | 
			
		||||
<path d="M314.731 -462.612L1919.22 463.467" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1807.54 245.932L203.055 1172.01" stroke="#8492C4"/>
 | 
			
		||||
<path d="M203.052 -398.154L1807.54 527.925" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1695.87 181.473L91.3788 1107.55" stroke="#8492C4"/>
 | 
			
		||||
<path d="M91.3744 -333.695L1695.86 592.384" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1584.19 117.014L-20.3012 1043.09" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-20.3044 -269.237L1584.19 656.843" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1472.51 52.5562L-131.98 978.636" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-131.983 -204.778L1472.51 721.301" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1360.83 -11.9023L-243.658 914.177" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-243.662 -140.319L1360.83 785.76" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1249.15 -76.3613L-355.336 849.718" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-355.341 -75.8608L1249.15 850.219" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1137.48 -140.819L-467.014 785.26" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-467.017 -11.4023L1137.47 914.677" stroke="#8492C4"/>
 | 
			
		||||
<path d="M1025.8 -205.278L-578.692 720.801" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-578.693 53.0562L1025.8 979.136" stroke="#8492C4"/>
 | 
			
		||||
<path d="M914.119 -269.736L-690.371 656.343" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-690.379 117.515L914.111 1043.59" stroke="#8492C4"/>
 | 
			
		||||
<path d="M802.441 -334.195L-802.052 591.887" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-802.055 181.974L802.435 1108.05" stroke="#8492C4"/>
 | 
			
		||||
<path d="M690.762 -398.654L-913.728 527.426" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-913.731 246.432L690.759 1172.51" stroke="#8492C4"/>
 | 
			
		||||
<path d="M579.084 -463.112L-1025.41 462.967" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-1025.41 310.891L579.083 1236.97" stroke="#8492C4"/>
 | 
			
		||||
<path d="M467.406 -527.571L-1136.91 398.811" stroke="#8492C4"/>
 | 
			
		||||
<path d="M-1137.09 375.35L467.397 1301.43" stroke="#8492C4"/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.2 KiB  | 
							
								
								
									
										39
									
								
								web/berry/src/assets/images/auth/auth-pattern.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
<svg width="670" height="903" viewBox="0 0 670 903" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="670" height="903">
 | 
			
		||||
<g opacity="0.2">
 | 
			
		||||
<path d="M0 0H670V903H0V0Z" fill="white"/>
 | 
			
		||||
</g>
 | 
			
		||||
</mask>
 | 
			
		||||
<g mask="url(#mask0)">
 | 
			
		||||
<path d="M2030.91 374.849L426.331 1300.78" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M426.409 -527.071L2030.72 399.311" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1919.22 310.39L314.731 1236.47" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M314.731 -462.612L1919.22 463.467" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1807.54 245.932L203.055 1172.01" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M203.052 -398.154L1807.54 527.925" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1695.87 181.473L91.3788 1107.55" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M91.3744 -333.695L1695.86 592.384" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1584.19 117.014L-20.3012 1043.09" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-20.3044 -269.237L1584.19 656.843" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1472.51 52.5562L-131.98 978.636" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-131.983 -204.778L1472.51 721.301" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1360.83 -11.9023L-243.658 914.177" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-243.662 -140.319L1360.83 785.76" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1249.15 -76.3613L-355.336 849.718" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-355.341 -75.8608L1249.15 850.219" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1137.48 -140.819L-467.014 785.26" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-467.017 -11.4023L1137.47 914.677" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M1025.8 -205.278L-578.692 720.801" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-578.693 53.0562L1025.8 979.136" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M914.119 -269.736L-690.371 656.343" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-690.379 117.515L914.111 1043.59" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M802.441 -334.195L-802.052 591.887" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-802.055 181.974L802.435 1108.05" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M690.762 -398.654L-913.728 527.426" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-913.731 246.432L690.759 1172.51" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M579.084 -463.112L-1025.41 462.967" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-1025.41 310.891L579.083 1236.97" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M467.406 -527.571L-1136.91 398.811" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
<path d="M-1137.09 375.35L467.397 1301.43" stroke="rgba(0,0,0,0.30)"/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										69
									
								
								web/berry/src/assets/images/auth/auth-purple-card.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 22 KiB  | 
| 
		 After Width: | Height: | Size: 272 KiB  | 
							
								
								
									
										40
									
								
								web/berry/src/assets/images/auth/auth-signup-white-card.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										5
									
								
								web/berry/src/assets/images/icons/earning.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<path d="M19 9H9C7.89543 9 7 9.89543 7 11V17C7 18.1046 7.89543 19 9 19H19C20.1046 19 21 18.1046 21 17V11C21 9.89543 20.1046 9 19 9Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
<path d="M14 16C15.1046 16 16 15.1046 16 14C16 12.8954 15.1046 12 14 12C12.8954 12 12 12.8954 12 14C12 15.1046 12.8954 16 14 16Z" fill="#90CAF9"/>
 | 
			
		||||
<path d="M17 9V7C17 6.46957 16.7893 5.96086 16.4142 5.58579C16.0391 5.21071 15.5304 5 15 5H5C4.46957 5 3.96086 5.21071 3.58579 5.58579C3.21071 5.96086 3 6.46957 3 7V13C3 13.5304 3.21071 14.0391 3.58579 14.4142C3.96086 14.7893 4.46957 15 5 15H7" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 794 B  | 
							
								
								
									
										1
									
								
								web/berry/src/assets/images/icons/github.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg t="1702350903010" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4215" width="32" height="32"><path d="M512 85.333333C276.266667 85.333333 85.333333 276.266667 85.333333 512a426.410667 426.410667 0 0 0 291.754667 404.821333c21.333333 3.712 29.312-9.088 29.312-20.309333 0-10.112-0.554667-43.690667-0.554667-79.445333-107.178667 19.754667-134.912-26.112-143.445333-50.133334-4.821333-12.288-25.6-50.133333-43.733333-60.288-14.933333-7.978667-36.266667-27.733333-0.554667-28.245333 33.621333-0.554667 57.6 30.933333 65.621333 43.733333 38.4 64.512 99.754667 46.378667 124.245334 35.2 3.754667-27.733333 14.933333-46.378667 27.221333-57.045333-94.933333-10.666667-194.133333-47.488-194.133333-210.688 0-46.421333 16.512-84.778667 43.733333-114.688-4.266667-10.666667-19.2-54.4 4.266667-113.066667 0 0 35.712-11.178667 117.333333 43.776a395.946667 395.946667 0 0 1 106.666667-14.421333c36.266667 0 72.533333 4.778667 106.666666 14.378667 81.578667-55.466667 117.333333-43.690667 117.333334-43.690667 23.466667 58.666667 8.533333 102.4 4.266666 113.066667 27.178667 29.866667 43.733333 67.712 43.733334 114.645333 0 163.754667-99.712 200.021333-194.645334 210.688 15.445333 13.312 28.8 38.912 28.8 78.933333 0 57.045333-0.554667 102.912-0.554666 117.333334 0 11.178667 8.021333 24.490667 29.354666 20.224A427.349333 427.349333 0 0 0 938.666667 512c0-235.733333-190.933333-426.666667-426.666667-426.666667z" fill="#000000" p-id="4216"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										1
									
								
								web/berry/src/assets/images/icons/shape-avatar.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg height="62" viewBox="0 0 144 62" width="144" xmlns="http://www.w3.org/2000/svg"><path d="m111.34 23.88c-10.62-10.46-18.5-23.88-38.74-23.88h-1.2c-20.24 0-28.12 13.42-38.74 23.88-7.72 9.64-19.44 11.74-32.66 12.12v26h144v-26c-13.22-.38-24.94-2.48-32.66-12.12z" fill="#fff" fill-rule="evenodd"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 302 B  | 
							
								
								
									
										6
									
								
								web/berry/src/assets/images/icons/social-google.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
<path d="M5.06129 13.2253L4.31871 15.9975L1.60458 16.0549C0.793457 14.5504 0.333374 12.8292 0.333374 11C0.333374 9.23119 0.763541 7.56319 1.52604 6.09448H1.52662L3.94296 6.53748L5.00146 8.93932C4.77992 9.58519 4.65917 10.2785 4.65917 11C4.65925 11.783 4.80108 12.5332 5.06129 13.2253Z" fill="#FBBB00"/>
 | 
			
		||||
<path d="M21.4804 9.00732C21.6029 9.65257 21.6668 10.3189 21.6668 11C21.6668 11.7637 21.5865 12.5086 21.4335 13.2271C20.9143 15.6722 19.5575 17.8073 17.678 19.3182L17.6774 19.3177L14.6339 19.1624L14.2031 16.4734C15.4503 15.742 16.425 14.5974 16.9384 13.2271H11.2346V9.00732H17.0216H21.4804Z" fill="#518EF8"/>
 | 
			
		||||
<path d="M17.6772 19.3176L17.6777 19.3182C15.8498 20.7875 13.5277 21.6666 11 21.6666C6.93783 21.6666 3.40612 19.3962 1.60449 16.0549L5.0612 13.2253C5.96199 15.6294 8.28112 17.3408 11 17.3408C12.1686 17.3408 13.2634 17.0249 14.2029 16.4734L17.6772 19.3176Z" fill="#28B446"/>
 | 
			
		||||
<path d="M17.8085 2.78892L14.353 5.61792C13.3807 5.01017 12.2313 4.65908 11 4.65908C8.21963 4.65908 5.85713 6.44896 5.00146 8.93925L1.52658 6.09442H1.526C3.30125 2.67171 6.8775 0.333252 11 0.333252C13.5881 0.333252 15.9612 1.25517 17.8085 2.78892Z" fill="#F14336"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										1
									
								
								web/berry/src/assets/images/icons/wechat.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg t="1702350975929" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2474" width="32" height="32"><path d="M512 1024C229.23264 1024 0 794.76736 0 512S229.23264 0 512 0s512 229.23264 512 512-229.23264 512-512 512z m212.0704-290.0992l41.5744 24.064c9.216 5.3248 14.5408 1.26976 11.83744-9.03168l-9.0112-34.4064a9.76896 9.76896 0 0 1 3.76832-10.4448C813.50656 674.75456 839.68 631.0912 839.68 582.32832c0-88.28928-85.79072-159.86688-191.61088-159.86688s-191.61088 71.5776-191.61088 159.86688c0 88.2688 85.79072 159.86688 191.61088 159.86688 22.9376 0 44.91264-3.35872 65.26976-9.5232a13.55776 13.55776 0 0 1 10.73152 1.2288z m-366.63296-116.08064a271.85152 271.85152 0 0 0 89.98912 10.89536 146.61632 146.61632 0 0 1-7.7824-47.16544c0-96.31744 94.33088-174.3872 210.71872-174.3872 4.07552 0 8.11008 0.08192 12.12416 0.28672C645.2224 315.84256 549.96992 245.76 435.03616 245.76 307.87584 245.76 204.8 331.55072 204.8 437.37088c0 58.1632 31.15008 110.2848 80.32256 145.42848 4.62848 3.31776 6.7584 9.13408 5.28384 14.62272l-10.83392 40.38656c-3.2768 12.1856 2.99008 16.95744 13.88544 10.58816l49.29536-28.79488a18.59584 18.59584 0 0 1 14.68416-1.78176z m353.73056-60.74368a26.0096 26.0096 0 1 1 0-52.0192 26.0096 26.0096 0 0 1 0 52.0192z m-126.976 0a26.0096 26.0096 0 1 1 0-52.0192 26.0096 26.0096 0 0 1 0 52.0192z m-72.66304-150.69184a30.59712 30.59712 0 1 1 0-61.19424 30.59712 30.59712 0 0 1 0 61.19424z m-153.74336 0a30.59712 30.59712 0 1 1 0-61.19424 30.59712 30.59712 0 0 1 0 61.19424z" fill="#2BD418" p-id="2475"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/berry/src/assets/images/invite/cover.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 86 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/berry/src/assets/images/invite/cwok_casual_19.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 174 KiB  | 
							
								
								
									
										15
									
								
								web/berry/src/assets/images/logo-2.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
<svg viewBox="0 0 590 265" xmlns="http://www.w3.org/2000/svg" version="1.1">
 | 
			
		||||
 | 
			
		||||
 <g>
 | 
			
		||||
  <title>Layer 1</title>
 | 
			
		||||
  <ellipse transform="rotate(103.6 287.6 32.87)" id="svg_1" ry="28.14" rx="28.47" cy="32.87" cx="287.60001" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_2" d="m232.92,128.89c3.78,27.29 -1.81,55.44 -17.71,78.09a2.62,2.62 0 0 0 -0.06,2.92c1.24,1.92 2.96,5.05 5.56,4.94q5.25,-0.22 10.79,0.11a1.26,1.26 0 0 1 1.19,1.27l-0.4,42.53a1.31,1.31 0 0 1 -1.31,1.3q-16.77,-0.09 -36.53,0.01q-2.25,0.02 -3.71,-1.56q-16.02,-17.28 -31.98,-35.32c-5.13,-5.8 -10.18,-11.16 -14.86,-17.59a1.35,1.34 -31.1 0 1 0.5,-2q12.88,-6.32 22.13,-17.12q18.18,-21.23 15.08,-48.84q-2.66,-23.7 -22.4,-40.46q-23.43,-19.9 -54.88,-13.86c-4.1,0.79 -7.83,2.5 -11.72,4.12q-11.86,4.94 -20.59,14.64c-14.25,15.81 -20.07,36.4 -15.05,57.16q4.99,20.63 22.86,35.71c10.45,8.81 23.7,13.12 37.26,14.18q1.47,0.11 3.6,2.65c11.68,13.89 24.48,27.72 35.94,41.96a0.43,0.43 0 0 1 -0.21,0.68q-22.51,7.27 -47.37,5.37q-19.4,-1.47 -39.74,-11.22q-18.27,-8.75 -30.59,-21.28q-18.66,-18.98 -28.02,-43.57q-10.8,-28.4 -4.93,-58.67c1.59,-8.17 4.03,-17 7.42,-24.61q5.08,-11.38 11.61,-20.64q25.41,-36.03 68.45,-46.13q32.42,-7.61 64.23,3.92q25.31,9.17 43.2,27.31c16.85,17.09 28.91,40.01 32.24,64z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_3" d="m499.47,180.61c6.45,13.53 16.44,21.75 31.96,22q11.94,0.19 22.17,-5.36q2.21,-1.2 3.93,0.69q12.56,13.78 24.89,28.47q1.21,1.44 1.44,3.13a0.95,0.95 0 0 1 -0.36,0.89c-1.62,1.23 -3.33,2.71 -5.03,3.69q-29.37,17.01 -62.47,11.31c-20.61,-3.55 -39.05,-15.24 -51.47,-32.51q-6.4,-8.89 -9.91,-17.08c-2.62,-6.12 -4.73,-13.3 -5.41,-20.08q-3.96,-39.88 22.94,-67.74c9.48,-9.81 21.15,-16.67 34.39,-19.49c16.54,-3.53 34.64,-1.83 48.77,7.1q13.92,8.79 21.13,20.4q11.07,17.84 10.48,38.92c-0.02,0.94 -0.21,1.81 -0.85,2.54q-7.73,8.77 -18.71,20.16c-1.28,1.32 -2.61,2.26 -4.51,2.23q-24.45,-0.37 -51.64,-0.41q-5.03,0 -10.84,-0.22a0.96,0.95 -11.7 0 0 -0.9,1.36zm1.12,-37.17q-0.55,1.19 -0.63,2.34q-0.08,1.01 0.94,1.03q19.01,0.25 36.98,0.01q0.5,0 0.94,-0.22q0.57,-0.28 0.44,-0.9q-2.34,-11.6 -14.11,-15.25q-3.59,-1.11 -6.44,-0.57q-13.07,2.5 -18.12,13.56z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_4" d="m312.3,100.22a0.5,0.49 -22.1 0 0 0.84,0.35q2.76,-2.64 5.82,-4.31q8.45,-4.62 16.71,-6.57c15.81,-3.72 33.58,-3.2 48.2,3.95q24.49,11.98 35.05,35.76c4.66,10.5 5.44,22.96 5.5,35.35q0.21,49.99 -0.12,88q-0.03,3.06 -0.08,6.16a1.32,1.32 0 0 1 -1.33,1.3q-20.22,-0.18 -40.18,-0.23q-3.64,-0.01 -8.13,-0.44a1.06,1.05 -87.3 0 1 -0.95,-1.05q0.02,-45.49 -0.22,-92.99c-0.03,-6.25 -1.21,-13.88 -5.05,-18.95q-5.33,-7.03 -12.32,-10.18c-10.99,-4.93 -24.52,-1.84 -33.13,6.37q-10.01,9.53 -10.07,23.76q-0.11,25.46 -0.1,48.98c0,3.52 -0.06,8.31 -1.1,11.68c-4.37,14.04 -17.31,19.5 -31.04,16.77c-8.22,-1.64 -15.07,-7.75 -17.62,-15.62q-1.45,-4.49 -1.42,-10.2q0.3,-64.69 0.1,-129.86a0.47,0.47 0 0 1 0.47,-0.47l48.46,-0.35a1.56,1.55 89.4 0 1 1.56,1.54l0.15,11.25z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_5" d="m265.63,344.43a2.02,2.01 76.7 0 0 -1.85,-1.15l-17.03,0.24a2.25,2.22 9.3 0 0 -2.06,1.46l-2.86,7.84a2.47,2.46 -79.1 0 1 -2.38,1.62l-6.23,-0.19q-1.19,-0.04 -0.88,-1.19q1.38,-5.23 2.81,-8.7c3.41,-8.3 6.48,-16.83 10.12,-25.35q2.96,-6.93 5.21,-14.24c0.46,-1.52 1.69,-2.64 3.37,-2.63c2.02,0 4.68,-0.78 5.7,1.58q7.68,17.74 18.16,44.75q0.96,2.46 1.48,5a0.67,0.66 84.3 0 1 -0.65,0.8l-6.05,-0.02q-2.16,-0.01 -3.1,-1.96l-3.76,-7.86zm-16.73,-10.31a0.34,0.34 0 0 0 0.32,0.47l12.85,-0.36a0.34,0.34 0 0 0 0.3,-0.48l-6.84,-14.7a0.34,0.34 0 0 0 -0.62,0.02l-6.01,15.05z" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_6" rx="2.17" height="52.28" width="9.84" y="302.19" x="345.67" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_7" d="m303.07,338.46l-0.15,14.42q-0.01,1.55 -1.56,1.52l-5.84,-0.12q-1.79,-0.04 -1.81,-1.83c-0.24,-15.33 -0.25,-30.89 -0.27,-47.22q-0.01,-2.99 2.55,-3.06q12.47,-0.33 20.15,0.8q8.61,1.25 12.86,9.17c2.95,5.49 2.53,13.5 -1.5,18.65c-5.57,7.14 -14.88,6.62 -23.24,6.51a1.17,1.17 0 0 0 -1.19,1.16zm-0.15,-24.81l0.16,12.72a1.72,1.72 0 0 0 1.74,1.7l6.07,-0.08a10.01,7.98 -0.7 0 0 9.91,-8.1l0,-0.2a10.01,7.98 -0.7 0 0 -10.11,-7.86l-6.07,0.08a1.72,1.72 0 0 0 -1.7,1.74z" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_8" rx="3.58" height="7.26" width="79.2" y="322.99" x="107" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_9" rx="3.81" height="7.72" width="79.1" y="322.78" x="417.27" fill="#0d161f"/>
 | 
			
		||||
 </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										13
									
								
								web/berry/src/assets/images/logo.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
<svg viewBox="0 0 590 360" xmlns="http://www.w3.org/2000/svg" version="1.1">
 | 
			
		||||
 <g>
 | 
			
		||||
  <ellipse transform="rotate(103.6 287.6 32.87)" id="svg_1" ry="28.14" rx="28.47" cy="32.87" cx="287.60001" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_2" d="m232.92,128.89c3.78,27.29 -1.81,55.44 -17.71,78.09a2.62,2.62 0 0 0 -0.06,2.92c1.24,1.92 2.96,5.05 5.56,4.94q5.25,-0.22 10.79,0.11a1.26,1.26 0 0 1 1.19,1.27l-0.4,42.53a1.31,1.31 0 0 1 -1.31,1.3q-16.77,-0.09 -36.53,0.01q-2.25,0.02 -3.71,-1.56q-16.02,-17.28 -31.98,-35.32c-5.13,-5.8 -10.18,-11.16 -14.86,-17.59a1.35,1.34 -31.1 0 1 0.5,-2q12.88,-6.32 22.13,-17.12q18.18,-21.23 15.08,-48.84q-2.66,-23.7 -22.4,-40.46q-23.43,-19.9 -54.88,-13.86c-4.1,0.79 -7.83,2.5 -11.72,4.12q-11.86,4.94 -20.59,14.64c-14.25,15.81 -20.07,36.4 -15.05,57.16q4.99,20.63 22.86,35.71c10.45,8.81 23.7,13.12 37.26,14.18q1.47,0.11 3.6,2.65c11.68,13.89 24.48,27.72 35.94,41.96a0.43,0.43 0 0 1 -0.21,0.68q-22.51,7.27 -47.37,5.37q-19.4,-1.47 -39.74,-11.22q-18.27,-8.75 -30.59,-21.28q-18.66,-18.98 -28.02,-43.57q-10.8,-28.4 -4.93,-58.67c1.59,-8.17 4.03,-17 7.42,-24.61q5.08,-11.38 11.61,-20.64q25.41,-36.03 68.45,-46.13q32.42,-7.61 64.23,3.92q25.31,9.17 43.2,27.31c16.85,17.09 28.91,40.01 32.24,64z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_3" d="m499.47,180.61c6.45,13.53 16.44,21.75 31.96,22q11.94,0.19 22.17,-5.36q2.21,-1.2 3.93,0.69q12.56,13.78 24.89,28.47q1.21,1.44 1.44,3.13a0.95,0.95 0 0 1 -0.36,0.89c-1.62,1.23 -3.33,2.71 -5.03,3.69q-29.37,17.01 -62.47,11.31c-20.61,-3.55 -39.05,-15.24 -51.47,-32.51q-6.4,-8.89 -9.91,-17.08c-2.62,-6.12 -4.73,-13.3 -5.41,-20.08q-3.96,-39.88 22.94,-67.74c9.48,-9.81 21.15,-16.67 34.39,-19.49c16.54,-3.53 34.64,-1.83 48.77,7.1q13.92,8.79 21.13,20.4q11.07,17.84 10.48,38.92c-0.02,0.94 -0.21,1.81 -0.85,2.54q-7.73,8.77 -18.71,20.16c-1.28,1.32 -2.61,2.26 -4.51,2.23q-24.45,-0.37 -51.64,-0.41q-5.03,0 -10.84,-0.22a0.96,0.95 -11.7 0 0 -0.9,1.36zm1.12,-37.17q-0.55,1.19 -0.63,2.34q-0.08,1.01 0.94,1.03q19.01,0.25 36.98,0.01q0.5,0 0.94,-0.22q0.57,-0.28 0.44,-0.9q-2.34,-11.6 -14.11,-15.25q-3.59,-1.11 -6.44,-0.57q-13.07,2.5 -18.12,13.56z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_4" d="m312.3,100.22a0.5,0.49 -22.1 0 0 0.84,0.35q2.76,-2.64 5.82,-4.31q8.45,-4.62 16.71,-6.57c15.81,-3.72 33.58,-3.2 48.2,3.95q24.49,11.98 35.05,35.76c4.66,10.5 5.44,22.96 5.5,35.35q0.21,49.99 -0.12,88q-0.03,3.06 -0.08,6.16a1.32,1.32 0 0 1 -1.33,1.3q-20.22,-0.18 -40.18,-0.23q-3.64,-0.01 -8.13,-0.44a1.06,1.05 -87.3 0 1 -0.95,-1.05q0.02,-45.49 -0.22,-92.99c-0.03,-6.25 -1.21,-13.88 -5.05,-18.95q-5.33,-7.03 -12.32,-10.18c-10.99,-4.93 -24.52,-1.84 -33.13,6.37q-10.01,9.53 -10.07,23.76q-0.11,25.46 -0.1,48.98c0,3.52 -0.06,8.31 -1.1,11.68c-4.37,14.04 -17.31,19.5 -31.04,16.77c-8.22,-1.64 -15.07,-7.75 -17.62,-15.62q-1.45,-4.49 -1.42,-10.2q0.3,-64.69 0.1,-129.86a0.47,0.47 0 0 1 0.47,-0.47l48.46,-0.35a1.56,1.55 89.4 0 1 1.56,1.54l0.15,11.25z" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_5" d="m265.63,344.43a2.02,2.01 76.7 0 0 -1.85,-1.15l-17.03,0.24a2.25,2.22 9.3 0 0 -2.06,1.46l-2.86,7.84a2.47,2.46 -79.1 0 1 -2.38,1.62l-6.23,-0.19q-1.19,-0.04 -0.88,-1.19q1.38,-5.23 2.81,-8.7c3.41,-8.3 6.48,-16.83 10.12,-25.35q2.96,-6.93 5.21,-14.24c0.46,-1.52 1.69,-2.64 3.37,-2.63c2.02,0 4.68,-0.78 5.7,1.58q7.68,17.74 18.16,44.75q0.96,2.46 1.48,5a0.67,0.66 84.3 0 1 -0.65,0.8l-6.05,-0.02q-2.16,-0.01 -3.1,-1.96l-3.76,-7.86zm-16.73,-10.31a0.34,0.34 0 0 0 0.32,0.47l12.85,-0.36a0.34,0.34 0 0 0 0.3,-0.48l-6.84,-14.7a0.34,0.34 0 0 0 -0.62,0.02l-6.01,15.05z" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_6" rx="2.17" height="52.28" width="9.84" y="302.19" x="345.67" fill="#0d161f"/>
 | 
			
		||||
  <path id="svg_7" d="m303.07,338.46l-0.15,14.42q-0.01,1.55 -1.56,1.52l-5.84,-0.12q-1.79,-0.04 -1.81,-1.83c-0.24,-15.33 -0.25,-30.89 -0.27,-47.22q-0.01,-2.99 2.55,-3.06q12.47,-0.33 20.15,0.8q8.61,1.25 12.86,9.17c2.95,5.49 2.53,13.5 -1.5,18.65c-5.57,7.14 -14.88,6.62 -23.24,6.51a1.17,1.17 0 0 0 -1.19,1.16zm-0.15,-24.81l0.16,12.72a1.72,1.72 0 0 0 1.74,1.7l6.07,-0.08a10.01,7.98 -0.7 0 0 9.91,-8.1l0,-0.2a10.01,7.98 -0.7 0 0 -10.11,-7.86l-6.07,0.08a1.72,1.72 0 0 0 -1.7,1.74z" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_8" rx="3.58" height="7.26" width="79.2" y="322.99" x="107" fill="#0d161f"/>
 | 
			
		||||
  <rect id="svg_9" rx="3.81" height="7.72" width="79.1" y="322.78" x="417.27" fill="#0d161f"/>
 | 
			
		||||
 </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.1 KiB  | 
							
								
								
									
										1
									
								
								web/berry/src/assets/images/users/user-round.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										157
									
								
								web/berry/src/assets/scss/_themes-vars.module.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,157 @@
 | 
			
		||||
// paper & background
 | 
			
		||||
$paper: #ffffff;
 | 
			
		||||
 | 
			
		||||
// primary
 | 
			
		||||
$primaryLight: #eef2f6;
 | 
			
		||||
$primaryMain: #2196f3;
 | 
			
		||||
$primaryDark: #1e88e5;
 | 
			
		||||
$primary200: #90caf9;
 | 
			
		||||
$primary800: #1565c0;
 | 
			
		||||
 | 
			
		||||
// secondary
 | 
			
		||||
$secondaryLight: #ede7f6;
 | 
			
		||||
$secondaryMain: #673ab7;
 | 
			
		||||
$secondaryDark: #5e35b1;
 | 
			
		||||
$secondary200: #b39ddb;
 | 
			
		||||
$secondary800: #4527a0;
 | 
			
		||||
 | 
			
		||||
// success Colors
 | 
			
		||||
$successLight: #b9f6ca;
 | 
			
		||||
$success200: #69f0ae;
 | 
			
		||||
$successMain: #00e676;
 | 
			
		||||
$successDark: #00c853;
 | 
			
		||||
 | 
			
		||||
// error
 | 
			
		||||
$errorLight: #ef9a9a;
 | 
			
		||||
$errorMain: #f44336;
 | 
			
		||||
$errorDark: #c62828;
 | 
			
		||||
 | 
			
		||||
// orange
 | 
			
		||||
$orangeLight: #fbe9e7;
 | 
			
		||||
$orangeMain: #ffab91;
 | 
			
		||||
$orangeDark: #d84315;
 | 
			
		||||
 | 
			
		||||
// warning
 | 
			
		||||
$warningLight: #fff8e1;
 | 
			
		||||
$warningMain: #ffe57f;
 | 
			
		||||
$warningDark: #ffc107;
 | 
			
		||||
 | 
			
		||||
// grey
 | 
			
		||||
$grey50: #f8fafc;
 | 
			
		||||
$grey100: #eef2f6;
 | 
			
		||||
$grey200: #e3e8ef;
 | 
			
		||||
$grey300: #cdd5df;
 | 
			
		||||
$grey500: #697586;
 | 
			
		||||
$grey600: #4b5565;
 | 
			
		||||
$grey700: #364152;
 | 
			
		||||
$grey900: #121926;
 | 
			
		||||
 | 
			
		||||
// ==============================|| DARK THEME VARIANTS ||============================== //
 | 
			
		||||
 | 
			
		||||
// paper & background
 | 
			
		||||
$darkBackground: #1a223f; // level 3
 | 
			
		||||
$darkPaper: #111936; // level 4
 | 
			
		||||
 | 
			
		||||
// dark 800 & 900
 | 
			
		||||
$darkLevel1: #29314f; // level 1
 | 
			
		||||
$darkLevel2: #212946; // level 2
 | 
			
		||||
 | 
			
		||||
// primary dark
 | 
			
		||||
$darkPrimaryLight: #eef2f6;
 | 
			
		||||
$darkPrimaryMain: #2196f3;
 | 
			
		||||
$darkPrimaryDark: #1e88e5;
 | 
			
		||||
$darkPrimary200: #90caf9;
 | 
			
		||||
$darkPrimary800: #1565c0;
 | 
			
		||||
 | 
			
		||||
// secondary dark
 | 
			
		||||
$darkSecondaryLight: #d1c4e9;
 | 
			
		||||
$darkSecondaryMain: #7c4dff;
 | 
			
		||||
$darkSecondaryDark: #651fff;
 | 
			
		||||
$darkSecondary200: #b39ddb;
 | 
			
		||||
$darkSecondary800: #6200ea;
 | 
			
		||||
 | 
			
		||||
// text variants
 | 
			
		||||
$darkTextTitle: #d7dcec;
 | 
			
		||||
$darkTextPrimary: #bdc8f0;
 | 
			
		||||
$darkTextSecondary: #8492c4;
 | 
			
		||||
 | 
			
		||||
// ==============================|| JAVASCRIPT ||============================== //
 | 
			
		||||
 | 
			
		||||
:export {
 | 
			
		||||
  // paper & background
 | 
			
		||||
  paper: $paper;
 | 
			
		||||
 | 
			
		||||
  // primary
 | 
			
		||||
  primaryLight: $primaryLight;
 | 
			
		||||
  primary200: $primary200;
 | 
			
		||||
  primaryMain: $primaryMain;
 | 
			
		||||
  primaryDark: $primaryDark;
 | 
			
		||||
  primary800: $primary800;
 | 
			
		||||
 | 
			
		||||
  // secondary
 | 
			
		||||
  secondaryLight: $secondaryLight;
 | 
			
		||||
  secondary200: $secondary200;
 | 
			
		||||
  secondaryMain: $secondaryMain;
 | 
			
		||||
  secondaryDark: $secondaryDark;
 | 
			
		||||
  secondary800: $secondary800;
 | 
			
		||||
 | 
			
		||||
  // success
 | 
			
		||||
  successLight: $successLight;
 | 
			
		||||
  success200: $success200;
 | 
			
		||||
  successMain: $successMain;
 | 
			
		||||
  successDark: $successDark;
 | 
			
		||||
 | 
			
		||||
  // error
 | 
			
		||||
  errorLight: $errorLight;
 | 
			
		||||
  errorMain: $errorMain;
 | 
			
		||||
  errorDark: $errorDark;
 | 
			
		||||
 | 
			
		||||
  // orange
 | 
			
		||||
  orangeLight: $orangeLight;
 | 
			
		||||
  orangeMain: $orangeMain;
 | 
			
		||||
  orangeDark: $orangeDark;
 | 
			
		||||
 | 
			
		||||
  // warning
 | 
			
		||||
  warningLight: $warningLight;
 | 
			
		||||
  warningMain: $warningMain;
 | 
			
		||||
  warningDark: $warningDark;
 | 
			
		||||
 | 
			
		||||
  // grey
 | 
			
		||||
  grey50: $grey50;
 | 
			
		||||
  grey100: $grey100;
 | 
			
		||||
  grey200: $grey200;
 | 
			
		||||
  grey300: $grey300;
 | 
			
		||||
  grey500: $grey500;
 | 
			
		||||
  grey600: $grey600;
 | 
			
		||||
  grey700: $grey700;
 | 
			
		||||
  grey900: $grey900;
 | 
			
		||||
 | 
			
		||||
  // ==============================|| DARK THEME VARIANTS ||============================== //
 | 
			
		||||
 | 
			
		||||
  // paper & background
 | 
			
		||||
  darkPaper: $darkPaper;
 | 
			
		||||
  darkBackground: $darkBackground;
 | 
			
		||||
 | 
			
		||||
  // dark 800 & 900
 | 
			
		||||
  darkLevel1: $darkLevel1;
 | 
			
		||||
  darkLevel2: $darkLevel2;
 | 
			
		||||
 | 
			
		||||
  // text variants
 | 
			
		||||
  darkTextTitle: $darkTextTitle;
 | 
			
		||||
  darkTextPrimary: $darkTextPrimary;
 | 
			
		||||
  darkTextSecondary: $darkTextSecondary;
 | 
			
		||||
 | 
			
		||||
  // primary dark
 | 
			
		||||
  darkPrimaryLight: $darkPrimaryLight;
 | 
			
		||||
  darkPrimaryMain: $darkPrimaryMain;
 | 
			
		||||
  darkPrimaryDark: $darkPrimaryDark;
 | 
			
		||||
  darkPrimary200: $darkPrimary200;
 | 
			
		||||
  darkPrimary800: $darkPrimary800;
 | 
			
		||||
 | 
			
		||||
  // secondary dark
 | 
			
		||||
  darkSecondaryLight: $darkSecondaryLight;
 | 
			
		||||
  darkSecondaryMain: $darkSecondaryMain;
 | 
			
		||||
  darkSecondaryDark: $darkSecondaryDark;
 | 
			
		||||
  darkSecondary200: $darkSecondary200;
 | 
			
		||||
  darkSecondary800: $darkSecondary800;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								web/berry/src/assets/scss/style.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
			
		||||
// color variants
 | 
			
		||||
@import 'themes-vars.module.scss';
 | 
			
		||||
 | 
			
		||||
// third-party
 | 
			
		||||
@import '~react-perfect-scrollbar/dist/css/styles.css';
 | 
			
		||||
 | 
			
		||||
// ==============================|| LIGHT BOX ||============================== //
 | 
			
		||||
.fullscreen .react-images__blanket {
 | 
			
		||||
  z-index: 1200;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==============================|| APEXCHART ||============================== //
 | 
			
		||||
 | 
			
		||||
.apexcharts-legend-series .apexcharts-legend-marker {
 | 
			
		||||
  margin-right: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==============================|| PERFECT SCROLLBAR ||============================== //
 | 
			
		||||
 | 
			
		||||
.scrollbar-container {
 | 
			
		||||
  .ps__rail-y {
 | 
			
		||||
    &:hover > .ps__thumb-y,
 | 
			
		||||
    &:focus > .ps__thumb-y,
 | 
			
		||||
    &.ps--clicking .ps__thumb-y {
 | 
			
		||||
      background-color: $grey500;
 | 
			
		||||
      width: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  .ps__thumb-y {
 | 
			
		||||
    background-color: $grey500;
 | 
			
		||||
    border-radius: 6px;
 | 
			
		||||
    width: 5px;
 | 
			
		||||
    right: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollbar-container.ps,
 | 
			
		||||
.scrollbar-container > .ps {
 | 
			
		||||
  &.ps--active-y > .ps__rail-y {
 | 
			
		||||
    width: 5px;
 | 
			
		||||
    background-color: transparent !important;
 | 
			
		||||
    z-index: 999;
 | 
			
		||||
    &:hover,
 | 
			
		||||
    &.ps--clicking {
 | 
			
		||||
      width: 5px;
 | 
			
		||||
      background-color: transparent;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  &.ps--scrolling-y > .ps__rail-y,
 | 
			
		||||
  &.ps--scrolling-x > .ps__rail-x {
 | 
			
		||||
    opacity: 0.4;
 | 
			
		||||
    background-color: transparent;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==============================|| ANIMATION KEYFRAMES ||============================== //
 | 
			
		||||
 | 
			
		||||
@keyframes wings {
 | 
			
		||||
  50% {
 | 
			
		||||
    transform: translateY(-40px);
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(0px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes blink {
 | 
			
		||||
  50% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes bounce {
 | 
			
		||||
  0%,
 | 
			
		||||
  20%,
 | 
			
		||||
  53%,
 | 
			
		||||
  to {
 | 
			
		||||
    animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
 | 
			
		||||
    transform: translateZ(0);
 | 
			
		||||
  }
 | 
			
		||||
  40%,
 | 
			
		||||
  43% {
 | 
			
		||||
    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
 | 
			
		||||
    transform: translate3d(0, -5px, 0);
 | 
			
		||||
  }
 | 
			
		||||
  70% {
 | 
			
		||||
    animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
 | 
			
		||||
    transform: translate3d(0, -7px, 0);
 | 
			
		||||
  }
 | 
			
		||||
  80% {
 | 
			
		||||
    transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
 | 
			
		||||
    transform: translateZ(0);
 | 
			
		||||
  }
 | 
			
		||||
  90% {
 | 
			
		||||
    transform: translate3d(0, -2px, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideY {
 | 
			
		||||
  0%,
 | 
			
		||||
  50%,
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateY(0px);
 | 
			
		||||
  }
 | 
			
		||||
  25% {
 | 
			
		||||
    transform: translateY(-10px);
 | 
			
		||||
  }
 | 
			
		||||
  75% {
 | 
			
		||||
    transform: translateY(10px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideX {
 | 
			
		||||
  0%,
 | 
			
		||||
  50%,
 | 
			
		||||
  100% {
 | 
			
		||||
    transform: translateX(0px);
 | 
			
		||||
  }
 | 
			
		||||
  25% {
 | 
			
		||||
    transform: translateX(-10px);
 | 
			
		||||
  }
 | 
			
		||||
  75% {
 | 
			
		||||
    transform: translateX(10px);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								web/berry/src/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
const config = {
 | 
			
		||||
  // basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
 | 
			
		||||
  // like '/berry-material-react/react/default'
 | 
			
		||||
  basename: '/',
 | 
			
		||||
  defaultPath: '/panel/dashboard',
 | 
			
		||||
  fontFamily: `'Roboto', sans-serif, Helvetica, Arial, sans-serif`,
 | 
			
		||||
  borderRadius: 12,
 | 
			
		||||
  siteInfo: {
 | 
			
		||||
    chat_link: '',
 | 
			
		||||
    display_in_currency: true,
 | 
			
		||||
    email_verification: false,
 | 
			
		||||
    footer_html: '',
 | 
			
		||||
    github_client_id: '',
 | 
			
		||||
    github_oauth: false,
 | 
			
		||||
    logo: '',
 | 
			
		||||
    quota_per_unit: 500000,
 | 
			
		||||
    server_address: '',
 | 
			
		||||
    start_time: 0,
 | 
			
		||||
    system_name: 'One API',
 | 
			
		||||
    top_up_link: '',
 | 
			
		||||
    turnstile_check: false,
 | 
			
		||||
    turnstile_site_key: '',
 | 
			
		||||
    version: '',
 | 
			
		||||
    wechat_login: false,
 | 
			
		||||
    wechat_qrcode: ''
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										146
									
								
								web/berry/src/constants/ChannelConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,146 @@
 | 
			
		||||
export const CHANNEL_OPTIONS = {
 | 
			
		||||
  1: {
 | 
			
		||||
    key: 1,
 | 
			
		||||
    text: 'OpenAI',
 | 
			
		||||
    value: 1,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  14: {
 | 
			
		||||
    key: 14,
 | 
			
		||||
    text: 'Anthropic Claude',
 | 
			
		||||
    value: 14,
 | 
			
		||||
    color: 'info'
 | 
			
		||||
  },
 | 
			
		||||
  3: {
 | 
			
		||||
    key: 3,
 | 
			
		||||
    text: 'Azure OpenAI',
 | 
			
		||||
    value: 3,
 | 
			
		||||
    color: 'orange'
 | 
			
		||||
  },
 | 
			
		||||
  11: {
 | 
			
		||||
    key: 11,
 | 
			
		||||
    text: 'Google PaLM2',
 | 
			
		||||
    value: 11,
 | 
			
		||||
    color: 'orange'
 | 
			
		||||
  },
 | 
			
		||||
  24: {
 | 
			
		||||
    key: 24,
 | 
			
		||||
    text: 'Google Gemini',
 | 
			
		||||
    value: 24,
 | 
			
		||||
    color: 'orange'
 | 
			
		||||
  },
 | 
			
		||||
  15: {
 | 
			
		||||
    key: 15,
 | 
			
		||||
    text: '百度文心千帆',
 | 
			
		||||
    value: 15,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  17: {
 | 
			
		||||
    key: 17,
 | 
			
		||||
    text: '阿里通义千问',
 | 
			
		||||
    value: 17,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  18: {
 | 
			
		||||
    key: 18,
 | 
			
		||||
    text: '讯飞星火认知',
 | 
			
		||||
    value: 18,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  16: {
 | 
			
		||||
    key: 16,
 | 
			
		||||
    text: '智谱 ChatGLM',
 | 
			
		||||
    value: 16,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  19: {
 | 
			
		||||
    key: 19,
 | 
			
		||||
    text: '360 智脑',
 | 
			
		||||
    value: 19,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  23: {
 | 
			
		||||
    key: 23,
 | 
			
		||||
    text: '腾讯混元',
 | 
			
		||||
    value: 23,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  8: {
 | 
			
		||||
    key: 8,
 | 
			
		||||
    text: '自定义渠道',
 | 
			
		||||
    value: 8,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  22: {
 | 
			
		||||
    key: 22,
 | 
			
		||||
    text: '知识库:FastGPT',
 | 
			
		||||
    value: 22,
 | 
			
		||||
    color: 'default'
 | 
			
		||||
  },
 | 
			
		||||
  21: {
 | 
			
		||||
    key: 21,
 | 
			
		||||
    text: '知识库:AI Proxy',
 | 
			
		||||
    value: 21,
 | 
			
		||||
    color: 'purple'
 | 
			
		||||
  },
 | 
			
		||||
  20: {
 | 
			
		||||
    key: 20,
 | 
			
		||||
    text: '代理:OpenRouter',
 | 
			
		||||
    value: 20,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  2: {
 | 
			
		||||
    key: 2,
 | 
			
		||||
    text: '代理:API2D',
 | 
			
		||||
    value: 2,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  5: {
 | 
			
		||||
    key: 5,
 | 
			
		||||
    text: '代理:OpenAI-SB',
 | 
			
		||||
    value: 5,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  7: {
 | 
			
		||||
    key: 7,
 | 
			
		||||
    text: '代理:OhMyGPT',
 | 
			
		||||
    value: 7,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  10: {
 | 
			
		||||
    key: 10,
 | 
			
		||||
    text: '代理:AI Proxy',
 | 
			
		||||
    value: 10,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  4: {
 | 
			
		||||
    key: 4,
 | 
			
		||||
    text: '代理:CloseAI',
 | 
			
		||||
    value: 4,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  6: {
 | 
			
		||||
    key: 6,
 | 
			
		||||
    text: '代理:OpenAI Max',
 | 
			
		||||
    value: 6,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  9: {
 | 
			
		||||
    key: 9,
 | 
			
		||||
    text: '代理:AI.LS',
 | 
			
		||||
    value: 9,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  12: {
 | 
			
		||||
    key: 12,
 | 
			
		||||
    text: '代理:API2GPT',
 | 
			
		||||
    value: 12,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  },
 | 
			
		||||
  13: {
 | 
			
		||||
    key: 13,
 | 
			
		||||
    text: '代理:AIGC2D',
 | 
			
		||||
    value: 13,
 | 
			
		||||
    color: 'primary'
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										1
									
								
								web/berry/src/constants/CommonConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
 | 
			
		||||
							
								
								
									
										27
									
								
								web/berry/src/constants/SnackbarConstants.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
			
		||||
export const snackbarConstants = {
 | 
			
		||||
  Common: {
 | 
			
		||||
    ERROR: {
 | 
			
		||||
      variant: 'error',
 | 
			
		||||
      autoHideDuration: 5000
 | 
			
		||||
    },
 | 
			
		||||
    WARNING: {
 | 
			
		||||
      variant: 'warning',
 | 
			
		||||
      autoHideDuration: 10000
 | 
			
		||||
    },
 | 
			
		||||
    SUCCESS: {
 | 
			
		||||
      variant: 'success',
 | 
			
		||||
      autoHideDuration: 1500
 | 
			
		||||
    },
 | 
			
		||||
    INFO: {
 | 
			
		||||
      variant: 'info',
 | 
			
		||||
      autoHideDuration: 3000
 | 
			
		||||
    },
 | 
			
		||||
    NOTICE: {
 | 
			
		||||
      variant: 'info',
 | 
			
		||||
      autoHideDuration: 20000
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  Mobile: {
 | 
			
		||||
    anchorOrigin: { vertical: 'bottom', horizontal: 'center' }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										3
									
								
								web/berry/src/constants/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
export * from './SnackbarConstants';
 | 
			
		||||
export * from './CommonConstants';
 | 
			
		||||
export * from './ChannelConstants';
 | 
			
		||||
							
								
								
									
										70
									
								
								web/berry/src/contexts/StatusContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,70 @@
 | 
			
		||||
import { useEffect, useCallback, createContext } from "react";
 | 
			
		||||
import { API } from "utils/api";
 | 
			
		||||
import { showNotice, showError } from "utils/common";
 | 
			
		||||
import { SET_SITE_INFO } from "store/actions";
 | 
			
		||||
import { useDispatch } from "react-redux";
 | 
			
		||||
 | 
			
		||||
export const LoadStatusContext = createContext();
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
const StatusProvider = ({ children }) => {
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
 | 
			
		||||
  const loadStatus = useCallback(async () => {
 | 
			
		||||
    const res = await API.get("/api/status");
 | 
			
		||||
    const { success, data } = res.data;
 | 
			
		||||
    let system_name = "";
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (!data.chat_link) {
 | 
			
		||||
        delete data.chat_link;
 | 
			
		||||
      }
 | 
			
		||||
      localStorage.setItem("siteInfo", JSON.stringify(data));
 | 
			
		||||
      localStorage.setItem("quota_per_unit", data.quota_per_unit);
 | 
			
		||||
      localStorage.setItem("display_in_currency", data.display_in_currency);
 | 
			
		||||
      dispatch({ type: SET_SITE_INFO, payload: data });
 | 
			
		||||
      if (
 | 
			
		||||
        data.version !== process.env.REACT_APP_VERSION &&
 | 
			
		||||
        data.version !== "v0.0.0" &&
 | 
			
		||||
        data.version !== "" &&
 | 
			
		||||
        process.env.REACT_APP_VERSION !== ""
 | 
			
		||||
      ) {
 | 
			
		||||
        showNotice(
 | 
			
		||||
          `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (data.system_name) {
 | 
			
		||||
        system_name = data.system_name;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const backupSiteInfo = localStorage.getItem("siteInfo");
 | 
			
		||||
      if (backupSiteInfo) {
 | 
			
		||||
        const data = JSON.parse(backupSiteInfo);
 | 
			
		||||
        if (data.system_name) {
 | 
			
		||||
          system_name = data.system_name;
 | 
			
		||||
        }
 | 
			
		||||
        dispatch({
 | 
			
		||||
          type: SET_SITE_INFO,
 | 
			
		||||
          payload: data,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      showError("无法正常连接至服务器!");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (system_name) {
 | 
			
		||||
      document.title = system_name;
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadStatus().then();
 | 
			
		||||
  }, [loadStatus]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LoadStatusContext.Provider value={loadStatus}>
 | 
			
		||||
      {" "}
 | 
			
		||||
      {children}{" "}
 | 
			
		||||
    </LoadStatusContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default StatusProvider;
 | 
			
		||||
							
								
								
									
										29
									
								
								web/berry/src/contexts/UserContext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,29 @@
 | 
			
		||||
// contexts/User/index.jsx
 | 
			
		||||
import React, { useEffect, useCallback, createContext, useState } from 'react';
 | 
			
		||||
import { LOGIN } from 'store/actions';
 | 
			
		||||
import { useDispatch } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
export const UserContext = createContext();
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
const UserProvider = ({ children }) => {
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const [isUserLoaded, setIsUserLoaded] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const loadUser = useCallback(() => {
 | 
			
		||||
    let user = localStorage.getItem('user');
 | 
			
		||||
    if (user) {
 | 
			
		||||
      let data = JSON.parse(user);
 | 
			
		||||
      dispatch({ type: LOGIN, payload: data });
 | 
			
		||||
    }
 | 
			
		||||
    setIsUserLoaded(true);
 | 
			
		||||
  }, [dispatch]);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadUser();
 | 
			
		||||
  }, [loadUser]);
 | 
			
		||||
 | 
			
		||||
  return <UserContext.Provider value={{ loadUser, isUserLoaded }}> {children} </UserContext.Provider>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default UserProvider;
 | 
			
		||||
							
								
								
									
										13
									
								
								web/berry/src/hooks/useAuth.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,13 @@
 | 
			
		||||
import { isAdmin } from 'utils/common';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
const useAuth = () => {
 | 
			
		||||
  const userIsAdmin = isAdmin();
 | 
			
		||||
 | 
			
		||||
  if (!userIsAdmin) {
 | 
			
		||||
    navigate('/panel/404');
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useAuth;
 | 
			
		||||
							
								
								
									
										78
									
								
								web/berry/src/hooks/useLogin.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,78 @@
 | 
			
		||||
import { API } from 'utils/api';
 | 
			
		||||
import { useDispatch } from 'react-redux';
 | 
			
		||||
import { LOGIN } from 'store/actions';
 | 
			
		||||
import { useNavigate } from 'react-router';
 | 
			
		||||
import { showSuccess } from 'utils/common';
 | 
			
		||||
 | 
			
		||||
const useLogin = () => {
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const login = async (username, password) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.post(`/api/user/login`, {
 | 
			
		||||
        username,
 | 
			
		||||
        password
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
        dispatch({ type: LOGIN, payload: data });
 | 
			
		||||
        navigate('/panel');
 | 
			
		||||
      }
 | 
			
		||||
      return { success, message };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // 请求失败,设置错误信息
 | 
			
		||||
      return { success: false, message: '' };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const githubLogin = async (code, state) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.get(`/api/oauth/github?code=${code}&state=${state}`);
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        if (message === 'bind') {
 | 
			
		||||
          showSuccess('绑定成功!');
 | 
			
		||||
          navigate('/panel');
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch({ type: LOGIN, payload: data });
 | 
			
		||||
          localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
          showSuccess('登录成功!');
 | 
			
		||||
          navigate('/panel');
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return { success, message };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // 请求失败,设置错误信息
 | 
			
		||||
      return { success: false, message: '' };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const wechatLogin = async (code) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.get(`/api/oauth/wechat?code=${code}`);
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        dispatch({ type: LOGIN, payload: data });
 | 
			
		||||
        localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
        showSuccess('登录成功!');
 | 
			
		||||
        navigate('/panel');
 | 
			
		||||
      }
 | 
			
		||||
      return { success, message };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // 请求失败,设置错误信息
 | 
			
		||||
      return { success: false, message: '' };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const logout = async () => {
 | 
			
		||||
    await API.get('/api/user/logout');
 | 
			
		||||
    localStorage.removeItem('user');
 | 
			
		||||
    dispatch({ type: LOGIN, payload: null });
 | 
			
		||||
    navigate('/');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return { login, logout, githubLogin, wechatLogin };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useLogin;
 | 
			
		||||
							
								
								
									
										39
									
								
								web/berry/src/hooks/useRegister.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
import { API } from 'utils/api';
 | 
			
		||||
import { useNavigate } from 'react-router';
 | 
			
		||||
import { showSuccess } from 'utils/common';
 | 
			
		||||
 | 
			
		||||
const useRegister = () => {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const register = async (input, turnstile) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.post(`/api/user/register?turnstile=${turnstile}`, input);
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        showSuccess('注册成功!');
 | 
			
		||||
        navigate('/login');
 | 
			
		||||
      }
 | 
			
		||||
      return { success, message };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // 请求失败,设置错误信息
 | 
			
		||||
      return { success: false, message: '' };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const sendVerificationCode = async (email, turnstile) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.get(`/api/verification?email=${email}&turnstile=${turnstile}`);
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        showSuccess('验证码发送成功,请检查你的邮箱!');
 | 
			
		||||
      }
 | 
			
		||||
      return { success, message };
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      // 请求失败,设置错误信息
 | 
			
		||||
      return { success: false, message: '' };
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return { register, sendVerificationCode };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useRegister;
 | 
			
		||||
							
								
								
									
										18
									
								
								web/berry/src/hooks/useScriptRef.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
import { useEffect, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
// ==============================|| ELEMENT REFERENCE HOOKS  ||============================== //
 | 
			
		||||
 | 
			
		||||
const useScriptRef = () => {
 | 
			
		||||
  const scripted = useRef(true);
 | 
			
		||||
 | 
			
		||||
  useEffect(
 | 
			
		||||
    () => () => {
 | 
			
		||||
      scripted.current = true;
 | 
			
		||||
    },
 | 
			
		||||
    []
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return scripted;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useScriptRef;
 | 
			
		||||
							
								
								
									
										31
									
								
								web/berry/src/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
import { createRoot } from 'react-dom/client';
 | 
			
		||||
 | 
			
		||||
// third party
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom';
 | 
			
		||||
import { Provider } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import * as serviceWorker from 'serviceWorker';
 | 
			
		||||
import App from 'App';
 | 
			
		||||
import { store } from 'store';
 | 
			
		||||
 | 
			
		||||
// style + assets
 | 
			
		||||
import 'assets/scss/style.scss';
 | 
			
		||||
import config from './config';
 | 
			
		||||
 | 
			
		||||
// ==============================|| REACT DOM RENDER  ||============================== //
 | 
			
		||||
 | 
			
		||||
const container = document.getElementById('root');
 | 
			
		||||
const root = createRoot(container); // createRoot(container!) if you use TypeScript
 | 
			
		||||
root.render(
 | 
			
		||||
  <Provider store={store}>
 | 
			
		||||
    <BrowserRouter basename={config.basename}>
 | 
			
		||||
      <App />
 | 
			
		||||
    </BrowserRouter>
 | 
			
		||||
  </Provider>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// If you want your app to work offline and load faster, you can change
 | 
			
		||||
// unregister() to register() below. Note this comes with some pitfalls.
 | 
			
		||||
// Learn more about service workers: https://bit.ly/CRA-PWA
 | 
			
		||||
serviceWorker.register();
 | 
			
		||||
							
								
								
									
										173
									
								
								web/berry/src/layout/MainLayout/Header/ProfileSection/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,173 @@
 | 
			
		||||
import { useState, useRef, useEffect } from 'react';
 | 
			
		||||
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Chip,
 | 
			
		||||
  ClickAwayListener,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItemButton,
 | 
			
		||||
  ListItemIcon,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Paper,
 | 
			
		||||
  Popper,
 | 
			
		||||
  Typography
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import MainCard from 'ui-component/cards/MainCard';
 | 
			
		||||
import Transitions from 'ui-component/extended/Transitions';
 | 
			
		||||
import User1 from 'assets/images/users/user-round.svg';
 | 
			
		||||
import useLogin from 'hooks/useLogin';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import { IconLogout, IconSettings, IconUserScan } from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// ==============================|| PROFILE MENU ||============================== //
 | 
			
		||||
 | 
			
		||||
const ProfileSection = () => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  const customization = useSelector((state) => state.customization);
 | 
			
		||||
  const { logout } = useLogin();
 | 
			
		||||
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  /**
 | 
			
		||||
   * anchorRef is used on different componets and specifying one type leads to other components throwing an error
 | 
			
		||||
   * */
 | 
			
		||||
  const anchorRef = useRef(null);
 | 
			
		||||
  const handleLogout = async () => {
 | 
			
		||||
    logout();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleClose = (event) => {
 | 
			
		||||
    if (anchorRef.current && anchorRef.current.contains(event.target)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setOpen(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleToggle = () => {
 | 
			
		||||
    setOpen((prevOpen) => !prevOpen);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const prevOpen = useRef(open);
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (prevOpen.current === true && open === false) {
 | 
			
		||||
      anchorRef.current.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    prevOpen.current = open;
 | 
			
		||||
  }, [open]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Chip
 | 
			
		||||
        sx={{
 | 
			
		||||
          height: '48px',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          borderRadius: '27px',
 | 
			
		||||
          transition: 'all .2s ease-in-out',
 | 
			
		||||
          borderColor: theme.palette.primary.light,
 | 
			
		||||
          backgroundColor: theme.palette.primary.light,
 | 
			
		||||
          '&[aria-controls="menu-list-grow"], &:hover': {
 | 
			
		||||
            borderColor: theme.palette.primary.main,
 | 
			
		||||
            background: `${theme.palette.primary.main}!important`,
 | 
			
		||||
            color: theme.palette.primary.light,
 | 
			
		||||
            '& svg': {
 | 
			
		||||
              stroke: theme.palette.primary.light
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          '& .MuiChip-label': {
 | 
			
		||||
            lineHeight: 0
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        icon={
 | 
			
		||||
          <Avatar
 | 
			
		||||
            src={User1}
 | 
			
		||||
            sx={{
 | 
			
		||||
              ...theme.typography.mediumAvatar,
 | 
			
		||||
              margin: '8px 0 8px 8px !important',
 | 
			
		||||
              cursor: 'pointer'
 | 
			
		||||
            }}
 | 
			
		||||
            ref={anchorRef}
 | 
			
		||||
            aria-controls={open ? 'menu-list-grow' : undefined}
 | 
			
		||||
            aria-haspopup="true"
 | 
			
		||||
            color="inherit"
 | 
			
		||||
          />
 | 
			
		||||
        }
 | 
			
		||||
        label={<IconSettings stroke={1.5} size="1.5rem" color={theme.palette.primary.main} />}
 | 
			
		||||
        variant="outlined"
 | 
			
		||||
        ref={anchorRef}
 | 
			
		||||
        aria-controls={open ? 'menu-list-grow' : undefined}
 | 
			
		||||
        aria-haspopup="true"
 | 
			
		||||
        onClick={handleToggle}
 | 
			
		||||
        color="primary"
 | 
			
		||||
      />
 | 
			
		||||
      <Popper
 | 
			
		||||
        placement="bottom-end"
 | 
			
		||||
        open={open}
 | 
			
		||||
        anchorEl={anchorRef.current}
 | 
			
		||||
        role={undefined}
 | 
			
		||||
        transition
 | 
			
		||||
        disablePortal
 | 
			
		||||
        popperOptions={{
 | 
			
		||||
          modifiers: [
 | 
			
		||||
            {
 | 
			
		||||
              name: 'offset',
 | 
			
		||||
              options: {
 | 
			
		||||
                offset: [0, 14]
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          ]
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {({ TransitionProps }) => (
 | 
			
		||||
          <Transitions in={open} {...TransitionProps}>
 | 
			
		||||
            <Paper>
 | 
			
		||||
              <ClickAwayListener onClickAway={handleClose}>
 | 
			
		||||
                <MainCard border={false} elevation={16} content={false} boxShadow shadow={theme.shadows[16]}>
 | 
			
		||||
                  <List
 | 
			
		||||
                    component="nav"
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      width: '100%',
 | 
			
		||||
                      maxWidth: 350,
 | 
			
		||||
                      minWidth: 150,
 | 
			
		||||
                      backgroundColor: theme.palette.background.paper,
 | 
			
		||||
                      borderRadius: '10px',
 | 
			
		||||
                      [theme.breakpoints.down('md')]: {
 | 
			
		||||
                        minWidth: '100%'
 | 
			
		||||
                      },
 | 
			
		||||
                      '& .MuiListItemButton-root': {
 | 
			
		||||
                        mt: 0.5
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={() => navigate('/panel/profile')}>
 | 
			
		||||
                      <ListItemIcon>
 | 
			
		||||
                        <IconUserScan stroke={1.5} size="1.3rem" />
 | 
			
		||||
                      </ListItemIcon>
 | 
			
		||||
                      <ListItemText primary={<Typography variant="body2">设置</Typography>} />
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
 | 
			
		||||
                    <ListItemButton sx={{ borderRadius: `${customization.borderRadius}px` }} onClick={handleLogout}>
 | 
			
		||||
                      <ListItemIcon>
 | 
			
		||||
                        <IconLogout stroke={1.5} size="1.3rem" />
 | 
			
		||||
                      </ListItemIcon>
 | 
			
		||||
                      <ListItemText primary={<Typography variant="body2">登出</Typography>} />
 | 
			
		||||
                    </ListItemButton>
 | 
			
		||||
                  </List>
 | 
			
		||||
                </MainCard>
 | 
			
		||||
              </ClickAwayListener>
 | 
			
		||||
            </Paper>
 | 
			
		||||
          </Transitions>
 | 
			
		||||
        )}
 | 
			
		||||
      </Popper>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProfileSection;
 | 
			
		||||
							
								
								
									
										68
									
								
								web/berry/src/layout/MainLayout/Header/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,68 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Avatar, Box, ButtonBase } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import LogoSection from '../LogoSection';
 | 
			
		||||
import ProfileSection from './ProfileSection';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import { IconMenu2 } from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
 | 
			
		||||
 | 
			
		||||
const Header = ({ handleLeftDrawerToggle }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* logo & toggler button */}
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          width: 228,
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          [theme.breakpoints.down('md')]: {
 | 
			
		||||
            width: 'auto'
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Box component="span" sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
 | 
			
		||||
          <LogoSection />
 | 
			
		||||
        </Box>
 | 
			
		||||
        <ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
 | 
			
		||||
          <Avatar
 | 
			
		||||
            variant="rounded"
 | 
			
		||||
            sx={{
 | 
			
		||||
              ...theme.typography.commonAvatar,
 | 
			
		||||
              ...theme.typography.mediumAvatar,
 | 
			
		||||
              transition: 'all .2s ease-in-out',
 | 
			
		||||
              background: theme.palette.secondary.light,
 | 
			
		||||
              color: theme.palette.secondary.dark,
 | 
			
		||||
              '&:hover': {
 | 
			
		||||
                background: theme.palette.secondary.dark,
 | 
			
		||||
                color: theme.palette.secondary.light
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={handleLeftDrawerToggle}
 | 
			
		||||
            color="inherit"
 | 
			
		||||
          >
 | 
			
		||||
            <IconMenu2 stroke={1.5} size="1.3rem" />
 | 
			
		||||
          </Avatar>
 | 
			
		||||
        </ButtonBase>
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <Box sx={{ flexGrow: 1 }} />
 | 
			
		||||
      <Box sx={{ flexGrow: 1 }} />
 | 
			
		||||
 | 
			
		||||
      <ProfileSection />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Header.propTypes = {
 | 
			
		||||
  handleLeftDrawerToggle: PropTypes.func
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Header;
 | 
			
		||||
							
								
								
									
										23
									
								
								web/berry/src/layout/MainLayout/LogoSection/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,23 @@
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { ButtonBase } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import Logo from 'ui-component/Logo';
 | 
			
		||||
import { MENU_OPEN } from 'store/actions';
 | 
			
		||||
 | 
			
		||||
// ==============================|| MAIN LOGO ||============================== //
 | 
			
		||||
 | 
			
		||||
const LogoSection = () => {
 | 
			
		||||
  const defaultId = useSelector((state) => state.customization.defaultId);
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  return (
 | 
			
		||||
    <ButtonBase disableRipple onClick={() => dispatch({ type: MENU_OPEN, id: defaultId })} component={Link} to="/">
 | 
			
		||||
      <Logo />
 | 
			
		||||
    </ButtonBase>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LogoSection;
 | 
			
		||||
							
								
								
									
										130
									
								
								web/berry/src/layout/MainLayout/Sidebar/MenuCard/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,130 @@
 | 
			
		||||
// import PropTypes from 'prop-types';
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { styled, useTheme } from '@mui/material/styles';
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  // Grid,
 | 
			
		||||
  // LinearProgress,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemAvatar,
 | 
			
		||||
  ListItemText,
 | 
			
		||||
  Typography
 | 
			
		||||
  // linearProgressClasses
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
import User1 from 'assets/images/users/user-round.svg';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
// import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined';
 | 
			
		||||
 | 
			
		||||
// styles
 | 
			
		||||
// const BorderLinearProgress = styled(LinearProgress)(({ theme }) => ({
 | 
			
		||||
//   height: 10,
 | 
			
		||||
//   borderRadius: 30,
 | 
			
		||||
//   [`&.${linearProgressClasses.colorPrimary}`]: {
 | 
			
		||||
//     backgroundColor: '#fff'
 | 
			
		||||
//   },
 | 
			
		||||
//   [`& .${linearProgressClasses.bar}`]: {
 | 
			
		||||
//     borderRadius: 5,
 | 
			
		||||
//     backgroundColor: theme.palette.primary.main
 | 
			
		||||
//   }
 | 
			
		||||
// }));
 | 
			
		||||
 | 
			
		||||
const CardStyle = styled(Card)(({ theme }) => ({
 | 
			
		||||
  background: theme.palette.primary.light,
 | 
			
		||||
  marginBottom: '22px',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  '&:after': {
 | 
			
		||||
    content: '""',
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    width: '157px',
 | 
			
		||||
    height: '157px',
 | 
			
		||||
    background: theme.palette.primary[200],
 | 
			
		||||
    borderRadius: '50%',
 | 
			
		||||
    top: '-105px',
 | 
			
		||||
    right: '-96px'
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// ==============================|| PROGRESS BAR WITH LABEL ||============================== //
 | 
			
		||||
 | 
			
		||||
// function LinearProgressWithLabel({ value, ...others }) {
 | 
			
		||||
//   const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
//   return (
 | 
			
		||||
//     <Grid container direction="column" spacing={1} sx={{ mt: 1.5 }}>
 | 
			
		||||
//       <Grid item>
 | 
			
		||||
//         <Grid container justifyContent="space-between">
 | 
			
		||||
//           <Grid item>
 | 
			
		||||
//             <Typography variant="h6" sx={{ color: theme.palette.primary[800] }}>
 | 
			
		||||
//               Progress
 | 
			
		||||
//             </Typography>
 | 
			
		||||
//           </Grid>
 | 
			
		||||
//           <Grid item>
 | 
			
		||||
//             <Typography variant="h6" color="inherit">{`${Math.round(value)}%`}</Typography>
 | 
			
		||||
//           </Grid>
 | 
			
		||||
//         </Grid>
 | 
			
		||||
//       </Grid>
 | 
			
		||||
//       <Grid item>
 | 
			
		||||
//         <BorderLinearProgress variant="determinate" value={value} {...others} />
 | 
			
		||||
//       </Grid>
 | 
			
		||||
//     </Grid>
 | 
			
		||||
//   );
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// LinearProgressWithLabel.propTypes = {
 | 
			
		||||
//   value: PropTypes.number
 | 
			
		||||
// };
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR MENU Card ||============================== //
 | 
			
		||||
 | 
			
		||||
const MenuCard = () => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const account = useSelector((state) => state.account);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CardStyle>
 | 
			
		||||
      <CardContent sx={{ p: 2 }}>
 | 
			
		||||
        <List sx={{ p: 0, m: 0 }}>
 | 
			
		||||
          <ListItem alignItems="flex-start" disableGutters sx={{ p: 0 }}>
 | 
			
		||||
            <ListItemAvatar sx={{ mt: 0 }}>
 | 
			
		||||
              <Avatar
 | 
			
		||||
                variant="rounded"
 | 
			
		||||
                src={User1}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  ...theme.typography.commonAvatar,
 | 
			
		||||
                  ...theme.typography.largeAvatar,
 | 
			
		||||
                  color: theme.palette.primary.main,
 | 
			
		||||
                  border: 'none',
 | 
			
		||||
                  borderColor: theme.palette.primary.main,
 | 
			
		||||
                  background: '#fff',
 | 
			
		||||
                  marginRight: '12px'
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={() => navigate('/panel/profile')}
 | 
			
		||||
              ></Avatar>
 | 
			
		||||
            </ListItemAvatar>
 | 
			
		||||
            <ListItemText
 | 
			
		||||
              sx={{ mt: 0 }}
 | 
			
		||||
              primary={
 | 
			
		||||
                <Typography variant="subtitle1" sx={{ color: theme.palette.primary[800] }}>
 | 
			
		||||
                  {account.user?.username}
 | 
			
		||||
                </Typography>
 | 
			
		||||
              }
 | 
			
		||||
              secondary={<Typography variant="caption"> 欢迎回来 </Typography>}
 | 
			
		||||
            />
 | 
			
		||||
          </ListItem>
 | 
			
		||||
        </List>
 | 
			
		||||
        {/* <LinearProgressWithLabel value={80} /> */}
 | 
			
		||||
      </CardContent>
 | 
			
		||||
    </CardStyle>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MenuCard;
 | 
			
		||||
@@ -0,0 +1,158 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
import { useLocation, useNavigate } from 'react-router';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import NavItem from '../NavItem';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
 | 
			
		||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
 | 
			
		||||
 | 
			
		||||
const NavCollapse = ({ menu, level }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const customization = useSelector((state) => state.customization);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  const [selected, setSelected] = useState(null);
 | 
			
		||||
 | 
			
		||||
  const handleClick = () => {
 | 
			
		||||
    setOpen(!open);
 | 
			
		||||
    setSelected(!selected ? menu.id : null);
 | 
			
		||||
    if (menu?.id !== 'authentication') {
 | 
			
		||||
      navigate(menu.children[0]?.url);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const { pathname } = useLocation();
 | 
			
		||||
  const checkOpenForParent = (child, id) => {
 | 
			
		||||
    child.forEach((item) => {
 | 
			
		||||
      if (item.url === pathname) {
 | 
			
		||||
        setOpen(true);
 | 
			
		||||
        setSelected(id);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // menu collapse for sub-levels
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setOpen(false);
 | 
			
		||||
    setSelected(null);
 | 
			
		||||
    if (menu.children) {
 | 
			
		||||
      menu.children.forEach((item) => {
 | 
			
		||||
        if (item.children?.length) {
 | 
			
		||||
          checkOpenForParent(item.children, menu.id);
 | 
			
		||||
        }
 | 
			
		||||
        if (item.url === pathname) {
 | 
			
		||||
          setSelected(menu.id);
 | 
			
		||||
          setOpen(true);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
  }, [pathname, menu.children]);
 | 
			
		||||
 | 
			
		||||
  // menu collapse & item
 | 
			
		||||
  const menus = menu.children?.map((item) => {
 | 
			
		||||
    switch (item.type) {
 | 
			
		||||
      case 'collapse':
 | 
			
		||||
        return <NavCollapse key={item.id} menu={item} level={level + 1} />;
 | 
			
		||||
      case 'item':
 | 
			
		||||
        return <NavItem key={item.id} item={item} level={level + 1} />;
 | 
			
		||||
      default:
 | 
			
		||||
        return (
 | 
			
		||||
          <Typography key={item.id} variant="h6" color="error" align="center">
 | 
			
		||||
            Menu Items Error
 | 
			
		||||
          </Typography>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const Icon = menu.icon;
 | 
			
		||||
  const menuIcon = menu.icon ? (
 | 
			
		||||
    <Icon strokeWidth={1.5} size="1.3rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <FiberManualRecordIcon
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: selected === menu.id ? 8 : 6,
 | 
			
		||||
        height: selected === menu.id ? 8 : 6
 | 
			
		||||
      }}
 | 
			
		||||
      fontSize={level > 0 ? 'inherit' : 'medium'}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <ListItemButton
 | 
			
		||||
        sx={{
 | 
			
		||||
          borderRadius: `${customization.borderRadius}px`,
 | 
			
		||||
          mb: 0.5,
 | 
			
		||||
          alignItems: 'flex-start',
 | 
			
		||||
          backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
 | 
			
		||||
          py: level > 1 ? 1 : 1.25,
 | 
			
		||||
          pl: `${level * 24}px`
 | 
			
		||||
        }}
 | 
			
		||||
        selected={selected === menu.id}
 | 
			
		||||
        onClick={handleClick}
 | 
			
		||||
      >
 | 
			
		||||
        <ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
 | 
			
		||||
        <ListItemText
 | 
			
		||||
          primary={
 | 
			
		||||
            <Typography variant={selected === menu.id ? 'h5' : 'body1'} color="inherit" sx={{ my: 'auto' }}>
 | 
			
		||||
              {menu.title}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          }
 | 
			
		||||
          secondary={
 | 
			
		||||
            menu.caption && (
 | 
			
		||||
              <Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
 | 
			
		||||
                {menu.caption}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        />
 | 
			
		||||
        {open ? (
 | 
			
		||||
          <IconChevronUp stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <IconChevronDown stroke={1.5} size="1rem" style={{ marginTop: 'auto', marginBottom: 'auto' }} />
 | 
			
		||||
        )}
 | 
			
		||||
      </ListItemButton>
 | 
			
		||||
      <Collapse in={open} timeout="auto" unmountOnExit>
 | 
			
		||||
        <List
 | 
			
		||||
          component="div"
 | 
			
		||||
          disablePadding
 | 
			
		||||
          sx={{
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            '&:after': {
 | 
			
		||||
              content: "''",
 | 
			
		||||
              position: 'absolute',
 | 
			
		||||
              left: '32px',
 | 
			
		||||
              top: 0,
 | 
			
		||||
              height: '100%',
 | 
			
		||||
              width: '1px',
 | 
			
		||||
              opacity: 1,
 | 
			
		||||
              background: theme.palette.primary.light
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {menus}
 | 
			
		||||
        </List>
 | 
			
		||||
      </Collapse>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
NavCollapse.propTypes = {
 | 
			
		||||
  menu: PropTypes.object,
 | 
			
		||||
  level: PropTypes.number
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default NavCollapse;
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Divider, List, Typography } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import NavItem from '../NavItem';
 | 
			
		||||
import NavCollapse from '../NavCollapse';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
 | 
			
		||||
 | 
			
		||||
const NavGroup = ({ item }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  // menu list collapse & items
 | 
			
		||||
  const items = item.children?.map((menu) => {
 | 
			
		||||
    switch (menu.type) {
 | 
			
		||||
      case 'collapse':
 | 
			
		||||
        return <NavCollapse key={menu.id} menu={menu} level={1} />;
 | 
			
		||||
      case 'item':
 | 
			
		||||
        return <NavItem key={menu.id} item={menu} level={1} />;
 | 
			
		||||
      default:
 | 
			
		||||
        return (
 | 
			
		||||
          <Typography key={menu.id} variant="h6" color="error" align="center">
 | 
			
		||||
            Menu Items Error
 | 
			
		||||
          </Typography>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <List
 | 
			
		||||
        subheader={
 | 
			
		||||
          item.title && (
 | 
			
		||||
            <Typography variant="caption" sx={{ ...theme.typography.menuCaption }} display="block" gutterBottom>
 | 
			
		||||
              {item.title}
 | 
			
		||||
              {item.caption && (
 | 
			
		||||
                <Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
 | 
			
		||||
                  {item.caption}
 | 
			
		||||
                </Typography>
 | 
			
		||||
              )}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        {items}
 | 
			
		||||
      </List>
 | 
			
		||||
 | 
			
		||||
      {/* group divider */}
 | 
			
		||||
      <Divider sx={{ mt: 0.25, mb: 1.25 }} />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
NavGroup.propTypes = {
 | 
			
		||||
  item: PropTypes.object
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default NavGroup;
 | 
			
		||||
@@ -0,0 +1,115 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef, useEffect } from 'react';
 | 
			
		||||
import { Link, useLocation } from 'react-router-dom';
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import { MENU_OPEN, SET_MENU } from 'store/actions';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
 | 
			
		||||
 | 
			
		||||
const NavItem = ({ item, level }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const { pathname } = useLocation();
 | 
			
		||||
  const customization = useSelector((state) => state.customization);
 | 
			
		||||
  const matchesSM = useMediaQuery(theme.breakpoints.down('lg'));
 | 
			
		||||
 | 
			
		||||
  const Icon = item.icon;
 | 
			
		||||
  const itemIcon = item?.icon ? (
 | 
			
		||||
    <Icon stroke={1.5} size="1.3rem" />
 | 
			
		||||
  ) : (
 | 
			
		||||
    <FiberManualRecordIcon
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
 | 
			
		||||
        height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
 | 
			
		||||
      }}
 | 
			
		||||
      fontSize={level > 0 ? 'inherit' : 'medium'}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  let itemTarget = '_self';
 | 
			
		||||
  if (item.target) {
 | 
			
		||||
    itemTarget = '_blank';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  let listItemProps = {
 | 
			
		||||
    component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />)
 | 
			
		||||
  };
 | 
			
		||||
  if (item?.external) {
 | 
			
		||||
    listItemProps = { component: 'a', href: item.url, target: itemTarget };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const itemHandler = (id) => {
 | 
			
		||||
    dispatch({ type: MENU_OPEN, id });
 | 
			
		||||
    if (matchesSM) dispatch({ type: SET_MENU, opened: false });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // active menu item on page load
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const currentIndex = document.location.pathname
 | 
			
		||||
      .toString()
 | 
			
		||||
      .split('/')
 | 
			
		||||
      .findIndex((id) => id === item.id);
 | 
			
		||||
    if (currentIndex > -1) {
 | 
			
		||||
      dispatch({ type: MENU_OPEN, id: item.id });
 | 
			
		||||
    }
 | 
			
		||||
    // eslint-disable-next-line
 | 
			
		||||
  }, [pathname]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ListItemButton
 | 
			
		||||
      {...listItemProps}
 | 
			
		||||
      disabled={item.disabled}
 | 
			
		||||
      sx={{
 | 
			
		||||
        borderRadius: `${customization.borderRadius}px`,
 | 
			
		||||
        mb: 0.5,
 | 
			
		||||
        alignItems: 'flex-start',
 | 
			
		||||
        backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
 | 
			
		||||
        py: level > 1 ? 1 : 1.25,
 | 
			
		||||
        pl: `${level * 24}px`
 | 
			
		||||
      }}
 | 
			
		||||
      selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
 | 
			
		||||
      onClick={() => itemHandler(item.id)}
 | 
			
		||||
    >
 | 
			
		||||
      <ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
 | 
			
		||||
      <ListItemText
 | 
			
		||||
        primary={
 | 
			
		||||
          <Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color="inherit">
 | 
			
		||||
            {item.title}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        }
 | 
			
		||||
        secondary={
 | 
			
		||||
          item.caption && (
 | 
			
		||||
            <Typography variant="caption" sx={{ ...theme.typography.subMenuCaption }} display="block" gutterBottom>
 | 
			
		||||
              {item.caption}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      {item.chip && (
 | 
			
		||||
        <Chip
 | 
			
		||||
          color={item.chip.color}
 | 
			
		||||
          variant={item.chip.variant}
 | 
			
		||||
          size={item.chip.size}
 | 
			
		||||
          label={item.chip.label}
 | 
			
		||||
          avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </ListItemButton>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
NavItem.propTypes = {
 | 
			
		||||
  item: PropTypes.object,
 | 
			
		||||
  level: PropTypes.number
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default NavItem;
 | 
			
		||||
							
								
								
									
										36
									
								
								web/berry/src/layout/MainLayout/Sidebar/MenuList/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Typography } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import NavGroup from './NavGroup';
 | 
			
		||||
import menuItem from 'menu-items';
 | 
			
		||||
import { isAdmin } from 'utils/common';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR MENU LIST ||============================== //
 | 
			
		||||
const MenuList = () => {
 | 
			
		||||
  const userIsAdmin = isAdmin();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {menuItem.items.map((item) => {
 | 
			
		||||
        if (item.type !== 'group') {
 | 
			
		||||
          return (
 | 
			
		||||
            <Typography key={item.id} variant="h6" color="error" align="center">
 | 
			
		||||
              Menu Items Error
 | 
			
		||||
            </Typography>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const filteredChildren = item.children.filter((child) => !child.isAdmin || userIsAdmin);
 | 
			
		||||
 | 
			
		||||
        if (filteredChildren.length === 0) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return <NavGroup key={item.id} item={{ ...item, children: filteredChildren }} />;
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MenuList;
 | 
			
		||||
							
								
								
									
										94
									
								
								web/berry/src/layout/MainLayout/Sidebar/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,94 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Box, Chip, Drawer, Stack, useMediaQuery } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// third-party
 | 
			
		||||
import PerfectScrollbar from 'react-perfect-scrollbar';
 | 
			
		||||
import { BrowserView, MobileView } from 'react-device-detect';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import MenuList from './MenuList';
 | 
			
		||||
import LogoSection from '../LogoSection';
 | 
			
		||||
import MenuCard from './MenuCard';
 | 
			
		||||
import { drawerWidth } from 'store/constant';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SIDEBAR DRAWER ||============================== //
 | 
			
		||||
 | 
			
		||||
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const matchUpMd = useMediaQuery(theme.breakpoints.up('md'));
 | 
			
		||||
 | 
			
		||||
  const drawer = (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box sx={{ display: { xs: 'block', md: 'none' } }}>
 | 
			
		||||
        <Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
 | 
			
		||||
          <LogoSection />
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <BrowserView>
 | 
			
		||||
        <PerfectScrollbar
 | 
			
		||||
          component="div"
 | 
			
		||||
          style={{
 | 
			
		||||
            height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
 | 
			
		||||
            paddingLeft: '16px',
 | 
			
		||||
            paddingRight: '16px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <MenuList />
 | 
			
		||||
          <MenuCard />
 | 
			
		||||
          <Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
 | 
			
		||||
            <Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </PerfectScrollbar>
 | 
			
		||||
      </BrowserView>
 | 
			
		||||
      <MobileView>
 | 
			
		||||
        <Box sx={{ px: 2 }}>
 | 
			
		||||
          <MenuList />
 | 
			
		||||
          <MenuCard />
 | 
			
		||||
          <Stack direction="row" justifyContent="center" sx={{ mb: 2 }}>
 | 
			
		||||
            <Chip label={process.env.REACT_APP_VERSION} disabled chipcolor="secondary" size="small" sx={{ cursor: 'pointer' }} />
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </MobileView>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const container = window !== undefined ? () => window.document.body : undefined;
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box component="nav" sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label="mailbox folders">
 | 
			
		||||
      <Drawer
 | 
			
		||||
        container={container}
 | 
			
		||||
        variant={matchUpMd ? 'persistent' : 'temporary'}
 | 
			
		||||
        anchor="left"
 | 
			
		||||
        open={drawerOpen}
 | 
			
		||||
        onClose={drawerToggle}
 | 
			
		||||
        sx={{
 | 
			
		||||
          '& .MuiDrawer-paper': {
 | 
			
		||||
            width: drawerWidth,
 | 
			
		||||
            background: theme.palette.background.default,
 | 
			
		||||
            color: theme.palette.text.primary,
 | 
			
		||||
            borderRight: 'none',
 | 
			
		||||
            [theme.breakpoints.up('md')]: {
 | 
			
		||||
              top: '88px'
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
        ModalProps={{ keepMounted: true }}
 | 
			
		||||
        color="inherit"
 | 
			
		||||
      >
 | 
			
		||||
        {drawer}
 | 
			
		||||
      </Drawer>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Sidebar.propTypes = {
 | 
			
		||||
  drawerOpen: PropTypes.bool,
 | 
			
		||||
  drawerToggle: PropTypes.func,
 | 
			
		||||
  window: PropTypes.object
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Sidebar;
 | 
			
		||||
							
								
								
									
										103
									
								
								web/berry/src/layout/MainLayout/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,103 @@
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux';
 | 
			
		||||
import { Outlet } from 'react-router-dom';
 | 
			
		||||
import AuthGuard from 'utils/route-guard/AuthGuard';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { styled, useTheme } from '@mui/material/styles';
 | 
			
		||||
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material';
 | 
			
		||||
import AdminContainer from 'ui-component/AdminContainer';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import Breadcrumbs from 'ui-component/extended/Breadcrumbs';
 | 
			
		||||
import Header from './Header';
 | 
			
		||||
import Sidebar from './Sidebar';
 | 
			
		||||
import navigation from 'menu-items';
 | 
			
		||||
import { drawerWidth } from 'store/constant';
 | 
			
		||||
import { SET_MENU } from 'store/actions';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import { IconChevronRight } from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// styles
 | 
			
		||||
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
 | 
			
		||||
  ...theme.typography.mainContent,
 | 
			
		||||
  borderBottomLeftRadius: 0,
 | 
			
		||||
  borderBottomRightRadius: 0,
 | 
			
		||||
  transition: theme.transitions.create(
 | 
			
		||||
    'margin',
 | 
			
		||||
    open
 | 
			
		||||
      ? {
 | 
			
		||||
          easing: theme.transitions.easing.easeOut,
 | 
			
		||||
          duration: theme.transitions.duration.enteringScreen
 | 
			
		||||
        }
 | 
			
		||||
      : {
 | 
			
		||||
          easing: theme.transitions.easing.sharp,
 | 
			
		||||
          duration: theme.transitions.duration.leavingScreen
 | 
			
		||||
        }
 | 
			
		||||
  ),
 | 
			
		||||
  [theme.breakpoints.up('md')]: {
 | 
			
		||||
    marginLeft: open ? 0 : -(drawerWidth - 20),
 | 
			
		||||
    width: `calc(100% - ${drawerWidth}px)`
 | 
			
		||||
  },
 | 
			
		||||
  [theme.breakpoints.down('md')]: {
 | 
			
		||||
    marginLeft: '20px',
 | 
			
		||||
    width: `calc(100% - ${drawerWidth}px)`,
 | 
			
		||||
    padding: '16px'
 | 
			
		||||
  },
 | 
			
		||||
  [theme.breakpoints.down('sm')]: {
 | 
			
		||||
    marginLeft: '10px',
 | 
			
		||||
    width: `calc(100% - ${drawerWidth}px)`,
 | 
			
		||||
    padding: '16px',
 | 
			
		||||
    marginRight: '10px'
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
// ==============================|| MAIN LAYOUT ||============================== //
 | 
			
		||||
 | 
			
		||||
const MainLayout = () => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const matchDownMd = useMediaQuery(theme.breakpoints.down('md'));
 | 
			
		||||
  // Handle left drawer
 | 
			
		||||
  const leftDrawerOpened = useSelector((state) => state.customization.opened);
 | 
			
		||||
  const dispatch = useDispatch();
 | 
			
		||||
  const handleLeftDrawerToggle = () => {
 | 
			
		||||
    dispatch({ type: SET_MENU, opened: !leftDrawerOpened });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box sx={{ display: 'flex' }}>
 | 
			
		||||
      <CssBaseline />
 | 
			
		||||
      {/* header */}
 | 
			
		||||
      <AppBar
 | 
			
		||||
        enableColorOnDark
 | 
			
		||||
        position="fixed"
 | 
			
		||||
        color="inherit"
 | 
			
		||||
        elevation={0}
 | 
			
		||||
        sx={{
 | 
			
		||||
          bgcolor: theme.palette.background.default,
 | 
			
		||||
          transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Toolbar>
 | 
			
		||||
          <Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
 | 
			
		||||
        </Toolbar>
 | 
			
		||||
      </AppBar>
 | 
			
		||||
 | 
			
		||||
      {/* drawer */}
 | 
			
		||||
      <Sidebar drawerOpen={!matchDownMd ? leftDrawerOpened : !leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
 | 
			
		||||
 | 
			
		||||
      {/* main content */}
 | 
			
		||||
      <Main theme={theme} open={leftDrawerOpened}>
 | 
			
		||||
        {/* breadcrumb */}
 | 
			
		||||
        <Breadcrumbs separator={IconChevronRight} navigation={navigation} icon title rightAlign />
 | 
			
		||||
        <AuthGuard>
 | 
			
		||||
          <AdminContainer>
 | 
			
		||||
            <Outlet />
 | 
			
		||||
          </AdminContainer>
 | 
			
		||||
        </AuthGuard>
 | 
			
		||||
      </Main>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MainLayout;
 | 
			
		||||
							
								
								
									
										75
									
								
								web/berry/src/layout/MinimalLayout/Header/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,75 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from "@mui/material/styles";
 | 
			
		||||
import { Box, Button, Stack } from "@mui/material";
 | 
			
		||||
import LogoSection from "layout/MainLayout/LogoSection";
 | 
			
		||||
import { Link } from "react-router-dom";
 | 
			
		||||
import { useLocation } from "react-router-dom";
 | 
			
		||||
import { useSelector } from "react-redux";
 | 
			
		||||
 | 
			
		||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
 | 
			
		||||
 | 
			
		||||
const Header = () => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const { pathname } = useLocation();
 | 
			
		||||
  const account = useSelector((state) => state.account);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          width: 228,
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          [theme.breakpoints.down("md")]: {
 | 
			
		||||
            width: "auto",
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Box component="span" sx={{ flexGrow: 1 }}>
 | 
			
		||||
          <LogoSection />
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <Box sx={{ flexGrow: 1 }} />
 | 
			
		||||
      <Box sx={{ flexGrow: 1 }} />
 | 
			
		||||
      <Stack spacing={2} direction="row">
 | 
			
		||||
        <Button
 | 
			
		||||
          component={Link}
 | 
			
		||||
          variant="text"
 | 
			
		||||
          to="/"
 | 
			
		||||
          color={pathname === "/" ? "primary" : "inherit"}
 | 
			
		||||
        >
 | 
			
		||||
          首页
 | 
			
		||||
        </Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          component={Link}
 | 
			
		||||
          variant="text"
 | 
			
		||||
          to="/about"
 | 
			
		||||
          color={pathname === "/about" ? "primary" : "inherit"}
 | 
			
		||||
        >
 | 
			
		||||
          关于
 | 
			
		||||
        </Button>
 | 
			
		||||
        {account.user ? (
 | 
			
		||||
          <Button
 | 
			
		||||
            component={Link}
 | 
			
		||||
            variant="contained"
 | 
			
		||||
            to="/panel"
 | 
			
		||||
            color="primary"
 | 
			
		||||
          >
 | 
			
		||||
            控制台
 | 
			
		||||
          </Button>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Button
 | 
			
		||||
            component={Link}
 | 
			
		||||
            variant="contained"
 | 
			
		||||
            to="/login"
 | 
			
		||||
            color="primary"
 | 
			
		||||
          >
 | 
			
		||||
            登入
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </Stack>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Header;
 | 
			
		||||
							
								
								
									
										39
									
								
								web/berry/src/layout/MinimalLayout/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
import { Outlet } from 'react-router-dom';
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { AppBar, Box, CssBaseline, Toolbar } from '@mui/material';
 | 
			
		||||
import Header from './Header';
 | 
			
		||||
import Footer from 'ui-component/Footer';
 | 
			
		||||
 | 
			
		||||
// ==============================|| MINIMAL LAYOUT ||============================== //
 | 
			
		||||
 | 
			
		||||
const MinimalLayout = () => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box sx={{ display: 'flex', flexDirection: 'column', minHeight: '100vh' }}>
 | 
			
		||||
      <CssBaseline />
 | 
			
		||||
      <AppBar
 | 
			
		||||
        enableColorOnDark
 | 
			
		||||
        position="fixed"
 | 
			
		||||
        color="inherit"
 | 
			
		||||
        elevation={0}
 | 
			
		||||
        sx={{
 | 
			
		||||
          bgcolor: theme.palette.background.default,
 | 
			
		||||
          flex: 'none'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Toolbar>
 | 
			
		||||
          <Header />
 | 
			
		||||
        </Toolbar>
 | 
			
		||||
      </AppBar>
 | 
			
		||||
      <Box sx={{ flex: '1 1 auto', overflow: 'auto' }} paddingTop={'64px'}>
 | 
			
		||||
        <Outlet />
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Box sx={{ flex: 'none' }}>
 | 
			
		||||
        <Footer />
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MinimalLayout;
 | 
			
		||||
							
								
								
									
										39
									
								
								web/berry/src/layout/NavMotion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { motion } from 'framer-motion';
 | 
			
		||||
 | 
			
		||||
// ==============================|| ANIMATION FOR CONTENT ||============================== //
 | 
			
		||||
 | 
			
		||||
const NavMotion = ({ children }) => {
 | 
			
		||||
  const motionVariants = {
 | 
			
		||||
    initial: {
 | 
			
		||||
      opacity: 0,
 | 
			
		||||
      scale: 0.99
 | 
			
		||||
    },
 | 
			
		||||
    in: {
 | 
			
		||||
      opacity: 1,
 | 
			
		||||
      scale: 1
 | 
			
		||||
    },
 | 
			
		||||
    out: {
 | 
			
		||||
      opacity: 0,
 | 
			
		||||
      scale: 1.01
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const motionTransition = {
 | 
			
		||||
    type: 'tween',
 | 
			
		||||
    ease: 'anticipate',
 | 
			
		||||
    duration: 0.4
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <motion.div initial="initial" animate="in" exit="out" variants={motionVariants} transition={motionTransition}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </motion.div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
NavMotion.propTypes = {
 | 
			
		||||
  children: PropTypes.node
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default NavMotion;
 | 
			
		||||
							
								
								
									
										26
									
								
								web/berry/src/layout/NavigationScroll.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useEffect } from 'react';
 | 
			
		||||
import { useLocation } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
// ==============================|| NAVIGATION SCROLL TO TOP ||============================== //
 | 
			
		||||
 | 
			
		||||
const NavigationScroll = ({ children }) => {
 | 
			
		||||
  const location = useLocation();
 | 
			
		||||
  const { pathname } = location;
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    window.scrollTo({
 | 
			
		||||
      top: 0,
 | 
			
		||||
      left: 0,
 | 
			
		||||
      behavior: 'smooth'
 | 
			
		||||
    });
 | 
			
		||||
  }, [pathname]);
 | 
			
		||||
 | 
			
		||||
  return children || null;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
NavigationScroll.propTypes = {
 | 
			
		||||
  children: PropTypes.node
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default NavigationScroll;
 | 
			
		||||
							
								
								
									
										18
									
								
								web/berry/src/menu-items/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
import panel from './panel';
 | 
			
		||||
 | 
			
		||||
// ==============================|| MENU ITEMS ||============================== //
 | 
			
		||||
 | 
			
		||||
const menuItems = {
 | 
			
		||||
  items: [panel],
 | 
			
		||||
  urlMap: {}
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Initialize urlMap
 | 
			
		||||
menuItems.urlMap = menuItems.items.reduce((map, item) => {
 | 
			
		||||
  item.children.forEach((child) => {
 | 
			
		||||
    map[child.url] = child;
 | 
			
		||||
  });
 | 
			
		||||
  return map;
 | 
			
		||||
}, {});
 | 
			
		||||
 | 
			
		||||
export default menuItems;
 | 
			
		||||
							
								
								
									
										104
									
								
								web/berry/src/menu-items/panel.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,104 @@
 | 
			
		||||
// assets
 | 
			
		||||
import {
 | 
			
		||||
  IconDashboard,
 | 
			
		||||
  IconSitemap,
 | 
			
		||||
  IconArticle,
 | 
			
		||||
  IconCoin,
 | 
			
		||||
  IconAdjustments,
 | 
			
		||||
  IconKey,
 | 
			
		||||
  IconGardenCart,
 | 
			
		||||
  IconUser,
 | 
			
		||||
  IconUserScan
 | 
			
		||||
} from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// constant
 | 
			
		||||
const icons = { IconDashboard, IconSitemap, IconArticle, IconCoin, IconAdjustments, IconKey, IconGardenCart, IconUser, IconUserScan };
 | 
			
		||||
 | 
			
		||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
 | 
			
		||||
 | 
			
		||||
const panel = {
 | 
			
		||||
  id: 'panel',
 | 
			
		||||
  type: 'group',
 | 
			
		||||
  children: [
 | 
			
		||||
    {
 | 
			
		||||
      id: 'dashboard',
 | 
			
		||||
      title: '总览',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/dashboard',
 | 
			
		||||
      icon: icons.IconDashboard,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'channel',
 | 
			
		||||
      title: '渠道',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/channel',
 | 
			
		||||
      icon: icons.IconSitemap,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'token',
 | 
			
		||||
      title: '令牌',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/token',
 | 
			
		||||
      icon: icons.IconKey,
 | 
			
		||||
      breadcrumbs: false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'log',
 | 
			
		||||
      title: '日志',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/log',
 | 
			
		||||
      icon: icons.IconArticle,
 | 
			
		||||
      breadcrumbs: false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'redemption',
 | 
			
		||||
      title: '兑换',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/redemption',
 | 
			
		||||
      icon: icons.IconCoin,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'topup',
 | 
			
		||||
      title: '充值',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/topup',
 | 
			
		||||
      icon: icons.IconGardenCart,
 | 
			
		||||
      breadcrumbs: false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'user',
 | 
			
		||||
      title: '用户',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/user',
 | 
			
		||||
      icon: icons.IconUser,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'profile',
 | 
			
		||||
      title: '我的',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/profile',
 | 
			
		||||
      icon: icons.IconUserScan,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: true
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      id: 'setting',
 | 
			
		||||
      title: '设置',
 | 
			
		||||
      type: 'item',
 | 
			
		||||
      url: '/panel/setting',
 | 
			
		||||
      icon: icons.IconAdjustments,
 | 
			
		||||
      breadcrumbs: false,
 | 
			
		||||
      isAdmin: true
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default panel;
 | 
			
		||||
							
								
								
									
										73
									
								
								web/berry/src/routes/MainRoutes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,73 @@
 | 
			
		||||
import { lazy } from 'react';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import MainLayout from 'layout/MainLayout';
 | 
			
		||||
import Loadable from 'ui-component/Loadable';
 | 
			
		||||
 | 
			
		||||
const Channel = Loadable(lazy(() => import('views/Channel')));
 | 
			
		||||
const Log = Loadable(lazy(() => import('views/Log')));
 | 
			
		||||
const Redemption = Loadable(lazy(() => import('views/Redemption')));
 | 
			
		||||
const Setting = Loadable(lazy(() => import('views/Setting')));
 | 
			
		||||
const Token = Loadable(lazy(() => import('views/Token')));
 | 
			
		||||
const Topup = Loadable(lazy(() => import('views/Topup')));
 | 
			
		||||
const User = Loadable(lazy(() => import('views/User')));
 | 
			
		||||
const Profile = Loadable(lazy(() => import('views/Profile')));
 | 
			
		||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
 | 
			
		||||
 | 
			
		||||
// dashboard routing
 | 
			
		||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
 | 
			
		||||
 | 
			
		||||
// ==============================|| MAIN ROUTING ||============================== //
 | 
			
		||||
 | 
			
		||||
const MainRoutes = {
 | 
			
		||||
  path: '/panel',
 | 
			
		||||
  element: <MainLayout />,
 | 
			
		||||
  children: [
 | 
			
		||||
    {
 | 
			
		||||
      path: '',
 | 
			
		||||
      element: <Dashboard />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'dashboard',
 | 
			
		||||
      element: <Dashboard />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'channel',
 | 
			
		||||
      element: <Channel />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'log',
 | 
			
		||||
      element: <Log />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'redemption',
 | 
			
		||||
      element: <Redemption />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'setting',
 | 
			
		||||
      element: <Setting />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'token',
 | 
			
		||||
      element: <Token />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'topup',
 | 
			
		||||
      element: <Topup />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'user',
 | 
			
		||||
      element: <User />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: 'profile',
 | 
			
		||||
      element: <Profile />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '404',
 | 
			
		||||
      element: <NotFoundView />
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MainRoutes;
 | 
			
		||||
							
								
								
									
										58
									
								
								web/berry/src/routes/OtherRoutes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,58 @@
 | 
			
		||||
import { lazy } from 'react';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import Loadable from 'ui-component/Loadable';
 | 
			
		||||
import MinimalLayout from 'layout/MinimalLayout';
 | 
			
		||||
 | 
			
		||||
// login option 3 routing
 | 
			
		||||
const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')));
 | 
			
		||||
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
 | 
			
		||||
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
 | 
			
		||||
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
 | 
			
		||||
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
 | 
			
		||||
const Home = Loadable(lazy(() => import('views/Home')));
 | 
			
		||||
const About = Loadable(lazy(() => import('views/About')));
 | 
			
		||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
 | 
			
		||||
 | 
			
		||||
// ==============================|| AUTHENTICATION ROUTING ||============================== //
 | 
			
		||||
 | 
			
		||||
const OtherRoutes = {
 | 
			
		||||
  path: '/',
 | 
			
		||||
  element: <MinimalLayout />,
 | 
			
		||||
  children: [
 | 
			
		||||
    {
 | 
			
		||||
      path: '',
 | 
			
		||||
      element: <Home />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/about',
 | 
			
		||||
      element: <About />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/login',
 | 
			
		||||
      element: <AuthLogin />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/register',
 | 
			
		||||
      element: <AuthRegister />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/reset',
 | 
			
		||||
      element: <ForgetPassword />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/user/reset',
 | 
			
		||||
      element: <ResetPassword />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/oauth/github',
 | 
			
		||||
      element: <GitHubOAuth />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      path: '/404',
 | 
			
		||||
      element: <NotFoundView />
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default OtherRoutes;
 | 
			
		||||
							
								
								
									
										11
									
								
								web/berry/src/routes/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
			
		||||
import { useRoutes } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
// routes
 | 
			
		||||
import MainRoutes from './MainRoutes';
 | 
			
		||||
import OtherRoutes from './OtherRoutes';
 | 
			
		||||
 | 
			
		||||
// ==============================|| ROUTING RENDER ||============================== //
 | 
			
		||||
 | 
			
		||||
export default function ThemeRoutes() {
 | 
			
		||||
  return useRoutes([MainRoutes, OtherRoutes]);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										128
									
								
								web/berry/src/serviceWorker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
			
		||||
// This optional code is used to register a service worker.
 | 
			
		||||
// register() is not called by default.
 | 
			
		||||
 | 
			
		||||
// This lets the app load faster on subsequent visits in production, and gives
 | 
			
		||||
// it offline capabilities. However, it also means that developers (and users)
 | 
			
		||||
// will only see deployed updates on subsequent visits to a page, after all the
 | 
			
		||||
// existing tabs open on the page have been closed, since previously cached
 | 
			
		||||
// resources are updated in the background.
 | 
			
		||||
 | 
			
		||||
// To learn more about the benefits of this model and instructions on how to
 | 
			
		||||
// opt-in, read https://bit.ly/CRA-PWA
 | 
			
		||||
 | 
			
		||||
const isLocalhost = Boolean(
 | 
			
		||||
  window.location.hostname === 'localhost' ||
 | 
			
		||||
    // [::1] is the IPv6 localhost address.
 | 
			
		||||
    window.location.hostname === '[::1]' ||
 | 
			
		||||
    // 127.0.0.0/8 are considered localhost for IPv4.
 | 
			
		||||
    window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
function registerValidSW(swUrl, config) {
 | 
			
		||||
  navigator.serviceWorker
 | 
			
		||||
    .register(swUrl)
 | 
			
		||||
    .then((registration) => {
 | 
			
		||||
      registration.onupdatefound = () => {
 | 
			
		||||
        const installingWorker = registration.installing;
 | 
			
		||||
        if (installingWorker == null) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        installingWorker.onstatechange = () => {
 | 
			
		||||
          if (installingWorker.state === 'installed') {
 | 
			
		||||
            if (navigator.serviceWorker.controller) {
 | 
			
		||||
              // At this point, the updated precached content has been fetched,
 | 
			
		||||
              // but the previous service worker will still serve the older
 | 
			
		||||
              // content until all client tabs are closed.
 | 
			
		||||
              console.log('New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA.');
 | 
			
		||||
 | 
			
		||||
              // Execute callback
 | 
			
		||||
              if (config && config.onUpdate) {
 | 
			
		||||
                config.onUpdate(registration);
 | 
			
		||||
              }
 | 
			
		||||
            } else {
 | 
			
		||||
              // At this point, everything has been precached.
 | 
			
		||||
              // It's the perfect time to display a
 | 
			
		||||
              // "Content is cached for offline use." message.
 | 
			
		||||
              console.log('Content is cached for offline use.');
 | 
			
		||||
 | 
			
		||||
              // Execute callback
 | 
			
		||||
              if (config && config.onSuccess) {
 | 
			
		||||
                config.onSuccess(registration);
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        };
 | 
			
		||||
      };
 | 
			
		||||
    })
 | 
			
		||||
    .catch((error) => {
 | 
			
		||||
      console.error('Error during service worker registration:', error);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function checkValidServiceWorker(swUrl, config) {
 | 
			
		||||
  // Check if the service worker can be found. If it can't reload the page.
 | 
			
		||||
  fetch(swUrl, {
 | 
			
		||||
    headers: { 'Service-Worker': 'script' }
 | 
			
		||||
  })
 | 
			
		||||
    .then((response) => {
 | 
			
		||||
      // Ensure service worker exists, and that we really are getting a JS file.
 | 
			
		||||
      const contentType = response.headers.get('content-type');
 | 
			
		||||
      if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
 | 
			
		||||
        // No service worker found. Probably a different app. Reload the page.
 | 
			
		||||
        navigator.serviceWorker.ready.then((registration) => {
 | 
			
		||||
          registration.unregister().then(() => {
 | 
			
		||||
            window.location.reload();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Service worker found. Proceed as normal.
 | 
			
		||||
        registerValidSW(swUrl, config);
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(() => {
 | 
			
		||||
      console.log('No internet connection found. App is running in offline mode.');
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function register(config) {
 | 
			
		||||
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
 | 
			
		||||
    // The URL constructor is available in all browsers that support SW.
 | 
			
		||||
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
 | 
			
		||||
    if (publicUrl.origin !== window.location.origin) {
 | 
			
		||||
      // Our service worker won't work if PUBLIC_URL is on a different origin
 | 
			
		||||
      // from what our page is served on. This might happen if a CDN is used to
 | 
			
		||||
      // serve assets; see https://github.com/facebook/create-react-app/issues/2374
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.addEventListener('load', () => {
 | 
			
		||||
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
 | 
			
		||||
 | 
			
		||||
      if (isLocalhost) {
 | 
			
		||||
        // This is running on localhost. Let's check if a service worker still exists or not.
 | 
			
		||||
        checkValidServiceWorker(swUrl, config);
 | 
			
		||||
 | 
			
		||||
        // Add some additional logging to localhost, pointing developers to the
 | 
			
		||||
        // service worker/PWA documentation.
 | 
			
		||||
        navigator.serviceWorker.ready.then(() => {
 | 
			
		||||
          console.log('This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA');
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        // Is not localhost. Just register service worker
 | 
			
		||||
        registerValidSW(swUrl, config);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function unregister() {
 | 
			
		||||
  if ('serviceWorker' in navigator) {
 | 
			
		||||
    navigator.serviceWorker.ready
 | 
			
		||||
      .then((registration) => {
 | 
			
		||||
        registration.unregister();
 | 
			
		||||
      })
 | 
			
		||||
      .catch((error) => {
 | 
			
		||||
        console.error(error.message);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								web/berry/src/store/accountReducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
import * as actionTypes from './actions';
 | 
			
		||||
 | 
			
		||||
export const initialState = {
 | 
			
		||||
  user: undefined
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const accountReducer = (state = initialState, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case actionTypes.LOGIN:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: action.payload
 | 
			
		||||
      };
 | 
			
		||||
    case actionTypes.LOGOUT:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: undefined
 | 
			
		||||
      };
 | 
			
		||||
    default:
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default accountReducer;
 | 
			
		||||
							
								
								
									
										9
									
								
								web/berry/src/store/actions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
// action - customization reducer
 | 
			
		||||
export const SET_MENU = '@customization/SET_MENU';
 | 
			
		||||
export const MENU_TOGGLE = '@customization/MENU_TOGGLE';
 | 
			
		||||
export const MENU_OPEN = '@customization/MENU_OPEN';
 | 
			
		||||
export const SET_FONT_FAMILY = '@customization/SET_FONT_FAMILY';
 | 
			
		||||
export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS';
 | 
			
		||||
export const SET_SITE_INFO = '@siteInfo/SET_SITE_INFO';
 | 
			
		||||
export const LOGIN = '@account/LOGIN';
 | 
			
		||||
export const LOGOUT = '@account/LOGOUT';
 | 
			
		||||
							
								
								
									
										4
									
								
								web/berry/src/store/constant.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
// theme constant
 | 
			
		||||
export const gridSpacing = 3;
 | 
			
		||||
export const drawerWidth = 260;
 | 
			
		||||
export const appDrawerWidth = 320;
 | 
			
		||||
							
								
								
									
										46
									
								
								web/berry/src/store/customizationReducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,46 @@
 | 
			
		||||
// project imports
 | 
			
		||||
import config from 'config';
 | 
			
		||||
 | 
			
		||||
// action - state management
 | 
			
		||||
import * as actionTypes from './actions';
 | 
			
		||||
 | 
			
		||||
export const initialState = {
 | 
			
		||||
  isOpen: [], // for active default menu
 | 
			
		||||
  defaultId: 'default',
 | 
			
		||||
  fontFamily: config.fontFamily,
 | 
			
		||||
  borderRadius: config.borderRadius,
 | 
			
		||||
  opened: true
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ==============================|| CUSTOMIZATION REDUCER ||============================== //
 | 
			
		||||
 | 
			
		||||
const customizationReducer = (state = initialState, action) => {
 | 
			
		||||
  let id;
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case actionTypes.MENU_OPEN:
 | 
			
		||||
      id = action.id;
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        isOpen: [id]
 | 
			
		||||
      };
 | 
			
		||||
    case actionTypes.SET_MENU:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        opened: action.opened
 | 
			
		||||
      };
 | 
			
		||||
    case actionTypes.SET_FONT_FAMILY:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        fontFamily: action.fontFamily
 | 
			
		||||
      };
 | 
			
		||||
    case actionTypes.SET_BORDER_RADIUS:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        borderRadius: action.borderRadius
 | 
			
		||||
      };
 | 
			
		||||
    default:
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default customizationReducer;
 | 
			
		||||
							
								
								
									
										9
									
								
								web/berry/src/store/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
import { createStore } from 'redux';
 | 
			
		||||
import reducer from './reducer';
 | 
			
		||||
 | 
			
		||||
// ==============================|| REDUX - MAIN STORE ||============================== //
 | 
			
		||||
 | 
			
		||||
const store = createStore(reducer);
 | 
			
		||||
const persister = 'Free';
 | 
			
		||||
 | 
			
		||||
export { store, persister };
 | 
			
		||||
							
								
								
									
										16
									
								
								web/berry/src/store/reducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
import { combineReducers } from 'redux';
 | 
			
		||||
 | 
			
		||||
// reducer import
 | 
			
		||||
import customizationReducer from './customizationReducer';
 | 
			
		||||
import accountReducer from './accountReducer';
 | 
			
		||||
import siteInfoReducer from './siteInfoReducer';
 | 
			
		||||
 | 
			
		||||
// ==============================|| COMBINE REDUCER ||============================== //
 | 
			
		||||
 | 
			
		||||
const reducer = combineReducers({
 | 
			
		||||
  customization: customizationReducer,
 | 
			
		||||
  account: accountReducer,
 | 
			
		||||
  siteInfo: siteInfoReducer
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default reducer;
 | 
			
		||||
							
								
								
									
										18
									
								
								web/berry/src/store/siteInfoReducer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,18 @@
 | 
			
		||||
import config from 'config';
 | 
			
		||||
import * as actionTypes from './actions';
 | 
			
		||||
 | 
			
		||||
export const initialState = config.siteInfo;
 | 
			
		||||
 | 
			
		||||
const siteInfoReducer = (state = initialState, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case actionTypes.SET_SITE_INFO:
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        ...action.payload
 | 
			
		||||
      };
 | 
			
		||||
    default:
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default siteInfoReducer;
 | 
			
		||||
							
								
								
									
										256
									
								
								web/berry/src/themes/compStyleOverride.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,256 @@
 | 
			
		||||
export default function componentStyleOverrides(theme) {
 | 
			
		||||
  const bgColor = theme.colors?.grey50;
 | 
			
		||||
  return {
 | 
			
		||||
    MuiButton: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          fontWeight: 500,
 | 
			
		||||
          borderRadius: '4px',
 | 
			
		||||
          '&.Mui-disabled': {
 | 
			
		||||
            color: theme.colors?.grey600
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiMenuItem: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: theme.colors?.grey100
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }, //MuiAutocomplete-popper MuiPopover-root
 | 
			
		||||
    MuiAutocomplete: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        popper: {
 | 
			
		||||
          // 继承 MuiPopover-root
 | 
			
		||||
          boxShadow: '0px 5px 5px -3px rgba(0,0,0,0.2),0px 8px 10px 1px rgba(0,0,0,0.14),0px 3px 14px 2px rgba(0,0,0,0.12)',
 | 
			
		||||
          borderRadius: '12px',
 | 
			
		||||
          color: '#364152'
 | 
			
		||||
        },
 | 
			
		||||
        listbox: {
 | 
			
		||||
          // 继承 MuiPopover-root
 | 
			
		||||
          padding: '0px',
 | 
			
		||||
          paddingTop: '8px',
 | 
			
		||||
          paddingBottom: '8px'
 | 
			
		||||
        },
 | 
			
		||||
        option: {
 | 
			
		||||
          fontSize: '16px',
 | 
			
		||||
          fontWeight: '400',
 | 
			
		||||
          lineHeight: '1.334em',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          paddingTop: '6px',
 | 
			
		||||
          paddingBottom: '6px',
 | 
			
		||||
          paddingLeft: '16px',
 | 
			
		||||
          paddingRight: '16px'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiIconButton: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          color: theme.darkTextPrimary,
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: theme.colors?.grey200
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiPaper: {
 | 
			
		||||
      defaultProps: {
 | 
			
		||||
        elevation: 0
 | 
			
		||||
      },
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          backgroundImage: 'none'
 | 
			
		||||
        },
 | 
			
		||||
        rounded: {
 | 
			
		||||
          borderRadius: `${theme?.customization?.borderRadius}px`
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiCardHeader: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          color: theme.colors?.textDark,
 | 
			
		||||
          padding: '24px'
 | 
			
		||||
        },
 | 
			
		||||
        title: {
 | 
			
		||||
          fontSize: '1.125rem'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiCardContent: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          padding: '24px'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiCardActions: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          padding: '24px'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiListItemButton: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          color: theme.darkTextPrimary,
 | 
			
		||||
          paddingTop: '10px',
 | 
			
		||||
          paddingBottom: '10px',
 | 
			
		||||
          '&.Mui-selected': {
 | 
			
		||||
            color: theme.menuSelected,
 | 
			
		||||
            backgroundColor: theme.menuSelectedBack,
 | 
			
		||||
            '&:hover': {
 | 
			
		||||
              backgroundColor: theme.menuSelectedBack
 | 
			
		||||
            },
 | 
			
		||||
            '& .MuiListItemIcon-root': {
 | 
			
		||||
              color: theme.menuSelected
 | 
			
		||||
            }
 | 
			
		||||
          },
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: theme.menuSelectedBack,
 | 
			
		||||
            color: theme.menuSelected,
 | 
			
		||||
            '& .MuiListItemIcon-root': {
 | 
			
		||||
              color: theme.menuSelected
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiListItemIcon: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          color: theme.darkTextPrimary,
 | 
			
		||||
          minWidth: '36px'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiListItemText: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        primary: {
 | 
			
		||||
          color: theme.textDark
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiInputBase: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        input: {
 | 
			
		||||
          color: theme.textDark,
 | 
			
		||||
          '&::placeholder': {
 | 
			
		||||
            color: theme.darkTextSecondary,
 | 
			
		||||
            fontSize: '0.875rem'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiOutlinedInput: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          background: bgColor,
 | 
			
		||||
          borderRadius: `${theme?.customization?.borderRadius}px`,
 | 
			
		||||
          '& .MuiOutlinedInput-notchedOutline': {
 | 
			
		||||
            borderColor: theme.colors?.grey400
 | 
			
		||||
          },
 | 
			
		||||
          '&:hover $notchedOutline': {
 | 
			
		||||
            borderColor: theme.colors?.primaryLight
 | 
			
		||||
          },
 | 
			
		||||
          '&.MuiInputBase-multiline': {
 | 
			
		||||
            padding: 1
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        input: {
 | 
			
		||||
          fontWeight: 500,
 | 
			
		||||
          background: bgColor,
 | 
			
		||||
          padding: '15.5px 14px',
 | 
			
		||||
          borderRadius: `${theme?.customization?.borderRadius}px`,
 | 
			
		||||
          '&.MuiInputBase-inputSizeSmall': {
 | 
			
		||||
            padding: '10px 14px',
 | 
			
		||||
            '&.MuiInputBase-inputAdornedStart': {
 | 
			
		||||
              paddingLeft: 0
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        inputAdornedStart: {
 | 
			
		||||
          paddingLeft: 4
 | 
			
		||||
        },
 | 
			
		||||
        notchedOutline: {
 | 
			
		||||
          borderRadius: `${theme?.customization?.borderRadius}px`
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiSlider: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          '&.Mui-disabled': {
 | 
			
		||||
            color: theme.colors?.grey300
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        mark: {
 | 
			
		||||
          backgroundColor: theme.paper,
 | 
			
		||||
          width: '4px'
 | 
			
		||||
        },
 | 
			
		||||
        valueLabel: {
 | 
			
		||||
          color: theme?.colors?.primaryLight
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiDivider: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          borderColor: theme.divider,
 | 
			
		||||
          opacity: 1
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiAvatar: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          color: theme.colors?.primaryDark,
 | 
			
		||||
          background: theme.colors?.primary200
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiChip: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          '&.MuiChip-deletable .MuiChip-deleteIcon': {
 | 
			
		||||
            color: 'inherit'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiTableCell: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          borderBottom: '1px solid rgb(241, 243, 244)',
 | 
			
		||||
          textAlign: 'center'
 | 
			
		||||
        },
 | 
			
		||||
        head: {
 | 
			
		||||
          color: theme.darkTextSecondary,
 | 
			
		||||
          backgroundColor: 'rgb(244, 246, 248)'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiTableRow: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        root: {
 | 
			
		||||
          '&:hover': {
 | 
			
		||||
            backgroundColor: 'rgb(244, 246, 248)'
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    MuiTooltip: {
 | 
			
		||||
      styleOverrides: {
 | 
			
		||||
        tooltip: {
 | 
			
		||||
          color: theme.paper,
 | 
			
		||||
          background: theme.colors?.grey700
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								web/berry/src/themes/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
import { createTheme } from '@mui/material/styles';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import colors from 'assets/scss/_themes-vars.module.scss';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import componentStyleOverrides from './compStyleOverride';
 | 
			
		||||
import themePalette from './palette';
 | 
			
		||||
import themeTypography from './typography';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represent theme style and structure as per Material-UI
 | 
			
		||||
 * @param {JsonObject} customization customization parameter object
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export const theme = (customization) => {
 | 
			
		||||
  const color = colors;
 | 
			
		||||
 | 
			
		||||
  const themeOption = {
 | 
			
		||||
    colors: color,
 | 
			
		||||
    heading: color.grey900,
 | 
			
		||||
    paper: color.paper,
 | 
			
		||||
    backgroundDefault: color.paper,
 | 
			
		||||
    background: color.primaryLight,
 | 
			
		||||
    darkTextPrimary: color.grey700,
 | 
			
		||||
    darkTextSecondary: color.grey500,
 | 
			
		||||
    textDark: color.grey900,
 | 
			
		||||
    menuSelected: color.secondaryDark,
 | 
			
		||||
    menuSelectedBack: color.secondaryLight,
 | 
			
		||||
    divider: color.grey200,
 | 
			
		||||
    customization
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const themeOptions = {
 | 
			
		||||
    direction: 'ltr',
 | 
			
		||||
    palette: themePalette(themeOption),
 | 
			
		||||
    mixins: {
 | 
			
		||||
      toolbar: {
 | 
			
		||||
        minHeight: '48px',
 | 
			
		||||
        padding: '16px',
 | 
			
		||||
        '@media (min-width: 600px)': {
 | 
			
		||||
          minHeight: '48px'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    typography: themeTypography(themeOption)
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const themes = createTheme(themeOptions);
 | 
			
		||||
  themes.components = componentStyleOverrides(themeOption);
 | 
			
		||||
 | 
			
		||||
  return themes;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default theme;
 | 
			
		||||
							
								
								
									
										73
									
								
								web/berry/src/themes/palette.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,73 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Color intention that you want to used in your theme
 | 
			
		||||
 * @param {JsonObject} theme Theme customization object
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export default function themePalette(theme) {
 | 
			
		||||
  return {
 | 
			
		||||
    mode: 'light',
 | 
			
		||||
    common: {
 | 
			
		||||
      black: theme.colors?.darkPaper
 | 
			
		||||
    },
 | 
			
		||||
    primary: {
 | 
			
		||||
      light: theme.colors?.primaryLight,
 | 
			
		||||
      main: theme.colors?.primaryMain,
 | 
			
		||||
      dark: theme.colors?.primaryDark,
 | 
			
		||||
      200: theme.colors?.primary200,
 | 
			
		||||
      800: theme.colors?.primary800
 | 
			
		||||
    },
 | 
			
		||||
    secondary: {
 | 
			
		||||
      light: theme.colors?.secondaryLight,
 | 
			
		||||
      main: theme.colors?.secondaryMain,
 | 
			
		||||
      dark: theme.colors?.secondaryDark,
 | 
			
		||||
      200: theme.colors?.secondary200,
 | 
			
		||||
      800: theme.colors?.secondary800
 | 
			
		||||
    },
 | 
			
		||||
    error: {
 | 
			
		||||
      light: theme.colors?.errorLight,
 | 
			
		||||
      main: theme.colors?.errorMain,
 | 
			
		||||
      dark: theme.colors?.errorDark
 | 
			
		||||
    },
 | 
			
		||||
    orange: {
 | 
			
		||||
      light: theme.colors?.orangeLight,
 | 
			
		||||
      main: theme.colors?.orangeMain,
 | 
			
		||||
      dark: theme.colors?.orangeDark
 | 
			
		||||
    },
 | 
			
		||||
    warning: {
 | 
			
		||||
      light: theme.colors?.warningLight,
 | 
			
		||||
      main: theme.colors?.warningMain,
 | 
			
		||||
      dark: theme.colors?.warningDark
 | 
			
		||||
    },
 | 
			
		||||
    success: {
 | 
			
		||||
      light: theme.colors?.successLight,
 | 
			
		||||
      200: theme.colors?.success200,
 | 
			
		||||
      main: theme.colors?.successMain,
 | 
			
		||||
      dark: theme.colors?.successDark
 | 
			
		||||
    },
 | 
			
		||||
    grey: {
 | 
			
		||||
      50: theme.colors?.grey50,
 | 
			
		||||
      100: theme.colors?.grey100,
 | 
			
		||||
      500: theme.darkTextSecondary,
 | 
			
		||||
      600: theme.heading,
 | 
			
		||||
      700: theme.darkTextPrimary,
 | 
			
		||||
      900: theme.textDark
 | 
			
		||||
    },
 | 
			
		||||
    dark: {
 | 
			
		||||
      light: theme.colors?.darkTextPrimary,
 | 
			
		||||
      main: theme.colors?.darkLevel1,
 | 
			
		||||
      dark: theme.colors?.darkLevel2,
 | 
			
		||||
      800: theme.colors?.darkBackground,
 | 
			
		||||
      900: theme.colors?.darkPaper
 | 
			
		||||
    },
 | 
			
		||||
    text: {
 | 
			
		||||
      primary: theme.darkTextPrimary,
 | 
			
		||||
      secondary: theme.darkTextSecondary,
 | 
			
		||||
      dark: theme.textDark,
 | 
			
		||||
      hint: theme.colors?.grey100
 | 
			
		||||
    },
 | 
			
		||||
    background: {
 | 
			
		||||
      paper: theme.paper,
 | 
			
		||||
      default: theme.backgroundDefault
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										137
									
								
								web/berry/src/themes/typography.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,137 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Typography used in theme
 | 
			
		||||
 * @param {JsonObject} theme theme customization object
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export default function themeTypography(theme) {
 | 
			
		||||
  return {
 | 
			
		||||
    fontFamily: theme?.customization?.fontFamily,
 | 
			
		||||
    h6: {
 | 
			
		||||
      fontWeight: 500,
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontSize: '0.75rem'
 | 
			
		||||
    },
 | 
			
		||||
    h5: {
 | 
			
		||||
      fontSize: '0.875rem',
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontWeight: 500
 | 
			
		||||
    },
 | 
			
		||||
    h4: {
 | 
			
		||||
      fontSize: '1rem',
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontWeight: 600
 | 
			
		||||
    },
 | 
			
		||||
    h3: {
 | 
			
		||||
      fontSize: '1.25rem',
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontWeight: 600
 | 
			
		||||
    },
 | 
			
		||||
    h2: {
 | 
			
		||||
      fontSize: '1.5rem',
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontWeight: 700
 | 
			
		||||
    },
 | 
			
		||||
    h1: {
 | 
			
		||||
      fontSize: '2.125rem',
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      fontWeight: 700
 | 
			
		||||
    },
 | 
			
		||||
    subtitle1: {
 | 
			
		||||
      fontSize: '0.875rem',
 | 
			
		||||
      fontWeight: 500,
 | 
			
		||||
      color: theme.textDark
 | 
			
		||||
    },
 | 
			
		||||
    subtitle2: {
 | 
			
		||||
      fontSize: '0.75rem',
 | 
			
		||||
      fontWeight: 400,
 | 
			
		||||
      color: theme.darkTextSecondary
 | 
			
		||||
    },
 | 
			
		||||
    caption: {
 | 
			
		||||
      fontSize: '0.75rem',
 | 
			
		||||
      color: theme.darkTextSecondary,
 | 
			
		||||
      fontWeight: 400
 | 
			
		||||
    },
 | 
			
		||||
    body1: {
 | 
			
		||||
      fontSize: '0.875rem',
 | 
			
		||||
      fontWeight: 400,
 | 
			
		||||
      lineHeight: '1.334em'
 | 
			
		||||
    },
 | 
			
		||||
    body2: {
 | 
			
		||||
      letterSpacing: '0em',
 | 
			
		||||
      fontWeight: 400,
 | 
			
		||||
      lineHeight: '1.5em',
 | 
			
		||||
      color: theme.darkTextPrimary
 | 
			
		||||
    },
 | 
			
		||||
    button: {
 | 
			
		||||
      textTransform: 'capitalize'
 | 
			
		||||
    },
 | 
			
		||||
    customInput: {
 | 
			
		||||
      marginTop: 1,
 | 
			
		||||
      marginBottom: 1,
 | 
			
		||||
      '& > label': {
 | 
			
		||||
        top: 23,
 | 
			
		||||
        left: 0,
 | 
			
		||||
        color: theme.grey500,
 | 
			
		||||
        '&[data-shrink="false"]': {
 | 
			
		||||
          top: 5
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      '& > div > input': {
 | 
			
		||||
        padding: '30.5px 14px 11.5px !important'
 | 
			
		||||
      },
 | 
			
		||||
      '& legend': {
 | 
			
		||||
        display: 'none'
 | 
			
		||||
      },
 | 
			
		||||
      '& fieldset': {
 | 
			
		||||
        top: 0
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    otherInput: {
 | 
			
		||||
      marginTop: 1,
 | 
			
		||||
      marginBottom: 1
 | 
			
		||||
    },
 | 
			
		||||
    mainContent: {
 | 
			
		||||
      backgroundColor: theme.background,
 | 
			
		||||
      width: '100%',
 | 
			
		||||
      minHeight: 'calc(100vh - 88px)',
 | 
			
		||||
      flexGrow: 1,
 | 
			
		||||
      padding: '20px',
 | 
			
		||||
      marginTop: '88px',
 | 
			
		||||
      marginRight: '20px',
 | 
			
		||||
      borderRadius: `${theme?.customization?.borderRadius}px`
 | 
			
		||||
    },
 | 
			
		||||
    menuCaption: {
 | 
			
		||||
      fontSize: '0.875rem',
 | 
			
		||||
      fontWeight: 500,
 | 
			
		||||
      color: theme.heading,
 | 
			
		||||
      padding: '6px',
 | 
			
		||||
      textTransform: 'capitalize',
 | 
			
		||||
      marginTop: '10px'
 | 
			
		||||
    },
 | 
			
		||||
    subMenuCaption: {
 | 
			
		||||
      fontSize: '0.6875rem',
 | 
			
		||||
      fontWeight: 500,
 | 
			
		||||
      color: theme.darkTextSecondary,
 | 
			
		||||
      textTransform: 'capitalize'
 | 
			
		||||
    },
 | 
			
		||||
    commonAvatar: {
 | 
			
		||||
      cursor: 'pointer',
 | 
			
		||||
      borderRadius: '8px'
 | 
			
		||||
    },
 | 
			
		||||
    smallAvatar: {
 | 
			
		||||
      width: '22px',
 | 
			
		||||
      height: '22px',
 | 
			
		||||
      fontSize: '1rem'
 | 
			
		||||
    },
 | 
			
		||||
    mediumAvatar: {
 | 
			
		||||
      width: '34px',
 | 
			
		||||
      height: '34px',
 | 
			
		||||
      fontSize: '1.2rem'
 | 
			
		||||
    },
 | 
			
		||||
    largeAvatar: {
 | 
			
		||||
      width: '44px',
 | 
			
		||||
      height: '44px',
 | 
			
		||||
      fontSize: '1.5rem'
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								web/berry/src/ui-component/AdminContainer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
			
		||||
import { styled } from '@mui/material/styles';
 | 
			
		||||
import { Container } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
const AdminContainer = styled(Container)(({ theme }) => ({
 | 
			
		||||
  [theme.breakpoints.down('md')]: {
 | 
			
		||||
    paddingLeft: '0px',
 | 
			
		||||
    paddingRight: '0px'
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export default AdminContainer;
 | 
			
		||||
							
								
								
									
										37
									
								
								web/berry/src/ui-component/Footer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Link, Container, Box } from '@mui/material';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
// ==============================|| FOOTER - AUTHENTICATION 2 & 3 ||============================== //
 | 
			
		||||
 | 
			
		||||
const Footer = () => {
 | 
			
		||||
  const siteInfo = useSelector((state) => state.siteInfo);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Container sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '64px' }}>
 | 
			
		||||
      <Box sx={{ textAlign: 'center' }}>
 | 
			
		||||
        {siteInfo.footer_html ? (
 | 
			
		||||
          <div className="custom-footer" dangerouslySetInnerHTML={{ __html: siteInfo.footer_html }}></div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <>
 | 
			
		||||
            <Link href="https://github.com/songquanpeng/one-api" target="_blank">
 | 
			
		||||
              {siteInfo.system_name} {process.env.REACT_APP_VERSION}{' '}
 | 
			
		||||
            </Link>
 | 
			
		||||
            由{' '}
 | 
			
		||||
            <Link href="https://github.com/songquanpeng" target="_blank">
 | 
			
		||||
              JustSong
 | 
			
		||||
            </Link>{' '}
 | 
			
		||||
            构建,主题 berry 来自{' '}
 | 
			
		||||
            <Link href="https://github.com/MartialBE" target="_blank">
 | 
			
		||||
              MartialBE
 | 
			
		||||
            </Link>{' '},源代码遵循
 | 
			
		||||
            <Link href="https://opensource.org/licenses/mit-license.php"> MIT 协议</Link>
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Container>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Footer;
 | 
			
		||||
							
								
								
									
										158
									
								
								web/berry/src/ui-component/Label.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,158 @@
 | 
			
		||||
/*
 | 
			
		||||
 * Label.js
 | 
			
		||||
 *
 | 
			
		||||
 * This file uses code from the Minimal UI project, available at
 | 
			
		||||
 * https://github.com/minimal-ui-kit/material-kit-react/blob/main/src/components/label/label.jsx
 | 
			
		||||
 *
 | 
			
		||||
 * Minimal UI is licensed under the MIT License. A copy of the license is included below:
 | 
			
		||||
 *
 | 
			
		||||
 * MIT License
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (c) 2021 Minimal UI (https://minimals.cc/)
 | 
			
		||||
 *
 | 
			
		||||
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
 * of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 * in the Software without restriction, including without limitation the rights
 | 
			
		||||
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
 * copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
 * furnished to do so, subject to the following conditions:
 | 
			
		||||
 *
 | 
			
		||||
 * The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
 * copies or substantial portions of the Software.
 | 
			
		||||
 *
 | 
			
		||||
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
 * SOFTWARE.
 | 
			
		||||
 */
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
 | 
			
		||||
import Box from '@mui/material/Box';
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { alpha, styled } from '@mui/material/styles';
 | 
			
		||||
 | 
			
		||||
// ----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
const Label = forwardRef(({ children, color = 'default', variant = 'soft', startIcon, endIcon, sx, ...other }, ref) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  const iconStyles = {
 | 
			
		||||
    width: 16,
 | 
			
		||||
    height: 16,
 | 
			
		||||
    '& svg, img': { width: 1, height: 1, objectFit: 'cover' }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledLabel
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      component="span"
 | 
			
		||||
      ownerState={{ color, variant }}
 | 
			
		||||
      sx={{
 | 
			
		||||
        ...(startIcon && { pl: 0.75 }),
 | 
			
		||||
        ...(endIcon && { pr: 0.75 }),
 | 
			
		||||
        ...sx
 | 
			
		||||
      }}
 | 
			
		||||
      theme={theme}
 | 
			
		||||
      {...other}
 | 
			
		||||
    >
 | 
			
		||||
      {startIcon && <Box sx={{ mr: 0.75, ...iconStyles }}> {startIcon} </Box>}
 | 
			
		||||
 | 
			
		||||
      {children}
 | 
			
		||||
 | 
			
		||||
      {endIcon && <Box sx={{ ml: 0.75, ...iconStyles }}> {endIcon} </Box>}
 | 
			
		||||
    </StyledLabel>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Label.propTypes = {
 | 
			
		||||
  children: PropTypes.node,
 | 
			
		||||
  endIcon: PropTypes.object,
 | 
			
		||||
  startIcon: PropTypes.object,
 | 
			
		||||
  sx: PropTypes.object,
 | 
			
		||||
  variant: PropTypes.oneOf(['filled', 'outlined', 'ghost', 'soft']),
 | 
			
		||||
  color: PropTypes.oneOf(['default', 'primary', 'secondary', 'info', 'success', 'warning', 'orange', 'error'])
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Label;
 | 
			
		||||
 | 
			
		||||
const StyledLabel = styled(Box)(({ theme, ownerState }) => {
 | 
			
		||||
  // const lightMode = theme.palette.mode === 'light';
 | 
			
		||||
 | 
			
		||||
  const filledVariant = ownerState.variant === 'filled';
 | 
			
		||||
 | 
			
		||||
  const outlinedVariant = ownerState.variant === 'outlined';
 | 
			
		||||
 | 
			
		||||
  const softVariant = ownerState.variant === 'soft';
 | 
			
		||||
 | 
			
		||||
  const ghostVariant = ownerState.variant === 'ghost';
 | 
			
		||||
 | 
			
		||||
  const defaultStyle = {
 | 
			
		||||
    ...(ownerState.color === 'default' && {
 | 
			
		||||
      // FILLED
 | 
			
		||||
      ...(filledVariant && {
 | 
			
		||||
        color: theme.palette.grey[300],
 | 
			
		||||
        backgroundColor: theme.palette.text.primary
 | 
			
		||||
      }),
 | 
			
		||||
      // OUTLINED
 | 
			
		||||
      ...(outlinedVariant && {
 | 
			
		||||
        color: theme.palette.grey[500],
 | 
			
		||||
        border: `2px solid ${theme.palette.grey[500]}`
 | 
			
		||||
      }),
 | 
			
		||||
      // SOFT
 | 
			
		||||
      ...(softVariant && {
 | 
			
		||||
        color: theme.palette.text.secondary,
 | 
			
		||||
        backgroundColor: alpha(theme.palette.grey[500], 0.16)
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const colorStyle = {
 | 
			
		||||
    ...(ownerState.color !== 'default' && {
 | 
			
		||||
      // FILLED
 | 
			
		||||
      ...(filledVariant && {
 | 
			
		||||
        color: theme.palette.background.paper,
 | 
			
		||||
        backgroundColor: theme.palette[ownerState.color]?.main
 | 
			
		||||
      }),
 | 
			
		||||
      // OUTLINED
 | 
			
		||||
      ...(outlinedVariant && {
 | 
			
		||||
        backgroundColor: 'transparent',
 | 
			
		||||
        color: theme.palette[ownerState.color]?.main,
 | 
			
		||||
        border: `2px solid ${theme.palette[ownerState.color]?.main}`
 | 
			
		||||
      }),
 | 
			
		||||
      // SOFT
 | 
			
		||||
      ...(softVariant && {
 | 
			
		||||
        color: theme.palette[ownerState.color]['dark'],
 | 
			
		||||
        backgroundColor: alpha(theme.palette[ownerState.color]?.main, 0.16)
 | 
			
		||||
      }),
 | 
			
		||||
      // GHOST
 | 
			
		||||
      ...(ghostVariant && {
 | 
			
		||||
        color: theme.palette[ownerState.color]?.main
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    height: 24,
 | 
			
		||||
    minWidth: 24,
 | 
			
		||||
    lineHeight: 0,
 | 
			
		||||
    borderRadius: 6,
 | 
			
		||||
    cursor: 'default',
 | 
			
		||||
    alignItems: 'center',
 | 
			
		||||
    whiteSpace: 'nowrap',
 | 
			
		||||
    display: 'inline-flex',
 | 
			
		||||
    justifyContent: 'center',
 | 
			
		||||
    // textTransform: 'capitalize',
 | 
			
		||||
    padding: theme.spacing(0, 0.75),
 | 
			
		||||
    fontSize: theme.typography.pxToRem(12),
 | 
			
		||||
    fontWeight: theme.typography.fontWeightBold,
 | 
			
		||||
    transition: theme.transitions.create('all', {
 | 
			
		||||
      duration: theme.transitions.duration.shorter
 | 
			
		||||
    }),
 | 
			
		||||
    ...defaultStyle,
 | 
			
		||||
    ...colorStyle
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										15
									
								
								web/berry/src/ui-component/Loadable.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
import { Suspense } from 'react';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import Loader from './Loader';
 | 
			
		||||
 | 
			
		||||
// ==============================|| LOADABLE - LAZY LOADING ||============================== //
 | 
			
		||||
 | 
			
		||||
const Loadable = (Component) => (props) =>
 | 
			
		||||
  (
 | 
			
		||||
    <Suspense fallback={<Loader />}>
 | 
			
		||||
      <Component {...props} />
 | 
			
		||||
    </Suspense>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
export default Loadable;
 | 
			
		||||
							
								
								
									
										21
									
								
								web/berry/src/ui-component/Loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import LinearProgress from '@mui/material/LinearProgress';
 | 
			
		||||
import { styled } from '@mui/material/styles';
 | 
			
		||||
 | 
			
		||||
// styles
 | 
			
		||||
const LoaderWrapper = styled('div')({
 | 
			
		||||
  position: 'fixed',
 | 
			
		||||
  top: 0,
 | 
			
		||||
  left: 0,
 | 
			
		||||
  zIndex: 1301,
 | 
			
		||||
  width: '100%'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// ==============================|| LOADER ||============================== //
 | 
			
		||||
const Loader = () => (
 | 
			
		||||
  <LoaderWrapper>
 | 
			
		||||
    <LinearProgress color="primary" />
 | 
			
		||||
  </LoaderWrapper>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default Loader;
 | 
			
		||||
							
								
								
									
										21
									
								
								web/berry/src/ui-component/Logo.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import logo from 'assets/images/logo.svg';
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * if you want to use image instead of <svg> uncomment following.
 | 
			
		||||
 *
 | 
			
		||||
 * import logoDark from 'assets/images/logo-dark.svg';
 | 
			
		||||
 * import logo from 'assets/images/logo.svg';
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// ==============================|| LOGO SVG ||============================== //
 | 
			
		||||
 | 
			
		||||
const Logo = () => {
 | 
			
		||||
  const siteInfo = useSelector((state) => state.siteInfo);
 | 
			
		||||
 | 
			
		||||
  return <img src={siteInfo.logo || logo} alt={siteInfo.system_name} width="80" />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Logo;
 | 
			
		||||
							
								
								
									
										31
									
								
								web/berry/src/ui-component/SvgColor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,31 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
 | 
			
		||||
import Box from '@mui/material/Box';
 | 
			
		||||
 | 
			
		||||
// ----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
const SvgColor = forwardRef(({ src, sx, ...other }, ref) => (
 | 
			
		||||
  <Box
 | 
			
		||||
    component="span"
 | 
			
		||||
    className="svg-color"
 | 
			
		||||
    ref={ref}
 | 
			
		||||
    sx={{
 | 
			
		||||
      width: 24,
 | 
			
		||||
      height: 24,
 | 
			
		||||
      display: 'inline-block',
 | 
			
		||||
      bgcolor: 'currentColor',
 | 
			
		||||
      mask: `url(${src}) no-repeat center / contain`,
 | 
			
		||||
      WebkitMask: `url(${src}) no-repeat center / contain`,
 | 
			
		||||
      ...sx
 | 
			
		||||
    }}
 | 
			
		||||
    {...other}
 | 
			
		||||
  />
 | 
			
		||||
));
 | 
			
		||||
 | 
			
		||||
SvgColor.propTypes = {
 | 
			
		||||
  src: PropTypes.string,
 | 
			
		||||
  sx: PropTypes.object
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SvgColor;
 | 
			
		||||
							
								
								
									
										37
									
								
								web/berry/src/ui-component/Switch.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,37 @@
 | 
			
		||||
import { styled } from '@mui/material/styles';
 | 
			
		||||
import Switch from '@mui/material/Switch';
 | 
			
		||||
 | 
			
		||||
const TableSwitch = styled(Switch)(({ theme }) => ({
 | 
			
		||||
  padding: 8,
 | 
			
		||||
  '& .MuiSwitch-track': {
 | 
			
		||||
    borderRadius: 22 / 2,
 | 
			
		||||
    '&:before, &:after': {
 | 
			
		||||
      content: '""',
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: '50%',
 | 
			
		||||
      transform: 'translateY(-50%)',
 | 
			
		||||
      width: 16,
 | 
			
		||||
      height: 16
 | 
			
		||||
    },
 | 
			
		||||
    '&:before': {
 | 
			
		||||
      backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
 | 
			
		||||
        theme.palette.getContrastText(theme.palette.primary.main)
 | 
			
		||||
      )}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
 | 
			
		||||
      left: 12
 | 
			
		||||
    },
 | 
			
		||||
    '&:after': {
 | 
			
		||||
      backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
 | 
			
		||||
        theme.palette.getContrastText(theme.palette.primary.main)
 | 
			
		||||
      )}" d="M19,13H5V11H19V13Z" /></svg>')`,
 | 
			
		||||
      right: 12
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '& .MuiSwitch-thumb': {
 | 
			
		||||
    boxShadow: 'none',
 | 
			
		||||
    width: 16,
 | 
			
		||||
    height: 16,
 | 
			
		||||
    margin: 2
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export default TableSwitch;
 | 
			
		||||
							
								
								
									
										47
									
								
								web/berry/src/ui-component/TableToolBar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import Toolbar from '@mui/material/Toolbar';
 | 
			
		||||
import OutlinedInput from '@mui/material/OutlinedInput';
 | 
			
		||||
import InputAdornment from '@mui/material/InputAdornment';
 | 
			
		||||
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { IconSearch } from '@tabler/icons-react';
 | 
			
		||||
 | 
			
		||||
// ----------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export default function TableToolBar({ filterName, handleFilterName, placeholder }) {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
  const grey500 = theme.palette.grey[500];
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Toolbar
 | 
			
		||||
      sx={{
 | 
			
		||||
        height: 80,
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        justifyContent: 'space-between',
 | 
			
		||||
        p: (theme) => theme.spacing(0, 1, 0, 3)
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <OutlinedInput
 | 
			
		||||
        id="keyword"
 | 
			
		||||
        sx={{
 | 
			
		||||
          minWidth: '100%'
 | 
			
		||||
        }}
 | 
			
		||||
        value={filterName}
 | 
			
		||||
        onChange={handleFilterName}
 | 
			
		||||
        placeholder={placeholder}
 | 
			
		||||
        startAdornment={
 | 
			
		||||
          <InputAdornment position="start">
 | 
			
		||||
            <IconSearch stroke={1.5} size="20px" color={grey500} />
 | 
			
		||||
          </InputAdornment>
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
    </Toolbar>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TableToolBar.propTypes = {
 | 
			
		||||
  filterName: PropTypes.string,
 | 
			
		||||
  handleFilterName: PropTypes.func,
 | 
			
		||||
  placeholder: PropTypes.string
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										55
									
								
								web/berry/src/ui-component/cards/CardSecondaryAction.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { ButtonBase, Link, Tooltip } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import Avatar from '../extended/Avatar';
 | 
			
		||||
 | 
			
		||||
// ==============================|| CARD SECONDARY ACTION ||============================== //
 | 
			
		||||
 | 
			
		||||
const CardSecondaryAction = ({ title, link, icon }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip title={title || 'Reference'} placement="left">
 | 
			
		||||
      <ButtonBase disableRipple>
 | 
			
		||||
        {!icon && (
 | 
			
		||||
          <Avatar component={Link} href={link} target="_blank" alt="MUI Logo" size="badge" color="primary" outline>
 | 
			
		||||
            <svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
              <g clipPath="url(#clip0)">
 | 
			
		||||
                <path d="M100 260.9V131L212.5 195.95V239.25L137.5 195.95V282.55L100 260.9Z" fill={theme.palette.primary[800]} />
 | 
			
		||||
                <path
 | 
			
		||||
                  d="M212.5 195.95L325 131V260.9L250 304.2L212.5 282.55L287.5 239.25V195.95L212.5 239.25V195.95Z"
 | 
			
		||||
                  fill={theme.palette.primary.main}
 | 
			
		||||
                />
 | 
			
		||||
                <path d="M212.5 282.55V325.85L287.5 369.15V325.85L212.5 282.55Z" fill={theme.palette.primary[800]} />
 | 
			
		||||
                <path
 | 
			
		||||
                  d="M287.5 369.15L400 304.2V217.6L362.5 239.25V282.55L287.5 325.85V369.15ZM362.5 195.95V152.65L400 131V174.3L362.5 195.95Z"
 | 
			
		||||
                  fill={theme.palette.primary.main}
 | 
			
		||||
                />
 | 
			
		||||
              </g>
 | 
			
		||||
              <defs>
 | 
			
		||||
                <clipPath id="clip0">
 | 
			
		||||
                  <rect width="300" height="238.3" fill="white" transform="translate(100 131)" />
 | 
			
		||||
                </clipPath>
 | 
			
		||||
              </defs>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </Avatar>
 | 
			
		||||
        )}
 | 
			
		||||
        {icon && (
 | 
			
		||||
          <Avatar component={Link} href={link} target="_blank" size="badge" color="primary" outline>
 | 
			
		||||
            {icon}
 | 
			
		||||
          </Avatar>
 | 
			
		||||
        )}
 | 
			
		||||
      </ButtonBase>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
CardSecondaryAction.propTypes = {
 | 
			
		||||
  icon: PropTypes.node,
 | 
			
		||||
  link: PropTypes.string,
 | 
			
		||||
  title: PropTypes.string
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default CardSecondaryAction;
 | 
			
		||||
							
								
								
									
										80
									
								
								web/berry/src/ui-component/cards/MainCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// constant
 | 
			
		||||
const headerSX = {
 | 
			
		||||
  '& .MuiCardHeader-action': { mr: 0 }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ==============================|| CUSTOM MAIN CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const MainCard = forwardRef(
 | 
			
		||||
  (
 | 
			
		||||
    {
 | 
			
		||||
      border = true,
 | 
			
		||||
      boxShadow,
 | 
			
		||||
      children,
 | 
			
		||||
      content = true,
 | 
			
		||||
      contentClass = '',
 | 
			
		||||
      contentSX = {},
 | 
			
		||||
      darkTitle,
 | 
			
		||||
      secondary,
 | 
			
		||||
      shadow,
 | 
			
		||||
      sx = {},
 | 
			
		||||
      title,
 | 
			
		||||
      ...others
 | 
			
		||||
    },
 | 
			
		||||
    ref
 | 
			
		||||
  ) => {
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Card
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        {...others}
 | 
			
		||||
        sx={{
 | 
			
		||||
          border: border ? '1px solid' : 'none',
 | 
			
		||||
          borderColor: theme.palette.primary[200] + 25,
 | 
			
		||||
          ':hover': {
 | 
			
		||||
            boxShadow: boxShadow ? shadow || '0 2px 14px 0 rgb(32 40 45 / 8%)' : 'inherit'
 | 
			
		||||
          },
 | 
			
		||||
          ...sx
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {/* card header and action */}
 | 
			
		||||
        {title && <CardHeader sx={headerSX} title={darkTitle ? <Typography variant="h3">{title}</Typography> : title} action={secondary} />}
 | 
			
		||||
 | 
			
		||||
        {/* content & header divider */}
 | 
			
		||||
        {title && <Divider />}
 | 
			
		||||
 | 
			
		||||
        {/* card content */}
 | 
			
		||||
        {content && (
 | 
			
		||||
          <CardContent sx={contentSX} className={contentClass}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        )}
 | 
			
		||||
        {!content && children}
 | 
			
		||||
      </Card>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
MainCard.propTypes = {
 | 
			
		||||
  border: PropTypes.bool,
 | 
			
		||||
  boxShadow: PropTypes.bool,
 | 
			
		||||
  children: PropTypes.node,
 | 
			
		||||
  content: PropTypes.bool,
 | 
			
		||||
  contentClass: PropTypes.string,
 | 
			
		||||
  contentSX: PropTypes.object,
 | 
			
		||||
  darkTitle: PropTypes.bool,
 | 
			
		||||
  secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
 | 
			
		||||
  shadow: PropTypes.string,
 | 
			
		||||
  sx: PropTypes.object,
 | 
			
		||||
  title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default MainCard;
 | 
			
		||||
							
								
								
									
										32
									
								
								web/berry/src/ui-component/cards/Skeleton/EarningCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,32 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Card, CardContent, Grid } from '@mui/material';
 | 
			
		||||
import Skeleton from '@mui/material/Skeleton';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SKELETON - EARNING CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const EarningCard = () => (
 | 
			
		||||
  <Card>
 | 
			
		||||
    <CardContent>
 | 
			
		||||
      <Grid container direction="column">
 | 
			
		||||
        <Grid item>
 | 
			
		||||
          <Grid container justifyContent="space-between">
 | 
			
		||||
            <Grid item>
 | 
			
		||||
              <Skeleton variant="rectangular" width={44} height={44} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item>
 | 
			
		||||
              <Skeleton variant="rectangular" width={34} height={34} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item>
 | 
			
		||||
          <Skeleton variant="rectangular" sx={{ my: 2 }} height={40} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item>
 | 
			
		||||
          <Skeleton variant="rectangular" height={30} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </CardContent>
 | 
			
		||||
  </Card>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default EarningCard;
 | 
			
		||||
@@ -0,0 +1,8 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import Skeleton from '@mui/material/Skeleton';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SKELETON IMAGE CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const ImagePlaceholder = ({ ...others }) => <Skeleton variant="rectangular" {...others} animation="wave" />;
 | 
			
		||||
 | 
			
		||||
export default ImagePlaceholder;
 | 
			
		||||
							
								
								
									
										155
									
								
								web/berry/src/ui-component/cards/Skeleton/PopularCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,155 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Card, CardContent, Grid } from '@mui/material';
 | 
			
		||||
import Skeleton from '@mui/material/Skeleton';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import { gridSpacing } from 'store/constant';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SKELETON - POPULAR CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const PopularCard = () => (
 | 
			
		||||
  <Card>
 | 
			
		||||
    <CardContent>
 | 
			
		||||
      <Grid container spacing={gridSpacing}>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container alignItems="center" justifyContent="space-between" spacing={gridSpacing}>
 | 
			
		||||
            <Grid item xs zeroMinWidth>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} width={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Skeleton variant="rectangular" height={150} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container spacing={1}>
 | 
			
		||||
            <Grid item xs={12}>
 | 
			
		||||
              <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                    <Grid item xs zeroMinWidth>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={16} width={16} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                  </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item xs={6}>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container spacing={1}>
 | 
			
		||||
            <Grid item xs={12}>
 | 
			
		||||
              <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                    <Grid item xs zeroMinWidth>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={16} width={16} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                  </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item xs={6}>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container spacing={1}>
 | 
			
		||||
            <Grid item xs={12}>
 | 
			
		||||
              <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                    <Grid item xs zeroMinWidth>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={16} width={16} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                  </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item xs={6}>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container spacing={1}>
 | 
			
		||||
            <Grid item xs={12}>
 | 
			
		||||
              <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                    <Grid item xs zeroMinWidth>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={16} width={16} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                  </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item xs={6}>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container spacing={1}>
 | 
			
		||||
            <Grid item xs={12}>
 | 
			
		||||
              <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={6}>
 | 
			
		||||
                  <Grid container alignItems="center" spacing={gridSpacing} justifyContent="space-between">
 | 
			
		||||
                    <Grid item xs zeroMinWidth>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                    <Grid item>
 | 
			
		||||
                      <Skeleton variant="rectangular" height={16} width={16} />
 | 
			
		||||
                    </Grid>
 | 
			
		||||
                  </Grid>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item xs={6}>
 | 
			
		||||
              <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </CardContent>
 | 
			
		||||
    <CardContent sx={{ p: 1.25, display: 'flex', pt: 0, justifyContent: 'center' }}>
 | 
			
		||||
      <Skeleton variant="rectangular" height={25} width={75} />
 | 
			
		||||
    </CardContent>
 | 
			
		||||
  </Card>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default PopularCard;
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { CardContent, Grid, Skeleton, Stack } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// project import
 | 
			
		||||
import MainCard from '../MainCard';
 | 
			
		||||
 | 
			
		||||
// ===========================|| SKELETON TOTAL GROWTH BAR CHART ||=========================== //
 | 
			
		||||
 | 
			
		||||
const ProductPlaceholder = () => (
 | 
			
		||||
  <MainCard content={false} boxShadow>
 | 
			
		||||
    <Skeleton variant="rectangular" height={220} />
 | 
			
		||||
    <CardContent sx={{ p: 2 }}>
 | 
			
		||||
      <Grid container spacing={2}>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Skeleton variant="rectangular" height={45} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12} sx={{ pt: '8px !important' }}>
 | 
			
		||||
          <Stack direction="row" alignItems="center" spacing={1}>
 | 
			
		||||
            <Skeleton variant="rectangular" height={20} width={90} />
 | 
			
		||||
            <Skeleton variant="rectangular" height={20} width={38} />
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Stack direction="row" justifyContent="space-between" alignItems="center">
 | 
			
		||||
            <Grid container spacing={1}>
 | 
			
		||||
              <Grid item>
 | 
			
		||||
                <Skeleton variant="rectangular" height={20} width={40} />
 | 
			
		||||
              </Grid>
 | 
			
		||||
              <Grid item>
 | 
			
		||||
                <Skeleton variant="rectangular" height={17} width={20} />
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Skeleton variant="rectangular" height={32} width={47} />
 | 
			
		||||
          </Stack>
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </CardContent>
 | 
			
		||||
  </MainCard>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default ProductPlaceholder;
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Card, CardContent, Grid } from '@mui/material';
 | 
			
		||||
import Skeleton from '@mui/material/Skeleton';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import { gridSpacing } from 'store/constant';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SKELETON TOTAL GROWTH BAR CHART ||============================== //
 | 
			
		||||
 | 
			
		||||
const TotalGrowthBarChart = () => (
 | 
			
		||||
  <Card>
 | 
			
		||||
    <CardContent>
 | 
			
		||||
      <Grid container spacing={gridSpacing}>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Grid container alignItems="center" justifyContent="space-between" spacing={gridSpacing}>
 | 
			
		||||
            <Grid item xs zeroMinWidth>
 | 
			
		||||
              <Grid container spacing={1}>
 | 
			
		||||
                <Grid item xs={12}>
 | 
			
		||||
                  <Skeleton variant="text" />
 | 
			
		||||
                </Grid>
 | 
			
		||||
                <Grid item xs={12}>
 | 
			
		||||
                  <Skeleton variant="rectangular" height={20} />
 | 
			
		||||
                </Grid>
 | 
			
		||||
              </Grid>
 | 
			
		||||
            </Grid>
 | 
			
		||||
            <Grid item>
 | 
			
		||||
              <Skeleton variant="rectangular" height={50} width={80} />
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Grid>
 | 
			
		||||
        </Grid>
 | 
			
		||||
        <Grid item xs={12}>
 | 
			
		||||
          <Skeleton variant="rectangular" height={530} />
 | 
			
		||||
        </Grid>
 | 
			
		||||
      </Grid>
 | 
			
		||||
    </CardContent>
 | 
			
		||||
  </Card>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default TotalGrowthBarChart;
 | 
			
		||||
							
								
								
									
										19
									
								
								web/berry/src/ui-component/cards/Skeleton/TotalIncomeCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Card, List, ListItem, ListItemAvatar, ListItemText, Skeleton } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// ==============================|| SKELETON - TOTAL INCOME DARK/LIGHT CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const TotalIncomeCard = () => (
 | 
			
		||||
  <Card sx={{ p: 2 }}>
 | 
			
		||||
    <List sx={{ py: 0 }}>
 | 
			
		||||
      <ListItem alignItems="center" disableGutters sx={{ py: 0 }}>
 | 
			
		||||
        <ListItemAvatar>
 | 
			
		||||
          <Skeleton variant="rectangular" width={44} height={44} />
 | 
			
		||||
        </ListItemAvatar>
 | 
			
		||||
        <ListItemText sx={{ py: 0 }} primary={<Skeleton variant="rectangular" height={20} />} secondary={<Skeleton variant="text" />} />
 | 
			
		||||
      </ListItem>
 | 
			
		||||
    </List>
 | 
			
		||||
  </Card>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default TotalIncomeCard;
 | 
			
		||||
							
								
								
									
										72
									
								
								web/berry/src/ui-component/cards/SubCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Card, CardContent, CardHeader, Divider, Typography } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// ==============================|| CUSTOM SUB CARD ||============================== //
 | 
			
		||||
 | 
			
		||||
const SubCard = forwardRef(
 | 
			
		||||
  ({ children, content, contentClass, darkTitle, secondary, sx = {}, contentSX = {}, title, subTitle, ...others }, ref) => {
 | 
			
		||||
    const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <Card
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        sx={{
 | 
			
		||||
          border: '1px solid',
 | 
			
		||||
          borderColor: theme.palette.primary.light,
 | 
			
		||||
          ':hover': {
 | 
			
		||||
            boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)'
 | 
			
		||||
          },
 | 
			
		||||
          ...sx
 | 
			
		||||
        }}
 | 
			
		||||
        {...others}
 | 
			
		||||
      >
 | 
			
		||||
        {/* card header and action */}
 | 
			
		||||
        {!darkTitle && title && (
 | 
			
		||||
          <CardHeader sx={{ p: 2.5 }} title={<Typography variant="h5">{title}</Typography>} action={secondary} subheader={subTitle} />
 | 
			
		||||
        )}
 | 
			
		||||
        {darkTitle && title && (
 | 
			
		||||
          <CardHeader sx={{ p: 2.5 }} title={<Typography variant="h4">{title}</Typography>} action={secondary} subheader={subTitle} />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {/* content & header divider */}
 | 
			
		||||
        {title && (
 | 
			
		||||
          <Divider
 | 
			
		||||
            sx={{
 | 
			
		||||
              opacity: 1,
 | 
			
		||||
              borderColor: theme.palette.primary.light
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        {/* card content */}
 | 
			
		||||
        {content && (
 | 
			
		||||
          <CardContent sx={{ p: 2.5, ...contentSX }} className={contentClass || ''}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        )}
 | 
			
		||||
        {!content && children}
 | 
			
		||||
      </Card>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
SubCard.propTypes = {
 | 
			
		||||
  children: PropTypes.node,
 | 
			
		||||
  content: PropTypes.bool,
 | 
			
		||||
  contentClass: PropTypes.string,
 | 
			
		||||
  darkTitle: PropTypes.bool,
 | 
			
		||||
  secondary: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object]),
 | 
			
		||||
  sx: PropTypes.object,
 | 
			
		||||
  contentSX: PropTypes.object,
 | 
			
		||||
  title: PropTypes.oneOfType([PropTypes.node, PropTypes.string, PropTypes.object])
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SubCard.defaultProps = {
 | 
			
		||||
  content: true
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SubCard;
 | 
			
		||||
							
								
								
									
										121
									
								
								web/berry/src/ui-component/cards/UserCard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
/*
 | 
			
		||||
 * UserCard.js
 | 
			
		||||
 *
 | 
			
		||||
 * This file uses code from the Minimal UI project, available at
 | 
			
		||||
 * https://github.com/minimal-ui-kit/material-kit-react/blob/main/src/sections/blog/post-card.jsx
 | 
			
		||||
 *
 | 
			
		||||
 * Minimal UI is licensed under the MIT License. A copy of the license is included below:
 | 
			
		||||
 *
 | 
			
		||||
 * MIT License
 | 
			
		||||
 *
 | 
			
		||||
 * Copyright (c) 2021 Minimal UI (https://minimals.cc/)
 | 
			
		||||
 *
 | 
			
		||||
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
 * of this software and associated documentation files (the "Software"), to deal
 | 
			
		||||
 * in the Software without restriction, including without limitation the rights
 | 
			
		||||
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
			
		||||
 * copies of the Software, and to permit persons to whom the Software is
 | 
			
		||||
 * furnished to do so, subject to the following conditions:
 | 
			
		||||
 *
 | 
			
		||||
 * The above copyright notice and this permission notice shall be included in all
 | 
			
		||||
 * copies or substantial portions of the Software.
 | 
			
		||||
 *
 | 
			
		||||
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
			
		||||
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
			
		||||
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
			
		||||
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
			
		||||
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
			
		||||
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
			
		||||
 * SOFTWARE.
 | 
			
		||||
 */
 | 
			
		||||
import { Box, Avatar } from '@mui/material';
 | 
			
		||||
import { alpha } from '@mui/material/styles';
 | 
			
		||||
import Card from '@mui/material/Card';
 | 
			
		||||
import shapeAvatar from 'assets/images/icons/shape-avatar.svg';
 | 
			
		||||
import coverAvatar from 'assets/images/invite/cover.jpg';
 | 
			
		||||
import userAvatar from 'assets/images/users/user-round.svg';
 | 
			
		||||
import SvgColor from 'ui-component/SvgColor';
 | 
			
		||||
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
export default function UserCard({ children }) {
 | 
			
		||||
  const renderShape = (
 | 
			
		||||
    <SvgColor
 | 
			
		||||
      color="paper"
 | 
			
		||||
      src={shapeAvatar}
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        height: 62,
 | 
			
		||||
        zIndex: 10,
 | 
			
		||||
        bottom: -26,
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        color: 'background.paper'
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const renderAvatar = (
 | 
			
		||||
    <Avatar
 | 
			
		||||
      src={userAvatar}
 | 
			
		||||
      sx={{
 | 
			
		||||
        zIndex: 11,
 | 
			
		||||
        width: 64,
 | 
			
		||||
        height: 64,
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        marginLeft: 'auto',
 | 
			
		||||
        marginRight: 'auto',
 | 
			
		||||
        left: 0,
 | 
			
		||||
        right: 0,
 | 
			
		||||
        bottom: (theme) => theme.spacing(-4)
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const renderCover = (
 | 
			
		||||
    <Box
 | 
			
		||||
      component="img"
 | 
			
		||||
      src={coverAvatar}
 | 
			
		||||
      sx={{
 | 
			
		||||
        top: 0,
 | 
			
		||||
        width: 1,
 | 
			
		||||
        height: 1,
 | 
			
		||||
        objectFit: 'cover',
 | 
			
		||||
        position: 'absolute'
 | 
			
		||||
      }}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Card>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'relative',
 | 
			
		||||
          '&:after': {
 | 
			
		||||
            top: 0,
 | 
			
		||||
            content: "''",
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            height: '100%',
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            bgcolor: (theme) => alpha(theme.palette.primary.main, 0.42)
 | 
			
		||||
          },
 | 
			
		||||
          pt: {
 | 
			
		||||
            xs: 'calc(100% / 3)',
 | 
			
		||||
            sm: 'calc(100% / 4.66)'
 | 
			
		||||
          }
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {renderShape}
 | 
			
		||||
        {renderAvatar}
 | 
			
		||||
        {renderCover}
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          p: (theme) => theme.spacing(4, 3, 3, 3)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Card>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								web/berry/src/ui-component/extended/AnimateButton.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,92 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
// third-party
 | 
			
		||||
import { motion, useCycle } from 'framer-motion';
 | 
			
		||||
 | 
			
		||||
// ==============================|| ANIMATION BUTTON ||============================== //
 | 
			
		||||
 | 
			
		||||
const AnimateButton = forwardRef(({ children, type, direction, offset, scale }, ref) => {
 | 
			
		||||
  let offset1;
 | 
			
		||||
  let offset2;
 | 
			
		||||
  switch (direction) {
 | 
			
		||||
    case 'up':
 | 
			
		||||
    case 'left':
 | 
			
		||||
      offset1 = offset;
 | 
			
		||||
      offset2 = 0;
 | 
			
		||||
      break;
 | 
			
		||||
    case 'right':
 | 
			
		||||
    case 'down':
 | 
			
		||||
    default:
 | 
			
		||||
      offset1 = 0;
 | 
			
		||||
      offset2 = offset;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [x, cycleX] = useCycle(offset1, offset2);
 | 
			
		||||
  const [y, cycleY] = useCycle(offset1, offset2);
 | 
			
		||||
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'rotate':
 | 
			
		||||
      return (
 | 
			
		||||
        <motion.div
 | 
			
		||||
          ref={ref}
 | 
			
		||||
          animate={{ rotate: 360 }}
 | 
			
		||||
          transition={{
 | 
			
		||||
            repeat: Infinity,
 | 
			
		||||
            repeatType: 'loop',
 | 
			
		||||
            duration: 2,
 | 
			
		||||
            repeatDelay: 0
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </motion.div>
 | 
			
		||||
      );
 | 
			
		||||
    case 'slide':
 | 
			
		||||
      if (direction === 'up' || direction === 'down') {
 | 
			
		||||
        return (
 | 
			
		||||
          <motion.div ref={ref} animate={{ y: y !== undefined ? y : '' }} onHoverEnd={() => cycleY()} onHoverStart={() => cycleY()}>
 | 
			
		||||
            {children}
 | 
			
		||||
          </motion.div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <motion.div ref={ref} animate={{ x: x !== undefined ? x : '' }} onHoverEnd={() => cycleX()} onHoverStart={() => cycleX()}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </motion.div>
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
    case 'scale':
 | 
			
		||||
    default:
 | 
			
		||||
      if (typeof scale === 'number') {
 | 
			
		||||
        scale = {
 | 
			
		||||
          hover: scale,
 | 
			
		||||
          tap: scale
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      return (
 | 
			
		||||
        <motion.div ref={ref} whileHover={{ scale: scale?.hover }} whileTap={{ scale: scale?.tap }}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </motion.div>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
AnimateButton.propTypes = {
 | 
			
		||||
  children: PropTypes.node,
 | 
			
		||||
  offset: PropTypes.number,
 | 
			
		||||
  type: PropTypes.oneOf(['slide', 'scale', 'rotate']),
 | 
			
		||||
  direction: PropTypes.oneOf(['up', 'down', 'left', 'right']),
 | 
			
		||||
  scale: PropTypes.oneOfType([PropTypes.number, PropTypes.object])
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
AnimateButton.defaultProps = {
 | 
			
		||||
  type: 'scale',
 | 
			
		||||
  offset: 10,
 | 
			
		||||
  direction: 'right',
 | 
			
		||||
  scale: {
 | 
			
		||||
    hover: 1,
 | 
			
		||||
    tap: 0.9
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AnimateButton;
 | 
			
		||||
							
								
								
									
										72
									
								
								web/berry/src/ui-component/extended/Avatar.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import MuiAvatar from '@mui/material/Avatar';
 | 
			
		||||
 | 
			
		||||
// ==============================|| AVATAR ||============================== //
 | 
			
		||||
 | 
			
		||||
const Avatar = ({ color, outline, size, sx, ...others }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  const colorSX = color && !outline && { color: theme.palette.background.paper, bgcolor: `${color}.main` };
 | 
			
		||||
  const outlineSX = outline && {
 | 
			
		||||
    color: color ? `${color}.main` : `primary.main`,
 | 
			
		||||
    bgcolor: theme.palette.background.paper,
 | 
			
		||||
    border: '2px solid',
 | 
			
		||||
    borderColor: color ? `${color}.main` : `primary.main`
 | 
			
		||||
  };
 | 
			
		||||
  let sizeSX = {};
 | 
			
		||||
  switch (size) {
 | 
			
		||||
    case 'badge':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(3.5),
 | 
			
		||||
        height: theme.spacing(3.5)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'xs':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(4.25),
 | 
			
		||||
        height: theme.spacing(4.25)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'sm':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(5),
 | 
			
		||||
        height: theme.spacing(5)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'lg':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(9),
 | 
			
		||||
        height: theme.spacing(9)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'xl':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(10.25),
 | 
			
		||||
        height: theme.spacing(10.25)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'md':
 | 
			
		||||
      sizeSX = {
 | 
			
		||||
        width: theme.spacing(7.5),
 | 
			
		||||
        height: theme.spacing(7.5)
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      sizeSX = {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <MuiAvatar sx={{ ...colorSX, ...outlineSX, ...sizeSX, ...sx }} {...others} />;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Avatar.propTypes = {
 | 
			
		||||
  className: PropTypes.string,
 | 
			
		||||
  color: PropTypes.string,
 | 
			
		||||
  outline: PropTypes.bool,
 | 
			
		||||
  size: PropTypes.string,
 | 
			
		||||
  sx: PropTypes.object
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Avatar;
 | 
			
		||||
							
								
								
									
										187
									
								
								web/berry/src/ui-component/extended/Breadcrumbs.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,187 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { useTheme } from '@mui/material/styles';
 | 
			
		||||
import { Box, Card, Divider, Grid, Typography } from '@mui/material';
 | 
			
		||||
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
 | 
			
		||||
 | 
			
		||||
// project imports
 | 
			
		||||
import config from 'config';
 | 
			
		||||
import { gridSpacing } from 'store/constant';
 | 
			
		||||
 | 
			
		||||
// assets
 | 
			
		||||
import { IconTallymark1 } from '@tabler/icons-react';
 | 
			
		||||
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone';
 | 
			
		||||
import HomeIcon from '@mui/icons-material/Home';
 | 
			
		||||
import HomeTwoToneIcon from '@mui/icons-material/HomeTwoTone';
 | 
			
		||||
 | 
			
		||||
const linkSX = {
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  color: 'grey.900',
 | 
			
		||||
  textDecoration: 'none',
 | 
			
		||||
  alignContent: 'center',
 | 
			
		||||
  alignItems: 'center'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// ==============================|| BREADCRUMBS ||============================== //
 | 
			
		||||
 | 
			
		||||
const Breadcrumbs = ({ card, divider, icon, icons, maxItems, navigation, rightAlign, separator, title, titleBottom, ...others }) => {
 | 
			
		||||
  const theme = useTheme();
 | 
			
		||||
 | 
			
		||||
  const iconStyle = {
 | 
			
		||||
    marginRight: theme.spacing(0.75),
 | 
			
		||||
    marginTop: `-${theme.spacing(0.25)}`,
 | 
			
		||||
    width: '1rem',
 | 
			
		||||
    height: '1rem',
 | 
			
		||||
    color: theme.palette.secondary.main
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const [main, setMain] = useState();
 | 
			
		||||
  const [item, setItem] = useState();
 | 
			
		||||
 | 
			
		||||
  // set active item state
 | 
			
		||||
  const getCollapse = (menu) => {
 | 
			
		||||
    if (menu.children) {
 | 
			
		||||
      menu.children.filter((collapse) => {
 | 
			
		||||
        if (collapse.type && collapse.type === 'collapse') {
 | 
			
		||||
          getCollapse(collapse);
 | 
			
		||||
        } else if (collapse.type && collapse.type === 'item') {
 | 
			
		||||
          if (document.location.pathname === config.basename + collapse.url) {
 | 
			
		||||
            setMain(menu);
 | 
			
		||||
            setItem(collapse);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return false;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    navigation?.items?.map((menu) => {
 | 
			
		||||
      if (menu.type && menu.type === 'group') {
 | 
			
		||||
        getCollapse(menu);
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // item separator
 | 
			
		||||
  const SeparatorIcon = separator;
 | 
			
		||||
  const separatorIcon = separator ? <SeparatorIcon stroke={1.5} size="1rem" /> : <IconTallymark1 stroke={1.5} size="1rem" />;
 | 
			
		||||
 | 
			
		||||
  let mainContent;
 | 
			
		||||
  let itemContent;
 | 
			
		||||
  let breadcrumbContent = <Typography />;
 | 
			
		||||
  let itemTitle = '';
 | 
			
		||||
  let CollapseIcon;
 | 
			
		||||
  let ItemIcon;
 | 
			
		||||
 | 
			
		||||
  // collapse item
 | 
			
		||||
  if (main && main.type === 'collapse') {
 | 
			
		||||
    CollapseIcon = main.icon ? main.icon : AccountTreeTwoToneIcon;
 | 
			
		||||
    mainContent = (
 | 
			
		||||
      <Typography component={Link} to="#" variant="subtitle1" sx={linkSX}>
 | 
			
		||||
        {icons && <CollapseIcon style={iconStyle} />}
 | 
			
		||||
        {main.title}
 | 
			
		||||
      </Typography>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // items
 | 
			
		||||
  if (item && item.type === 'item') {
 | 
			
		||||
    itemTitle = item.title;
 | 
			
		||||
 | 
			
		||||
    ItemIcon = item.icon ? item.icon : AccountTreeTwoToneIcon;
 | 
			
		||||
    itemContent = (
 | 
			
		||||
      <Typography
 | 
			
		||||
        variant="subtitle1"
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          textDecoration: 'none',
 | 
			
		||||
          alignContent: 'center',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          color: 'grey.500'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {icons && <ItemIcon style={iconStyle} />}
 | 
			
		||||
        {itemTitle}
 | 
			
		||||
      </Typography>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // main
 | 
			
		||||
    if (item.breadcrumbs !== false) {
 | 
			
		||||
      breadcrumbContent = (
 | 
			
		||||
        <Card
 | 
			
		||||
          sx={{
 | 
			
		||||
            marginBottom: card === false ? 0 : theme.spacing(gridSpacing),
 | 
			
		||||
            border: card === false ? 'none' : '1px solid',
 | 
			
		||||
            borderColor: theme.palette.primary[200] + 75,
 | 
			
		||||
            background: card === false ? 'transparent' : theme.palette.background.default
 | 
			
		||||
          }}
 | 
			
		||||
          {...others}
 | 
			
		||||
        >
 | 
			
		||||
          <Box sx={{ p: 2, pl: card === false ? 0 : 2 }}>
 | 
			
		||||
            <Grid
 | 
			
		||||
              container
 | 
			
		||||
              direction={rightAlign ? 'row' : 'column'}
 | 
			
		||||
              justifyContent={rightAlign ? 'space-between' : 'flex-start'}
 | 
			
		||||
              alignItems={rightAlign ? 'center' : 'flex-start'}
 | 
			
		||||
              spacing={1}
 | 
			
		||||
            >
 | 
			
		||||
              {title && !titleBottom && (
 | 
			
		||||
                <Grid item>
 | 
			
		||||
                  <Typography variant="h3" sx={{ fontWeight: 500 }}>
 | 
			
		||||
                    {item.title}
 | 
			
		||||
                  </Typography>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              )}
 | 
			
		||||
              <Grid item>
 | 
			
		||||
                <MuiBreadcrumbs
 | 
			
		||||
                  sx={{ '& .MuiBreadcrumbs-separator': { width: 16, ml: 1.25, mr: 1.25 } }}
 | 
			
		||||
                  aria-label="breadcrumb"
 | 
			
		||||
                  maxItems={maxItems || 8}
 | 
			
		||||
                  separator={separatorIcon}
 | 
			
		||||
                >
 | 
			
		||||
                  <Typography component={Link} to="/" color="inherit" variant="subtitle1" sx={linkSX}>
 | 
			
		||||
                    {icons && <HomeTwoToneIcon sx={iconStyle} />}
 | 
			
		||||
                    {icon && <HomeIcon sx={{ ...iconStyle, mr: 0 }} />}
 | 
			
		||||
                    {!icon && 'Dashboard'}
 | 
			
		||||
                  </Typography>
 | 
			
		||||
                  {mainContent}
 | 
			
		||||
                  {itemContent}
 | 
			
		||||
                </MuiBreadcrumbs>
 | 
			
		||||
              </Grid>
 | 
			
		||||
              {title && titleBottom && (
 | 
			
		||||
                <Grid item>
 | 
			
		||||
                  <Typography variant="h3" sx={{ fontWeight: 500 }}>
 | 
			
		||||
                    {item.title}
 | 
			
		||||
                  </Typography>
 | 
			
		||||
                </Grid>
 | 
			
		||||
              )}
 | 
			
		||||
            </Grid>
 | 
			
		||||
          </Box>
 | 
			
		||||
          {card === false && divider !== false && <Divider sx={{ borderColor: theme.palette.primary.main, mb: gridSpacing }} />}
 | 
			
		||||
        </Card>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return breadcrumbContent;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Breadcrumbs.propTypes = {
 | 
			
		||||
  card: PropTypes.bool,
 | 
			
		||||
  divider: PropTypes.bool,
 | 
			
		||||
  icon: PropTypes.bool,
 | 
			
		||||
  icons: PropTypes.bool,
 | 
			
		||||
  maxItems: PropTypes.number,
 | 
			
		||||
  navigation: PropTypes.object,
 | 
			
		||||
  rightAlign: PropTypes.bool,
 | 
			
		||||
  separator: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
 | 
			
		||||
  title: PropTypes.bool,
 | 
			
		||||
  titleBottom: PropTypes.bool
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Breadcrumbs;
 | 
			
		||||
							
								
								
									
										107
									
								
								web/berry/src/ui-component/extended/Transitions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,107 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { forwardRef } from 'react';
 | 
			
		||||
 | 
			
		||||
// material-ui
 | 
			
		||||
import { Collapse, Fade, Box, Grow, Slide, Zoom } from '@mui/material';
 | 
			
		||||
 | 
			
		||||
// ==============================|| TRANSITIONS ||============================== //
 | 
			
		||||
 | 
			
		||||
const Transitions = forwardRef(({ children, position, type, direction, ...others }, ref) => {
 | 
			
		||||
  let positionSX = {
 | 
			
		||||
    transformOrigin: '0 0 0'
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  switch (position) {
 | 
			
		||||
    case 'top-right':
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: 'top right'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'top':
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: 'top'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'bottom-left':
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: 'bottom left'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'bottom-right':
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: 'bottom right'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'bottom':
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: 'bottom'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
    case 'top-left':
 | 
			
		||||
    default:
 | 
			
		||||
      positionSX = {
 | 
			
		||||
        transformOrigin: '0 0 0'
 | 
			
		||||
      };
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box ref={ref}>
 | 
			
		||||
      {type === 'grow' && (
 | 
			
		||||
        <Grow {...others}>
 | 
			
		||||
          <Box sx={positionSX}>{children}</Box>
 | 
			
		||||
        </Grow>
 | 
			
		||||
      )}
 | 
			
		||||
      {type === 'collapse' && (
 | 
			
		||||
        <Collapse {...others} sx={positionSX}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </Collapse>
 | 
			
		||||
      )}
 | 
			
		||||
      {type === 'fade' && (
 | 
			
		||||
        <Fade
 | 
			
		||||
          {...others}
 | 
			
		||||
          timeout={{
 | 
			
		||||
            appear: 500,
 | 
			
		||||
            enter: 600,
 | 
			
		||||
            exit: 400
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box sx={positionSX}>{children}</Box>
 | 
			
		||||
        </Fade>
 | 
			
		||||
      )}
 | 
			
		||||
      {type === 'slide' && (
 | 
			
		||||
        <Slide
 | 
			
		||||
          {...others}
 | 
			
		||||
          timeout={{
 | 
			
		||||
            appear: 0,
 | 
			
		||||
            enter: 400,
 | 
			
		||||
            exit: 200
 | 
			
		||||
          }}
 | 
			
		||||
          direction={direction}
 | 
			
		||||
        >
 | 
			
		||||
          <Box sx={positionSX}>{children}</Box>
 | 
			
		||||
        </Slide>
 | 
			
		||||
      )}
 | 
			
		||||
      {type === 'zoom' && (
 | 
			
		||||
        <Zoom {...others}>
 | 
			
		||||
          <Box sx={positionSX}>{children}</Box>
 | 
			
		||||
        </Zoom>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
Transitions.propTypes = {
 | 
			
		||||
  children: PropTypes.node,
 | 
			
		||||
  type: PropTypes.oneOf(['grow', 'fade', 'collapse', 'slide', 'zoom']),
 | 
			
		||||
  position: PropTypes.oneOf(['top-left', 'top-right', 'top', 'bottom-left', 'bottom-right', 'bottom']),
 | 
			
		||||
  direction: PropTypes.oneOf(['up', 'down', 'left', 'right'])
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Transitions.defaultProps = {
 | 
			
		||||
  type: 'grow',
 | 
			
		||||
  position: 'top-left',
 | 
			
		||||
  direction: 'up'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Transitions;
 | 
			
		||||
							
								
								
									
										26
									
								
								web/berry/src/utils/api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
			
		||||
import { showError } from './common';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { store } from 'store/index';
 | 
			
		||||
import { LOGIN } from 'store/actions';
 | 
			
		||||
import config from 'config';
 | 
			
		||||
 | 
			
		||||
export const API = axios.create({
 | 
			
		||||
  baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '/'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
API.interceptors.response.use(
 | 
			
		||||
  (response) => response,
 | 
			
		||||
  (error) => {
 | 
			
		||||
    if (error.response?.status === 401) {
 | 
			
		||||
      localStorage.removeItem('user');
 | 
			
		||||
      store.dispatch({ type: LOGIN, payload: null });
 | 
			
		||||
      window.location.href = config.basename + '/login';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (error.response?.data?.message) {
 | 
			
		||||
      error.message = error.response.data.message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    showError(error);
 | 
			
		||||
  }
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										96
									
								
								web/berry/src/utils/chart.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,96 @@
 | 
			
		||||
export function getLastSevenDays() {
 | 
			
		||||
  const dates = [];
 | 
			
		||||
  for (let i = 6; i >= 0; i--) {
 | 
			
		||||
    const d = new Date();
 | 
			
		||||
    d.setDate(d.getDate() - i);
 | 
			
		||||
    const month = '' + (d.getMonth() + 1);
 | 
			
		||||
    const day = '' + d.getDate();
 | 
			
		||||
    const year = d.getFullYear();
 | 
			
		||||
 | 
			
		||||
    const formattedDate = [year, month.padStart(2, '0'), day.padStart(2, '0')].join('-');
 | 
			
		||||
    dates.push(formattedDate);
 | 
			
		||||
  }
 | 
			
		||||
  return dates;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getTodayDay() {
 | 
			
		||||
  let today = new Date();
 | 
			
		||||
  return today.toISOString().slice(0, 10);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function generateChartOptions(data, unit) {
 | 
			
		||||
  const dates = data.map((item) => item.date);
 | 
			
		||||
  const values = data.map((item) => item.value);
 | 
			
		||||
 | 
			
		||||
  const minDate = dates[0];
 | 
			
		||||
  const maxDate = dates[dates.length - 1];
 | 
			
		||||
 | 
			
		||||
  const minValue = Math.min(...values);
 | 
			
		||||
  const maxValue = Math.max(...values);
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    series: [
 | 
			
		||||
      {
 | 
			
		||||
        data: values
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    type: 'line',
 | 
			
		||||
    height: 90,
 | 
			
		||||
    options: {
 | 
			
		||||
      chart: {
 | 
			
		||||
        sparkline: {
 | 
			
		||||
          enabled: true
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      dataLabels: {
 | 
			
		||||
        enabled: false
 | 
			
		||||
      },
 | 
			
		||||
      colors: ['#fff'],
 | 
			
		||||
      fill: {
 | 
			
		||||
        type: 'solid',
 | 
			
		||||
        opacity: 1
 | 
			
		||||
      },
 | 
			
		||||
      stroke: {
 | 
			
		||||
        curve: 'smooth',
 | 
			
		||||
        width: 3
 | 
			
		||||
      },
 | 
			
		||||
      xaxis: {
 | 
			
		||||
        categories: dates,
 | 
			
		||||
        labels: {
 | 
			
		||||
          show: false
 | 
			
		||||
        },
 | 
			
		||||
        min: minDate,
 | 
			
		||||
        max: maxDate
 | 
			
		||||
      },
 | 
			
		||||
      yaxis: {
 | 
			
		||||
        min: minValue,
 | 
			
		||||
        max: maxValue,
 | 
			
		||||
        labels: {
 | 
			
		||||
          show: false
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      tooltip: {
 | 
			
		||||
        theme: 'dark',
 | 
			
		||||
        fixed: {
 | 
			
		||||
          enabled: false
 | 
			
		||||
        },
 | 
			
		||||
        x: {
 | 
			
		||||
          format: 'yyyy-MM-dd'
 | 
			
		||||
        },
 | 
			
		||||
        y: {
 | 
			
		||||
          formatter: function (val) {
 | 
			
		||||
            return val + ` ${unit}`;
 | 
			
		||||
          },
 | 
			
		||||
          title: {
 | 
			
		||||
            formatter: function () {
 | 
			
		||||
              return '';
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        marker: {
 | 
			
		||||
          show: false
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										188
									
								
								web/berry/src/utils/common.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,188 @@
 | 
			
		||||
import { enqueueSnackbar } from 'notistack';
 | 
			
		||||
import { snackbarConstants } from 'constants/SnackbarConstants';
 | 
			
		||||
import { API } from './api';
 | 
			
		||||
 | 
			
		||||
export function getSystemName() {
 | 
			
		||||
  let system_name = localStorage.getItem('system_name');
 | 
			
		||||
  if (!system_name) return 'One API';
 | 
			
		||||
  return system_name;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isMobile() {
 | 
			
		||||
  return window.innerWidth <= 600;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line
 | 
			
		||||
export function SnackbarHTMLContent({ htmlContent }) {
 | 
			
		||||
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getSnackbarOptions(variant) {
 | 
			
		||||
  let options = snackbarConstants.Common[variant];
 | 
			
		||||
  if (isMobile()) {
 | 
			
		||||
    // 合并 options 和 snackbarConstants.Mobile
 | 
			
		||||
    options = { ...options, ...snackbarConstants.Mobile };
 | 
			
		||||
  }
 | 
			
		||||
  return options;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showError(error) {
 | 
			
		||||
  if (error.message) {
 | 
			
		||||
    if (error.name === 'AxiosError') {
 | 
			
		||||
      switch (error.response.status) {
 | 
			
		||||
        case 429:
 | 
			
		||||
          enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
 | 
			
		||||
          break;
 | 
			
		||||
        case 500:
 | 
			
		||||
          enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
 | 
			
		||||
          break;
 | 
			
		||||
        case 405:
 | 
			
		||||
          enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showNotice(message, isHTML = false) {
 | 
			
		||||
  if (isHTML) {
 | 
			
		||||
    enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('INFO'));
 | 
			
		||||
  } else {
 | 
			
		||||
    enqueueSnackbar(message, getSnackbarOptions('INFO'));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showWarning(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('WARNING'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showSuccess(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function showInfo(message) {
 | 
			
		||||
  enqueueSnackbar(message, getSnackbarOptions('INFO'));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getOAuthState() {
 | 
			
		||||
  const res = await API.get('/api/oauth/state');
 | 
			
		||||
  const { success, message, data } = res.data;
 | 
			
		||||
  if (success) {
 | 
			
		||||
    return data;
 | 
			
		||||
  } else {
 | 
			
		||||
    showError(message);
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function onGitHubOAuthClicked(github_client_id, openInNewTab = false) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
 | 
			
		||||
  if (openInNewTab) {
 | 
			
		||||
    window.open(url);
 | 
			
		||||
  } else {
 | 
			
		||||
    window.location.href = url;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isAdmin() {
 | 
			
		||||
  let user = localStorage.getItem('user');
 | 
			
		||||
  if (!user) return false;
 | 
			
		||||
  user = JSON.parse(user);
 | 
			
		||||
  return user.role >= 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function timestamp2string(timestamp) {
 | 
			
		||||
  let date = new Date(timestamp * 1000);
 | 
			
		||||
  let year = date.getFullYear().toString();
 | 
			
		||||
  let month = (date.getMonth() + 1).toString();
 | 
			
		||||
  let day = date.getDate().toString();
 | 
			
		||||
  let hour = date.getHours().toString();
 | 
			
		||||
  let minute = date.getMinutes().toString();
 | 
			
		||||
  let second = date.getSeconds().toString();
 | 
			
		||||
  if (month.length === 1) {
 | 
			
		||||
    month = '0' + month;
 | 
			
		||||
  }
 | 
			
		||||
  if (day.length === 1) {
 | 
			
		||||
    day = '0' + day;
 | 
			
		||||
  }
 | 
			
		||||
  if (hour.length === 1) {
 | 
			
		||||
    hour = '0' + hour;
 | 
			
		||||
  }
 | 
			
		||||
  if (minute.length === 1) {
 | 
			
		||||
    minute = '0' + minute;
 | 
			
		||||
  }
 | 
			
		||||
  if (second.length === 1) {
 | 
			
		||||
    second = '0' + second;
 | 
			
		||||
  }
 | 
			
		||||
  return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function calculateQuota(quota, digits = 2) {
 | 
			
		||||
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
  quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
 | 
			
		||||
  return (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuota(quota, digits = 2) {
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return '$' + calculateQuota(quota, digits);
 | 
			
		||||
  }
 | 
			
		||||
  return renderNumber(quota);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const verifyJSON = (str) => {
 | 
			
		||||
  try {
 | 
			
		||||
    JSON.parse(str);
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function renderNumber(num) {
 | 
			
		||||
  if (num >= 1000000000) {
 | 
			
		||||
    return (num / 1000000000).toFixed(1) + 'B';
 | 
			
		||||
  } else if (num >= 1000000) {
 | 
			
		||||
    return (num / 1000000).toFixed(1) + 'M';
 | 
			
		||||
  } else if (num >= 10000) {
 | 
			
		||||
    return (num / 1000).toFixed(1) + 'k';
 | 
			
		||||
  } else {
 | 
			
		||||
    return num;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuotaWithPrompt(quota, digits) {
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function downloadTextAsFile(text, filename) {
 | 
			
		||||
  let blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
 | 
			
		||||
  let url = URL.createObjectURL(blob);
 | 
			
		||||
  let a = document.createElement('a');
 | 
			
		||||
  a.href = url;
 | 
			
		||||
  a.download = filename;
 | 
			
		||||
  a.click();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function removeTrailingSlash(url) {
 | 
			
		||||
  if (url.endsWith('/')) {
 | 
			
		||||
    return url.slice(0, -1);
 | 
			
		||||
  } else {
 | 
			
		||||
    return url;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										34
									
								
								web/berry/src/utils/password-strength.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Password validator for login pages
 | 
			
		||||
 */
 | 
			
		||||
import value from 'assets/scss/_themes-vars.module.scss';
 | 
			
		||||
 | 
			
		||||
// has number
 | 
			
		||||
const hasNumber = (number) => new RegExp(/[0-9]/).test(number);
 | 
			
		||||
 | 
			
		||||
// has mix of small and capitals
 | 
			
		||||
const hasMixed = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A-Z]/).test(number);
 | 
			
		||||
 | 
			
		||||
// has special chars
 | 
			
		||||
const hasSpecial = (number) => new RegExp(/[!#@$%^&*)(+=._-]/).test(number);
 | 
			
		||||
 | 
			
		||||
// set color based on password strength
 | 
			
		||||
export const strengthColor = (count) => {
 | 
			
		||||
  if (count < 2) return { label: 'Poor', color: value.errorMain };
 | 
			
		||||
  if (count < 3) return { label: 'Weak', color: value.warningDark };
 | 
			
		||||
  if (count < 4) return { label: 'Normal', color: value.orangeMain };
 | 
			
		||||
  if (count < 5) return { label: 'Good', color: value.successMain };
 | 
			
		||||
  if (count < 6) return { label: 'Strong', color: value.successDark };
 | 
			
		||||
  return { label: 'Poor', color: value.errorMain };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// password strength indicator
 | 
			
		||||
export const strengthIndicator = (number) => {
 | 
			
		||||
  let strengths = 0;
 | 
			
		||||
  if (number.length > 5) strengths += 1;
 | 
			
		||||
  if (number.length > 7) strengths += 1;
 | 
			
		||||
  if (hasNumber(number)) strengths += 1;
 | 
			
		||||
  if (hasSpecial(number)) strengths += 1;
 | 
			
		||||
  if (hasMixed(number)) strengths += 1;
 | 
			
		||||
  return strengths;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										20
									
								
								web/berry/src/utils/route-guard/AuthGuard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
import { useSelector } from 'react-redux';
 | 
			
		||||
import { useEffect, useContext } from 'react';
 | 
			
		||||
import { UserContext } from 'contexts/UserContext';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const AuthGuard = ({ children }) => {
 | 
			
		||||
  const account = useSelector((state) => state.account);
 | 
			
		||||
  const { isUserLoaded } = useContext(UserContext);
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isUserLoaded && !account.user) {
 | 
			
		||||
      navigate('/login');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }, [account, navigate, isUserLoaded]);
 | 
			
		||||
 | 
			
		||||
  return children;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default AuthGuard;
 | 
			
		||||
							
								
								
									
										69
									
								
								web/berry/src/views/About/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,69 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API } from 'utils/api';
 | 
			
		||||
import { showError } from 'utils/common';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
import { Box, Container, Typography } from '@mui/material';
 | 
			
		||||
import MainCard from 'ui-component/cards/MainCard';
 | 
			
		||||
 | 
			
		||||
const About = () => {
 | 
			
		||||
  const [about, setAbout] = useState('');
 | 
			
		||||
  const [aboutLoaded, setAboutLoaded] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const displayAbout = async () => {
 | 
			
		||||
    setAbout(localStorage.getItem('about') || '');
 | 
			
		||||
    const res = await API.get('/api/about');
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let aboutContent = data;
 | 
			
		||||
      if (!data.startsWith('https://')) {
 | 
			
		||||
        aboutContent = marked.parse(data);
 | 
			
		||||
      }
 | 
			
		||||
      setAbout(aboutContent);
 | 
			
		||||
      localStorage.setItem('about', aboutContent);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
      setAbout('加载关于内容失败...');
 | 
			
		||||
    }
 | 
			
		||||
    setAboutLoaded(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    displayAbout().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {aboutLoaded && about === '' ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Box>
 | 
			
		||||
            <Container sx={{ paddingTop: '40px' }}>
 | 
			
		||||
              <MainCard title="关于">
 | 
			
		||||
                <Typography variant="body2">
 | 
			
		||||
                  可在设置页面设置关于内容,支持 HTML & Markdown <br />
 | 
			
		||||
                  项目仓库地址:
 | 
			
		||||
                  <a href="https://github.com/songquanpeng/one-api">https://github.com/songquanpeng/one-api</a>
 | 
			
		||||
                </Typography>
 | 
			
		||||
              </MainCard>
 | 
			
		||||
            </Container>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          <Box>
 | 
			
		||||
            {about.startsWith('https://') ? (
 | 
			
		||||
              <iframe title="about" src={about} style={{ width: '100%', height: '100vh', border: 'none' }} />
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <Container>
 | 
			
		||||
                  <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
 | 
			
		||||
                </Container>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default About;
 | 
			
		||||