├── CODEOWNERS ├── README.md ├── index.html ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── _redirects ├── favicon.svg └── robots.txt ├── src ├── App.tsx ├── analytics │ ├── index.ts │ └── reportWebVitals.ts ├── assets │ ├── images │ │ ├── announcement_background.svg │ │ ├── auth │ │ │ └── AuthBackground.tsx │ │ ├── icons │ │ │ ├── facebook.svg │ │ │ ├── google.svg │ │ │ └── twitter.svg │ │ ├── landing │ │ │ ├── codedthemes-logo.svg │ │ │ ├── img-footer.png │ │ │ ├── img-soc1.svg │ │ │ ├── img-soc2.svg │ │ │ └── img-soc3.svg │ │ ├── logo-dark.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ ├── maintenance │ │ │ ├── Error404.png │ │ │ ├── Error500.png │ │ │ ├── TwoCone.png │ │ │ ├── coming-soon-1.png │ │ │ ├── coming-soon.png │ │ │ ├── under-construction-2.svg │ │ │ └── under-construction.svg │ │ ├── mega-menu │ │ │ ├── back.svg │ │ │ └── chart.svg │ │ ├── software │ │ │ ├── clash.png │ │ │ ├── clashx.png │ │ │ ├── quantumultx.png │ │ │ ├── shadowrocket.png │ │ │ ├── stash.png │ │ │ ├── surfboard.png │ │ │ └── surge.png │ │ └── users │ │ │ ├── avatar-1.png │ │ │ ├── avatar-2.png │ │ │ ├── avatar-3.png │ │ │ ├── avatar-4.png │ │ │ └── avatar-5.png │ └── third-party │ │ ├── apex-chart.css │ │ └── react-table.css ├── components │ ├── @extended │ │ ├── AnimateButton.tsx │ │ ├── Avatar.tsx │ │ ├── Breadcrumbs.tsx │ │ ├── DataGrid.tsx │ │ ├── Dot.tsx │ │ ├── IconButton.tsx │ │ ├── LoadingButton.tsx │ │ ├── Progress │ │ │ ├── CircularWithLabel.tsx │ │ │ ├── LinearWithIcon.tsx │ │ │ └── LinearWithLabel.tsx │ │ └── Transitions.tsx │ ├── KeyValueTable.tsx │ ├── Loadable.tsx │ ├── Loader.tsx │ ├── MainCard.tsx │ ├── RTLLayout.tsx │ ├── ScrollTop.tsx │ ├── ScrollX.tsx │ ├── SecondaryAction.tsx │ ├── cards │ │ ├── AuthFooter.tsx │ │ ├── ComponentHeader.tsx │ │ ├── skeleton │ │ │ └── ProductPlaceholder.tsx │ │ └── statistics │ │ │ ├── AnalyticEcommerce.tsx │ │ │ └── AnalyticsDataCard.tsx │ ├── logo │ │ ├── LogoIcon.tsx │ │ ├── LogoMain.tsx │ │ └── index.tsx │ └── third-party │ │ └── SimpleBar.tsx ├── config.ts ├── contexts │ └── ConfigContext.tsx ├── hooks │ ├── useAuthStateDetector.ts │ ├── useConfig.ts │ ├── useHtmlLangSelector.ts │ ├── useKnowledge.ts │ ├── usePageAnalyticsEffect.ts │ ├── useQuery.ts │ └── useTitle.ts ├── i18n │ ├── i18next.d.ts │ ├── index.ts │ └── resources │ │ ├── en-us │ │ ├── common.json │ │ ├── general.json │ │ ├── index.ts │ │ ├── language.json │ │ ├── notice.json │ │ └── title.json │ │ ├── index.ts │ │ ├── zh-cn │ │ ├── common.json │ │ ├── general.json │ │ ├── index.ts │ │ ├── language.json │ │ ├── notice.json │ │ └── title.json │ │ └── zh-tw │ │ ├── common.json │ │ ├── general.json │ │ ├── index.ts │ │ ├── language.json │ │ ├── notice.json │ │ └── title.json ├── index.tsx ├── layout │ ├── CommonLayout │ │ ├── FooterBlock.tsx │ │ ├── Header.tsx │ │ └── index.tsx │ └── MainLayout │ │ ├── Drawer │ │ ├── DrawerContent │ │ │ ├── Navigation │ │ │ │ ├── NavCollapse.tsx │ │ │ │ ├── NavGroup.tsx │ │ │ │ ├── NavItem.tsx │ │ │ │ └── index.tsx │ │ │ └── index.tsx │ │ ├── DrawerHeader │ │ │ ├── DrawerHeaderStyled.ts │ │ │ └── index.tsx │ │ ├── MiniDrawerStyled.ts │ │ └── index.tsx │ │ ├── Footer.tsx │ │ ├── Header │ │ ├── AppBarStyled.tsx │ │ ├── HeaderContent │ │ │ ├── DarkModeSwitchButton.tsx │ │ │ ├── I18nSwitchButton.tsx │ │ │ ├── Profile │ │ │ │ ├── MenuList.tsx │ │ │ │ └── index.tsx │ │ │ ├── TicketMenu.tsx │ │ │ ├── Title.tsx │ │ │ └── index.tsx │ │ └── index.tsx │ │ └── index.tsx ├── menu-items │ └── index.tsx ├── middleware │ ├── api │ │ └── index.ts │ └── route-guard │ │ ├── AuthGuard.tsx │ │ └── GuestGuard.tsx ├── model │ ├── api_response.ts │ ├── commission.ts │ ├── config.ts │ ├── coupon.ts │ ├── invite_data.ts │ ├── knowledge.ts │ ├── login.ts │ ├── notice.ts │ ├── order.ts │ ├── password.ts │ ├── payment.ts │ ├── plan.ts │ ├── register.ts │ ├── reset_password.ts │ ├── send_mail.ts │ ├── server.ts │ ├── subscription.ts │ ├── telegram.ts │ ├── ticket.ts │ ├── traffic.ts │ ├── user.ts │ └── withdraw.ts ├── pages │ ├── auth │ │ ├── forgot-password.tsx │ │ ├── login.tsx │ │ └── register.tsx │ ├── dashboard │ │ └── index.tsx │ ├── invite │ │ ├── commissions.tsx │ │ └── index.tsx │ ├── knowledge │ │ ├── [id].tsx │ │ └── index.tsx │ ├── maintenance │ │ ├── 404.tsx │ │ ├── 500.tsx │ │ ├── coming-soon.tsx │ │ └── under-construction.tsx │ ├── node │ │ └── status.tsx │ ├── order │ │ ├── [id].tsx │ │ └── index.tsx │ ├── plan │ │ └── buy │ │ │ ├── [id].tsx │ │ │ └── index.tsx │ ├── profile │ │ └── index.tsx │ ├── ticket │ │ ├── [id].tsx │ │ └── index.tsx │ └── traffic │ │ └── index.tsx ├── routes │ ├── LoginRoutes.tsx │ ├── MainRoutes.tsx │ └── index.tsx ├── sections │ ├── auth │ │ ├── AuthCard.tsx │ │ ├── AuthWrapper.tsx │ │ └── auth-forms │ │ │ ├── AuthCodeVerification.tsx │ │ │ ├── AuthForgotPassword.tsx │ │ │ ├── AuthLogin.tsx │ │ │ ├── AuthRegister.tsx │ │ │ └── SendMailButton.tsx │ ├── dashboard │ │ ├── alerts │ │ │ └── orderPendingAlert.tsx │ │ ├── index.tsx │ │ ├── noticeCarousel.tsx │ │ ├── shortcutCard │ │ │ ├── index.tsx │ │ │ ├── purchaseButton.tsx │ │ │ ├── subscribeButton │ │ │ │ ├── clashButton.tsx │ │ │ │ ├── clashxButton.tsx │ │ │ │ ├── copyLinkButton.tsx │ │ │ │ ├── index.tsx │ │ │ │ ├── quantumultxButton.tsx │ │ │ │ ├── scanQrCodeButton.tsx │ │ │ │ ├── shadowrocketButton.tsx │ │ │ │ ├── stashButton.tsx │ │ │ │ ├── surfboardButton.tsx │ │ │ │ └── surgeButton.tsx │ │ │ ├── ticketButton.tsx │ │ │ └── tutorialButton.tsx │ │ └── subscriptionCard.tsx │ ├── invite │ │ ├── commissionsPage │ │ │ └── index.tsx │ │ └── invitePage │ │ │ ├── index.tsx │ │ │ ├── infoCard.tsx │ │ │ ├── inviteCodesTable.tsx │ │ │ └── myInvitationCard.tsx │ ├── knowledge │ │ ├── postCard.tsx │ │ ├── postList.tsx │ │ └── search.tsx │ ├── node │ │ └── status │ │ │ ├── index.tsx │ │ │ ├── table.tsx │ │ │ └── wrapper.tsx │ ├── order │ │ ├── checkoutPage │ │ │ ├── billingCard.tsx │ │ │ ├── context.ts │ │ │ ├── index.tsx │ │ │ ├── orderInfoCard.tsx │ │ │ ├── paymentMethodCard.tsx │ │ │ ├── productInfoCard.tsx │ │ │ └── statusCard.tsx │ │ └── listPage │ │ │ ├── index.tsx │ │ │ ├── table.tsx │ │ │ └── wrapper.tsx │ ├── profile │ │ ├── accountInfoCard.tsx │ │ ├── changePasswordCard.tsx │ │ ├── index.tsx │ │ ├── notificationCard.tsx │ │ ├── resetSubscriptionCard.tsx │ │ ├── telegramCard.tsx │ │ └── walletCard.tsx │ ├── subscription │ │ ├── buyPage │ │ │ ├── context.ts │ │ │ ├── index.tsx │ │ │ ├── products.tsx │ │ │ ├── productsFilter.tsx │ │ │ └── productsHeader.tsx │ │ └── planDetailsPage │ │ │ ├── context.ts │ │ │ ├── couponCard.tsx │ │ │ ├── index.tsx │ │ │ ├── orderInfoCard.tsx │ │ │ ├── periodSelectCard.tsx │ │ │ └── planInfoCard.tsx │ ├── ticket │ │ └── detailPage │ │ │ ├── context.ts │ │ │ ├── createTicketButton.tsx │ │ │ ├── createTicketTxt.tsx │ │ │ ├── drawer.tsx │ │ │ ├── index.tsx │ │ │ ├── main │ │ │ ├── chatHistory.tsx │ │ │ ├── emojiChoose.tsx │ │ │ ├── index.tsx │ │ │ ├── inputArea.tsx │ │ │ └── topBar │ │ │ │ ├── closeButton.tsx │ │ │ │ └── index.tsx │ │ │ └── userList.tsx │ └── traffic │ │ ├── index.tsx │ │ ├── trafficAlert.tsx │ │ ├── trafficChart.tsx │ │ ├── trafficInfoCard.tsx │ │ └── trafficTable.tsx ├── store │ ├── index.ts │ ├── reducers │ │ ├── auth.ts │ │ ├── index.ts │ │ ├── menu.ts │ │ └── view.ts │ └── services │ │ └── api.ts ├── themes │ ├── cache.ts │ ├── hooks.ts │ ├── index.tsx │ ├── overrides │ │ ├── Accordion.ts │ │ ├── AccordionDetails.ts │ │ ├── AccordionSummary.tsx │ │ ├── Alert.ts │ │ ├── AlertTitle.ts │ │ ├── Autocomplete.ts │ │ ├── Badge.ts │ │ ├── Button.ts │ │ ├── ButtonBase.ts │ │ ├── ButtonGroup.ts │ │ ├── CardContent.ts │ │ ├── Checkbox.tsx │ │ ├── Chip.ts │ │ ├── Dialog.ts │ │ ├── DialogContentText.ts │ │ ├── DialogTitle.ts │ │ ├── Fab.ts │ │ ├── IconButton.ts │ │ ├── InputBase.ts │ │ ├── InputLabel.ts │ │ ├── LinearProgress.ts │ │ ├── Link.ts │ │ ├── ListItemButton.tsx │ │ ├── ListItemIcon.tsx │ │ ├── LoadingButton.ts │ │ ├── OutlinedInput.ts │ │ ├── Pagination.ts │ │ ├── PaginationItem.ts │ │ ├── Popover.ts │ │ ├── Radio.tsx │ │ ├── Slider.ts │ │ ├── Switch.ts │ │ ├── Tab.ts │ │ ├── TableBody.ts │ │ ├── TableCell.ts │ │ ├── TableFooter.ts │ │ ├── TableHead.ts │ │ ├── TablePagination.ts │ │ ├── TableRow.ts │ │ ├── Tabs.ts │ │ ├── ToggleButton.ts │ │ ├── TreeItem.ts │ │ ├── Typography.ts │ │ └── index.ts │ ├── palette.ts │ ├── shadows.tsx │ ├── theme │ │ └── index.ts │ └── typography.ts ├── types │ ├── auth.ts │ ├── config.ts │ ├── extended.ts │ ├── menu.ts │ ├── overrides │ │ ├── Alert.d.ts │ │ ├── Badge.d.ts │ │ ├── Button.d.ts │ │ ├── Checkbox.d.ts │ │ ├── Chip.d.ts │ │ ├── Pagination.d.ts │ │ ├── Radio.d.ts │ │ ├── Switch.d.ts │ │ ├── createPalette.d.ts │ │ ├── createTheme.d.ts │ │ └── index.d.ts │ ├── password.ts │ ├── plan.ts │ ├── root.ts │ ├── snackbar.ts │ └── theme.ts ├── utils │ ├── crypto.ts │ ├── getColors.ts │ ├── getShadow.ts │ ├── isBrowser.ts │ ├── locales │ │ └── en.json │ ├── password-strength.ts │ ├── password-validation.ts │ └── plan.ts └── vite-env.d.ts ├── stats.html ├── tsconfig.json └── vite.config.ts /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in the repo. 2 | * @AH-dark 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # v2board-theme 2 | 3 | # 为V2board提供的前后端分离主题。 4 | 此套主题是按照开源主题Mantis二次开发所得,此项目代码已修改所有bug,后续有bug或新功能会持续修改更新。\ 5 | 演示站的部分功能例如:签到、充值、礼品卡等定制功能均不在源码内,如有需要请前往TG频道联系开发。\ 6 | TG频道:https://t.me/maimai778 7 | 8 | ## 环境配置: 9 | nodejs v18.12.1\ 10 | pnpm v9.6.0 11 | 12 | ## 打包部署: 13 | 对接修改\ 14 | /src/config.ts\ 15 | Logo修改\ 16 | /src/components/logo/LogoMain.tsx\ 17 | 服务协议/隐私协议链接修改\ 18 | /src/sections/auth/auth-forms/AuthRegister.tsx\ 19 | /src/layout/MainLayout/Footer.tsx\ 20 | 如需添加crisp客服请在index.html中head标签内添加script\ 21 | 其余修改请自行search 22 | 23 | ## 打包命令 24 | ``` 25 | pnpm install 26 | pnpm build 27 | ``` 28 | 打包完成后得到dist文件夹,丢到网站根目录即可。 29 | 30 | ## 关于提交BUG 31 | 可在上方频道中或Issues提问 32 | 33 | ## 关于部署完成后刷新页面出现404 34 | 如果aapanel部署可在网站URL Rewrite中填入 35 | ``` 36 | location / { 37 | try_files $uri $uri/ /index.html$is_args$query_string; 38 | } 39 | ``` 40 | 41 | ## 友情捐赠 42 | TRC-20 TSLZs2cJorBgMDrWLaTA2dBxWqLCLJbY3o\ 43 | Polygon 0xB578cb7F5A47a9856BC20C083E9c47b5d932522E 44 | 45 | 46 | ## 更新日志 47 | 2024.10.07\ 48 | 套餐详情适配json数据\ 49 | 仅支持以下格式以及参数 50 | ``` 51 | [{ 52 | "feature":"每月 100G 流量", 53 | "support":true 54 | } 55 | ] 56 | ``` 57 | 2024.10.05\ 58 | 修复订单完成页的订单状态显示&套餐金额为0时的显示问题\ 59 | 2024.10.03\ 60 | 无关紧要的更新:修改了仪表盘订阅卡片的显示样式\ 61 | 2024.09.06\ 62 | 修复当后台开启邮箱后缀白名单时的限制;\ 63 | 修复流量统计页面的明细显示问题\ 64 | 2024.08.30\ 65 | 添加进入仪表盘页面最新公告弹框,以及修改公告图片自适应\ 66 | 修复注册发送验证码按钮的显示与按钮的禁用问题\ 67 | 2024.08.27\ 68 | 修复流量统计页面中低倍率节点流量记录的显示问题\ 69 | 2024.08.25\ 70 | 修复仪表盘页面未识别到设备类型导致的无法正常复制订阅链接的问题\ 71 | 2024.08.06\ 72 | 将Google Recaptcha更换为Cloudflare Turnstile 73 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | Allow: /login 4 | Allow: /register 5 | Allow: /forgot-password 6 | -------------------------------------------------------------------------------- /src/analytics/index.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from "react-ga4"; 2 | import reportWebVitals from "@/analytics/reportWebVitals"; 3 | import config from "@/config"; 4 | 5 | if (config.googleAnalytics) { 6 | ReactGA.initialize( 7 | [ 8 | { 9 | trackingId: config.googleAnalytics.measurementId 10 | } 11 | ], 12 | { 13 | testMode: import.meta.env.DEV, 14 | legacyDimensionMetric: false, 15 | gaOptions: { 16 | siteSpeedSampleRate: 100 17 | } 18 | } 19 | ); 20 | 21 | reportWebVitals(({ name, delta, id }) => { 22 | ReactGA.ga("send", "event", { 23 | eventCategory: "Web Vitals", 24 | eventAction: name, 25 | eventLabel: id, 26 | eventValue: Math.round(name === "CLS" ? delta * 1000 : delta), 27 | nonInteraction: true, 28 | transport: "beacon" 29 | }); 30 | }); 31 | } 32 | 33 | export default ReactGA; 34 | -------------------------------------------------------------------------------- /src/analytics/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/assets/images/announcement_background.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/auth/AuthBackground.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { useTheme } from '@mui/material/styles'; 3 | import { Box } from '@mui/material'; 4 | 5 | // ==============================|| AUTH BLUR BACK SVG ||============================== // 6 | 7 | const AuthBackground = () => { 8 | const theme = useTheme(); 9 | return ( 10 | 11 | 12 | 16 | 21 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AuthBackground; 32 | -------------------------------------------------------------------------------- /src/assets/images/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/icons/google.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/images/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/landing/img-footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/landing/img-footer.png -------------------------------------------------------------------------------- /src/assets/images/landing/img-soc2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/landing/img-soc3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/assets/images/maintenance/Error404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/maintenance/Error404.png -------------------------------------------------------------------------------- /src/assets/images/maintenance/Error500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/maintenance/Error500.png -------------------------------------------------------------------------------- /src/assets/images/maintenance/TwoCone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/maintenance/TwoCone.png -------------------------------------------------------------------------------- /src/assets/images/maintenance/coming-soon-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/maintenance/coming-soon-1.png -------------------------------------------------------------------------------- /src/assets/images/maintenance/coming-soon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/maintenance/coming-soon.png -------------------------------------------------------------------------------- /src/assets/images/software/clash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/clash.png -------------------------------------------------------------------------------- /src/assets/images/software/clashx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/clashx.png -------------------------------------------------------------------------------- /src/assets/images/software/quantumultx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/quantumultx.png -------------------------------------------------------------------------------- /src/assets/images/software/shadowrocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/shadowrocket.png -------------------------------------------------------------------------------- /src/assets/images/software/stash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/stash.png -------------------------------------------------------------------------------- /src/assets/images/software/surfboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/surfboard.png -------------------------------------------------------------------------------- /src/assets/images/software/surge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/software/surge.png -------------------------------------------------------------------------------- /src/assets/images/users/avatar-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/users/avatar-1.png -------------------------------------------------------------------------------- /src/assets/images/users/avatar-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/users/avatar-2.png -------------------------------------------------------------------------------- /src/assets/images/users/avatar-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/users/avatar-3.png -------------------------------------------------------------------------------- /src/assets/images/users/avatar-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/users/avatar-4.png -------------------------------------------------------------------------------- /src/assets/images/users/avatar-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moeumwy/v2board-theme/a439d34dfb6178b60e6dde20d5b8be3cad787676/src/assets/images/users/avatar-5.png -------------------------------------------------------------------------------- /src/assets/third-party/apex-chart.css: -------------------------------------------------------------------------------- 1 | .apexcharts-legend-series .apexcharts-legend-marker { 2 | left: -4px !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/third-party/react-table.css: -------------------------------------------------------------------------------- 1 | .cell-center { 2 | text-align: center; 3 | } 4 | .cell-center > * { 5 | margin: 0 auto; 6 | } 7 | 8 | .cell-right { 9 | text-align: right; 10 | } 11 | .cell-right > * { 12 | margin: 0 0 0 auto; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/@extended/Dot.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { CSSObject, useTheme } from "@mui/material/styles"; 3 | import { Box } from "@mui/material"; 4 | 5 | // project import 6 | import { ColorProps } from "@/types/extended"; 7 | import getColors from "@/utils/getColors"; 8 | 9 | interface Props { 10 | color?: ColorProps; 11 | size?: number; 12 | variant?: string; 13 | sx?: CSSObject; 14 | } 15 | 16 | const Dot = ({ color, size, variant, sx }: Props) => { 17 | const theme = useTheme(); 18 | const colors = getColors(theme, color || "primary"); 19 | const { main } = colors; 20 | 21 | return ( 22 | 35 | ); 36 | }; 37 | 38 | export default Dot; 39 | -------------------------------------------------------------------------------- /src/components/@extended/Progress/CircularWithLabel.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Box, CircularProgress, CircularProgressProps, Typography } from '@mui/material'; 3 | 4 | // ==============================|| PROGRESS - CIRCULAR LABEL ||============================== // 5 | 6 | export default function CircularWithLabel({ value, ...others }: CircularProgressProps) { 7 | return ( 8 | 9 | 10 | 22 | {`${Math.round(value!)}%`} 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/@extended/Progress/LinearWithIcon.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | // material-ui 4 | import { Box, LinearProgress, LinearProgressProps } from '@mui/material'; 5 | 6 | // ==============================|| PROGRESS - LINEAR ICON ||============================== // 7 | 8 | export default function LinearWithIcon({ icon, value, ...others }: LinearProgressProps & { icon: ReactNode }) { 9 | return ( 10 | 11 | 12 | 13 | 14 | {icon} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/@extended/Progress/LinearWithLabel.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Box, LinearProgress, LinearProgressProps, Typography } from '@mui/material'; 3 | 4 | // ==============================|| PROGRESS - LINEAR WITH LABEL ||============================== // 5 | 6 | export default function LinearWithLabel({ value, ...others }: LinearProgressProps) { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | {`${Math.round(value!)}%`} 14 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /src/components/KeyValueTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid, GridSpacing, Skeleton } from "@mui/material"; 3 | import { ResponsiveStyleValue } from "@mui/system"; 4 | 5 | export interface KeyValueData { 6 | key?: React.ReactNode; 7 | value?: React.ReactNode; 8 | } 9 | 10 | export interface KeyValueTableProps { 11 | data: KeyValueData[]; 12 | rowSpacing?: ResponsiveStyleValue; 13 | columnSpacing?: ResponsiveStyleValue; 14 | classes?: { 15 | rowContainer?: string; 16 | rowItem?: string; 17 | columnContainer?: string; 18 | columnItemKey?: string; 19 | columnItemValue?: string; 20 | }; 21 | isLoading?: boolean; 22 | isValueLoading?: boolean; 23 | } 24 | 25 | const KeyValueTable: React.FC = ({ 26 | data, 27 | rowSpacing, 28 | columnSpacing, 29 | classes, 30 | isLoading, 31 | isValueLoading 32 | }) => ( 33 | 34 | {data.map((item, index) => ( 35 | 36 | {isLoading ? ( 37 | 38 | ) : ( 39 | 40 | 41 | {item.key} 42 | 43 | 44 | {isValueLoading ? : item.value} 45 | 46 | 47 | )} 48 | 49 | ))} 50 | 51 | ); 52 | 53 | export default KeyValueTable; 54 | -------------------------------------------------------------------------------- /src/components/Loadable.tsx: -------------------------------------------------------------------------------- 1 | import { ElementType, Suspense } from 'react'; 2 | 3 | // project import 4 | import Loader from './Loader'; 5 | 6 | // ==============================|| LOADABLE - LAZY LOADING ||============================== // 7 | 8 | const Loadable = (Component: ElementType) => (props: any) => 9 | ( 10 | }> 11 | 12 | 13 | ); 14 | 15 | export default Loadable; 16 | -------------------------------------------------------------------------------- /src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import LinearProgress, { LinearProgressProps } from "@mui/material/LinearProgress"; 5 | import { Box } from "@mui/material"; 6 | 7 | // project import 8 | import { makeStyles } from "@/themes/hooks"; 9 | 10 | const useStyles = makeStyles()((theme) => ({ 11 | wrapper: { 12 | position: "fixed", 13 | top: 0, 14 | left: 0, 15 | zIndex: 2001, 16 | width: "100%", 17 | "& > * + *": { 18 | marginTop: theme.spacing(2) 19 | } 20 | } 21 | })); 22 | 23 | // ==============================|| Loader ||============================== // 24 | 25 | export interface LoaderProps extends LinearProgressProps {} 26 | 27 | const Loader: React.FC = (props) => { 28 | const { classes } = useStyles(); 29 | 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default Loader; 38 | -------------------------------------------------------------------------------- /src/components/RTLLayout.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, ReactNode } from "react"; 2 | 3 | // material-ui 4 | import { CacheProvider } from "@emotion/react"; 5 | import createCache, { StylisPlugin } from "@emotion/cache"; 6 | 7 | // third-party 8 | import rtlPlugin from "stylis-plugin-rtl"; 9 | 10 | // project import 11 | import useConfig from "@/hooks/useConfig"; 12 | 13 | // ==============================|| RTL LAYOUT ||============================== // 14 | 15 | interface Props { 16 | children: ReactNode; 17 | } 18 | 19 | const RTLLayout = ({ children }: Props) => { 20 | const { themeDirection } = useConfig(); 21 | 22 | useEffect(() => { 23 | document.dir = themeDirection; 24 | }, [themeDirection]); 25 | 26 | const cacheRtl = createCache({ 27 | key: themeDirection === "rtl" ? "rtl" : "css", 28 | prepend: true, 29 | stylisPlugins: themeDirection === "rtl" ? [rtlPlugin as StylisPlugin] : [] 30 | }); 31 | 32 | return {children}; 33 | }; 34 | 35 | export default RTLLayout; 36 | -------------------------------------------------------------------------------- /src/components/ScrollTop.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect } from 'react'; 2 | import { useLocation } from 'react-router-dom'; 3 | 4 | // ==============================|| NAVIGATION - SCROLL TO TOP ||============================== // 5 | 6 | const ScrollTop = ({ children }: { children: ReactElement | null }) => { 7 | const location = useLocation(); 8 | const { pathname } = location; 9 | 10 | useEffect(() => { 11 | window.scrollTo({ 12 | top: 0, 13 | left: 0, 14 | behavior: 'smooth' 15 | }); 16 | }, [pathname]); 17 | 18 | return children || null; 19 | }; 20 | 21 | export default ScrollTop; 22 | -------------------------------------------------------------------------------- /src/components/ScrollX.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { styled } from '@mui/material/styles'; 3 | 4 | const ScrollX = styled('div')({ 5 | width: '100%', 6 | overflowX: 'auto', 7 | display: 'block' 8 | }); 9 | 10 | export default ScrollX; 11 | -------------------------------------------------------------------------------- /src/components/cards/ComponentHeader.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Box, Grid, Link, Stack, Typography } from '@mui/material'; 3 | 4 | // assets 5 | import { GlobalOutlined, NodeExpandOutlined } from '@ant-design/icons'; 6 | 7 | // ==============================|| COMPONENTS - BREADCRUMBS ||============================== // 8 | 9 | interface Props { 10 | title: string; 11 | caption?: string; 12 | directory?: string; 13 | link?: string; 14 | } 15 | 16 | const ComponentHeader = ({ title, caption, directory, link }: Props) => ( 17 | 18 | 19 | {title} 20 | {caption && ( 21 | 22 | {caption} 23 | 24 | )} 25 | 26 | 27 | {directory && ( 28 | 29 | 30 | 31 | {directory} 32 | 33 | 34 | )} 35 | {link && ( 36 | 37 | 38 | 39 | {link} 40 | 41 | 42 | )} 43 | 44 | 45 | ); 46 | 47 | export default ComponentHeader; 48 | -------------------------------------------------------------------------------- /src/components/cards/skeleton/ProductPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { CardContent, Grid, Skeleton, Stack } from "@mui/material"; 3 | 4 | // project import 5 | import MainCard from "@/components/MainCard"; 6 | 7 | // ===========================|| SKELETON TOTAL GROWTH BAR CHART ||=========================== // 8 | 9 | const ProductPlaceholder = () => ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | export default ProductPlaceholder; 45 | -------------------------------------------------------------------------------- /src/components/cards/statistics/AnalyticEcommerce.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Box, Chip, ChipProps, Grid, Stack, Typography } from "@mui/material"; 3 | 4 | // project import 5 | import MainCard from "@/components/MainCard"; 6 | 7 | // assets 8 | import { RiseOutlined, FallOutlined } from "@ant-design/icons"; 9 | 10 | // ==============================|| STATISTICS - ECOMMERCE CARD ||============================== // 11 | 12 | interface Props { 13 | title: string; 14 | count: string; 15 | percentage?: number; 16 | isLoss?: boolean; 17 | color?: ChipProps["color"]; 18 | extra: string; 19 | } 20 | 21 | const AnalyticEcommerce = ({ color = "primary", title, count, percentage, isLoss, extra }: Props) => ( 22 | 23 | 24 | 25 | {title} 26 | 27 | 28 | 29 | 30 | {count} 31 | 32 | 33 | {percentage && ( 34 | 35 | 40 | {!isLoss && } 41 | {isLoss && } 42 | 43 | } 44 | label={`${percentage}%`} 45 | sx={{ ml: 1.25, pl: 1 }} 46 | size="small" 47 | /> 48 | 49 | )} 50 | 51 | 52 | 53 | 54 | You made an extra{" "} 55 | 56 | {extra} 57 | {" "} 58 | this year 59 | 60 | 61 | 62 | ); 63 | 64 | export default AnalyticEcommerce; 65 | -------------------------------------------------------------------------------- /src/components/cards/statistics/AnalyticsDataCard.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Box, Chip, ChipProps, Stack, Typography } from "@mui/material"; 3 | 4 | // project import 5 | import MainCard from "@/components/MainCard"; 6 | 7 | // assets 8 | import { RiseOutlined, FallOutlined } from "@ant-design/icons"; 9 | 10 | // ==============================|| STATISTICS - ECOMMERCE CARD ||============================== // 11 | 12 | interface Props { 13 | title: string; 14 | count: string; 15 | percentage?: number; 16 | isLoss?: boolean; 17 | color?: ChipProps["color"]; 18 | children: any; 19 | } 20 | 21 | const AnalyticsDataCard = ({ color = "primary", title, count, percentage, isLoss, children }: Props) => ( 22 | 23 | 24 | 25 | 26 | {title} 27 | 28 | 29 | 30 | {count} 31 | 32 | {percentage && ( 33 | 38 | {!isLoss && } 39 | {isLoss && } 40 | 41 | } 42 | label={`${percentage}%`} 43 | sx={{ ml: 1.25, pl: 1 }} 44 | size="small" 45 | /> 46 | )} 47 | 48 | 49 | 50 | {children} 51 | 52 | ); 53 | 54 | export default AnalyticsDataCard; 55 | -------------------------------------------------------------------------------- /src/components/logo/LogoMain.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { useTheme } from '@mui/material/styles'; 3 | // import logoDark from 'assets/images/logo-dark.svg'; 4 | // import logo from 'assets/images/logo.svg'; 5 | 6 | /** 7 | * if you want to use image instead of uncomment following. 8 | * 9 | * import logoDark from 'assets/images/logo-dark.svg'; 10 | * import logo from 'assets/images/logo.svg'; 11 | * 12 | */ 13 | 14 | // ==============================|| LOGO SVG ||============================== // 15 | 16 | const LogoMain = ({ reverse, ...others }: { reverse?: boolean }) => { 17 | const theme = useTheme(); 18 | return ( 19 | /** 20 | * if you want to use image instead of svg uncomment following, and comment out element. 21 | * 22 | * Mantis 23 | * 24 | */ 25 | <> 26 | MOEU 27 | 28 | ); 29 | }; 30 | 31 | export default LogoMain; 32 | -------------------------------------------------------------------------------- /src/components/logo/index.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | import { To } from "history"; 3 | 4 | // material-ui 5 | import { ButtonBase } from "@mui/material"; 6 | import { SxProps } from "@mui/system"; 7 | 8 | // project import 9 | import Logo from "./LogoMain"; 10 | import LogoIcon from "./LogoIcon"; 11 | import config from "@/config"; 12 | 13 | // ==============================|| MAIN LOGO ||============================== // 14 | 15 | interface Props { 16 | reverse?: boolean; 17 | isIcon?: boolean; 18 | sx?: SxProps; 19 | to?: To; 20 | } 21 | 22 | const LogoSection = ({ reverse, isIcon, sx, to }: Props) => ( 23 | 24 | {isIcon ? : } 25 | 26 | ); 27 | 28 | export default LogoSection; 29 | -------------------------------------------------------------------------------- /src/components/third-party/SimpleBar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { alpha, Theme } from "@mui/material/styles"; 5 | import { Box } from "@mui/material"; 6 | 7 | // third-party 8 | import SimpleBar, { Props } from "simplebar-react"; 9 | import { BrowserView, MobileView } from "react-device-detect"; 10 | import { MUIStyledCommonProps } from "@mui/system"; 11 | import { makeStyles } from "@/themes/hooks"; 12 | 13 | const useStyles = makeStyles()((theme) => ({ 14 | root: { 15 | flexGrow: 1, 16 | height: "100%", 17 | overflow: "hidden" 18 | }, 19 | simpleBar: { 20 | maxHeight: "100%", 21 | "& .simplebar-scrollbar": { 22 | "&:before": { 23 | backgroundColor: alpha(theme.palette.grey[500], 0.48) 24 | }, 25 | "&.simplebar-visible:before": { 26 | opacity: 1 27 | } 28 | }, 29 | "& .simplebar-track.simplebar-vertical": { 30 | width: 10 31 | }, 32 | "& .simplebar-track.simplebar-horizontal .simplebar-scrollbar": { 33 | height: 6 34 | }, 35 | "& .simplebar-mask": { 36 | zIndex: "inherit" 37 | } 38 | } 39 | })); 40 | 41 | // ==============================|| SIMPLE SCROLL BAR ||============================== // 42 | 43 | type SimpleScrollBarProps = MUIStyledCommonProps & Props & { children?: React.ReactNode; className?: string }; 44 | 45 | const SimpleBarScroll: React.FC = ({ sx, children, className, ...other }) => { 46 | const { classes, cx } = useStyles(); 47 | 48 | return ( 49 | <> 50 | 51 | 52 | {children} 53 | 54 | 55 | 56 | 57 | {children} 58 | 59 | 60 | 61 | ); 62 | }; 63 | 64 | export default SimpleBarScroll; 65 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | // types 2 | import { DefaultConfigProps } from "@/types/config"; 3 | 4 | export const drawerWidth = 260; 5 | 6 | // ==============================|| THEME CONFIG ||============================== // 7 | 8 | const config: DefaultConfigProps = { 9 | defaultPath: "/", 10 | fontFamily: "'Roboto', sans-serif", 11 | miniDrawer: false, 12 | container: true, 13 | themeDirection: "ltr", 14 | title: "机场名称", 15 | title_split: " - ", 16 | background_url: "", 17 | description: "slogan", 18 | logo: "logourl", 19 | api: "后端API链接", 20 | languages: ["en-US","zh-CN","zh-TW"], 21 | emojiEndpoint: "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/{{code}}.png", 22 | startYear: 2021 23 | }; 24 | 25 | export default config; 26 | -------------------------------------------------------------------------------- /src/contexts/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ReactNode } from "react"; 2 | import { useLocalStorageState } from "ahooks"; 3 | 4 | // project import 5 | import config from "@/config"; 6 | 7 | // types 8 | import { CustomizationProps, FontFamily, ThemeDirection } from "@/types/config"; 9 | 10 | // initial state 11 | const initialState: CustomizationProps = { 12 | ...config, 13 | onChangeContainer: () => {}, 14 | onChangeDirection: (direction: ThemeDirection) => {}, 15 | onChangeMiniDrawer: (miniDrawer: boolean) => {}, 16 | onChangeFontFamily: (fontFamily: FontFamily) => {} 17 | }; 18 | 19 | // ==============================|| CONFIG CONTEXT & PROVIDER ||============================== // 20 | 21 | const ConfigContext = createContext(initialState); 22 | 23 | type ConfigProviderProps = { 24 | children: ReactNode; 25 | }; 26 | 27 | const ConfigProvider: React.FC = ({ children }) => { 28 | const [config, setConfig] = useLocalStorageState("mantis-react-ts-config", { 29 | defaultValue: initialState 30 | }); 31 | 32 | const onChangeContainer = () => { 33 | setConfig({ 34 | ...config, 35 | container: !config.container 36 | }); 37 | }; 38 | 39 | const onChangeDirection = (direction: ThemeDirection) => { 40 | setConfig({ 41 | ...config, 42 | themeDirection: direction 43 | }); 44 | }; 45 | 46 | const onChangeMiniDrawer = (miniDrawer: boolean) => { 47 | setConfig({ 48 | ...config, 49 | miniDrawer 50 | }); 51 | }; 52 | 53 | const onChangeFontFamily = (fontFamily: FontFamily) => { 54 | setConfig({ 55 | ...config, 56 | fontFamily 57 | }); 58 | }; 59 | 60 | return ( 61 | 70 | {children} 71 | 72 | ); 73 | }; 74 | 75 | export { ConfigProvider, ConfigContext }; 76 | -------------------------------------------------------------------------------- /src/hooks/useAuthStateDetector.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import lo from "lodash-es"; 3 | 4 | // project imports 5 | import { useDispatch, useSelector } from "@/store"; 6 | import { useGetUserInfoQuery } from "@/store/services/api"; 7 | import { logout } from "@/store/reducers/auth"; 8 | 9 | const useAuthStateDetector = () => { 10 | const dispatch = useDispatch(); 11 | const isLogin = useSelector((state) => state.auth.isLoggedIn); 12 | const { error } = useGetUserInfoQuery(undefined, { 13 | skip: !isLogin 14 | }); 15 | 16 | useEffect(() => { 17 | if (!lo.isEmpty(error)) { 18 | console.error(error); 19 | 20 | if (lo.isNumber((error as any).status)) { 21 | switch ((error as any).status) { 22 | case 401: 23 | case 403: 24 | dispatch(logout()); 25 | } 26 | } 27 | } 28 | }, [error]); 29 | 30 | return isLogin; 31 | }; 32 | 33 | export default useAuthStateDetector; 34 | -------------------------------------------------------------------------------- /src/hooks/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "react"; 2 | import { ConfigContext } from "@/contexts/ConfigContext"; 3 | 4 | // ==============================|| CONFIG - HOOKS ||============================== // 5 | 6 | const useConfig = () => useContext(ConfigContext); 7 | 8 | export default useConfig; 9 | -------------------------------------------------------------------------------- /src/hooks/useHtmlLangSelector.ts: -------------------------------------------------------------------------------- 1 | import { useTranslation } from "react-i18next"; 2 | import { useEffect } from "react"; 3 | 4 | const useHtmlLangSelector = () => { 5 | const { i18n } = useTranslation(); 6 | useEffect(() => { 7 | window.document.documentElement.lang = String(i18n.language).toLowerCase(); 8 | }, [i18n.language]); 9 | 10 | return window.document.documentElement.lang; 11 | }; 12 | 13 | export default useHtmlLangSelector; 14 | -------------------------------------------------------------------------------- /src/hooks/useKnowledge.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | // third-party 4 | import lo from "lodash-es"; 5 | import { useTranslation } from "react-i18next"; 6 | import { useParams } from "react-router-dom"; 7 | 8 | // project imports 9 | import { useGetKnowledgeQuery } from "@/store/services/api"; 10 | 11 | const useKnowledge = () => { 12 | const { i18n } = useTranslation(); 13 | const params = useParams<{ id: string }>(); 14 | const postId = useMemo(() => (!lo.isEmpty(params.id) && params.id ? parseInt(params.id) : null), [params.id]); 15 | return useGetKnowledgeQuery( 16 | { id: postId!, language: i18n.language }, 17 | { 18 | skip: !postId 19 | } 20 | ); 21 | }; 22 | 23 | export default useKnowledge; 24 | -------------------------------------------------------------------------------- /src/hooks/usePageAnalyticsEffect.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | // third-party 4 | import { useLocation } from "react-router-dom"; 5 | import ReactGA from "react-ga4"; 6 | 7 | // project imports 8 | import { useSelector } from "@/store"; 9 | import { useGetUserInfoQuery } from "@/store/services/api"; 10 | 11 | const usePageAnalyticsEffect = () => { 12 | const location = useLocation(); 13 | useEffect(() => { 14 | ReactGA.send({ 15 | hitType: "pageview", 16 | page: location.pathname + location.search 17 | }); 18 | console.log("PageviewStat:", location.pathname + location.search); 19 | }, [location]); 20 | 21 | const { isLoggedIn } = useSelector((state) => state.auth); 22 | const { data: userData } = useGetUserInfoQuery(undefined, { 23 | skip: !isLoggedIn 24 | }); 25 | useEffect(() => { 26 | ReactGA.set({ 27 | userEmail: userData?.email 28 | }); 29 | }, [userData]); 30 | }; 31 | 32 | export default usePageAnalyticsEffect; 33 | -------------------------------------------------------------------------------- /src/hooks/useQuery.ts: -------------------------------------------------------------------------------- 1 | import { useLocation } from "react-router-dom"; 2 | 3 | export interface useQueryFn { 4 | (): URLSearchParams; 5 | } 6 | 7 | const useQuery: useQueryFn = () => { 8 | const { search } = useLocation(); 9 | return new URLSearchParams(search); 10 | }; 11 | 12 | export default useQuery; 13 | -------------------------------------------------------------------------------- /src/hooks/useTitle.ts: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import lo from "lodash-es"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | // project imports 6 | import config from "@/config"; 7 | 8 | export interface useTitleFn { 9 | (title: string | null, deps?: React.DependencyList): void; 10 | } 11 | 12 | const useTitle: useTitleFn = (title, deps = []) => { 13 | const { t } = useTranslation(); 14 | useEffect(() => { 15 | window.document.title = lo.isNull(title) 16 | ? `${config.title}` 17 | : `${t(title, { ns: "title" })}${config.title_split}${config.title}`; 18 | }, [t, title, ...deps]); 19 | }; 20 | 21 | export default useTitle; 22 | -------------------------------------------------------------------------------- /src/i18n/i18next.d.ts: -------------------------------------------------------------------------------- 1 | import { KeyPrefix, Namespace } from "i18next"; 2 | import { UseTranslationOptions, UseTranslationResponse } from "react-i18next"; 3 | declare module "react-i18next" { 4 | export function useTranslation< 5 | N extends Namespace = "common", 6 | TKPrefix extends KeyPrefix = Record 7 | >(ns?: N | Readonly, options?: UseTranslationOptions): UseTranslationResponse; 8 | } 9 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18next from "i18next"; 2 | import { initReactI18next } from "react-i18next"; 3 | import i18nextBrowserLanguageDetector from "i18next-browser-languagedetector"; 4 | import resources from "./resources"; 5 | 6 | const i18n = i18next.createInstance({ 7 | lng: 'zh-CN', 8 | fallbackLng: "zh-CN", 9 | resources, 10 | fallbackNS: "common", 11 | nsSeparator: "::", 12 | interpolation: { 13 | escapeValue: false 14 | }, 15 | debug: import.meta.env.DEV 16 | }); 17 | 18 | i18n.use(initReactI18next); 19 | i18n.use(i18nextBrowserLanguageDetector); 20 | 21 | i18n.init().then( 22 | () => { 23 | console.log("i18n initialized"); 24 | }, 25 | (err) => { 26 | console.error("i18n initialization failed", err); 27 | } 28 | ); 29 | 30 | export default i18n; 31 | -------------------------------------------------------------------------------- /src/i18n/resources/en-us/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataGrid": { 3 | "no_rows": "No Data", 4 | "pagination": "{{from}} - {{to}} of {{count}} rows", 5 | "footer_row_selected_one": "{{count}} row selected", 6 | "footer_row_selected": "{{count}} rows selected", 7 | "rows_per_page": "Rows per page:", 8 | "sort_asc": "Sort Ascending", 9 | "sort_desc": "Sort Descending", 10 | "unsort": "Unsort", 11 | "filter": "Filter", 12 | "hide_column": "Hide Column", 13 | "show_column": "Show Column", 14 | "filter_panel_columns": "Columns", 15 | "filter_panel_operators": "Operators", 16 | "filter_panel_values": "Values", 17 | "filter_operator": { 18 | "is": "Is", 19 | "not": "Not", 20 | "after": "After", 21 | "on_or_after": "On or After", 22 | "after_or_equal": "After or Equal", 23 | "before": "Before", 24 | "on_or_before": "On or Before", 25 | "before_or_equal": "Before or Equal", 26 | "is_empty": "Is Empty", 27 | "is_not_empty": "Is Not Empty", 28 | "contains": "Contains", 29 | "does_not_contain": "Not Contain", 30 | "starts_with": "Starts With", 31 | "ends_with": "Ends With", 32 | "has": "Has", 33 | "has_not": "Has Not", 34 | "and": "And", 35 | "or": "Or", 36 | "add": "Add", 37 | "remove": "Remove", 38 | "between": "Between", 39 | "not_between": "Not Between", 40 | "is_any_of": "Is Any Of", 41 | "equals": "Equals", 42 | "not_equals": "Not Equals" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/i18n/resources/en-us/index.ts: -------------------------------------------------------------------------------- 1 | import { ResourceLanguage } from "i18next"; 2 | 3 | // translations 4 | import common from "./common.json"; 5 | import notice from "./notice.json"; 6 | import title from "./title.json"; 7 | import language from "./language.json"; 8 | import general from "./general.json"; 9 | 10 | export default { 11 | common, 12 | notice, 13 | title, 14 | language, 15 | general 16 | }; 17 | -------------------------------------------------------------------------------- /src/i18n/resources/en-us/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh-CN_name": "中文(简体)", 3 | "zh-TW_name": "中文(繁体)", 4 | "en-US_name": "English (US)" 5 | } -------------------------------------------------------------------------------- /src/i18n/resources/en-us/notice.json: -------------------------------------------------------------------------------- 1 | { 2 | "capslock_on": "Caps Lock is ON", 3 | "capslock_off": "Caps Lock is OFF", 4 | "send_email_code_success": "Email verification code sent successfully", 5 | "send_email_code_fail": "Failed to send email verification code", 6 | "remind_check_trash_box": "Please check your trash box", 7 | "forgot_password": { 8 | "reset_success": "Password reset successfully" 9 | }, 10 | "logout_success": "Logout succeeded", 11 | "logout_fail": "Logout failed", 12 | "login_success": "Login succeeded", 13 | "login_fail": "Login failed", 14 | "register_success": "Register succeeded", 15 | "register_fail": "Register failed", 16 | "copy_success": "Copy succeeded", 17 | "copy_fail": "Copy failed", 18 | "knowledge_post": { 19 | "loading_error": "Failed to load knowledge post" 20 | }, 21 | "refresh_success": "Refresh success", 22 | "refresh_fail": "Refresh failed", 23 | "language-change_success": "Language switched to $t(language::{{lng}}_name)", 24 | "language-change_fail": "Failed to switch language", 25 | "data-not-loaded": "The data has not been loaded, please refresh.", 26 | "period-not-selected": "Please select a payment period.", 27 | "order-created": "Order created.", 28 | "order-detail-error": "Failed to obtain order details: {{error}}", 29 | "order-cancel_success": "Order has been canceled.", 30 | "order-cancel_fail": "Failed to cancel order.", 31 | "checkout-failed": "Failed to call the payment gateway, please contact the manager.", 32 | "generate-invite-codes_success": "Invite codes generated successfully.", 33 | "generate-invite-codes_fail": "Failed to generate invite codes.", 34 | "update-user_success": "User information updated.", 35 | "update-user_fail": "Failed to update user information.", 36 | "reset-security_success": "Subscription reset.", 37 | "reset-security_fail": "Subscription reset failed.", 38 | "send-message_success": "Message sent.", 39 | "send-message_failed": "Failed to send message.", 40 | "send-message_empty": "Please enter a message.", 41 | "close-ticket_success": "Ticket closed.", 42 | "close-ticket_fail": "Failed to close ticket.", 43 | "no-subscription": "You have no subscription." 44 | } -------------------------------------------------------------------------------- /src/i18n/resources/en-us/title.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "App", 3 | "dashboard": "Dashboard", 4 | "login": "Login", 5 | "register": "Register", 6 | "forgot-password": "Forgot Password", 7 | "knowledge": "Knowledge", 8 | "subscription": "Subscription", 9 | "buy-plan": "Buy Plan", 10 | "not_found": "404 Not Found", 11 | "server_error": "Server Error", 12 | "checkout": "Checkout", 13 | "node-status": "Node Status", 14 | "order-list": "My Orders", 15 | "user": "User", 16 | "invitation": "Promotion", 17 | "invite": "Invite", 18 | "invite-commissions": "Commissions Log", 19 | "profile": "Profile", 20 | "service": "Service", 21 | "ticket": "My Tickets", 22 | "traffic": "Traffic" 23 | } -------------------------------------------------------------------------------- /src/i18n/resources/index.ts: -------------------------------------------------------------------------------- 1 | import { Resource } from "i18next"; 2 | 3 | // language resources 4 | import zhCn from "./zh-cn"; 5 | import enUs from "./en-us"; 6 | import zhTw from "./zh-tw"; 7 | 8 | const resources: Resource = { 9 | "zh-CN": zhCn, 10 | "en-US": enUs, 11 | "zh-TW": zhTw 12 | }; 13 | 14 | export default resources; 15 | -------------------------------------------------------------------------------- /src/i18n/resources/zh-cn/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataGrid": { 3 | "no_rows": "暂无数据", 4 | "pagination": "显示 {{from}} 到 {{to}} 项结果,共 {{count}} 项", 5 | "footer_row_selected": "已选择 {{count}} 项", 6 | "rows_per_page": "每页行数:", 7 | "sort_asc": "升序排列", 8 | "sort_desc": "降序排列", 9 | "unsort": "取消排序", 10 | "filter": "过滤", 11 | "hide_column": "隐藏列", 12 | "show_column": "显示列", 13 | "filter_panel_columns": "表头", 14 | "filter_panel_operators": "运算符", 15 | "filter_panel_values": "值", 16 | "filter_operator": { 17 | "is": "等于", 18 | "not": "不等于", 19 | "after": "晚于", 20 | "on_or_after": "晚于或等于", 21 | "after_or_equal": "晚于或等于", 22 | "before": "早于", 23 | "on_or_before": "早于或等于", 24 | "before_or_equal": "早于或等于", 25 | "is_empty": "为空", 26 | "is_not_empty": "不为空", 27 | "contains": "包含", 28 | "does_not_contain": "不包含", 29 | "starts_with": "开头是", 30 | "ends_with": "结尾是", 31 | "has": "包含", 32 | "has_not": "不包含", 33 | "and": "并且", 34 | "or": "或者", 35 | "add": "添加", 36 | "remove": "删除", 37 | "between": "介于", 38 | "not_between": "不介于", 39 | "is_any_of": "任意一个", 40 | "equals": "等于", 41 | "not_equals": "不等于" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-cn/index.ts: -------------------------------------------------------------------------------- 1 | import { ResourceLanguage } from "i18next"; 2 | 3 | // translations 4 | import common from "./common.json"; 5 | import notice from "./notice.json"; 6 | import title from "./title.json"; 7 | import language from "./language.json"; 8 | import general from "./general.json"; 9 | 10 | export default { 11 | common, 12 | notice, 13 | title, 14 | language, 15 | general 16 | }; 17 | -------------------------------------------------------------------------------- /src/i18n/resources/zh-cn/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh-CN_name": "中文(简体)", 3 | "zh-TW_name": "中文(繁体)", 4 | "en-US_name": "English (US)" 5 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-cn/notice.json: -------------------------------------------------------------------------------- 1 | { 2 | "capslock_on": "大写锁定已打开", 3 | "capslock_off": "大写锁定已关闭", 4 | "send_email_code_success": "验证码已发送至您的邮箱,请注意查收", 5 | "send_email_code_fail": "验证码发送失败,请稍后重试", 6 | "remind_check_trash_box": "请注意垃圾箱邮件", 7 | "forgot_password": { 8 | "reset_success": "密码重置成功" 9 | }, 10 | "logout_success": "登出成功", 11 | "logout_fail": "登出失败", 12 | "login_success": "登录成功", 13 | "login_fail": "登录失败", 14 | "register_success": "注册成功", 15 | "register_fail": "注册失败", 16 | "copy_success": "复制成功", 17 | "copy_fail": "复制失败", 18 | "knowledge_post": { 19 | "loading_error": "加载文章失败,请联系客服" 20 | }, 21 | "refresh_success": "刷新成功", 22 | "refresh_fail": "刷新失败", 23 | "language-change_success": "成功切换语言为 $t(language::{{lng}}_name)", 24 | "language-change_fail": "语言切换失败", 25 | "data-not-loaded": "数据未加载完成,请刷新页面", 26 | "period-not-selected": "请选择付款周期", 27 | "order-created": "订单已创建", 28 | "order-detail-error": "订单详情获取失败:{{error}}", 29 | "order-cancel_success": "订单已取消", 30 | "order-cancel_fail": "取消订单失败", 31 | "checkout-failed": "调用支付接口失败,请联系管理员", 32 | "generate-invite-codes_success": "邀请码已生成", 33 | "generate-invite-codes_fail": "邀请码生成失败", 34 | "update-user_success": "用户信息已更新", 35 | "update-user_fail": "用户信息更新失败", 36 | "reset-security_success": "订阅已重置", 37 | "reset-security_fail": "订阅重置失败", 38 | "send-message_success": "消息已发送", 39 | "send-message_failed": "消息发送失败", 40 | "send-message_empty": "消息不能为空", 41 | "close-ticket_success": "工单已关闭", 42 | "close-ticket_fail": "工单关闭失败", 43 | "no-subscription": "您还没有订阅,请先订阅" 44 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-cn/title.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "应用", 3 | "dashboard": "仪表盘", 4 | "login": "登录", 5 | "register": "注册", 6 | "forgot-password": "忘记密码", 7 | "knowledge": "使用文档", 8 | "subscription": "订阅", 9 | "buy-plan": "购买订阅", 10 | "not_found": "404 页面未找到", 11 | "server_error": "服务器错误", 12 | "checkout": "收银台", 13 | "node-status": "节点状态", 14 | "order-list": "订单列表", 15 | "user": "用户", 16 | "invitation": "推广管理", 17 | "invite": "邀请用户", 18 | "invite-commissions": "佣金记录", 19 | "profile": "个人资料", 20 | "service": "服务", 21 | "ticket": "我的工单", 22 | "traffic": "流量统计" 23 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-tw/general.json: -------------------------------------------------------------------------------- 1 | { 2 | "dataGrid": { 3 | "no_rows": "暫無數據", 4 | "pagination": "顯示 {{from}} 到 {{to}} 項結果,共 {{count}} 項", 5 | "footer_row_selected": "已選擇 {{count}} 項", 6 | "rows_per_page": "每頁行數:", 7 | "sort_asc": "升序排列", 8 | "sort_desc": "降序排列", 9 | "unsort": "取消排序", 10 | "filter": "過濾", 11 | "hide_column": "隱藏列", 12 | "show_column": "顯示列", 13 | "filter_panel_columns": "錶頭", 14 | "filter_panel_operators": "運算符", 15 | "filter_panel_values": "值", 16 | "filter_operator": { 17 | "is": "等於", 18 | "not": "不等於", 19 | "after": "晚於", 20 | "on_or_after": "晚於或等於", 21 | "after_or_equal": "晚於或等於", 22 | "before": "早於", 23 | "on_or_before": "早於或等於", 24 | "before_or_equal": "早於或等於", 25 | "is_empty": "為空", 26 | "is_not_empty": "不為空", 27 | "contains": "包含", 28 | "does_not_contain": "不包含", 29 | "starts_with": "開頭是", 30 | "ends_with": "結尾是", 31 | "has": "包含", 32 | "has_not": "不包含", 33 | "and": "併且", 34 | "or": "或者", 35 | "add": "添加", 36 | "remove": "刪除", 37 | "between": "介於", 38 | "not_between": "不介於", 39 | "is_any_of": "任意一個", 40 | "equals": "等於", 41 | "not_equals": "不等於" 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-tw/index.ts: -------------------------------------------------------------------------------- 1 | import { ResourceLanguage } from "i18next"; 2 | 3 | // translations 4 | import common from "./common.json"; 5 | import notice from "./notice.json"; 6 | import title from "./title.json"; 7 | import language from "./language.json"; 8 | import general from "./general.json"; 9 | 10 | export default { 11 | common, 12 | notice, 13 | title, 14 | language, 15 | general 16 | }; 17 | -------------------------------------------------------------------------------- /src/i18n/resources/zh-tw/language.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh-CN_name": "中文(简体)", 3 | "zh-TW_name": "中文(繁体)", 4 | "en-US_name": "English (US)" 5 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-tw/notice.json: -------------------------------------------------------------------------------- 1 | { 2 | "capslock_on": "大寫鎖定已打開", 3 | "capslock_off": "大寫鎖定已關閉", 4 | "send_email_code_success": "驗證碼已發送至您的信箱,請註意查收", 5 | "send_email_code_fail": "驗證碼發送失敗,請稍後重試", 6 | "remind_check_trash_box": "請註意垃圾箱郵件", 7 | "forgot_password": { 8 | "reset_success": "密碼重置成功" 9 | }, 10 | "logout_success": "登出成功", 11 | "logout_fail": "登出失敗", 12 | "login_success": "登入成功", 13 | "login_fail": "登入失敗", 14 | "register_success": "註冊成功", 15 | "register_fail": "註冊失敗", 16 | "copy_success": "復制成功", 17 | "copy_fail": "復制失敗", 18 | "knowledge_post": { 19 | "loading_error": "加載文章失敗,請聯繫客服" 20 | }, 21 | "refresh_success": "刷新成功", 22 | "refresh_fail": "刷新失敗", 23 | "language-change_success": "成功切換語言為 $t(language::{{lng}}_name)", 24 | "language-change_fail": "語言切換失敗", 25 | "data-not-loaded": "數據未加載完成,請刷新頁面", 26 | "period-not-selected": "請選擇付款周期", 27 | "order-created": "訂單已創建", 28 | "order-detail-error": "訂單詳情獲取失敗:{{error}}", 29 | "order-cancel_success": "訂單已取消", 30 | "order-cancel_fail": "取消訂單失敗", 31 | "checkout-failed": "調用支付接口失敗,請聯繫管理員", 32 | "generate-invite-codes_success": "邀請碼已生成", 33 | "generate-invite-codes_fail": "邀請碼生成失敗", 34 | "update-user_success": "用戶信息已更新", 35 | "update-user_fail": "用戶信息更新失敗", 36 | "reset-security_success": "訂閱已重置", 37 | "reset-security_fail": "訂閱重置失敗", 38 | "send-message_success": "消息已發送", 39 | "send-message_failed": "消息發送失敗", 40 | "send-message_empty": "消息不能為空", 41 | "close-ticket_success": "工單已關閉", 42 | "close-ticket_fail": "工單關閉失敗", 43 | "no-subscription": "您還沒有訂閱,請先訂閱" 44 | } -------------------------------------------------------------------------------- /src/i18n/resources/zh-tw/title.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "應用", 3 | "dashboard": "儀錶盤", 4 | "login": "登入", 5 | "register": "註冊", 6 | "forgot-password": "忘記密碼", 7 | "knowledge": "使用文檔", 8 | "subscription": "訂閱", 9 | "buy-plan": "購買訂閱", 10 | "not_found": "404 頁面未找到", 11 | "server_error": "服務器錯誤", 12 | "checkout": "收銀臺", 13 | "node-status": "節點狀態", 14 | "order-list": "訂單列錶", 15 | "user": "用戶", 16 | "invitation": "推廣管理", 17 | "invite": "邀請用戶", 18 | "invite-commissions": "傭金記錄", 19 | "profile": "個人資料", 20 | "service": "服務", 21 | "ticket": "我的工單", 22 | "traffic": "流量統計" 23 | } -------------------------------------------------------------------------------- /src/layout/CommonLayout/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy, Suspense } from "react"; 2 | import { Outlet } from "react-router-dom"; 3 | 4 | // material-ui 5 | import { Box } from "@mui/material"; 6 | import LinearProgress, { LinearProgressProps } from "@mui/material/LinearProgress"; 7 | 8 | // project import 9 | import Loadable from "@/components/Loadable"; 10 | import { makeStyles } from "@/themes/hooks"; 11 | 12 | const Header = lazy(() => import("./Header")); 13 | const FooterBlock = Loadable(lazy(() => import("./FooterBlock"))); 14 | 15 | // ==============================|| Loader ||============================== // 16 | 17 | const useStyles = makeStyles()((theme) => ({ 18 | loaderWrapper: { 19 | position: "fixed", 20 | top: 0, 21 | left: 0, 22 | zIndex: 2001, 23 | width: "100%", 24 | "& > * + *": { 25 | marginTop: theme.spacing(2) 26 | } 27 | } 28 | })); 29 | 30 | export interface LoaderProps extends LinearProgressProps {} 31 | 32 | const Loader: React.FC = (props) => { 33 | const { classes } = useStyles(); 34 | 35 | return ( 36 | 37 | 38 | 39 | ); 40 | }; 41 | 42 | // ==============================|| MINIMAL LAYOUT ||============================== // 43 | 44 | export interface CommonLayoutProps { 45 | layout?: string; 46 | } 47 | 48 | const CommonLayout: React.FC = ({ layout = "blank" }) => ( 49 | <> 50 | {/*{(layout === "landing" || layout === "simple") && (*/} 51 | {/* }>*/} 52 | {/*
*/} 53 | {/* */} 54 | {/* */} 55 | {/* */} 56 | {/*)}*/} 57 | {layout === "blank" && } 58 | 59 | ); 60 | 61 | export default CommonLayout; 62 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/DrawerContent/Navigation/NavGroup.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | // material-ui 4 | import { useTheme } from "@mui/material/styles"; 5 | import { Box, List, Typography } from "@mui/material"; 6 | 7 | // project import 8 | import NavItem from "./NavItem"; 9 | import NavCollapse from "./NavCollapse"; 10 | 11 | // types 12 | import { NavItemType } from "@/types/menu"; 13 | import { RootStateProps } from "@/types/root"; 14 | import { useTranslation } from "react-i18next"; 15 | 16 | // ==============================|| NAVIGATION - LIST GROUP ||============================== // 17 | 18 | interface Props { 19 | item: NavItemType; 20 | } 21 | 22 | const NavGroup = ({ item }: Props) => { 23 | const theme = useTheme(); 24 | const menu = useSelector((state: RootStateProps) => state.menu); 25 | const { t } = useTranslation(); 26 | const { drawerOpen } = menu; 27 | 28 | const navCollapse = item.children?.map((menuItem) => { 29 | switch (menuItem.type) { 30 | case "collapse": 31 | return ; 32 | case "item": 33 | return ; 34 | default: 35 | return ( 36 | 37 | Fix - Group Collapse or Items 38 | 39 | ); 40 | } 41 | }); 42 | 43 | return ( 44 | 49 | 50 | {t(item.title, { ns: "title" })} 51 | 52 | {item.caption && ( 53 | 54 | {item.caption} 55 | 56 | )} 57 | 58 | ) 59 | } 60 | sx={{ mt: drawerOpen && item.title ? 1.5 : 0, py: 0, zIndex: 0 }} 61 | > 62 | {navCollapse} 63 | 64 | ); 65 | }; 66 | 67 | export default NavGroup; 68 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/DrawerContent/Navigation/index.tsx: -------------------------------------------------------------------------------- 1 | import { useSelector } from "react-redux"; 2 | 3 | // material-ui 4 | import { Box, Typography } from "@mui/material"; 5 | 6 | // types 7 | import { RootStateProps } from "@/types/root"; 8 | 9 | // project import 10 | import NavGroup from "./NavGroup"; 11 | import menuItem from "@/menu-items"; 12 | 13 | // ==============================|| DRAWER CONTENT - NAVIGATION ||============================== // 14 | 15 | const Navigation = () => { 16 | const menu = useSelector((state: RootStateProps) => state.menu); 17 | const { drawerOpen } = menu; 18 | 19 | const navGroups = menuItem.items.map((item) => { 20 | switch (item.type) { 21 | case "group": 22 | return ; 23 | default: 24 | return ( 25 | 26 | Fix - Navigation Group 27 | 28 | ); 29 | } 30 | }); 31 | 32 | return ul:first-of-type": { mt: 0 } }}>{navGroups}; 33 | }; 34 | 35 | export default Navigation; 36 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/DrawerContent/index.tsx: -------------------------------------------------------------------------------- 1 | // project import 2 | import Navigation from "./Navigation"; 3 | import SimpleBar from "@/components/third-party/SimpleBar"; 4 | 5 | // ==============================|| DRAWER CONTENT ||============================== // 6 | 7 | const DrawerContent = () => ( 8 | 16 | 17 | 18 | ); 19 | 20 | export default DrawerContent; 21 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/DrawerHeader/DrawerHeaderStyled.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { styled, Theme } from '@mui/material/styles'; 3 | import { Box } from '@mui/material'; 4 | 5 | // ==============================|| DRAWER HEADER - STYLED ||============================== // 6 | 7 | interface Props { 8 | theme: Theme; 9 | open: boolean; 10 | } 11 | 12 | const DrawerHeaderStyled = styled(Box, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }: Props) => ({ 13 | ...theme.mixins.toolbar, 14 | display: 'flex', 15 | alignItems: 'center', 16 | justifyContent: open ? 'flex-start' : 'center', 17 | paddingLeft: theme.spacing(open ? 3 : 0) 18 | })); 19 | 20 | export default DrawerHeaderStyled; 21 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/DrawerHeader/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { useTheme } from "@mui/material/styles"; 5 | 6 | // project import 7 | import DrawerHeaderStyled from "./DrawerHeaderStyled"; 8 | import Logo from "@/components/logo"; 9 | 10 | // ==============================|| DRAWER HEADER ||============================== // 11 | 12 | interface Props { 13 | open: boolean; 14 | } 15 | 16 | const DrawerHeader: React.FC = ({ open }) => { 17 | const theme = useTheme(); 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default DrawerHeader; 27 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/MiniDrawerStyled.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { styled, Theme, CSSObject } from "@mui/material/styles"; 3 | import Drawer from "@mui/material/Drawer"; 4 | 5 | // project import 6 | import { drawerWidth } from "@/config"; 7 | 8 | const openedMixin = (theme: Theme): CSSObject => ({ 9 | width: drawerWidth, 10 | borderRight: `1px solid ${theme.palette.divider}`, 11 | transition: theme.transitions.create("width", { 12 | easing: theme.transitions.easing.sharp, 13 | duration: theme.transitions.duration.enteringScreen 14 | }), 15 | overflowX: "hidden", 16 | boxShadow: theme.palette.mode === "dark" ? theme.customShadows.z1 : "none" 17 | }); 18 | 19 | const closedMixin = (theme: Theme): CSSObject => ({ 20 | transition: theme.transitions.create("width", { 21 | easing: theme.transitions.easing.sharp, 22 | duration: theme.transitions.duration.leavingScreen 23 | }), 24 | overflowX: "hidden", 25 | width: theme.spacing(7.5), 26 | borderRight: "none", 27 | boxShadow: theme.customShadows.z1 28 | }); 29 | 30 | // ==============================|| DRAWER - MINI STYLED ||============================== // 31 | 32 | const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({ 33 | width: drawerWidth, 34 | flexShrink: 0, 35 | whiteSpace: "nowrap", 36 | boxSizing: "border-box", 37 | ...(open && { 38 | ...openedMixin(theme), 39 | "& .MuiDrawer-paper": openedMixin(theme) 40 | }), 41 | ...(!open && { 42 | ...closedMixin(theme), 43 | "& .MuiDrawer-paper": closedMixin(theme) 44 | }) 45 | })); 46 | 47 | export default MiniDrawerStyled; 48 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | 3 | // material-ui 4 | import { useTheme } from "@mui/material/styles"; 5 | import { Box, Drawer, useMediaQuery } from "@mui/material"; 6 | 7 | // project import 8 | import DrawerHeader from "./DrawerHeader"; 9 | import DrawerContent from "./DrawerContent"; 10 | import MiniDrawerStyled from "./MiniDrawerStyled"; 11 | import { drawerWidth } from "@/config"; 12 | 13 | // ==============================|| MAIN LAYOUT - DRAWER ||============================== // 14 | 15 | interface Props { 16 | open: boolean; 17 | window?: () => Window; 18 | handleDrawerToggle?: () => void; 19 | } 20 | 21 | const MainDrawer = ({ open, handleDrawerToggle, window }: Props) => { 22 | const theme = useTheme(); 23 | const matchDownMD = useMediaQuery(theme.breakpoints.down("lg")); 24 | 25 | // responsive drawer container 26 | const container = window !== undefined ? () => window().document.body : undefined; 27 | 28 | // header content 29 | const drawerContent = useMemo(() => , []); 30 | const drawerHeader = useMemo(() => , [open]); 31 | 32 | return ( 33 | 34 | {!matchDownMD ? ( 35 | 36 | {drawerHeader} 37 | {drawerContent} 38 | 39 | ) : ( 40 | 57 | {drawerHeader} 58 | {drawerContent} 59 | 60 | )} 61 | 62 | ); 63 | }; 64 | 65 | export default MainDrawer; 66 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Header/AppBarStyled.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { styled } from "@mui/material/styles"; 3 | import AppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; 4 | 5 | // project import 6 | import { drawerWidth } from "@/config"; 7 | 8 | // ==============================|| HEADER - APP BAR STYLED ||============================== // 9 | 10 | interface Props extends MuiAppBarProps { 11 | open?: boolean; 12 | } 13 | 14 | const AppBarStyled = styled(AppBar, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({ 15 | zIndex: theme.zIndex.drawer + 1, 16 | transition: theme.transitions.create(["width", "margin"], { 17 | easing: theme.transitions.easing.sharp, 18 | duration: theme.transitions.duration.leavingScreen 19 | }), 20 | ...(!open && { 21 | width: `calc(100% - ${theme.spacing(7.5)})` 22 | }), 23 | ...(open && { 24 | marginLeft: drawerWidth, 25 | width: `calc(100% - ${drawerWidth}px)`, 26 | transition: theme.transitions.create(["width", "margin"], { 27 | easing: theme.transitions.easing.sharp, 28 | duration: theme.transitions.duration.enteringScreen 29 | }) 30 | }) 31 | })); 32 | 33 | export default AppBarStyled; 34 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Header/HeaderContent/Title.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Typography } from "@mui/material"; 3 | import config from "@/config"; 4 | 5 | const Title: React.FC = () => ( 6 | 7 | {config.title} 8 | 9 | ); 10 | 11 | export default Title; 12 | -------------------------------------------------------------------------------- /src/layout/MainLayout/Header/HeaderContent/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Stack } from "@mui/material"; 5 | 6 | // project import 7 | /* import Search from "./Search"; */ 8 | import Profile from "./Profile"; 9 | import TicketMenu from "./TicketMenu"; 10 | import Title from "./Title"; 11 | import DarkModeSwitchButton from "@/layout/MainLayout/Header/HeaderContent/DarkModeSwitchButton"; 12 | import I18nSwitchButton from "@/layout/MainLayout/Header/HeaderContent/I18nSwitchButton"; 13 | 14 | // ==============================|| HEADER - CONTENT ||============================== // 15 | 16 | const HeaderContent: React.FC = () => { 17 | // const matchesXs = useMediaQuery((theme: Theme) => theme.breakpoints.down("md")); 18 | 19 | return ( 20 | <> 21 | 22 | {/*{!matchesXs && }*/} 23 | 24 | </Stack> 25 | 26 | <Stack direction={"row"} alignItems={"center"} spacing={1}> 27 | <TicketMenu /> 28 | <DarkModeSwitchButton /> 29 | <I18nSwitchButton /> 30 | <Profile /> 31 | </Stack> 32 | </> 33 | ); 34 | }; 35 | 36 | export default HeaderContent; 37 | -------------------------------------------------------------------------------- /src/middleware/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosHeaders } from "axios"; 2 | import lo from "lodash-es"; 3 | import config from "@/config"; 4 | 5 | const getBaseUrl = () => { 6 | if (import.meta.env.API_BASE_URL) { 7 | return import.meta.env.API_BASE_URL; 8 | } 9 | 10 | return new URL("/api/v1/", config.api).toString(); 11 | }; 12 | 13 | const instance = axios.create({ 14 | baseURL: getBaseUrl(), 15 | withCredentials: true, 16 | headers: { 17 | "Content-Type": "application/json, application/x-www-form-urlencoded", 18 | Accept: "application/json, application/x-www-form-urlencoded", 19 | Authorization: localStorage.getItem("gfw_token") || undefined 20 | }, 21 | timeout: 5000 22 | }); 23 | 24 | // monitor middleware 25 | if (import.meta.env.DEV) { 26 | instance.interceptors.request.use((config) => { 27 | console.log("Request", config); 28 | return config; 29 | }); 30 | instance.interceptors.response.use((response) => { 31 | console.log("Response", response); 32 | return response; 33 | }); 34 | } 35 | 36 | // Add authorization header to every request 37 | instance.interceptors.request.use((config) => { 38 | const token = localStorage.getItem("gfw_token"); 39 | if (!lo.isNull(token)) { 40 | if (config.headers instanceof AxiosHeaders) { 41 | config.headers.set("Authorization", token); 42 | } else { 43 | config.headers = new AxiosHeaders({ Authorization: token }); 44 | } 45 | } 46 | 47 | return config; 48 | }); 49 | 50 | export default instance; 51 | -------------------------------------------------------------------------------- /src/middleware/route-guard/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router"; 3 | 4 | // project import 5 | import { useSelector } from "@/store"; 6 | 7 | // types 8 | import { GuardProps } from "@/types/auth"; 9 | 10 | // ==============================|| AUTH GUARD ||============================== // 11 | 12 | const AuthGuard: React.FC<GuardProps> = ({ children }) => { 13 | const isLoggedIn = useSelector((state) => state.auth.isLoggedIn); 14 | 15 | return isLoggedIn ? children : <Navigate to={"/login"} />; 16 | }; 17 | 18 | export default AuthGuard; 19 | -------------------------------------------------------------------------------- /src/middleware/route-guard/GuestGuard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Navigate } from "react-router"; 3 | 4 | // project import 5 | import { useSelector } from "@/store"; 6 | 7 | // types 8 | import { GuardProps } from "@/types/auth"; 9 | 10 | // ==============================|| GUEST GUARD ||============================== // 11 | 12 | const GuestGuard: React.FC<GuardProps> = ({ children }: GuardProps) => { 13 | const { isLoggedIn } = useSelector((state) => state.auth); 14 | 15 | return isLoggedIn ? <Navigate to={"/dashboard"} /> : children; 16 | }; 17 | 18 | export default GuestGuard; 19 | -------------------------------------------------------------------------------- /src/model/api_response.ts: -------------------------------------------------------------------------------- 1 | export default interface ApiResponse<T = any> { 2 | data: T; 3 | errors?: Record<keyof T, string[]>; 4 | message?: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/model/commission.ts: -------------------------------------------------------------------------------- 1 | export default interface Commission { 2 | created_at: number; 3 | get_amount: number; 4 | id: number; 5 | order_amount: number; 6 | trade_no: string; 7 | } 8 | 9 | export interface CommissionQuery { 10 | current: number; 11 | page_size: number; 12 | } 13 | 14 | export interface CommissionResponse { 15 | data: Commission[]; 16 | total: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/model/config.ts: -------------------------------------------------------------------------------- 1 | export interface UserConfig { 2 | is_telegram: 0 | 1; 3 | telegram_discuss_link: string; 4 | stripe_pk: null; 5 | withdraw_methods: string[]; 6 | withdraw_close: number; 7 | currency: string; 8 | currency_symbol: string; 9 | commission_distribution_enable: number; 10 | commission_distribution_l1: null; 11 | commission_distribution_l2: null; 12 | commission_distribution_l3: null; 13 | } 14 | 15 | export interface GuestConfig { 16 | tos_url: null; 17 | is_email_verify: 0 | 1; 18 | is_invite_force: 0 | 1; 19 | email_whitelist_suffix: number; 20 | is_recaptcha: 0 | 1; 21 | recaptcha_site_key: null; 22 | app_description: string; 23 | app_url: string; 24 | logo: null; 25 | } 26 | -------------------------------------------------------------------------------- /src/model/coupon.ts: -------------------------------------------------------------------------------- 1 | import { PaymentPeriod } from "@/types/plan"; 2 | 3 | export default interface Coupon { 4 | id: number; 5 | code: string; 6 | name: string; 7 | type: 1 | 2; // @enum 1: 金额, 2: 折扣 8 | value: number; // 抵扣金额 or 折扣(0-1) 9 | show: 0 | 1; // @enum 0: 不显示, 1: 显示 10 | limit_use: null | number; // 最大使用次数 @enum null: 不限制, number: 限制次数 11 | limit_use_with_user: null | number; // 每个用户可使用次数 @enum null: 不限制, number: 限制次数 12 | limit_plan_ids: null | string[]; 13 | limit_period: null | PaymentPeriod[]; 14 | started_at: number | null; 15 | ended_at: number | null; 16 | created_at: number | null; 17 | updated_at: number | null; 18 | } 19 | 20 | export interface CouponPayload { 21 | code: string; 22 | plan_id: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/model/invite_data.ts: -------------------------------------------------------------------------------- 1 | export interface InviteCodeData { 2 | code: string; 3 | created_at: number; 4 | id: number; 5 | pv: number; 6 | status: InviteCodeStatus; 7 | updated_at: number; 8 | user_id: number; 9 | } 10 | 11 | export enum InviteCodeStatus { 12 | OK = 0 13 | } 14 | 15 | export default interface InviteData { 16 | codes: InviteCodeData[]; 17 | stat: { 18 | 0: number; // 已注册用户数 19 | 1: number; // 累计获得佣金 20 | 2: number; // 确认中的佣金 21 | 3: number; // 佣金比例 22 | 4: number; // 当前剩余佣金 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/model/knowledge.ts: -------------------------------------------------------------------------------- 1 | export interface KnowledgePayload { 2 | language: string; 3 | keyword: string; 4 | id: number; 5 | } 6 | 7 | export interface KnowledgeListResponse { 8 | id: number; 9 | title: string; 10 | category: string; 11 | updated_at: number; 12 | } 13 | 14 | export default interface Knowledge extends KnowledgeListResponse { 15 | body: string; 16 | sort: null; 17 | show: 0 | 1; 18 | language: string; 19 | created_at: number; 20 | } 21 | -------------------------------------------------------------------------------- /src/model/login.ts: -------------------------------------------------------------------------------- 1 | export type LoginPayload = { 2 | email: string; 3 | password: string; 4 | }; 5 | 6 | export type LoginResponse = { 7 | auth_data: string; 8 | is_admin: boolean; 9 | token: string; 10 | }; 11 | -------------------------------------------------------------------------------- /src/model/notice.ts: -------------------------------------------------------------------------------- 1 | export default interface Notice { 2 | id: number; 3 | title: string; 4 | content: string; 5 | created_at: number; 6 | updated_at: number; 7 | tags: string[] | null; 8 | img_url: string | null; 9 | show: 0 | 1; 10 | } 11 | -------------------------------------------------------------------------------- /src/model/order.ts: -------------------------------------------------------------------------------- 1 | import { PaymentPeriod } from "@/types/plan"; 2 | import Plan from "@/model/plan"; 3 | 4 | export enum OrderStatus { 5 | PENDING = 0, // 未支付 6 | PAID = 1, // 已支付 7 | CANCELLED = 2, // 已取消 8 | FINISHED = 3 // 已完成 9 | } 10 | 11 | export interface OrderPayload { 12 | plan_id: number; 13 | period: PaymentPeriod; 14 | coupon_code?: string; 15 | } 16 | 17 | export interface CheckoutOrderPayload { 18 | trade_no: string; 19 | method: number; 20 | } 21 | 22 | export default interface Order { 23 | id: number; 24 | invite_user_id: null | number; 25 | user_id: number; 26 | plan_id: number; 27 | coupon_id: null | number; 28 | payment_id: null | number; 29 | type: number; 30 | period: PaymentPeriod; 31 | trade_no: string; 32 | callback_no: null | string; 33 | total_amount: number; 34 | handling_amount: null | number; 35 | discount_amount: null | number; 36 | surplus_amount: null | number; 37 | refund_amount: null | number; 38 | balance_amount: null | number; 39 | surplus_order_ids: null | number[]; 40 | surplus_orders: null | Omit<Order, "plan">[]; 41 | status: OrderStatus; 42 | commission_status: number; 43 | commission_balance: number; 44 | actual_commission_balance: null; 45 | paid_at: null | number; 46 | created_at: number; 47 | updated_at: number; 48 | plan: Plan; 49 | try_out_plan_id: number; 50 | } 51 | -------------------------------------------------------------------------------- /src/model/password.ts: -------------------------------------------------------------------------------- 1 | export interface ChangePasswordPayload { 2 | old_password: string; 3 | new_password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/model/payment.ts: -------------------------------------------------------------------------------- 1 | export interface PaymentMethod { 2 | id: number; 3 | icon: string | null; 4 | name: string; 5 | payment: string; 6 | handling_fee_fixed: number | null; 7 | handling_fee_percent: number | null; 8 | } 9 | -------------------------------------------------------------------------------- /src/model/plan.ts: -------------------------------------------------------------------------------- 1 | export default interface Plan { 2 | id: number; 3 | group_id: number; 4 | transfer_enable: null | number; 5 | name: string; 6 | speed_limit: null | number; 7 | show: number; 8 | sort: number; 9 | renew: number; 10 | content: string; 11 | month_price: null | number; 12 | quarter_price: null | number; 13 | half_year_price: null | number; 14 | year_price: null | number; 15 | two_year_price: null | number; 16 | three_year_price: null | number; 17 | onetime_price: null | number; 18 | reset_price: null | number; 19 | reset_traffic_method: null; 20 | capacity_limit: null; 21 | created_at: number; 22 | updated_at: number; 23 | } 24 | -------------------------------------------------------------------------------- /src/model/register.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterPayload { 2 | email: string; 3 | password: string; 4 | invite_code: string; 5 | email_code: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/model/reset_password.ts: -------------------------------------------------------------------------------- 1 | export type ResetPasswordPayload = { 2 | email: string; 3 | password: string; 4 | email_code: string; 5 | }; 6 | -------------------------------------------------------------------------------- /src/model/send_mail.ts: -------------------------------------------------------------------------------- 1 | export default interface SendMailPayload { 2 | email: string; 3 | recaptcha_data?: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/model/server.ts: -------------------------------------------------------------------------------- 1 | export default interface Server { 2 | id: number; 3 | group_id: string[]; 4 | route_id: string[] | null; 5 | parent_id: number; 6 | tags: string[] | null; 7 | name: string; 8 | rate: string; 9 | host: string; 10 | port: number; 11 | server_port: number; 12 | cipher?: Cipher; 13 | obfs?: null; 14 | obfs_settings?: null; 15 | show: number; 16 | sort: number; 17 | created_at: number; 18 | updated_at: number; 19 | type: Type; 20 | last_check_at: string; 21 | tls?: number; 22 | network?: string; 23 | rules?: null; 24 | networkSettings?: null; 25 | tlsSettings?: null; 26 | ruleSettings?: null; 27 | dnsSettings?: null; 28 | } 29 | 30 | export enum Cipher { 31 | Chacha20IETFPoly1305 = "chacha20-ietf-poly1305" 32 | } 33 | export enum Type { 34 | Shadowsocks = "shadowsocks", 35 | V2Ray = "v2ray" 36 | } 37 | -------------------------------------------------------------------------------- /src/model/subscription.ts: -------------------------------------------------------------------------------- 1 | import type Plan from "@/model/plan"; 2 | 3 | export default interface Subscription { 4 | plan_id: number | null; 5 | token: string; 6 | expired_at: number | null; 7 | u: number; 8 | d: number; 9 | transfer_enable: number; 10 | email: string; 11 | uuid: string; 12 | plan: Plan | null; 13 | subscribe_url: string; 14 | reset_day: number | null; 15 | } 16 | -------------------------------------------------------------------------------- /src/model/telegram.ts: -------------------------------------------------------------------------------- 1 | export interface TelegramBotInfo { 2 | username: string; 3 | } 4 | -------------------------------------------------------------------------------- /src/model/ticket.ts: -------------------------------------------------------------------------------- 1 | export interface TicketPayload { 2 | subject: string; 3 | level: TicketLevel; 4 | message: string; 5 | } 6 | 7 | export enum TicketStatus { 8 | Open = 0, 9 | Closed = 1 10 | } 11 | 12 | export enum TicketReplyStatus { 13 | Replied = 0, 14 | Pending = 1 15 | } 16 | 17 | export enum TicketLevel { 18 | Low = 0, 19 | Medium = 1, 20 | High = 2 21 | } 22 | 23 | export const TicketLevelMap: Record<TicketLevel, string> = { 24 | [TicketLevel.Low]: "low", 25 | [TicketLevel.Medium]: "medium", 26 | [TicketLevel.High]: "high" 27 | }; 28 | 29 | export default interface Ticket { 30 | created_at: number; 31 | updated_at: number; 32 | id: number; 33 | level: TicketLevel; 34 | reply_status: TicketReplyStatus; 35 | status: TicketStatus; 36 | user_id: number; 37 | message: Message[]; 38 | subject: string; 39 | } 40 | 41 | export interface Message { 42 | id: number; 43 | user_id: number; 44 | ticket_id: number; 45 | message: string; 46 | created_at: number; 47 | updated_at: number; 48 | is_me: boolean; 49 | } 50 | 51 | export interface ReplyTicketPayload { 52 | id: number; 53 | message: string; 54 | } 55 | -------------------------------------------------------------------------------- /src/model/traffic.ts: -------------------------------------------------------------------------------- 1 | export interface TrafficLog { 2 | d: number; // download, in bytes 3 | record_at: number; 4 | server_rate: string; 5 | u: number; // upload, in bytes 6 | user_id: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/model/user.ts: -------------------------------------------------------------------------------- 1 | export default interface User { 2 | email: string; 3 | transfer_enable: number; 4 | last_login_at: null | number; 5 | created_at: number; 6 | banned: 0 | 1; 7 | remind_expire: 0 | 1; 8 | remind_traffic: 0 | 1; 9 | expired_at: null | number; 10 | balance: number; 11 | commission_balance: number; 12 | plan_id: number | null; 13 | discount: null | number; 14 | commission_rate: null | number; 15 | telegram_id: null | number; 16 | uuid: string; 17 | avatar_url: string; 18 | } 19 | 20 | export interface UserUpdatePayload { 21 | remind_traffic: 0 | 1; 22 | remind_expire: 0 | 1; 23 | } 24 | -------------------------------------------------------------------------------- /src/model/withdraw.ts: -------------------------------------------------------------------------------- 1 | export interface WithdrawPayload { 2 | withdraw_account: string; 3 | withdraw_method: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/auth/forgot-password.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Trans } from "react-i18next"; 3 | import { Link } from "react-router-dom"; 4 | 5 | // material-ui 6 | import { Grid, Stack, Typography } from "@mui/material"; 7 | 8 | // project import 9 | import AuthWrapper from "@/sections/auth/AuthWrapper"; 10 | import AuthForgotPassword from "@/sections/auth/auth-forms/AuthForgotPassword"; 11 | import useTitle from "@/hooks/useTitle"; 12 | 13 | // ================================|| FORGOT PASSWORD ||================================ // 14 | 15 | const ForgotPassword: React.FC = () => { 16 | useTitle("forgot-password"); 17 | 18 | return ( 19 | <AuthWrapper> 20 | <Grid container spacing={3}> 21 | <Grid item xs={12}> 22 | <Stack 23 | direction="row" 24 | justifyContent="space-between" 25 | alignItems="baseline" 26 | sx={{ mb: { xs: -0.5, sm: 0.5 } }} 27 | > 28 | <Typography variant="h3"> 29 | <Trans i18nKey={"forgot_password.title"}>Forgot password</Trans> 30 | </Typography> 31 | <Typography component={Link} to={"/login"} variant="body1" sx={{ textDecoration: "none" }} color="primary"> 32 | <Trans i18nKey={"forgot_password.back_to_login"}>Back to login</Trans> 33 | </Typography> 34 | </Stack> 35 | </Grid> 36 | <Grid item xs={12}> 37 | <AuthForgotPassword /> 38 | </Grid> 39 | </Grid> 40 | </AuthWrapper> 41 | ); 42 | }; 43 | 44 | export default ForgotPassword; 45 | -------------------------------------------------------------------------------- /src/pages/auth/login.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Trans } from "react-i18next"; 4 | 5 | // material-ui 6 | import { Grid, Stack, Typography } from "@mui/material"; 7 | 8 | // project import 9 | import AuthWrapper from "@/sections/auth/AuthWrapper"; 10 | import AuthLogin from "@/sections/auth/auth-forms/AuthLogin"; 11 | import useTitle from "@/hooks/useTitle"; 12 | 13 | // ================================|| LOGIN ||================================ // 14 | 15 | const Login: React.FC = () => { 16 | useTitle("login"); 17 | 18 | return ( 19 | <AuthWrapper> 20 | <Grid container spacing={3}> 21 | <Grid item xs={12}> 22 | <Stack 23 | direction="row" 24 | justifyContent="space-between" 25 | alignItems="baseline" 26 | sx={{ mb: { xs: -0.5, sm: 0.5 } }} 27 | > 28 | <Typography variant="h3"> 29 | <Trans>{"login.title"}</Trans> 30 | </Typography> 31 | <Typography 32 | component={Link} 33 | to={"/register"} 34 | variant="body1" 35 | sx={{ textDecoration: "none" }} 36 | color="primary" 37 | > 38 | <Trans>{"login.go-register"}</Trans> 39 | </Typography> 40 | </Stack> 41 | </Grid> 42 | <Grid item xs={12}> 43 | <AuthLogin /> 44 | </Grid> 45 | </Grid> 46 | </AuthWrapper> 47 | ); 48 | }; 49 | 50 | export default Login; 51 | -------------------------------------------------------------------------------- /src/pages/auth/register.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import { Trans } from "react-i18next"; 4 | 5 | // material-ui 6 | import { Grid, Stack, Typography } from "@mui/material"; 7 | 8 | // project import 9 | import AuthWrapper from "@/sections/auth/AuthWrapper"; 10 | import AuthRegister from "@/sections/auth/auth-forms/AuthRegister"; 11 | import useTitle from "@/hooks/useTitle"; 12 | 13 | // ================================|| REGISTER ||================================ // 14 | 15 | const Register: React.FC = () => { 16 | useTitle("register"); 17 | 18 | return ( 19 | <AuthWrapper> 20 | <Grid container spacing={3}> 21 | <Grid item xs={12}> 22 | <Stack 23 | direction="row" 24 | justifyContent="space-between" 25 | alignItems="baseline" 26 | sx={{ mb: { xs: -0.5, sm: 0.5 } }} 27 | > 28 | <Typography variant="h3"> 29 | <Trans>{"register.title"}</Trans> 30 | </Typography> 31 | <Typography component={Link} to={"/login"} variant="body1" sx={{ textDecoration: "none" }} color="primary"> 32 | <Trans>{"register.go-login"}</Trans> 33 | </Typography> 34 | </Stack> 35 | </Grid> 36 | <Grid item xs={12}> 37 | <AuthRegister /> 38 | </Grid> 39 | </Grid> 40 | </AuthWrapper> 41 | ); 42 | }; 43 | 44 | export default Register; 45 | -------------------------------------------------------------------------------- /src/pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project import 4 | import DashboardSection from "@/sections/dashboard"; 5 | import useTitle from "@/hooks/useTitle"; 6 | 7 | const Dashboard: React.FC = () => { 8 | useTitle("dashboard"); 9 | 10 | return <DashboardSection />; 11 | }; 12 | 13 | export default Dashboard; 14 | -------------------------------------------------------------------------------- /src/pages/invite/commissions.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useTitle from "@/hooks/useTitle"; 3 | import CommissionsPage from "@/sections/invite/commissionsPage"; 4 | 5 | const Commissions: React.FC = () => { 6 | useTitle("invite-commissions"); 7 | 8 | return <CommissionsPage />; 9 | }; 10 | 11 | export default Commissions; 12 | -------------------------------------------------------------------------------- /src/pages/invite/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import useTitle from "@/hooks/useTitle"; 3 | import InvitePage from "@/sections/invite/invitePage"; 4 | 5 | const Invite: React.FC = () => { 6 | useTitle("invite"); 7 | 8 | return <InvitePage />; 9 | }; 10 | 11 | export default Invite; 12 | -------------------------------------------------------------------------------- /src/pages/knowledge/[id].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Grid } from "@mui/material"; 5 | 6 | // project imports 7 | import useKnowledge from "@/hooks/useKnowledge"; 8 | import PostCard from "@/sections/knowledge/postCard"; 9 | import useTitle from "@/hooks/useTitle"; 10 | 11 | const KnowledgePost: React.FC = () => { 12 | const { data } = useKnowledge(); 13 | 14 | // set title 15 | useTitle(data?.title || "Loading...", [data]); 16 | 17 | return ( 18 | <Grid container> 19 | <Grid item xs={12}> 20 | <PostCard postId={data?.id || null} /> 21 | </Grid> 22 | </Grid> 23 | ); 24 | }; 25 | 26 | export default KnowledgePost; 27 | -------------------------------------------------------------------------------- /src/pages/knowledge/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Grid } from "@mui/material"; 5 | 6 | // project imports 7 | import useTitle from "@/hooks/useTitle"; 8 | 9 | // sections 10 | import Search from "@/sections/knowledge/search"; 11 | import PostList from "@/sections/knowledge/postList"; 12 | 13 | const Knowledge: React.FC = () => { 14 | useTitle("knowledge"); 15 | 16 | return ( 17 | <Grid container spacing={3}> 18 | <Grid item xs={12}> 19 | <Search /> 20 | </Grid> 21 | <PostList /> 22 | </Grid> 23 | ); 24 | }; 25 | 26 | export default Knowledge; 27 | -------------------------------------------------------------------------------- /src/pages/maintenance/500.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | // material-ui 4 | import { useTheme } from "@mui/material/styles"; 5 | import { Box, Button, Grid, Stack, Typography, useMediaQuery } from "@mui/material"; 6 | 7 | // project import 8 | import config from "@/config"; 9 | import useTitle from "@/hooks/useTitle"; 10 | 11 | // assets 12 | import error500 from "@/assets/images/maintenance/Error500.png"; 13 | 14 | // ==============================|| ERROR 500 - MAIN ||============================== // 15 | 16 | function Error500() { 17 | const theme = useTheme(); 18 | const matchDownSM = useMediaQuery(theme.breakpoints.down("sm")); 19 | useTitle("server_error"); 20 | 21 | return ( 22 | <> 23 | <Grid container direction="column" alignItems="center" justifyContent="center" sx={{ minHeight: "100vh" }}> 24 | <Grid item xs={12}> 25 | <Box sx={{ width: { xs: 350, sm: 396 } }}> 26 | <img src={error500} alt="mantis" style={{ height: "100%", width: "100%" }} /> 27 | </Box> 28 | </Grid> 29 | <Grid item xs={12}> 30 | <Stack justifyContent="center" alignItems="center"> 31 | <Typography align="center" variant={matchDownSM ? "h2" : "h1"}> 32 | Internal Server Error 33 | </Typography> 34 | <Typography 35 | color="textSecondary" 36 | variant="body2" 37 | align="center" 38 | sx={{ width: { xs: "73%", sm: "70%" }, mt: 1 }} 39 | > 40 | Server error 500. we fixing the problem. please try again at a later stage. 41 | </Typography> 42 | <Button component={Link} to={config.defaultPath} variant="contained" sx={{ textTransform: "none", mt: 4 }}> 43 | Go to homepage 44 | </Button> 45 | </Stack> 46 | </Grid> 47 | </Grid> 48 | </> 49 | ); 50 | } 51 | 52 | export default Error500; 53 | -------------------------------------------------------------------------------- /src/pages/maintenance/under-construction.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | // material-ui 4 | import { Box, Button, Grid, Stack, Typography } from "@mui/material"; 5 | 6 | // project import 7 | import config from "@/config"; 8 | 9 | // assets 10 | import construction from "@/assets/images/maintenance/under-construction.svg"; 11 | 12 | // ==============================|| UNDER CONSTRUCTION - MAIN ||============================== // 13 | 14 | function UnderConstruction() { 15 | return ( 16 | <Grid 17 | container 18 | spacing={4} 19 | direction="column" 20 | alignItems="center" 21 | justifyContent="center" 22 | sx={{ minHeight: "100vh", py: 2 }} 23 | > 24 | <Grid item xs={12}> 25 | <Box sx={{ width: { xs: 300, sm: 480 } }}> 26 | <img src={construction} alt="mantis" style={{ width: "100%", height: "auto" }} /> 27 | </Box> 28 | </Grid> 29 | <Grid item xs={12}> 30 | <Stack spacing={2} justifyContent="center" alignItems="center"> 31 | <Typography align="center" variant="h1"> 32 | Under Construction 33 | </Typography> 34 | <Typography color="textSecondary" align="center" sx={{ width: "85%" }}> 35 | Hey! Please check out this site later. We are doing some maintenance on it right now. 36 | </Typography> 37 | <Button component={Link} to={config.defaultPath} variant="contained"> 38 | Back To Home 39 | </Button> 40 | </Stack> 41 | </Grid> 42 | </Grid> 43 | ); 44 | } 45 | 46 | export default UnderConstruction; 47 | -------------------------------------------------------------------------------- /src/pages/node/status.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Status from "@/sections/node/status"; 3 | import useTitle from "@/hooks/useTitle"; 4 | 5 | const NodeStatus: React.FC = () => { 6 | useTitle("node-status"); 7 | 8 | return <Status />; 9 | }; 10 | 11 | export default NodeStatus; 12 | -------------------------------------------------------------------------------- /src/pages/order/[id].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | // material-ui 5 | import useTitle from "@/hooks/useTitle"; 6 | import { CheckoutProvider } from "@/sections/order/checkoutPage/context"; 7 | import CheckoutPage from "@/sections/order/checkoutPage"; 8 | 9 | const Checkout: React.FC = () => { 10 | useTitle("checkout"); 11 | 12 | const id = useParams<{ id: string }>().id; 13 | 14 | return ( 15 | <CheckoutProvider tradeNo={id || ""}> 16 | <CheckoutPage /> 17 | </CheckoutProvider> 18 | ); 19 | }; 20 | 21 | export default Checkout; 22 | -------------------------------------------------------------------------------- /src/pages/order/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project imports 4 | import useTitle from "@/hooks/useTitle"; 5 | import OrderListPage from "@/sections/order/listPage"; 6 | 7 | const OrderList: React.FC = () => { 8 | useTitle("order-list"); 9 | 10 | return <OrderListPage />; 11 | }; 12 | 13 | export default OrderList; 14 | -------------------------------------------------------------------------------- /src/pages/plan/buy/[id].tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | // project imports 5 | import useTitle from "@/hooks/useTitle"; 6 | import { PlanDetailProvider } from "@/sections/subscription/planDetailsPage/context"; 7 | import PlanDetailsPage from "@/sections/subscription/planDetailsPage"; 8 | 9 | const PlanDetails: React.FC = () => { 10 | useTitle("buy-plan"); 11 | 12 | const idRaw = useParams<{ id: string }>().id; 13 | const id = useMemo(() => (idRaw ? parseInt(idRaw) || 0 : 0), [idRaw]); 14 | 15 | return ( 16 | <PlanDetailProvider id={id}> 17 | <PlanDetailsPage /> 18 | </PlanDetailProvider> 19 | ); 20 | }; 21 | 22 | export default PlanDetails; 23 | -------------------------------------------------------------------------------- /src/pages/plan/buy/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project imports 4 | import { ShopProvider } from "@/sections/subscription/buyPage/context"; 5 | import useTitle from "@/hooks/useTitle"; 6 | import BuyPage from "@/sections/subscription/buyPage"; 7 | 8 | const PlanList: React.FC = () => { 9 | useTitle("buy-plan"); 10 | 11 | return ( 12 | <ShopProvider> 13 | <BuyPage /> 14 | </ShopProvider> 15 | ); 16 | }; 17 | 18 | export default PlanList; 19 | -------------------------------------------------------------------------------- /src/pages/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import ProfileSection from "@/sections/profile"; 4 | import useTitle from "@/hooks/useTitle"; 5 | 6 | const Profile: React.FC = () => { 7 | useTitle("profile"); 8 | 9 | return <ProfileSection />; 10 | }; 11 | 12 | export default Profile; 13 | -------------------------------------------------------------------------------- /src/pages/ticket/[id].tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useParams } from "react-router-dom"; 3 | 4 | // project imports 5 | import useTitle from "@/hooks/useTitle"; 6 | import { TicketProvider } from "@/sections/ticket/detailPage/context"; 7 | import TicketSection from "@/sections/ticket/detailPage"; 8 | 9 | // assets 10 | 11 | const TicketId: React.FC = () => { 12 | useTitle("ticket"); 13 | const id = useParams<{ id: string }>().id; 14 | 15 | return ( 16 | <TicketProvider id={parseInt(id ?? "0")}> 17 | <TicketSection /> 18 | </TicketProvider> 19 | ); 20 | }; 21 | 22 | export default TicketId; 23 | -------------------------------------------------------------------------------- /src/pages/ticket/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project imports 4 | import useTitle from "@/hooks/useTitle"; 5 | import { TicketProvider } from "@/sections/ticket/detailPage/context"; 6 | import TicketSection from "@/sections/ticket/detailPage"; 7 | 8 | // assets 9 | 10 | const Ticket: React.FC = () => { 11 | useTitle("ticket"); 12 | 13 | return ( 14 | <TicketProvider> 15 | <TicketSection /> 16 | </TicketProvider> 17 | ); 18 | }; 19 | 20 | export default Ticket; 21 | -------------------------------------------------------------------------------- /src/pages/traffic/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TrafficSection from "@/sections/traffic"; 3 | import useTitle from "@/hooks/useTitle"; 4 | 5 | const Traffic: React.FC = () => { 6 | useTitle("traffic"); 7 | 8 | return <TrafficSection />; 9 | }; 10 | 11 | export default Traffic; 12 | -------------------------------------------------------------------------------- /src/routes/LoginRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React, { lazy } from "react"; 2 | import { Navigate, RouteObject } from "react-router"; 3 | 4 | // project import 5 | import GuestGuard from "@/middleware/route-guard/GuestGuard"; 6 | import CommonLayout from "@/layout/CommonLayout"; 7 | import Loadable from "@/components/Loadable"; 8 | 9 | // render - login 10 | const AuthLogin = Loadable(lazy(() => import("@/pages/auth/login"))); 11 | const AuthRegister = Loadable(lazy(() => import("@/pages/auth/register"))); 12 | const AuthForgotPassword = Loadable(lazy(() => import("@/pages/auth/forgot-password"))); 13 | 14 | // ==============================|| AUTH ROUTING ||============================== // 15 | 16 | const LoginRoutes: RouteObject = { 17 | path: "/", 18 | children: [ 19 | { 20 | path: "/", 21 | element: ( 22 | <GuestGuard> 23 | <CommonLayout layout={"blank"} /> 24 | </GuestGuard> 25 | ), 26 | children: [ 27 | { 28 | path: "/", 29 | element: <Navigate to={"/login"} /> 30 | }, 31 | { 32 | path: "login", 33 | element: <AuthLogin /> 34 | }, 35 | { 36 | path: "register", 37 | element: <AuthRegister /> 38 | }, 39 | { 40 | path: "forgot-password", 41 | element: <AuthForgotPassword /> 42 | } 43 | // { 44 | // path: "reset-password", 45 | // element: <AuthResetPassword /> 46 | // } 47 | // { 48 | // path: "check-mail", 49 | // element: <AuthCheckMail /> 50 | // }, 51 | // { 52 | // path: "code-verification", 53 | // element: <AuthCodeVerification /> 54 | // } 55 | ] 56 | } 57 | ] 58 | }; 59 | 60 | export default LoginRoutes; 61 | -------------------------------------------------------------------------------- /src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRoutes } from "react-router-dom"; 2 | 3 | // project import 4 | import LoginRoutes from "./LoginRoutes"; 5 | import MainRoutes from "./MainRoutes"; 6 | 7 | // ==============================|| ROUTING RENDER ||============================== // 8 | 9 | export default () => useRoutes([LoginRoutes, MainRoutes]); 10 | -------------------------------------------------------------------------------- /src/sections/auth/AuthCard.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from "@mui/material/styles"; 3 | import { Box } from "@mui/material"; 4 | 5 | // project import 6 | import MainCard, { MainCardProps } from "@/components/MainCard"; 7 | 8 | // ==============================|| AUTHENTICATION - CARD WRAPPER ||============================== // 9 | 10 | const AuthCard = ({ children, ...other }: MainCardProps) => ( 11 | <MainCard 12 | sx={{ 13 | maxWidth: { xs: 400, lg: 475 }, 14 | margin: { xs: 2.5, md: 3 }, 15 | "& > *": { 16 | flexGrow: 1, 17 | flexBasis: "50%" 18 | } 19 | }} 20 | content={false} 21 | {...other} 22 | border={false} 23 | boxShadow 24 | shadow={(theme: Theme) => theme.customShadows.z1} 25 | > 26 | <Box sx={{ p: { xs: 2, sm: 3, md: 4, xl: 5 } }}>{children}</Box> 27 | </MainCard> 28 | ); 29 | 30 | export default AuthCard; 31 | -------------------------------------------------------------------------------- /src/sections/auth/AuthWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | 3 | // material-ui 4 | import { Box, Grid } from "@mui/material"; 5 | 6 | // project import 7 | import AuthFooter from "@/components/cards/AuthFooter"; 8 | import Logo from "@/components/logo"; 9 | import AuthCard from "./AuthCard"; 10 | import { makeStyles } from "@/themes/hooks"; 11 | 12 | // assets 13 | import AuthBackground from "@/assets/images/auth/AuthBackground"; 14 | 15 | interface Props { 16 | children: ReactNode; 17 | } 18 | 19 | const useStyles = makeStyles({ 20 | name: "AuthWrapper" 21 | })((theme) => ({ 22 | root: { minHeight: "100vh" }, 23 | logoGrid: { 24 | marginLeft: theme.spacing(3), 25 | marginTop: theme.spacing(3) 26 | }, 27 | authCardGrid: { 28 | minHeight: "calc(100vh - 210px)", 29 | [theme.breakpoints.up("sm")]: { 30 | minHeight: "calc(100vh - 134px)" 31 | }, 32 | [theme.breakpoints.up("md")]: { 33 | minHeight: "calc(100vh - 112px)" 34 | } 35 | }, 36 | footerGrid: { 37 | margin: theme.spacing(1, 3, 3, 3) 38 | } 39 | })); 40 | 41 | // ==============================|| AUTHENTICATION - WRAPPER ||============================== // 42 | 43 | const AuthWrapper: React.FC<Props> = ({ children }) => { 44 | const { classes } = useStyles(); 45 | 46 | return ( 47 | <Box className={classes.root}> 48 | <AuthBackground /> 49 | <Grid container direction="column" justifyContent="flex-end" className={classes.root}> 50 | <Grid item xs={12} className={classes.logoGrid}> 51 | <Logo /> 52 | </Grid> 53 | <Grid item xs={12}> 54 | <Grid item xs={12} container justifyContent="center" alignItems="center" className={classes.authCardGrid}> 55 | <Grid item> 56 | <AuthCard>{children}</AuthCard> 57 | </Grid> 58 | </Grid> 59 | </Grid> 60 | <Grid item xs={12} className={classes.footerGrid}> 61 | <AuthFooter /> 62 | </Grid> 63 | </Grid> 64 | </Box> 65 | ); 66 | }; 67 | 68 | export default AuthWrapper; 69 | -------------------------------------------------------------------------------- /src/sections/dashboard/alerts/orderPendingAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // third-party 4 | import { useTranslation } from "react-i18next"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | // material-ui 8 | import { Alert, Button } from "@mui/material"; 9 | 10 | // project imports 11 | import { useGetUserStatQuery } from "@/store/services/api"; 12 | 13 | const OrderPendingAlert: React.FC = () => { 14 | const { t } = useTranslation(); 15 | const navigate = useNavigate(); 16 | 17 | const { data } = useGetUserStatQuery(); 18 | 19 | return data && data[0] > 0 ? ( 20 | <Alert 21 | severity="warning" 22 | action={ 23 | <Button 24 | color="warning" 25 | size="small" 26 | onClick={() => { 27 | navigate("/order"); 28 | }} 29 | > 30 | {t("dashboard.alert.pending-order-action")} 31 | </Button> 32 | } 33 | > 34 | {t("dashboard.alert.pending-order", { count: data[0] })} 35 | </Alert> 36 | ) : ( 37 | <></> 38 | ); 39 | }; 40 | 41 | export default OrderPendingAlert; 42 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | // material-ui 5 | import { List } from "@mui/material"; 6 | 7 | // project imports 8 | import MainCard from "@/components/MainCard"; 9 | import TutorialButton from "@/sections/dashboard/shortcutCard/tutorialButton"; 10 | import SubscribeButton from "@/sections/dashboard/shortcutCard/subscribeButton"; 11 | import PurchaseButton from "@/sections/dashboard/shortcutCard/purchaseButton"; 12 | import TicketButton from "@/sections/dashboard/shortcutCard/ticketButton"; 13 | 14 | const ShortcutCard: React.FC = () => { 15 | const { t } = useTranslation(); 16 | 17 | return ( 18 | <MainCard title={t("dashboard.shortcut.title")} content={false}> 19 | <List 20 | sx={{ 21 | p: 0 22 | }} 23 | > 24 | <TutorialButton /> 25 | <SubscribeButton /> 26 | <PurchaseButton /> 27 | <TicketButton /> 28 | </List> 29 | </MainCard> 30 | ); 31 | }; 32 | 33 | export default ShortcutCard; 34 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/purchaseButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | 3 | // third-party 4 | import { useTranslation } from "react-i18next"; 5 | import { useNavigate } from "react-router-dom"; 6 | import ReactGA from "react-ga4"; 7 | 8 | // material-ui 9 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText, Typography } from "@mui/material"; 10 | 11 | // project imports 12 | import Avatar from "@/components/@extended/Avatar"; 13 | import { useGetUserInfoQuery } from "@/store/services/api"; 14 | 15 | // assets 16 | import { ShoppingOutlined } from "@ant-design/icons"; 17 | 18 | const PurchaseButton: React.FC = () => { 19 | const { t } = useTranslation(); 20 | const { data: userInfo } = useGetUserInfoQuery(); 21 | const hasPurchased = useMemo<boolean>(() => userInfo?.plan_id !== null, [userInfo]); 22 | 23 | const navigate = useNavigate(); 24 | const handleClick = (e: React.MouseEvent) => { 25 | e.preventDefault(); 26 | navigate(hasPurchased ? `/plan/buy/${userInfo?.plan_id}` : "/plan/buy"); 27 | 28 | ReactGA.event("click", { 29 | category: "shortcut", 30 | label: "go_purchase" 31 | }); 32 | }; 33 | 34 | return ( 35 | <ListItem disablePadding divider> 36 | <ListItemButton onClick={handleClick}> 37 | <ListItemAvatar> 38 | <Avatar alt="Basic" type="combined" color="warning"> 39 | <ShoppingOutlined /> 40 | </Avatar> 41 | </ListItemAvatar> 42 | <ListItemText 43 | primary={ 44 | <Typography variant={"body1"} noWrap> 45 | {t("dashboard.shortcut.purchase.primary", { 46 | context: hasPurchased ? "purchased" : "not_purchased" 47 | })} 48 | </Typography> 49 | } 50 | secondary={ 51 | <Typography variant={"caption"} color={"secondary"} noWrap> 52 | {t("dashboard.shortcut.purchase.secondary", { 53 | context: hasPurchased ? "purchased" : "not_purchased" 54 | })} 55 | </Typography> 56 | } 57 | /> 58 | </ListItemButton> 59 | </ListItem> 60 | ); 61 | }; 62 | 63 | export default PurchaseButton; 64 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/clashButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import MantisAvatar from "@/components/@extended/Avatar"; 10 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 11 | 12 | // asset imports 13 | import clashIcon from "@/assets/images/software/clash.png"; 14 | 15 | const ClashButton: React.FC = () => { 16 | const { t } = useTranslation(); 17 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 18 | 19 | const handleClick = () => { 20 | if (subscribeInfo) { 21 | const url = new URL(subscribeInfo.subscribe_url); 22 | url.searchParams.set("flag", "clash"); 23 | window.open(`clash://install-config?url=${encodeURIComponent(url.toString())}`, "_self"); 24 | 25 | ReactGA.event("click", { 26 | category: "shortcut", 27 | label: "quick_subscribe", 28 | method: "clash" 29 | }); 30 | } 31 | }; 32 | 33 | return ( 34 | <ListItem disablePadding divider> 35 | <ListItemButton onClick={handleClick}> 36 | <ListItemAvatar> 37 | <MantisAvatar alt="Clash" type="combined" color="secondary" src={clashIcon} /> 38 | </ListItemAvatar> 39 | <ListItemText primary={t("dashboard.shortcut.subscribe.clash")} /> 40 | </ListItemButton> 41 | </ListItem> 42 | ); 43 | }; 44 | 45 | export default ClashButton; 46 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/clashxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import MantisAvatar from "@/components/@extended/Avatar"; 10 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 11 | 12 | // asset imports 13 | import clashxIcon from "@/assets/images/software/clashx.png"; 14 | 15 | const ClashXButton: React.FC = () => { 16 | const { t } = useTranslation(); 17 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 18 | 19 | const handleClick = () => { 20 | if (subscribeInfo) { 21 | const url = new URL(subscribeInfo.subscribe_url); 22 | url.searchParams.set("flag", "clash"); 23 | window.open(`clash://install-config?url=${encodeURIComponent(url.toString())}`, "_self"); 24 | 25 | ReactGA.event("click", { 26 | category: "shortcut", 27 | label: "quick_subscribe", 28 | method: "clashX" 29 | }); 30 | } 31 | }; 32 | 33 | return ( 34 | <ListItem disablePadding divider> 35 | <ListItemButton onClick={handleClick}> 36 | <ListItemAvatar> 37 | <MantisAvatar alt="Clash X" type="combined" color="secondary" src={clashxIcon} /> 38 | </ListItemAvatar> 39 | <ListItemText primary={t("dashboard.shortcut.subscribe.clashx")} /> 40 | </ListItemButton> 41 | </ListItem> 42 | ); 43 | }; 44 | 45 | export default ClashXButton; 46 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/copyLinkButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { useSnackbar } from "notistack"; 4 | import ReactGA from "react-ga4"; 5 | 6 | // material-ui 7 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 8 | import { CopyOutlined } from "@ant-design/icons"; 9 | 10 | // project imports 11 | import MantisAvatar from "@/components/@extended/Avatar"; 12 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 13 | 14 | const CopyLinkButton: React.FC = () => { 15 | const { t } = useTranslation(); 16 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 17 | const { enqueueSnackbar } = useSnackbar(); 18 | 19 | const handleClick = () => { 20 | if (subscribeInfo) { 21 | navigator.clipboard.writeText(subscribeInfo.subscribe_url).then(() => { 22 | enqueueSnackbar(t("notice::copy_success"), { 23 | variant: "success" 24 | }); 25 | }); 26 | 27 | ReactGA.event("click", { 28 | category: "shortcut", 29 | label: "quick_subscribe", 30 | method: "copy" 31 | }); 32 | } else { 33 | enqueueSnackbar(t("notice::copy_fail"), { 34 | variant: "error" 35 | }); 36 | } 37 | }; 38 | 39 | return ( 40 | <ListItem disablePadding divider> 41 | <ListItemButton onClick={handleClick}> 42 | <ListItemAvatar> 43 | <MantisAvatar alt="Copy" type="combined" color="secondary"> 44 | <CopyOutlined /> 45 | </MantisAvatar> 46 | </ListItemAvatar> 47 | <ListItemText primary={t("dashboard.shortcut.subscribe.copy")} /> 48 | </ListItemButton> 49 | </ListItem> 50 | ); 51 | }; 52 | 53 | export default CopyLinkButton; 54 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/quantumultxButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 10 | import MantisAvatar from "@/components/@extended/Avatar"; 11 | import config from "@/config"; 12 | 13 | // assets 14 | import quantumultxIcon from "@/assets/images/software/quantumultx.png"; 15 | 16 | const QuantumultXButton: React.FC = () => { 17 | const { t } = useTranslation(); 18 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 19 | 20 | const handleClick = () => { 21 | if (subscribeInfo) { 22 | const url = new URL(subscribeInfo.subscribe_url); 23 | url.searchParams.set("flag", "quantumult x"); 24 | window.open( 25 | `quantumult-x:///update-configuration?remote-resource=${encodeURI( 26 | JSON.stringify({ 27 | server_remote: [`"${url.toString()}, tag=${config.title}"`] 28 | }) 29 | )}`, 30 | "_self" 31 | ); 32 | 33 | ReactGA.event("click", { 34 | category: "shortcut", 35 | label: "quick_subscribe", 36 | method: "quantumult-x" 37 | }); 38 | } 39 | }; 40 | 41 | return ( 42 | <ListItem disablePadding divider> 43 | <ListItemButton onClick={handleClick}> 44 | <ListItemAvatar> 45 | <MantisAvatar alt="Quantumult X" type="combined" color="secondary" src={quantumultxIcon} /> 46 | </ListItemAvatar> 47 | <ListItemText primary={t("dashboard.shortcut.subscribe.quantumultx")} /> 48 | </ListItemButton> 49 | </ListItem> 50 | ); 51 | }; 52 | 53 | export default QuantumultXButton; 54 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/scanQrCodeButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import { QRCodeCanvas } from "qrcode.react"; 4 | 5 | // material-ui 6 | import { 7 | Dialog, 8 | DialogContent, 9 | DialogTitle, 10 | ListItem, 11 | ListItemAvatar, 12 | ListItemButton, 13 | ListItemText, 14 | Typography 15 | } from "@mui/material"; 16 | import { QrcodeOutlined } from "@ant-design/icons"; 17 | 18 | // project imports 19 | import MantisAvatar from "@/components/@extended/Avatar"; 20 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 21 | 22 | const ScanQRCodeButton: React.FC = () => { 23 | const { t } = useTranslation(); 24 | const [open, setOpen] = useState(false); 25 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 26 | 27 | return ( 28 | <> 29 | <ListItem disablePadding divider> 30 | <ListItemButton onClick={() => setOpen(true)}> 31 | <ListItemAvatar> 32 | <MantisAvatar alt="Scan QRCode" type="combined" color="secondary"> 33 | <QrcodeOutlined /> 34 | </MantisAvatar> 35 | </ListItemAvatar> 36 | <ListItemText primary={t("dashboard.shortcut.subscribe.scan")} /> 37 | </ListItemButton> 38 | </ListItem> 39 | <Dialog open={open} onClose={() => setOpen(false)}> 40 | <DialogTitle>{t("dashboard.shortcut.subscribe.scan")}</DialogTitle> 41 | <DialogContent 42 | sx={{ 43 | display: "flex", 44 | flexDirection: "column", 45 | alignItems: "center" 46 | }} 47 | > 48 | {subscribeInfo && <QRCodeCanvas value={subscribeInfo.subscribe_url} level={"Q"} includeMargin size={256} />} 49 | <Typography variant="body2" color="textSecondary"> 50 | {t("dashboard.shortcut.subscribe.scan_tips")} 51 | </Typography> 52 | </DialogContent> 53 | </Dialog> 54 | </> 55 | ); 56 | }; 57 | 58 | export default ScanQRCodeButton; 59 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/shadowrocketButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import MantisAvatar from "@/components/@extended/Avatar"; 10 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 11 | import { Base64Encode } from "@/utils/crypto"; 12 | import config from "@/config"; 13 | 14 | // asset 15 | import shadowrocketIcon from "@/assets/images/software/shadowrocket.png"; 16 | 17 | const ShadowrocketButton: React.FC = () => { 18 | const { t } = useTranslation(); 19 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 20 | 21 | const handleClick = () => { 22 | if (subscribeInfo) { 23 | const url = new URL(subscribeInfo.subscribe_url); 24 | url.searchParams.set("flag", "shadowrocket"); 25 | window.open( 26 | `shadowrocket://add/sub://${Base64Encode(url.toString())}?remark=${encodeURIComponent(config.title)}`, 27 | "_self" 28 | ); 29 | 30 | ReactGA.event("click", { 31 | category: "shortcut", 32 | label: "quick_subscribe", 33 | method: "shadowrocket" 34 | }); 35 | } 36 | }; 37 | 38 | return ( 39 | <ListItem disablePadding divider> 40 | <ListItemButton onClick={handleClick}> 41 | <ListItemAvatar> 42 | <MantisAvatar alt="Shadowrocket" type="combined" color="secondary" src={shadowrocketIcon} /> 43 | </ListItemAvatar> 44 | <ListItemText primary={t("dashboard.shortcut.subscribe.shadowrocket")} /> 45 | </ListItemButton> 46 | </ListItem> 47 | ); 48 | }; 49 | 50 | export default ShadowrocketButton; 51 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/stashButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 10 | import MantisAvatar from "@/components/@extended/Avatar"; 11 | import config from "@/config"; 12 | 13 | // asset 14 | import stashIcon from "@/assets/images/software/stash.png"; 15 | 16 | const StashButton: React.FC = () => { 17 | const { t } = useTranslation(); 18 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 19 | 20 | const handleClick = () => { 21 | if (subscribeInfo) { 22 | const url = new URL(subscribeInfo.subscribe_url); 23 | url.searchParams.set("flag", "stash"); 24 | window.open( 25 | `stash:///install-config?url=${encodeURIComponent(url.toString())}&name=${encodeURIComponent(config.title)}`, 26 | "_self" 27 | ); 28 | 29 | ReactGA.event("click", { 30 | category: "shortcut", 31 | label: "quick_subscribe", 32 | method: "stash" 33 | }); 34 | } 35 | }; 36 | 37 | return ( 38 | <ListItem disablePadding divider> 39 | <ListItemButton onClick={handleClick}> 40 | <ListItemAvatar> 41 | <MantisAvatar alt="Stash" type="combined" color="secondary" src={stashIcon} /> 42 | </ListItemAvatar> 43 | <ListItemText primary={t("dashboard.shortcut.subscribe.stash")} /> 44 | </ListItemButton> 45 | </ListItem> 46 | ); 47 | }; 48 | 49 | export default StashButton; 50 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/surfboardButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 10 | import MantisAvatar from "@/components/@extended/Avatar"; 11 | import config from "@/config"; 12 | 13 | // asset 14 | import surfboardIcon from "@/assets/images/software/surfboard.png"; 15 | 16 | const SurfboardButton: React.FC = () => { 17 | const { t } = useTranslation(); 18 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 19 | 20 | const handleClick = () => { 21 | if (subscribeInfo) { 22 | const url = new URL(subscribeInfo.subscribe_url); 23 | url.searchParams.set("flag", "surfboard"); 24 | window.open( 25 | `surge:///install-config?url=${encodeURIComponent(url.toString())}&name=${encodeURIComponent(config.title)}`, 26 | "_self" 27 | ); 28 | 29 | ReactGA.event("click", { 30 | category: "shortcut", 31 | label: "quick_subscribe", 32 | method: "surfboard" 33 | }); 34 | } 35 | }; 36 | 37 | return ( 38 | <ListItem disablePadding divider> 39 | <ListItemButton onClick={handleClick}> 40 | <ListItemAvatar> 41 | <MantisAvatar alt="Surfboard" type="combined" color="secondary" src={surfboardIcon} /> 42 | </ListItemAvatar> 43 | <ListItemText primary={t("dashboard.shortcut.subscribe.surfboard")} /> 44 | </ListItemButton> 45 | </ListItem> 46 | ); 47 | }; 48 | 49 | export default SurfboardButton; 50 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/subscribeButton/surgeButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | import ReactGA from "react-ga4"; 4 | 5 | // material-ui 6 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material"; 7 | 8 | // project imports 9 | import { useGetUserSubscriptionQuery } from "@/store/services/api"; 10 | import MantisAvatar from "@/components/@extended/Avatar"; 11 | import config from "@/config"; 12 | 13 | // asset 14 | import surgeIcon from "@/assets/images/software/surge.png"; 15 | 16 | const SurgeButton: React.FC = () => { 17 | const { t } = useTranslation(); 18 | const { data: subscribeInfo } = useGetUserSubscriptionQuery(); 19 | 20 | const handleClick = () => { 21 | if (subscribeInfo) { 22 | const url = new URL(subscribeInfo.subscribe_url); 23 | url.searchParams.set("flag", "surge"); 24 | window.open( 25 | `surge:///install-config?url=${encodeURIComponent(url.toString())}&name=${encodeURIComponent(config.title)}`, 26 | "_self" 27 | ); 28 | 29 | ReactGA.event("click", { 30 | category: "shortcut", 31 | label: "quick_subscribe", 32 | method: "surge" 33 | }); 34 | } 35 | }; 36 | 37 | return ( 38 | <ListItem disablePadding divider> 39 | <ListItemButton onClick={handleClick}> 40 | <ListItemAvatar> 41 | <MantisAvatar alt="Surge" type="combined" color="secondary" src={surgeIcon} /> 42 | </ListItemAvatar> 43 | <ListItemText primary={t("dashboard.shortcut.subscribe.surge")} /> 44 | </ListItemButton> 45 | </ListItem> 46 | ); 47 | }; 48 | 49 | export default SurgeButton; 50 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/ticketButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // third-party 4 | import { useTranslation } from "react-i18next"; 5 | import { useNavigate } from "react-router-dom"; 6 | import ReactGA from "react-ga4"; 7 | 8 | // material-ui 9 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText, Typography } from "@mui/material"; 10 | import { CommentOutlined } from "@ant-design/icons"; 11 | 12 | // project imports 13 | import Avatar from "@/components/@extended/Avatar"; 14 | 15 | const TicketButton: React.FC = () => { 16 | const { t } = useTranslation(); 17 | 18 | const navigate = useNavigate(); 19 | const handleClick = (e: React.MouseEvent) => { 20 | e.preventDefault(); 21 | navigate("/ticket"); 22 | 23 | ReactGA.event("click", { 24 | category: "shortcut", 25 | label: "go_ticket" 26 | }); 27 | }; 28 | 29 | return ( 30 | <ListItem disablePadding divider> 31 | <ListItemButton onClick={handleClick}> 32 | <ListItemAvatar> 33 | <Avatar alt="Basic" type="combined" color="default"> 34 | <CommentOutlined /> 35 | </Avatar> 36 | </ListItemAvatar> 37 | <ListItemText 38 | primary={ 39 | <Typography variant={"body1"} noWrap> 40 | {t("dashboard.shortcut.ticket.primary")} 41 | </Typography> 42 | } 43 | secondary={ 44 | <Typography variant={"caption"} color={"secondary"} noWrap> 45 | {t("dashboard.shortcut.ticket.secondary")} 46 | </Typography> 47 | } 48 | /> 49 | </ListItemButton> 50 | </ListItem> 51 | ); 52 | }; 53 | 54 | export default TicketButton; 55 | -------------------------------------------------------------------------------- /src/sections/dashboard/shortcutCard/tutorialButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // third-party 4 | import { useTranslation } from "react-i18next"; 5 | import { useNavigate } from "react-router-dom"; 6 | import ReactGA from "react-ga4"; 7 | 8 | // material-ui 9 | import { ListItem, ListItemAvatar, ListItemButton, ListItemText, Typography } from "@mui/material"; 10 | 11 | // project imports 12 | import Avatar from "@/components/@extended/Avatar"; 13 | 14 | // assets 15 | import { QuestionOutlined } from "@ant-design/icons"; 16 | import config from "@/config"; 17 | 18 | const TutorialButton: React.FC = () => { 19 | const { t } = useTranslation(); 20 | 21 | const navigate = useNavigate(); 22 | const handleClick = (e: React.MouseEvent) => { 23 | e.preventDefault(); 24 | navigate("/knowledge"); 25 | 26 | ReactGA.event("click", { 27 | category: "shortcut", 28 | label: "go_tutorial" 29 | }); 30 | }; 31 | 32 | return ( 33 | <ListItem disablePadding divider> 34 | <ListItemButton onClick={handleClick}> 35 | <ListItemAvatar> 36 | <Avatar alt="Basic" type="combined" color="success"> 37 | <QuestionOutlined /> 38 | </Avatar> 39 | </ListItemAvatar> 40 | <ListItemText 41 | primary={ 42 | <Typography variant={"body1"} noWrap> 43 | {t("dashboard.shortcut.tutorial.primary", { siteName: config.title })} 44 | </Typography> 45 | } 46 | secondary={ 47 | <Typography variant={"caption"} color={"secondary"} noWrap> 48 | {t("dashboard.shortcut.tutorial.secondary")} 49 | </Typography> 50 | } 51 | /> 52 | </ListItemButton> 53 | </ListItem> 54 | ); 55 | }; 56 | 57 | export default TutorialButton; 58 | -------------------------------------------------------------------------------- /src/sections/invite/invitePage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid } from "@mui/material"; 3 | 4 | // project imports 5 | import MyInvitationCard from "@/sections/invite/invitePage/myInvitationCard"; 6 | import InfoCard from "@/sections/invite/invitePage/infoCard"; 7 | import InviteCodesTable from "@/sections/invite/invitePage/inviteCodesTable"; 8 | 9 | const InvitePage: React.FC = () => { 10 | return ( 11 | <Grid container spacing={2}> 12 | <Grid item xs={12} md={6}> 13 | <MyInvitationCard /> 14 | </Grid> 15 | <Grid item xs={12} md={6}> 16 | <InfoCard /> 17 | </Grid> 18 | <Grid item xs={12}> 19 | <InviteCodesTable /> 20 | </Grid> 21 | </Grid> 22 | ); 23 | }; 24 | 25 | export default InvitePage; 26 | -------------------------------------------------------------------------------- /src/sections/knowledge/postCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // third-party 4 | import lo from "lodash-es"; 5 | import { useTranslation } from "react-i18next"; 6 | 7 | // material-ui 8 | import { Skeleton, Stack } from "@mui/material"; 9 | import MuiMarkdown from "mui-markdown"; 10 | 11 | // project imports 12 | import MainCard from "@/components/MainCard"; 13 | import { useGetKnowledgeQuery } from "@/store/services/api"; 14 | 15 | export interface PostCardProps { 16 | postId: number | null; 17 | } 18 | 19 | const PostCard: React.FC<PostCardProps> = ({ postId }) => { 20 | const { i18n } = useTranslation(); 21 | const { data: post, isLoading } = useGetKnowledgeQuery( 22 | { 23 | id: postId || 0, 24 | language: i18n.language 25 | }, 26 | { 27 | skip: !postId 28 | } 29 | ); 30 | 31 | return ( 32 | <MainCard title={isLoading || !post ? <Skeleton variant="text" sx={{ fontSize: "1.5rem" }} /> : post!.title}> 33 | <Stack spacing={1}> 34 | {isLoading || !post ? ( 35 | lo.times(5, (i) => <Skeleton variant="text" sx={{ fontSize: "1rem" }} key={i} />) 36 | ) : ( 37 | <MuiMarkdown>{post!.body}</MuiMarkdown> 38 | )} 39 | </Stack> 40 | </MainCard> 41 | ); 42 | }; 43 | 44 | export default PostCard; 45 | -------------------------------------------------------------------------------- /src/sections/knowledge/search.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | // hooks 5 | import useUrlState from "@ahooksjs/use-url-state"; 6 | 7 | // material-ui 8 | import { Box, OutlinedInput } from "@mui/material"; 9 | 10 | // project imports 11 | import { makeStyles } from "@/themes/hooks"; 12 | 13 | const useStyles = makeStyles()((theme) => ({ 14 | root: { 15 | width: "100%" 16 | }, 17 | input: { 18 | backgroundColor: theme.palette.background.paper 19 | } 20 | })); 21 | 22 | const Search: React.FC = () => { 23 | const { t } = useTranslation(); 24 | const [state, setState] = useUrlState<{ s: string }>( 25 | { s: "" }, 26 | { 27 | navigateMode: "push" 28 | } 29 | ); 30 | 31 | const { classes } = useStyles(); 32 | 33 | const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { 34 | e.preventDefault(); 35 | setState({ s: e.target.value }); 36 | }; 37 | 38 | return ( 39 | <Box className={classes.root}> 40 | <OutlinedInput 41 | id={"search-knowledge"} 42 | className={classes.input} 43 | type={"text"} 44 | value={state.s} 45 | onChange={handleChange} 46 | placeholder={t("knowledge.search_placeholder").toString()} 47 | aria-label={t("knowledge.search_placeholder").toString()} 48 | autoComplete={"off"} 49 | fullWidth 50 | /> 51 | </Box> 52 | ); 53 | }; 54 | 55 | export default Search; 56 | -------------------------------------------------------------------------------- /src/sections/node/status/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project imports 4 | import Wrapper from "@/sections/node/status/wrapper"; 5 | import Table from "@/sections/node/status/table"; 6 | import { makeStyles } from "@/themes/hooks"; 7 | 8 | const useStyles = makeStyles()((theme) => ({ 9 | root: { 10 | flexGrow: 1, 11 | height: "100%", 12 | display: "flex", 13 | flexDirection: "column" 14 | }, 15 | dataGrip: { 16 | height: "100%" 17 | } 18 | })); 19 | 20 | const Status: React.FC = () => { 21 | const { classes } = useStyles(); 22 | 23 | return ( 24 | <Wrapper className={classes.root}> 25 | <Table className={classes.dataGrip} /> 26 | </Wrapper> 27 | ); 28 | }; 29 | 30 | export default Status; 31 | -------------------------------------------------------------------------------- /src/sections/node/status/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MainCard from "@/components/MainCard"; 3 | 4 | const Wrapper: React.FC<{ 5 | children: React.ReactNode; 6 | className?: string; 7 | }> = ({ children, className }) => ( 8 | <MainCard className={className} content={false}> 9 | {children} 10 | </MainCard> 11 | ); 12 | 13 | export default Wrapper; 14 | -------------------------------------------------------------------------------- /src/sections/order/checkoutPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Grid } from "@mui/material"; 5 | 6 | // project imports 7 | import { OrderStatus } from "@/model/order"; 8 | import { useCheckoutContext } from "./context"; 9 | import ProductInfoCard from "./productInfoCard"; 10 | import StatusCard from "./statusCard"; 11 | import OrderInfoCard from "./orderInfoCard"; 12 | import PaymentMethodCard from "./paymentMethodCard"; 13 | import BillingCard from "./billingCard"; 14 | 15 | const CheckoutPage: React.FC = () => { 16 | const { status } = useCheckoutContext(); 17 | 18 | return ( 19 | <Grid container spacing={2}> 20 | <Grid item xs> 21 | <Grid container spacing={2}> 22 | {status !== OrderStatus.PENDING && ( 23 | <Grid item xs={12}> 24 | <StatusCard /> 25 | </Grid> 26 | )} 27 | <Grid item xs={12}> 28 | <ProductInfoCard /> 29 | </Grid> 30 | <Grid item xs={12}> 31 | <OrderInfoCard /> 32 | </Grid> 33 | {status === OrderStatus.PENDING && <Grid item xs={12}></Grid>} 34 | </Grid> 35 | </Grid> 36 | {status === OrderStatus.PENDING && ( 37 | <Grid item xs={12} md={4}> 38 | <Grid container spacing={2}> 39 | <Grid item xs={12}> 40 | <PaymentMethodCard /> 41 | </Grid> 42 | <Grid item xs={12}> 43 | <BillingCard /> 44 | </Grid> 45 | </Grid> 46 | </Grid> 47 | )} 48 | </Grid> 49 | ); 50 | }; 51 | 52 | export default CheckoutPage; 53 | -------------------------------------------------------------------------------- /src/sections/order/listPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // project imports 4 | import OrderListWrapper from "./wrapper"; 5 | import OrderListTable from "./table"; 6 | import { makeStyles } from "@/themes/hooks"; 7 | 8 | const useStyles = makeStyles()((theme) => ({ 9 | root: { 10 | flexGrow: 1, 11 | height: "100%", 12 | display: "flex", 13 | flexDirection: "column" 14 | } 15 | })); 16 | 17 | const ListPage: React.FC = () => { 18 | const { classes } = useStyles(); 19 | 20 | return ( 21 | <OrderListWrapper className={classes.root}> 22 | <OrderListTable /> 23 | </OrderListWrapper> 24 | ); 25 | }; 26 | 27 | export default ListPage; 28 | -------------------------------------------------------------------------------- /src/sections/order/listPage/wrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import MainCard from "@/components/MainCard"; 3 | 4 | export interface WrapperProps { 5 | children: React.ReactNode; 6 | className?: string; 7 | } 8 | 9 | const Wrapper: React.FC<WrapperProps> = ({ children, className }) => ( 10 | <MainCard className={className} content={false}> 11 | {children} 12 | </MainCard> 13 | ); 14 | 15 | export default Wrapper; 16 | -------------------------------------------------------------------------------- /src/sections/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Masonry } from "@mui/lab"; 5 | 6 | // project imports 7 | import AccountInfoCard from "./accountInfoCard"; 8 | import WalletCard from "./walletCard"; 9 | import ChangePasswordCard from "./changePasswordCard"; 10 | import NotificationCard from "./notificationCard"; 11 | import TelegramCard from "./telegramCard"; 12 | import ResetSubscriptionCard from "./resetSubscriptionCard"; 13 | 14 | const Profile: React.FC = () => ( 15 | <Masonry 16 | spacing={2} 17 | columns={{ 18 | xs: 1, 19 | md: 2, 20 | xl: 3 21 | }} 22 | > 23 | <AccountInfoCard /> 24 | <WalletCard /> 25 | <ChangePasswordCard /> 26 | <NotificationCard /> 27 | <TelegramCard /> 28 | <ResetSubscriptionCard /> 29 | </Masonry> 30 | ); 31 | 32 | export default Profile; 33 | -------------------------------------------------------------------------------- /src/sections/profile/walletCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | // material-ui 5 | import { Typography } from "@mui/material"; 6 | 7 | // project imports 8 | import MainCard from "@/components/MainCard"; 9 | import KeyValueTable, { KeyValueData } from "@/components/KeyValueTable"; 10 | import { useGetUserInfoQuery } from "@/store/services/api"; 11 | 12 | const WalletCard: React.FC = () => { 13 | const { t } = useTranslation(); 14 | 15 | const { data, isLoading } = useGetUserInfoQuery(); 16 | 17 | const tableData = useMemo( 18 | () => 19 | ( 20 | [ 21 | { 22 | key: t("profile.wallet-card.table.balance", { context: "key" }), 23 | value: t("profile.wallet-card.table.balance", { 24 | context: "value", 25 | value: ((data?.balance ?? 0) / 100).toFixed(2), 26 | count: (data?.balance ?? 0) / 100 27 | }) 28 | }, 29 | { 30 | key: t("profile.wallet-card.table.commission_balance", { context: "key" }), 31 | value: t("profile.wallet-card.table.commission_balance", { 32 | context: "value", 33 | value: ((data?.commission_balance ?? 0) / 100).toFixed(2), 34 | count: (data?.commission_balance ?? 0) / 100 35 | }) 36 | } 37 | ] satisfies KeyValueData[] 38 | ).map((datum) => ({ 39 | key: <Typography noWrap>{datum.key}</Typography>, 40 | value: <Typography noWrap>{datum.value}</Typography> 41 | })), 42 | [data, t] 43 | ); 44 | 45 | return ( 46 | <MainCard title={t("profile.wallet-card.title")}> 47 | <KeyValueTable data={tableData} isValueLoading={isLoading} /> 48 | </MainCard> 49 | ); 50 | }; 51 | 52 | export default WalletCard; 53 | -------------------------------------------------------------------------------- /src/sections/subscription/buyPage/context.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | // third party 4 | import { useSet, useToggle } from "ahooks"; 5 | import constate from "constate"; 6 | 7 | // material-ui 8 | import { useMediaQuery } from "@mui/material"; 9 | import { useTheme } from "@mui/material/styles"; 10 | 11 | // types and utils 12 | import { PaymentPeriod, PlanType } from "@/types/plan"; 13 | import { paymentPriority } from "@/utils/plan"; 14 | 15 | const useShop = () => { 16 | const [planTypeAllow, { add: addPlanType, remove: removePlanType, reset: resetPlanType }] = useSet([ 17 | PlanType.PERIOD, 18 | PlanType.TRAFFIC 19 | ]); 20 | const [paymentAllow, { add: addPayment, remove: removePayment, reset: resetPayment }] = useSet(paymentPriority); 21 | const [keyword, setKeyword] = useState(""); 22 | 23 | const theme = useTheme(); 24 | const isMobile = useMediaQuery(theme.breakpoints.down("md")); 25 | const [drawerOpen, { set: setDrawerOpen, toggle: toggleDrawer }] = useToggle(!isMobile); 26 | 27 | const togglePlanType = (type: PlanType) => { 28 | if (planTypeAllow.has(type)) { 29 | removePlanType(type); 30 | } else { 31 | addPlanType(type); 32 | } 33 | }; 34 | 35 | const togglePayment = (key: PaymentPeriod) => { 36 | if (paymentAllow.has(key)) { 37 | removePayment(key); 38 | } else { 39 | addPayment(key); 40 | } 41 | }; 42 | 43 | // useEffect(() => { 44 | // console.log(planTypeAllow); 45 | // }, [planTypeAllow]); 46 | 47 | return { 48 | planType: planTypeAllow, 49 | addPlanType, 50 | removePlanType, 51 | resetPlanType, 52 | togglePlanType, 53 | keyword, 54 | setKeyword, 55 | drawerOpen, 56 | setDrawerOpen, 57 | toggleDrawer, 58 | paymentAllow, 59 | addPayment, 60 | removePayment, 61 | resetPayment, 62 | togglePayment 63 | }; 64 | }; 65 | 66 | const [ShopProvider, useShopContext] = constate(useShop); 67 | 68 | export { ShopProvider, useShopContext }; 69 | -------------------------------------------------------------------------------- /src/sections/subscription/buyPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Stack } from "@mui/material"; 3 | 4 | // project imports 5 | import ProductsFilter from "./productsFilter"; 6 | import ProductsHeader from "./productsHeader"; 7 | import Products from "./products"; 8 | import { useShopContext } from "./context"; 9 | import { makeStyles } from "@/themes/hooks"; 10 | 11 | const useStyles = makeStyles<{ open: boolean }>()((theme, { open }) => ({ 12 | root: { 13 | display: "flex", 14 | flexDirection: "row" 15 | }, 16 | main: { 17 | width: "100%", 18 | [theme.breakpoints.up("sm")]: { 19 | marginLeft: open ? 0 : -280, 20 | transition: theme.transitions.create("margin", { 21 | easing: theme.transitions.easing.easeInOut, 22 | duration: theme.transitions.duration.leavingScreen 23 | }) 24 | } 25 | } 26 | })); 27 | 28 | const BuyPage: React.FC = () => { 29 | const { drawerOpen } = useShopContext(); 30 | const { classes } = useStyles({ open: drawerOpen }); 31 | 32 | return ( 33 | <Box className={classes.root}> 34 | <ProductsFilter /> 35 | <Stack component={"main"} spacing={2} className={classes.main}> 36 | <ProductsHeader /> 37 | <Products /> 38 | </Stack> 39 | </Box> 40 | ); 41 | }; 42 | 43 | export default BuyPage; 44 | -------------------------------------------------------------------------------- /src/sections/subscription/buyPage/productsHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | // material-ui 5 | import { Button, FormControl, OutlinedInput, Stack, Typography } from "@mui/material"; 6 | import { FilterOutlined, SearchOutlined } from "@ant-design/icons"; 7 | 8 | // project imports 9 | import MainCard from "@/components/MainCard"; 10 | import { makeStyles } from "@/themes/hooks"; 11 | import { useShopContext } from "@/sections/subscription/buyPage/context"; 12 | 13 | const useStyles = makeStyles()((theme) => ({ 14 | root: { 15 | width: "100%", 16 | padding: theme.spacing(1) 17 | }, 18 | filterButton: {} 19 | })); 20 | 21 | const ProductsHeader: React.FC = () => { 22 | const { t } = useTranslation(); 23 | const { toggleDrawer, keyword, setKeyword } = useShopContext(); 24 | const { classes } = useStyles(); 25 | 26 | return ( 27 | <MainCard className={classes.root} content={false}> 28 | <Stack direction={"row"} justifyContent={"space-between"} alignItems={"center"}> 29 | <Stack direction={"row"} alignItems={"center"}> 30 | <FormControl variant={"standard"}> 31 | <OutlinedInput 32 | placeholder={t("subscription.buy.products-header.search").toString()} 33 | startAdornment={<SearchOutlined />} 34 | value={keyword} 35 | onChange={(e) => { 36 | setKeyword(e.target.value); 37 | }} 38 | /> 39 | </FormControl> 40 | </Stack> 41 | <Stack direction={"row"} alignItems={"center"}> 42 | <Button 43 | className={classes.filterButton} 44 | color={"secondary"} 45 | onClick={toggleDrawer} 46 | startIcon={<FilterOutlined style={{ color: "secondary.200" }} />} 47 | > 48 | <Typography variant={"h6"} color={"textPrimary"}> 49 | {t("subscription.buy.products-header.filter-button").toString()} 50 | </Typography> 51 | </Button> 52 | </Stack> 53 | </Stack> 54 | </MainCard> 55 | ); 56 | }; 57 | 58 | export default ProductsHeader; 59 | -------------------------------------------------------------------------------- /src/sections/subscription/planDetailsPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Grid } from "@mui/material"; 5 | 6 | // project imports 7 | import PlanInfoCard from "@/sections/subscription/planDetailsPage/planInfoCard"; 8 | import PeriodSelectCard from "@/sections/subscription/planDetailsPage/periodSelectCard"; 9 | import CouponCard from "@/sections/subscription/planDetailsPage/couponCard"; 10 | import OrderInfoCard from "@/sections/subscription/planDetailsPage/orderInfoCard"; 11 | 12 | const PlanDetailsPage: React.FC = () => ( 13 | <Grid container spacing={2}> 14 | <Grid item xs={12} md={8}> 15 | <Grid container spacing={2}> 16 | <Grid item xs={12}> 17 | <PlanInfoCard /> 18 | </Grid> 19 | <Grid item xs={12}> 20 | <PeriodSelectCard /> 21 | </Grid> 22 | </Grid> 23 | </Grid> 24 | <Grid item xs={12} md={4}> 25 | <Grid container spacing={2}> 26 | <Grid item xs={12}> 27 | <CouponCard /> 28 | </Grid> 29 | <Grid item xs={12}> 30 | <OrderInfoCard /> 31 | </Grid> 32 | </Grid> 33 | </Grid> 34 | </Grid> 35 | ); 36 | 37 | export default PlanDetailsPage; 38 | -------------------------------------------------------------------------------- /src/sections/ticket/detailPage/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // material-ui 4 | import { Box } from "@mui/material"; 5 | 6 | // project imports 7 | import Drawer from "./drawer"; 8 | import Main from "./main"; 9 | 10 | const TicketSection: React.FC = () => { 11 | return ( 12 | <Box display={"flex"} flexDirection={"row"} flexWrap={"nowrap"} flexGrow={"1"}> 13 | <Drawer /> 14 | <Main /> 15 | </Box> 16 | ); 17 | }; 18 | 19 | export default TicketSection; 20 | -------------------------------------------------------------------------------- /src/sections/traffic/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Grid } from "@mui/material"; 3 | 4 | // project imports 5 | import TrafficAlert from "@/sections/traffic/trafficAlert"; 6 | import TrafficTable from "@/sections/traffic/trafficTable"; 7 | import TrafficChart from "@/sections/traffic/trafficChart"; 8 | import TrafficInfoCard from "@/sections/traffic/trafficInfoCard"; 9 | 10 | const TrafficSection: React.FC = () => { 11 | return ( 12 | <Grid container spacing={2}> 13 | <Grid item xs={12}> 14 | <TrafficAlert /> 15 | </Grid> 16 | <Grid item xs={12} md={8}> 17 | <TrafficChart /> 18 | </Grid> 19 | <Grid item xs={12} md={4}> 20 | <TrafficInfoCard /> 21 | </Grid> 22 | <Grid item xs={12}> 23 | <TrafficTable /> 24 | </Grid> 25 | </Grid> 26 | ); 27 | }; 28 | 29 | export default TrafficSection; 30 | -------------------------------------------------------------------------------- /src/sections/traffic/trafficAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Alert } from "@mui/material"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | const TrafficAlert: React.FC = () => { 6 | const { t } = useTranslation(); 7 | 8 | return <Alert severity="info">{t("traffic.alert")}</Alert>; 9 | }; 10 | 11 | export default TrafficAlert; 12 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | // third-party 2 | import { configureStore } from "@reduxjs/toolkit"; 3 | import { useDispatch as useAppDispatch, useSelector as useAppSelector, TypedUseSelectorHook } from "react-redux"; 4 | 5 | // project import 6 | import reducers from "./reducers"; 7 | import api from "./services/api"; 8 | 9 | // ==============================|| REDUX TOOLKIT - MAIN STORE ||============================== // 10 | 11 | const store = configureStore({ 12 | reducer: { 13 | ...reducers, 14 | [api.reducerPath]: api.reducer 15 | }, 16 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(api.middleware), 17 | devTools: import.meta.env.DEV 18 | }); 19 | 20 | export type RootState = ReturnType<typeof store.getState>; 21 | export type AppDispatch = typeof store.dispatch; 22 | 23 | const { dispatch } = store; 24 | 25 | const useDispatch = () => useAppDispatch<AppDispatch>(); 26 | const useSelector: TypedUseSelectorHook<RootState> = useAppSelector; 27 | 28 | export { dispatch, useSelector, useDispatch }; 29 | export default store; 30 | -------------------------------------------------------------------------------- /src/store/reducers/auth.ts: -------------------------------------------------------------------------------- 1 | import lo from "lodash-es"; 2 | 3 | // types 4 | import { AuthProps, AuthActionProps } from "@/types/auth"; 5 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 6 | 7 | // initial state 8 | export const initialState: AuthProps = { 9 | isLoggedIn: !lo.isEmpty(localStorage.getItem("gfw_token")), 10 | isAdmin: localStorage.getItem("gfw_is_admin") === "true" 11 | }; 12 | 13 | // ==============================|| AUTH REDUCER ||============================== // 14 | 15 | const auth = createSlice({ 16 | name: "auth", 17 | initialState, 18 | reducers: { 19 | login: ( 20 | state, 21 | action: PayloadAction<{ 22 | isAdmin: boolean; 23 | }> 24 | ) => { 25 | state.isLoggedIn = true; 26 | state.isAdmin = action.payload.isAdmin; 27 | }, 28 | logout: (state) => { 29 | state.isLoggedIn = false; 30 | localStorage.removeItem("gfw_token"); 31 | localStorage.removeItem("gfw_is_admin"); 32 | } 33 | } 34 | }); 35 | 36 | export const { login, logout } = auth.actions; 37 | export default auth.reducer; 38 | -------------------------------------------------------------------------------- /src/store/reducers/index.ts: -------------------------------------------------------------------------------- 1 | // project import 2 | import menu from "./menu"; 3 | import auth from "./auth"; 4 | import view from "./view"; 5 | 6 | // ==============================|| COMBINE REDUCERS ||============================== // 7 | 8 | const reducers = { 9 | menu, 10 | auth, 11 | view 12 | }; 13 | 14 | export default reducers; 15 | -------------------------------------------------------------------------------- /src/store/reducers/menu.ts: -------------------------------------------------------------------------------- 1 | // types 2 | import { MenuProps } from "@/types/menu"; 3 | import { createSlice } from "@reduxjs/toolkit"; 4 | 5 | // initial state 6 | const initialState: MenuProps = { 7 | openItem: ["dashboard"], 8 | openComponent: "buttons", 9 | drawerOpen: localStorage.getItem("menu_drawerOpen") === "true", 10 | componentDrawerOpen: true 11 | }; 12 | 13 | // ==============================|| SLICE - MENU ||============================== // 14 | 15 | const menu = createSlice({ 16 | name: "menu", 17 | initialState, 18 | reducers: { 19 | activeItem(state, action) { 20 | state.openItem = action.payload.openItem; 21 | }, 22 | 23 | activeComponent(state, action) { 24 | state.openComponent = action.payload.openComponent; 25 | }, 26 | 27 | openDrawer(state, action) { 28 | state.drawerOpen = action.payload.drawerOpen; 29 | localStorage.setItem("menu_drawerOpen", state.drawerOpen.toString()); 30 | }, 31 | 32 | openComponentDrawer(state, action) { 33 | state.componentDrawerOpen = action.payload.componentDrawerOpen; 34 | } 35 | } 36 | }); 37 | 38 | export default menu.reducer; 39 | 40 | export const { activeItem, activeComponent, openDrawer, openComponentDrawer } = menu.actions; 41 | -------------------------------------------------------------------------------- /src/store/reducers/view.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from "@reduxjs/toolkit"; 2 | import { ThemeOptions } from "@mui/material/styles"; 3 | import { PaletteMode } from "@mui/material"; 4 | 5 | export interface ViewState { 6 | theme: { 7 | mode: PaletteMode | "system" | "time"; 8 | }; 9 | } 10 | 11 | const initialState: ViewState = { 12 | theme: { 13 | mode: (localStorage.getItem("theme_mode") as PaletteMode) || "system" 14 | } 15 | }; 16 | 17 | const view = createSlice({ 18 | name: "view", 19 | initialState, 20 | reducers: { 21 | setThemeMode(state, action: PayloadAction<ViewState["theme"]["mode"]>) { 22 | state.theme.mode = action.payload; 23 | localStorage.setItem("theme_mode", action.payload); 24 | } 25 | } 26 | }); 27 | 28 | export const { setThemeMode } = view.actions; 29 | export default view.reducer; 30 | -------------------------------------------------------------------------------- /src/themes/cache.ts: -------------------------------------------------------------------------------- 1 | import createCache from "@emotion/cache"; 2 | 3 | const cache = createCache({ 4 | key: "css" 5 | }); 6 | 7 | export default cache; 8 | -------------------------------------------------------------------------------- /src/themes/hooks.ts: -------------------------------------------------------------------------------- 1 | import { createMakeAndWithStyles } from "tss-react"; 2 | import { useTheme } from "@mui/material/styles"; 3 | import cache from "./cache"; 4 | 5 | export const { makeStyles, withStyles, useStyles } = createMakeAndWithStyles({ 6 | useTheme, 7 | cache 8 | }); 9 | -------------------------------------------------------------------------------- /src/themes/overrides/Accordion.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - ALERT TITLE ||============================== // 5 | 6 | export default function Accordion(theme: Theme) { 7 | return { 8 | MuiAccordion: { 9 | defaultProps: { 10 | disableGutters: true, 11 | square: true, 12 | elevation: 0 13 | }, 14 | styleOverrides: { 15 | root: { 16 | border: `1px solid ${theme.palette.secondary.light}`, 17 | '&:not(:last-child)': { 18 | borderBottom: 0 19 | }, 20 | '&:before': { 21 | display: 'none' 22 | }, 23 | '&.Mui-disabled': { 24 | backgroundColor: theme.palette.secondary.lighter 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/themes/overrides/AccordionDetails.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - ALERT TITLE ||============================== // 5 | 6 | export default function AccordionDetails(theme: Theme) { 7 | return { 8 | MuiAccordionDetails: { 9 | styleOverrides: { 10 | root: { 11 | padding: theme.spacing(2), 12 | borderTop: `1px solid ${theme.palette.secondary.light}` 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/overrides/AccordionSummary.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // assets 5 | import { RightOutlined } from '@ant-design/icons'; 6 | 7 | // ==============================|| OVERRIDES - ALERT TITLE ||============================== // 8 | 9 | export default function AccordionSummary(theme: Theme) { 10 | const { palette, spacing } = theme; 11 | 12 | return { 13 | MuiAccordionSummary: { 14 | defaultProps: { 15 | expandIcon: <RightOutlined style={{ fontSize: '0.75rem' }} /> 16 | }, 17 | styleOverrides: { 18 | root: { 19 | backgroundColor: palette.secondary.lighter, 20 | flexDirection: 'row-reverse', 21 | minHeight: 46 22 | }, 23 | expandIconWrapper: { 24 | '&.Mui-expanded': { 25 | transform: 'rotate(90deg)' 26 | } 27 | }, 28 | content: { 29 | marginTop: spacing(1.25), 30 | marginBottom: spacing(1.25), 31 | marginLeft: spacing(1) 32 | } 33 | } 34 | } 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/themes/overrides/AlertTitle.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - ALERT TITLE ||============================== // 2 | 3 | export default function AlertTitle() { 4 | return { 5 | MuiAlertTitle: { 6 | styleOverrides: { 7 | root: { 8 | marginBottom: 4, 9 | marginTop: 0, 10 | fontWeight: 400 11 | } 12 | } 13 | } 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /src/themes/overrides/Autocomplete.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - AUTOCOMPLETE ||============================== // 2 | 3 | export default function Autocomplete() { 4 | return { 5 | MuiAutocomplete: { 6 | styleOverrides: { 7 | root: { 8 | '& .MuiOutlinedInput-root': { 9 | padding: '3px 9px' 10 | } 11 | }, 12 | popupIndicator: { 13 | width: 'auto', 14 | height: 'auto' 15 | }, 16 | clearIndicator: { 17 | width: 'auto', 18 | height: 'auto' 19 | } 20 | } 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/themes/overrides/Badge.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from "@mui/material/styles"; 3 | 4 | // project import 5 | import getColors from "@/utils/getColors"; 6 | 7 | // types 8 | import { ExtendedStyleProps } from "@/types/extended"; 9 | 10 | // ==============================|| BADGE - COLORS ||============================== // 11 | 12 | function getColorStyle({ color, theme }: ExtendedStyleProps) { 13 | const colors = getColors(theme, color); 14 | const { lighter, main } = colors; 15 | 16 | return { 17 | color: main, 18 | backgroundColor: lighter 19 | }; 20 | } 21 | 22 | // ==============================|| OVERRIDES - BADGE ||============================== // 23 | 24 | export default function Badge(theme: Theme) { 25 | const defaultLightBadge = getColorStyle({ color: "primary", theme }); 26 | 27 | return { 28 | MuiBadge: { 29 | styleOverrides: { 30 | standard: { 31 | minWidth: theme.spacing(2), 32 | height: theme.spacing(2), 33 | padding: theme.spacing(0.5) 34 | }, 35 | light: { 36 | ...defaultLightBadge, 37 | "&.MuiBadge-colorPrimary": getColorStyle({ color: "primary", theme }), 38 | "&.MuiBadge-colorSecondary": getColorStyle({ color: "secondary", theme }), 39 | "&.MuiBadge-colorError": getColorStyle({ color: "error", theme }), 40 | "&.MuiBadge-colorInfo": getColorStyle({ color: "info", theme }), 41 | "&.MuiBadge-colorSuccess": getColorStyle({ color: "success", theme }), 42 | "&.MuiBadge-colorWarning": getColorStyle({ color: "warning", theme }) 43 | } 44 | } 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/themes/overrides/ButtonBase.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - BUTTON ||============================== // 2 | 3 | export default function ButtonBase() { 4 | return { 5 | MuiButtonBase: { 6 | defaultProps: { 7 | disableRipple: true 8 | } 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/overrides/ButtonGroup.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - BUTTON ||============================== // 2 | 3 | export default function ButtonGroup() { 4 | return { 5 | MuiButtonGroup: { 6 | defaultProps: { 7 | disableRipple: true 8 | } 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/overrides/CardContent.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - CARD CONTENT ||============================== // 2 | 3 | export default function CardContent() { 4 | return { 5 | MuiCardContent: { 6 | styleOverrides: { 7 | root: { 8 | padding: 20, 9 | '&:last-child': { 10 | paddingBottom: 20 11 | } 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/themes/overrides/Dialog.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { alpha } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - DIALOG ||============================== // 5 | 6 | export default function Dialog() { 7 | return { 8 | MuiDialog: { 9 | styleOverrides: { 10 | root: { 11 | '& .MuiBackdrop-root': { 12 | backgroundColor: alpha('#000', 0.7) 13 | } 14 | } 15 | } 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/themes/overrides/DialogContentText.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - DIALOG CONTENT TEXT ||============================== // 5 | 6 | export default function DialogContentText(theme: Theme) { 7 | return { 8 | MuiDialogContentText: { 9 | styleOverrides: { 10 | root: { 11 | fontSize: '0.875rem', 12 | color: theme.palette.text.primary 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/overrides/DialogTitle.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - DIALOG TITLE ||============================== // 2 | 3 | export default function DialogTitle() { 4 | return { 5 | MuiDialogTitle: { 6 | styleOverrides: { 7 | root: { 8 | fontSize: '1rem', 9 | fontWeight: 500 10 | } 11 | } 12 | } 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/themes/overrides/IconButton.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - ICON BUTTON ||============================== // 5 | 6 | export default function IconButton(theme: Theme) { 7 | return { 8 | MuiIconButton: { 9 | styleOverrides: { 10 | root: { 11 | borderRadius: 4 12 | }, 13 | sizeLarge: { 14 | width: theme.spacing(5.5), 15 | height: theme.spacing(5.5), 16 | fontSize: '1.25rem' 17 | }, 18 | sizeMedium: { 19 | width: theme.spacing(4.5), 20 | height: theme.spacing(4.5), 21 | fontSize: '1rem' 22 | }, 23 | sizeSmall: { 24 | width: theme.spacing(3.75), 25 | height: theme.spacing(3.75), 26 | fontSize: '0.75rem' 27 | } 28 | } 29 | } 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/themes/overrides/InputBase.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - INPUT BASE ||============================== // 2 | 3 | export default function InputBase() { 4 | return { 5 | MuiInputBase: { 6 | styleOverrides: { 7 | sizeSmall: { 8 | fontSize: '0.75rem' 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/themes/overrides/InputLabel.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from "@mui/material/styles"; 3 | 4 | // ==============================|| OVERRIDES - INPUT LABEL ||============================== // 5 | 6 | export default function InputLabel(theme: Theme) { 7 | return { 8 | MuiInputLabel: { 9 | styleOverrides: { 10 | root: { 11 | color: theme.palette.grey[600] 12 | }, 13 | outlined: { 14 | lineHeight: "0.8em", 15 | "&.MuiInputLabel-sizeSmall": { 16 | lineHeight: "1em" 17 | }, 18 | "&.MuiInputLabel-shrink": { 19 | backgroundColor: theme.palette.mode === "dark" ? "transparent" : theme.palette.background.paper, 20 | padding: "0 8px", 21 | marginLeft: -6, 22 | lineHeight: "1.4375em" 23 | } 24 | } 25 | } 26 | } 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/themes/overrides/LinearProgress.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - LINER PROGRESS ||============================== // 2 | 3 | export default function LinearProgress() { 4 | return { 5 | MuiLinearProgress: { 6 | styleOverrides: { 7 | root: { 8 | height: 6, 9 | borderRadius: 100 10 | }, 11 | bar: { 12 | borderRadius: 100 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/overrides/Link.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - LINK ||============================== // 2 | 3 | export default function Link() { 4 | return { 5 | MuiLink: { 6 | defaultProps: { 7 | underline: 'hover' 8 | } 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/overrides/ListItemButton.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - LIST ITEM ICON ||============================== // 5 | 6 | export default function ListItemButton(theme: Theme) { 7 | return { 8 | MuiListItemButton: { 9 | styleOverrides: { 10 | root: { 11 | '&.Mui-selected': { 12 | color: theme.palette.primary.main, 13 | '& .MuiListItemIcon-root': { 14 | color: theme.palette.primary.main 15 | } 16 | } 17 | } 18 | } 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/themes/overrides/ListItemIcon.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - LIST ITEM ICON ||============================== // 5 | 6 | export default function ListItemIcon(theme: Theme) { 7 | return { 8 | MuiListItemIcon: { 9 | styleOverrides: { 10 | root: { 11 | minWidth: 24, 12 | color: theme.palette.text.primary 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/overrides/LoadingButton.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - LOADING BUTTON ||============================== // 2 | 3 | export default function LoadingButton() { 4 | return { 5 | MuiLoadingButton: { 6 | styleOverrides: { 7 | root: { 8 | padding: '6px 16px', 9 | '&.MuiLoadingButton-loading': { 10 | opacity: 0.6, 11 | textShadow: 'none' 12 | } 13 | } 14 | } 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/themes/overrides/OutlinedInput.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from "@mui/material/styles"; 3 | import { ColorProps } from "@/types/extended"; 4 | 5 | // project import 6 | import getColors from "@/utils/getColors"; 7 | import getShadow from "@/utils/getShadow"; 8 | 9 | // types 10 | interface Props { 11 | variant: ColorProps; 12 | theme: Theme; 13 | } 14 | 15 | // ==============================|| OVERRIDES - INPUT BORDER & SHADOWS ||============================== // 16 | 17 | function getColor({ variant, theme }: Props) { 18 | const colors = getColors(theme, variant); 19 | const { light } = colors; 20 | 21 | const shadows = getShadow(theme, `${variant}`); 22 | 23 | return { 24 | "&:hover .MuiOutlinedInput-notchedOutline": { 25 | borderColor: light 26 | }, 27 | "&.Mui-focused": { 28 | boxShadow: shadows, 29 | "& .MuiOutlinedInput-notchedOutline": { 30 | border: `1px solid ${light}` 31 | } 32 | } 33 | }; 34 | } 35 | 36 | // ==============================|| OVERRIDES - OUTLINED INPUT ||============================== // 37 | 38 | export default function OutlinedInput(theme: Theme) { 39 | return { 40 | MuiOutlinedInput: { 41 | styleOverrides: { 42 | input: { 43 | padding: "10.5px 14px 10.5px 12px" 44 | }, 45 | notchedOutline: { 46 | borderColor: theme.palette.mode === "dark" ? theme.palette.grey[200] : theme.palette.grey[300] 47 | }, 48 | root: { 49 | ...getColor({ variant: "primary", theme }), 50 | "&.Mui-error": { 51 | ...getColor({ variant: "error", theme }) 52 | } 53 | }, 54 | inputSizeSmall: { 55 | padding: "7.5px 8px 7.5px 12px" 56 | }, 57 | inputMultiline: { 58 | padding: 0 59 | }, 60 | colorSecondary: getColor({ variant: "secondary", theme }), 61 | colorError: getColor({ variant: "error", theme }), 62 | colorWarning: getColor({ variant: "warning", theme }), 63 | colorInfo: getColor({ variant: "info", theme }), 64 | colorSuccess: getColor({ variant: "success", theme }) 65 | } 66 | } 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/themes/overrides/Pagination.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - PAGINATION ||============================== // 2 | 3 | export default function Pagination() { 4 | return { 5 | MuiPagination: { 6 | defaultProps: { 7 | shape: 'rounded' 8 | } 9 | } 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/themes/overrides/Popover.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - DIALOG CONTENT TEXT ||============================== // 5 | 6 | export default function Popover(theme: Theme) { 7 | return { 8 | MuiPopover: { 9 | styleOverrides: { 10 | paper: { 11 | boxShadow: theme.customShadows.z1 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/themes/overrides/Tab.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TAB ||============================== // 5 | 6 | export default function Tab(theme: Theme) { 7 | return { 8 | MuiTab: { 9 | styleOverrides: { 10 | root: { 11 | minHeight: 46, 12 | color: theme.palette.text.primary, 13 | borderRadius: 4, 14 | '&:hover': { 15 | backgroundColor: theme.palette.primary.lighter + 60, 16 | color: theme.palette.primary.main 17 | }, 18 | '&:focus-visible': { 19 | borderRadius: 4, 20 | outline: `2px solid ${theme.palette.secondary.dark}`, 21 | outlineOffset: -3 22 | } 23 | } 24 | } 25 | } 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /src/themes/overrides/TableBody.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TABLE ROW ||============================== // 5 | 6 | export default function TableBody(theme: Theme) { 7 | const hoverStyle = { 8 | '&:hover': { 9 | backgroundColor: theme.palette.secondary.lighter 10 | } 11 | }; 12 | 13 | return { 14 | MuiTableBody: { 15 | styleOverrides: { 16 | root: { 17 | '&.striped .MuiTableRow-root': { 18 | '&:nth-of-type(even)': { 19 | backgroundColor: theme.palette.grey[50] 20 | }, 21 | ...hoverStyle 22 | }, 23 | '& .MuiTableRow-root': { 24 | ...hoverStyle 25 | } 26 | } 27 | } 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/themes/overrides/TableCell.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TABLE CELL ||============================== // 5 | 6 | export default function TableCell(theme: Theme) { 7 | const commonCell = { 8 | '&:not(:last-of-type)': { 9 | position: 'relative', 10 | '&:after': { 11 | position: 'absolute', 12 | content: '""', 13 | backgroundColor: theme.palette.divider, 14 | width: 1, 15 | height: 'calc(100% - 30px)', 16 | right: 0, 17 | top: 16 18 | } 19 | } 20 | }; 21 | 22 | return { 23 | MuiTableCell: { 24 | styleOverrides: { 25 | root: { 26 | fontSize: '0.875rem', 27 | padding: 12, 28 | borderColor: theme.palette.divider 29 | }, 30 | sizeSmall: { 31 | padding: 8 32 | }, 33 | head: { 34 | fontSize: '0.75rem', 35 | fontWeight: 700, 36 | textTransform: 'uppercase', 37 | ...commonCell 38 | }, 39 | footer: { 40 | fontSize: '0.75rem', 41 | textTransform: 'uppercase', 42 | ...commonCell 43 | } 44 | } 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/themes/overrides/TableFooter.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TABLE CELL ||============================== // 5 | 6 | export default function TableFooter(theme: Theme) { 7 | return { 8 | MuiTableFooter: { 9 | styleOverrides: { 10 | root: { 11 | backgroundColor: theme.palette.grey[50], 12 | borderTop: `2px solid ${theme.palette.divider}`, 13 | borderBottom: `1px solid ${theme.palette.divider}` 14 | } 15 | } 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/themes/overrides/TableHead.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TABLE CELL ||============================== // 5 | 6 | export default function TableHead(theme: Theme) { 7 | return { 8 | MuiTableHead: { 9 | styleOverrides: { 10 | root: { 11 | backgroundColor: theme.palette.grey[50], 12 | borderTop: `1px solid ${theme.palette.divider}`, 13 | borderBottom: `2px solid ${theme.palette.divider}` 14 | } 15 | } 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/themes/overrides/TablePagination.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - TABLE PAGINATION ||============================== // 2 | 3 | export default function TablePagination() { 4 | return { 5 | MuiTablePagination: { 6 | styleOverrides: { 7 | selectLabel: { 8 | fontSize: '0.875rem' 9 | }, 10 | displayedRows: { 11 | fontSize: '0.875rem' 12 | } 13 | } 14 | } 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /src/themes/overrides/TableRow.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - TABLE ROW ||============================== // 2 | 3 | export default function TableRow() { 4 | return { 5 | MuiTableRow: { 6 | styleOverrides: { 7 | root: { 8 | '&:last-of-type': { 9 | '& .MuiTableCell-root': { 10 | borderBottom: 'none' 11 | } 12 | }, 13 | '& .MuiTableCell-root': { 14 | '&:last-of-type': { 15 | paddingRight: 24 16 | }, 17 | '&:first-of-type': { 18 | paddingLeft: 24 19 | } 20 | } 21 | } 22 | } 23 | } 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/themes/overrides/Tabs.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - TABS ||============================== // 2 | 3 | export default function Tabs() { 4 | return { 5 | MuiTabs: { 6 | styleOverrides: { 7 | vertical: { 8 | overflow: 'visible' 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/themes/overrides/ToggleButton.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| OVERRIDES - TOGGLE BUTTON ||============================== // 5 | 6 | export default function ToggleButton(theme: Theme) { 7 | return { 8 | MuiToggleButton: { 9 | styleOverrides: { 10 | root: { 11 | '&.Mui-disabled': { 12 | borderColor: theme.palette.divider, 13 | color: theme.palette.text.disabled 14 | }, 15 | '&:focus-visible': { 16 | outline: `2px solid ${theme.palette.secondary.dark}`, 17 | outlineOffset: 2 18 | } 19 | } 20 | } 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/themes/overrides/TreeItem.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - TREE ITEM ||============================== // 2 | 3 | export default function TreeItem() { 4 | return { 5 | MuiTreeItem: { 6 | styleOverrides: { 7 | content: { 8 | padding: 8 9 | }, 10 | iconContainer: { 11 | '& svg': { 12 | fontSize: '0.625rem' 13 | } 14 | } 15 | } 16 | } 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /src/themes/overrides/Typography.ts: -------------------------------------------------------------------------------- 1 | // ==============================|| OVERRIDES - TYPOGRAPHY ||============================== // 2 | 3 | export default function Typography() { 4 | return { 5 | MuiTypography: { 6 | styleOverrides: { 7 | gutterBottom: { 8 | marginBottom: 12 9 | } 10 | } 11 | } 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/themes/shadows.tsx: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { alpha, Theme } from "@mui/material/styles"; 3 | 4 | // types 5 | import { CustomShadowProps } from "@/types/theme"; 6 | 7 | // ==============================|| DEFAULT THEME - CUSTOM SHADOWS ||============================== // 8 | 9 | const CustomShadows = (theme: Theme): CustomShadowProps => ({ 10 | // z1: `0px 2px 8px rgba(0, 0, 0, 0.15)`, 11 | button: theme.palette.mode === "dark" ? `0 2px 0 rgb(0 0 0 / 5%)` : `0 2px #0000000b`, 12 | text: `0 -1px 0 rgb(0 0 0 / 12%)`, 13 | z1: 14 | theme.palette.mode === "dark" 15 | ? `0px 1px 1px rgb(0 0 0 / 14%), 0px 2px 1px rgb(0 0 0 / 12%), 0px 1px 3px rgb(0 0 0 / 20%)` 16 | : `0px 1px 4px ${alpha(theme.palette.grey[900], 0.08)}`, 17 | primary: `0 0 0 2px ${alpha(theme.palette.primary.main, 0.2)}`, 18 | secondary: `0 0 0 2px ${alpha(theme.palette.secondary.main, 0.2)}`, 19 | error: `0 0 0 2px ${alpha(theme.palette.error.main, 0.2)}`, 20 | warning: `0 0 0 2px ${alpha(theme.palette.warning.main, 0.2)}`, 21 | info: `0 0 0 2px ${alpha(theme.palette.info.main, 0.2)}`, 22 | success: `0 0 0 2px ${alpha(theme.palette.success.main, 0.2)}`, 23 | grey: `0 0 0 2px ${alpha(theme.palette.grey[500], 0.2)}`, 24 | primaryButton: `0 14px 12px ${alpha(theme.palette.primary.main, 0.2)}`, 25 | secondaryButton: `0 14px 12px ${alpha(theme.palette.secondary.main, 0.2)}`, 26 | errorButton: `0 14px 12px ${alpha(theme.palette.error.main, 0.2)}`, 27 | warningButton: `0 14px 12px ${alpha(theme.palette.warning.main, 0.2)}`, 28 | infoButton: `0 14px 12px ${alpha(theme.palette.info.main, 0.2)}`, 29 | successButton: `0 14px 12px ${alpha(theme.palette.success.main, 0.2)}`, 30 | greyButton: `0 14px 12px ${alpha(theme.palette.grey[500], 0.2)}` 31 | }); 32 | 33 | export default CustomShadows; 34 | -------------------------------------------------------------------------------- /src/themes/typography.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme, TypographyVariantsOptions } from "@mui/material/styles"; 3 | 4 | // types 5 | import { FontFamily } from "@/types/config"; 6 | import { PaletteMode } from "@mui/material"; 7 | 8 | // ==============================|| DEFAULT THEME - TYPOGRAPHY ||============================== // 9 | 10 | const Typography = (mode: PaletteMode, fontFamily: FontFamily, theme: Theme): TypographyVariantsOptions => ({ 11 | htmlFontSize: 16, 12 | fontFamily, 13 | fontWeightLight: 300, 14 | fontWeightRegular: 400, 15 | fontWeightMedium: 500, 16 | fontWeightBold: 600, 17 | h1: { 18 | fontWeight: 600, 19 | fontSize: "2.375rem", 20 | lineHeight: 1.21 21 | }, 22 | h2: { 23 | fontWeight: 600, 24 | fontSize: "1.875rem", 25 | lineHeight: 1.27 26 | }, 27 | h3: { 28 | fontWeight: 600, 29 | fontSize: "1.5rem", 30 | lineHeight: 1.33 31 | }, 32 | h4: { 33 | fontWeight: 600, 34 | fontSize: "1.25rem", 35 | lineHeight: 1.4 36 | }, 37 | h5: { 38 | fontWeight: 600, 39 | fontSize: "1rem", 40 | lineHeight: 1.5 41 | }, 42 | h6: { 43 | fontWeight: 400, 44 | fontSize: "0.875rem", 45 | lineHeight: 1.57 46 | }, 47 | caption: { 48 | fontWeight: 400, 49 | fontSize: "0.75rem", 50 | lineHeight: 1.66 51 | }, 52 | body1: { 53 | fontSize: "0.875rem", 54 | lineHeight: 1.57 55 | }, 56 | body2: { 57 | fontSize: "0.75rem", 58 | lineHeight: 1.66 59 | }, 60 | subtitle1: { 61 | fontSize: "0.875rem", 62 | fontWeight: 600, 63 | lineHeight: 1.57 64 | }, 65 | subtitle2: { 66 | fontSize: "0.75rem", 67 | fontWeight: 500, 68 | lineHeight: 1.66 69 | }, 70 | overline: { 71 | lineHeight: 1.66 72 | }, 73 | button: { 74 | textTransform: "capitalize" 75 | } 76 | }); 77 | 78 | export default Typography; 79 | -------------------------------------------------------------------------------- /src/types/auth.ts: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | 3 | // third-party 4 | 5 | // models 6 | import type User from "@/model/user"; 7 | 8 | // ==============================|| AUTH TYPES ||============================== // 9 | 10 | export type GuardProps = { 11 | children: ReactElement | null; 12 | }; 13 | 14 | export type UserProfile = { 15 | id?: string; 16 | email?: string; 17 | avatar?: string; 18 | image?: string; 19 | name?: string; 20 | role?: string; 21 | tier?: string; 22 | }; 23 | 24 | export interface AuthProps { 25 | isLoggedIn: boolean; 26 | isAdmin: boolean; 27 | } 28 | 29 | export interface AuthActionProps { 30 | type: string; 31 | payload?: AuthProps; 32 | } 33 | -------------------------------------------------------------------------------- /src/types/config.ts: -------------------------------------------------------------------------------- 1 | export type ThemeDirection = "ltr" | "rtl"; 2 | export type FontFamily = 3 | | `'Inter', sans-serif` 4 | | `'Poppins', sans-serif` 5 | | `'Roboto', sans-serif` 6 | | `'Public Sans', sans-serif`; 7 | 8 | // ==============================|| CONFIG TYPES ||============================== // 9 | 10 | export type CustomizationActionProps = { 11 | type: string; 12 | payload?: CustomizationProps; 13 | }; 14 | 15 | export type DefaultConfigProps = { 16 | defaultPath: string; 17 | fontFamily: FontFamily; 18 | miniDrawer: boolean; 19 | container: boolean; 20 | themeDirection: ThemeDirection; 21 | title: string; 22 | title_split: string; 23 | background_url: string; 24 | description: string; 25 | logo: string | null; 26 | api: string; 27 | languages: string[]; 28 | googleAnalytics?: { 29 | measurementId: string; 30 | }; 31 | emojiEndpoint?: string; 32 | startYear?: number; 33 | }; 34 | 35 | export type CustomizationProps = { 36 | defaultPath: string; 37 | fontFamily: FontFamily; 38 | miniDrawer: boolean; 39 | container: boolean; 40 | themeDirection: ThemeDirection; 41 | onChangeContainer: VoidFunction; 42 | onChangeDirection: (direction: ThemeDirection) => void; 43 | onChangeMiniDrawer: (miniDrawer: boolean) => void; 44 | onChangeFontFamily: (fontFamily: FontFamily) => void; 45 | }; 46 | -------------------------------------------------------------------------------- /src/types/extended.ts: -------------------------------------------------------------------------------- 1 | // material ui 2 | import { Theme } from '@mui/material/styles'; 3 | import { ButtonProps, ChipProps, IconButtonProps, SliderProps } from '@mui/material'; 4 | import { LoadingButtonProps } from '@mui/lab'; 5 | 6 | // ==============================|| EXTENDED COMPONENT - TYPES ||============================== // 7 | 8 | export type ButtonVariantProps = 'contained' | 'light' | 'outlined' | 'dashed' | 'text' | 'shadow'; 9 | export type IconButtonShapeProps = 'rounded' | 'square'; 10 | export type ColorProps = 11 | | ChipProps['color'] 12 | | ButtonProps['color'] 13 | | LoadingButtonProps['color'] 14 | | IconButtonProps['color'] 15 | | SliderProps['color']; 16 | export type AvatarTypeProps = 'filled' | 'outlined' | 'combined'; 17 | export type SizeProps = 'badge' | 'xs' | 'sm' | 'md' | 'lg' | 'xl'; 18 | 19 | export type ExtendedStyleProps = { 20 | color: ColorProps; 21 | theme: Theme; 22 | }; 23 | -------------------------------------------------------------------------------- /src/types/menu.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | // material-ui 4 | import { ChipProps } from "@mui/material"; 5 | 6 | import { GenericCardProps } from "./root"; 7 | 8 | // ==============================|| MENU TYPES ||============================== // 9 | 10 | export type NavItemType = { 11 | breadcrumbs?: boolean; 12 | caption?: ReactNode | string; 13 | children?: NavItemType[]; 14 | chip?: ChipProps; 15 | color?: "primary" | "secondary" | "default" | undefined; 16 | disabled?: boolean; 17 | external?: boolean; 18 | icon?: GenericCardProps["iconPrimary"]; 19 | id?: string; 20 | search?: string; 21 | target?: boolean; 22 | title?: string; 23 | type?: string; 24 | url?: string | undefined; 25 | }; 26 | 27 | export type LinkTarget = "_blank" | "_self" | "_parent" | "_top"; 28 | 29 | export type MenuProps = { 30 | openItem: string[]; 31 | openComponent: string; 32 | drawerOpen: boolean; 33 | componentDrawerOpen: boolean; 34 | }; 35 | -------------------------------------------------------------------------------- /src/types/overrides/Alert.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Alert from '@mui/material/Alert'; 3 | 4 | declare module '@mui/material/Alert' { 5 | interface AlertPropsColorOverrides { 6 | primary; 7 | secondary; 8 | } 9 | interface AlertPropsVariantOverrides { 10 | border; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/overrides/Badge.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Badge from '@mui/material/Badge'; 3 | 4 | declare module '@mui/material/Badge' { 5 | interface BadgePropsVariantOverrides { 6 | light; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/overrides/Button.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Button from '@mui/material/Button'; 3 | 4 | declare module '@mui/material/Button' { 5 | interface ButtonPropsVariantOverrides { 6 | dashed; 7 | shadow; 8 | light; 9 | } 10 | 11 | interface ButtonPropsSizeOverrides { 12 | extraSmall; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/types/overrides/Checkbox.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Checkbox from '@mui/material/Checkbox'; 3 | 4 | declare module '@mui/material/Checkbox' { 5 | interface CheckboxPropsSizeOverrides { 6 | large; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/overrides/Chip.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Chip from '@mui/material/Chip'; 3 | 4 | declare module '@mui/material/Chip' { 5 | interface ChipPropsVariantOverrides { 6 | light; 7 | combined; 8 | } 9 | interface ChipPropsSizeOverrides { 10 | large; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/overrides/Pagination.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Pagination from '@mui/material/Pagination'; 3 | 4 | declare module '@mui/material/Pagination' { 5 | interface PaginationPropsColorOverrides { 6 | error; 7 | success; 8 | warning; 9 | info; 10 | } 11 | interface PaginationPropsVariantOverrides { 12 | contained; 13 | combined; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/overrides/Radio.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Radio from '@mui/material/Radio'; 3 | 4 | declare module '@mui/material/Radio' { 5 | interface RadioPropsSizeOverrides { 6 | large; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/overrides/Switch.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as Switch from '@mui/material/Switch'; 3 | 4 | declare module '@mui/material/Switch' { 5 | interface SwitchPropsSizeOverrides { 6 | large; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/types/overrides/createPalette.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import * as createPalette from '@mui/material/styles'; 3 | 4 | declare module '@mui/material/styles' { 5 | interface SimplePaletteColorOptions { 6 | lighter?: string; 7 | darker?: string; 8 | 0?: string; 9 | 50?: string; 10 | 100?: string; 11 | 200?: string; 12 | 300?: string; 13 | 400?: string; 14 | 500?: string; 15 | 600?: string; 16 | 700?: string; 17 | 800?: string; 18 | 900?: string; 19 | A50?: string; 20 | A100?: string; 21 | A200?: string; 22 | A300?: string; 23 | A400?: string; 24 | A700?: string; 25 | A800?: string; 26 | } 27 | 28 | interface PaletteColor { 29 | lighter: string; 30 | darker: string; 31 | 0?: string; 32 | 50?: string; 33 | 100?: string; 34 | 200?: string; 35 | 300?: string; 36 | 400?: string; 37 | 500?: string; 38 | 600?: string; 39 | 700?: string; 40 | 800?: string; 41 | 900?: string; 42 | A50?: string; 43 | A100?: string; 44 | A200?: string; 45 | A300?: string; 46 | A400?: string; 47 | A700?: string; 48 | A800?: string; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/types/overrides/createTheme.d.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | // eslint-disable-next-line 3 | import * as Theme from "@mui/material/styles"; 4 | 5 | // project import 6 | import { CustomShadowProps } from "@/types/theme"; 7 | 8 | declare module "@mui/material/styles" { 9 | interface Theme { 10 | customShadows: CustomShadowProps; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/types/overrides/index.d.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | // eslint-disable-next-line 3 | import * as Color from '@mui/material'; 4 | 5 | declare module '@mui/material' { 6 | interface Color { 7 | 0?: string; 8 | A50?: string; 9 | A800?: string; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/types/password.ts: -------------------------------------------------------------------------------- 1 | export interface StringColorProps { 2 | id?: string; 3 | label?: string; 4 | color?: string; 5 | primary?: string; 6 | secondary?: string; 7 | } 8 | 9 | export type StringBoolFunc = (s: string) => boolean; 10 | export type StringNumFunc = (s: string) => number; 11 | export type NumbColorFunc = (n: number) => StringColorProps | undefined; 12 | -------------------------------------------------------------------------------- /src/types/plan.ts: -------------------------------------------------------------------------------- 1 | export enum PlanType { 2 | PERIOD = "period", // 周期 3 | TRAFFIC = "traffic" // 流量,一次性 4 | } 5 | 6 | export enum PaymentPeriod { 7 | ONETIME = "onetime_price", // 一次性 8 | MONTHLY = "month_price", // 每月 9 | QUARTERLY = "quarter_price", // 每季度 10 | HALF_YEARLY = "half_year_price", // 每半年 11 | YEARLY = "year_price", // 每年 12 | TWO_YEARLY = "two_year_price", // 每两年 13 | THREE_YEARLY = "three_year_price" // 每三年 14 | } 15 | -------------------------------------------------------------------------------- /src/types/root.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass, FunctionComponent } from 'react'; 2 | 3 | // material-ui 4 | import { SvgIconTypeMap } from '@mui/material'; 5 | import { OverridableComponent } from '@mui/material/OverridableComponent'; 6 | 7 | // types 8 | import { AuthProps } from './auth'; 9 | import { MenuProps } from './menu'; 10 | import { SnackbarProps } from './snackbar'; 11 | 12 | // ==============================|| ROOT TYPES ||============================== // 13 | 14 | export type RootStateProps = { 15 | auth: AuthProps; 16 | menu: MenuProps; 17 | snackbar: SnackbarProps; 18 | }; 19 | 20 | export type KeyedObject = { 21 | [key: string]: string | number | KeyedObject | any; 22 | }; 23 | 24 | export type OverrideIcon = 25 | | (OverridableComponent<SvgIconTypeMap<{}, 'svg'>> & { 26 | muiName: string; 27 | }) 28 | | ComponentClass<any> 29 | | FunctionComponent<any>; 30 | 31 | export interface GenericCardProps { 32 | title?: string; 33 | primary?: string | number | undefined; 34 | secondary?: string; 35 | content?: string; 36 | image?: string; 37 | dateTime?: string; 38 | iconPrimary?: OverrideIcon; 39 | color?: string; 40 | size?: string; 41 | } 42 | -------------------------------------------------------------------------------- /src/types/snackbar.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { AlertProps, SnackbarOrigin } from '@mui/material'; 3 | 4 | // ==============================|| SNACKBAR TYPES ||============================== // 5 | 6 | export type SnackbarActionProps = { 7 | payload?: SnackbarProps; 8 | }; 9 | 10 | export interface SnackbarProps { 11 | action: boolean; 12 | open: boolean; 13 | message: string; 14 | anchorOrigin: SnackbarOrigin; 15 | variant: string; 16 | alert: AlertProps; 17 | transition: string; 18 | close: boolean; 19 | actionButton: boolean; 20 | } 21 | -------------------------------------------------------------------------------- /src/types/theme.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { SimplePaletteColorOptions, PaletteColorOptions } from '@mui/material/styles'; 3 | 4 | // ==============================|| DEFAULT THEME - TYPES ||============================== // 5 | 6 | export type PaletteThemeProps = { 7 | primary: SimplePaletteColorOptions; 8 | secondary: SimplePaletteColorOptions; 9 | error: SimplePaletteColorOptions; 10 | warning: SimplePaletteColorOptions; 11 | info: SimplePaletteColorOptions; 12 | success: SimplePaletteColorOptions; 13 | grey: PaletteColorOptions; 14 | }; 15 | 16 | export type CustomShadowProps = { 17 | button: string; 18 | text: string; 19 | z1: string; 20 | primary: string; 21 | primaryButton: string; 22 | secondary: string; 23 | secondaryButton: string; 24 | error: string; 25 | errorButton: string; 26 | warning: string; 27 | warningButton: string; 28 | info: string; 29 | infoButton: string; 30 | success: string; 31 | successButton: string; 32 | grey: string; 33 | greyButton: string; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/crypto.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | 3 | export const Base64Encode = (input: string) => CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(input)); 4 | export const Base64Decode = (input: string) => CryptoJS.enc.Base64.parse(input).toString(CryptoJS.enc.Utf8); 5 | -------------------------------------------------------------------------------- /src/utils/getColors.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from "@mui/material/styles"; 3 | 4 | // types 5 | import { ColorProps } from "@/types/extended"; 6 | 7 | // ==============================|| CUSTOM FUNCTION - COLORS ||============================== // 8 | 9 | const getColors = (theme: Theme, color?: ColorProps) => { 10 | switch (color!) { 11 | case "secondary": 12 | return theme.palette.secondary; 13 | case "error": 14 | return theme.palette.error; 15 | case "warning": 16 | return theme.palette.warning; 17 | case "info": 18 | return theme.palette.info; 19 | case "success": 20 | return theme.palette.success; 21 | default: 22 | return theme.palette.primary; 23 | } 24 | }; 25 | 26 | export default getColors; 27 | -------------------------------------------------------------------------------- /src/utils/getShadow.ts: -------------------------------------------------------------------------------- 1 | // material-ui 2 | import { Theme } from '@mui/material/styles'; 3 | 4 | // ==============================|| CUSTOM FUNCTION - COLOR SHADOWS ||============================== // 5 | 6 | const getShadow = (theme: Theme, shadow: string) => { 7 | switch (shadow) { 8 | case 'secondary': 9 | return theme.customShadows.secondary; 10 | case 'error': 11 | return theme.customShadows.error; 12 | case 'warning': 13 | return theme.customShadows.warning; 14 | case 'info': 15 | return theme.customShadows.info; 16 | case 'success': 17 | return theme.customShadows.success; 18 | case 'primaryButton': 19 | return theme.customShadows.primaryButton; 20 | case 'secondaryButton': 21 | return theme.customShadows.secondaryButton; 22 | case 'errorButton': 23 | return theme.customShadows.errorButton; 24 | case 'warningButton': 25 | return theme.customShadows.warningButton; 26 | case 'infoButton': 27 | return theme.customShadows.infoButton; 28 | case 'successButton': 29 | return theme.customShadows.successButton; 30 | default: 31 | return theme.customShadows.primary; 32 | } 33 | }; 34 | 35 | export default getShadow; 36 | -------------------------------------------------------------------------------- /src/utils/isBrowser.ts: -------------------------------------------------------------------------------- 1 | const isBrowser = !!(typeof window !== "undefined" && window.document && window.document.createElement); 2 | 3 | export default isBrowser; 4 | -------------------------------------------------------------------------------- /src/utils/password-strength.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Password validator for login pages 3 | */ 4 | import { NumbColorFunc, StringBoolFunc, StringNumFunc } from "@/types/password"; 5 | 6 | // has number 7 | const hasNumber: StringBoolFunc = (number) => new RegExp(/[0-9]/).test(number); 8 | 9 | // has mix of small and capitals 10 | const hasMixed: StringBoolFunc = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A-Z]/).test(number); 11 | 12 | // has special chars 13 | const hasSpecial: StringBoolFunc = (number) => new RegExp(/[!#@$%^&*)(+=._-]/).test(number); 14 | 15 | // set color based on password strength 16 | export const strengthColor: NumbColorFunc = (count) => { 17 | if (count < 2) return { label: "Poor", color: "error.main" }; 18 | if (count < 3) return { label: "Weak", color: "warning.main" }; 19 | if (count < 4) return { label: "Normal", color: "warning.dark" }; 20 | if (count < 5) return { label: "Good", color: "success.main" }; 21 | if (count < 6) return { label: "Strong", color: "success.dark" }; 22 | return { label: "Poor", color: "error.main" }; 23 | }; 24 | 25 | // password strength indicator 26 | export const strengthIndicator: StringNumFunc = (number) => { 27 | let strengths = 0; 28 | if (number.length > 5) strengths += 1; 29 | if (number.length > 7) strengths += 1; 30 | if (hasNumber(number)) strengths += 1; 31 | if (hasSpecial(number)) strengths += 1; 32 | if (hasMixed(number)) strengths += 1; 33 | return strengths; 34 | }; 35 | -------------------------------------------------------------------------------- /src/utils/password-validation.ts: -------------------------------------------------------------------------------- 1 | function isNumber(value: string): boolean { 2 | return new RegExp('^(?=.*[0-9]).+$').test(value); 3 | } 4 | 5 | function isLowercaseChar(value: string): boolean { 6 | return new RegExp('^(?=.*[a-z]).+$').test(value); 7 | } 8 | 9 | function isUppercaseChar(value: string): boolean { 10 | return new RegExp('^(?=.*[A-Z]).+$').test(value); 11 | } 12 | 13 | function isSpecialChar(value: string): boolean { 14 | return new RegExp('^(?=.*[-+_!@#$%^&*.,?]).+$').test(value); 15 | } 16 | 17 | function minLength(value: string): boolean { 18 | return value.length > 7; 19 | } 20 | 21 | export { isNumber, isLowercaseChar, isUppercaseChar, isSpecialChar, minLength }; 22 | -------------------------------------------------------------------------------- /src/utils/plan.ts: -------------------------------------------------------------------------------- 1 | import lo from "lodash-es"; 2 | 3 | // types 4 | import Plan from "@/model/plan"; 5 | import { PaymentPeriod } from "@/types/plan"; 6 | 7 | export const getMode = (plan: Plan) => { 8 | let result: Partial<Record<PaymentPeriod, number>> = {}; 9 | 10 | if (lo.isNumber(plan.onetime_price)) { 11 | result[PaymentPeriod.ONETIME] = plan.onetime_price; 12 | } 13 | 14 | if (lo.isNumber(plan.month_price)) { 15 | result[PaymentPeriod.MONTHLY] = plan.month_price; 16 | } 17 | 18 | if (lo.isNumber(plan.quarter_price)) { 19 | result[PaymentPeriod.QUARTERLY] = plan.quarter_price; 20 | } 21 | 22 | if (lo.isNumber(plan.half_year_price)) { 23 | result[PaymentPeriod.HALF_YEARLY] = plan.half_year_price; 24 | } 25 | 26 | if (lo.isNumber(plan.year_price)) { 27 | result[PaymentPeriod.YEARLY] = plan.year_price; 28 | } 29 | 30 | if (lo.isNumber(plan.two_year_price)) { 31 | result[PaymentPeriod.TWO_YEARLY] = plan.two_year_price; 32 | } 33 | 34 | if (lo.isNumber(plan.three_year_price)) { 35 | result[PaymentPeriod.THREE_YEARLY] = plan.three_year_price; 36 | } 37 | 38 | return result; 39 | }; 40 | 41 | export const getMinPrice = (plan: Plan) => { 42 | const mode = getMode(plan); 43 | return lo.min(Object.values(mode)) || 0; 44 | }; 45 | 46 | export const getMaxPrice = (plan: Plan) => { 47 | const mode = getMode(plan); 48 | return lo.max(Object.values(mode)) || 0; 49 | }; 50 | 51 | export const getPrice = (plan: Plan, period: PaymentPeriod) => { 52 | const mode = getMode(plan); 53 | return mode[period] || 0; 54 | }; 55 | 56 | export const paymentPriority = [ 57 | PaymentPeriod.ONETIME, 58 | PaymentPeriod.MONTHLY, 59 | PaymentPeriod.QUARTERLY, 60 | PaymentPeriod.YEARLY, 61 | PaymentPeriod.HALF_YEARLY, 62 | PaymentPeriod.TWO_YEARLY, 63 | PaymentPeriod.THREE_YEARLY 64 | ]; 65 | export const getFirstPayment = (plan: Plan) => { 66 | const mode = getMode(plan); 67 | const firstPayment = paymentPriority.find((period) => lo.isNumber(mode[period])); 68 | return firstPayment || null; 69 | }; 70 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_VERSION: string; 5 | } 6 | 7 | interface ImportMeta { 8 | readonly env: ImportMetaEnv; 9 | } 10 | 11 | interface Window {} 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "ESNext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | // custom config 20 | "noUnusedLocals": false, // change 21 | "noImplicitAny": true, 22 | "noImplicitThis": true, 23 | "strictNullChecks": true, 24 | "types": ["node"], 25 | "typeRoots": ["./types"], 26 | "baseUrl": "src", 27 | "downlevelIteration": true, 28 | "paths": { 29 | "@/*": ["*"] 30 | } 31 | }, 32 | "include": ["src"], 33 | "exclude": ["node_modules", "dist", "scripts", "acceptance-tests", "jest", "cypress"] 34 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, splitVendorChunkPlugin } from "vite"; 2 | import legacy from "@vitejs/plugin-legacy"; 3 | import { fileURLToPath, URL } from "url"; 4 | import react from "@vitejs/plugin-react"; 5 | import { visualizer } from "rollup-plugin-visualizer"; 6 | import image from "@rollup/plugin-image"; 7 | 8 | /** 9 | * @type {import('vite').UserConfig} 10 | * @see https://vitejs.dev/config/ 11 | */ 12 | export default defineConfig({ 13 | plugins: [ 14 | react(), 15 | visualizer(), 16 | image(), 17 | splitVendorChunkPlugin(), 18 | legacy({ 19 | targets: ["defaults", "not IE 11"] 20 | }) 21 | ], 22 | resolve: { 23 | alias: { 24 | "@": fileURLToPath(new URL("./src", import.meta.url)), 25 | lodash: "lodash-es" 26 | } 27 | }, 28 | server: { 29 | port: 3000 30 | }, 31 | build: { 32 | outDir: "dist", 33 | assetsDir: "static", 34 | cssCodeSplit: true, 35 | chunkSizeWarningLimit: 500, 36 | modulePreload: { 37 | polyfill: true 38 | }, 39 | rollupOptions: { 40 | output: { 41 | chunkFileNames: "static/js/[name].[hash:12].chunk.js", 42 | entryFileNames: "static/js/[name].[hash:12].js", 43 | assetFileNames: (info) => { 44 | if (["css", "sass", "scss"].some((ext) => info.name?.endsWith("." + ext))) { 45 | return "static/css/[name].[hash:12].[ext]"; 46 | } 47 | 48 | if (["png", "jpg", "jpeg", "gif", "svg"].some((ext) => info.name?.endsWith("." + ext))) { 49 | return "static/img/[name].[hash:12].[ext]"; 50 | } 51 | 52 | return "static/media/[name].[hash:12].[ext]"; 53 | } 54 | } 55 | } 56 | } 57 | }); 58 | --------------------------------------------------------------------------------