diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index b0680587b..eca85b3e7 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -31,7 +31,7 @@ jobs: run: | cd vite corepack enable - yarn set version 4.10.3 + yarn set version 4.17.1 yarn yarn build diff --git a/next/.env b/next/.env new file mode 100644 index 000000000..86f1ef4b3 --- /dev/null +++ b/next/.env @@ -0,0 +1,5 @@ +NEXT_PUBLIC_VERSION=v5.2.0 +GENERATE_SOURCEMAP=false + +## Backend API URL +NEXT_PUBLIC_API_URL=https://mock-data-api-nextjs.vercel.app/ \ No newline at end of file diff --git a/next/.gitignore b/next/.gitignore new file mode 100644 index 000000000..d32ba23b7 --- /dev/null +++ b/next/.gitignore @@ -0,0 +1,43 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +!package-lock.json diff --git a/next/.prettierignore b/next/.prettierignore new file mode 100644 index 000000000..db5ddae55 --- /dev/null +++ b/next/.prettierignore @@ -0,0 +1,2 @@ +.next +node_modules \ No newline at end of file diff --git a/next/.prettierrc b/next/.prettierrc new file mode 100644 index 000000000..b5cde330e --- /dev/null +++ b/next/.prettierrc @@ -0,0 +1,9 @@ +{ + "bracketSpacing": true, + "printWidth": 140, + "singleQuote": true, + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false, + "endOfLine": "lf" +} diff --git a/next/.yarnrc.yml b/next/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/next/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/next/eslint.config.mjs b/next/eslint.config.mjs new file mode 100644 index 000000000..7b3c8f447 --- /dev/null +++ b/next/eslint.config.mjs @@ -0,0 +1,89 @@ +import eslint from '@eslint/js'; +import { defineConfig, globalIgnores } from 'eslint/config'; +import nextPlugin from '@next/eslint-plugin-next'; +import reactHooks from 'eslint-plugin-react-hooks'; +import prettier from 'eslint-config-prettier/flat'; +import prettierPlugin from 'eslint-plugin-prettier'; +import globals from 'globals'; + +// ESLint 10 flat config. +// eslint-config-next is intentionally not used: the plugins it bundles +// (eslint-plugin-react / import / jsx-a11y) have no ESLint 10 compatible +// release yet. We compose the pieces that do support ESLint 10 directly: +// the Next.js plugin (core-web-vitals rules) and react-hooks. +export default defineConfig([ + eslint.configs.recommended, + + // Next.js core-web-vitals rules (registers the @next/next plugin) + nextPlugin.configs['core-web-vitals'], + + // React Hooks recommended (ESLint 10 compatible flat config) + reactHooks.configs.flat['recommended-latest'], + + { + files: ['**/*.{js,jsx,mjs}'], + + plugins: { + prettier: prettierPlugin + }, + + languageOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + parserOptions: { + ecmaFeatures: { jsx: true } + }, + globals: { + ...globals.browser, + ...globals.node, + ...globals.es2021 + } + }, + + rules: { + // --- Core rule preferences --- + 'no-param-reassign': 'off', + 'no-console': 'off', + 'no-shadow': 'off', + 'prefer-destructuring': 'off', + + // --- React Hooks: silence the noisy new (v7) rules --- + 'react-hooks/purity': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/immutability': 'off', + 'react-hooks/static-components': 'off', + + // --- New in ESLint 10 `recommended`: noisy on default-init idioms + // (e.g. `let x = ; if (…) x = …`), keep it off --- + 'no-useless-assignment': 'off', + + // --- Project-specific restrictions --- + 'no-restricted-imports': [ + 'error', + { + patterns: ['@mui/*/*/*', '!@mui/material/test-utils/*'] + } + ], + + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'none', + // allow `const { omit, ...rest } = obj` to intentionally drop props + ignoreRestSiblings: true + } + ], + + // --- Prettier integration --- + 'prettier/prettier': 'warn' + } + }, + + // Disable conflicting rules from all shareable configs + prettier, + + // Global ignores (flat config replacement for .eslintignore) + globalIgnores(['**/node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.js']) +]); diff --git a/next/jsconfig.json b/next/jsconfig.json new file mode 100644 index 000000000..17334bc45 --- /dev/null +++ b/next/jsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "paths": { + "*": ["./src/*"] + }, + "noUnusedLocals": true, + "typeRoots": [ + "../node_modules/@types", + "../@types", + "./types" + ], + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.js", + "**/*.js", + "**/*.jsx", + ".next/types/**/*.js", + ".next/dev/types/**/*.js", + "next.config.mjs" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/next/next-env.d.js b/next/next-env.d.js new file mode 100644 index 000000000..7cc6ac764 --- /dev/null +++ b/next/next-env.d.js @@ -0,0 +1,6 @@ +/// +/// +import "./.next/types/routes.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. \ No newline at end of file diff --git a/next/next.config.mjs b/next/next.config.mjs new file mode 100644 index 000000000..c5d624933 --- /dev/null +++ b/next/next.config.mjs @@ -0,0 +1,32 @@ +const nextConfig = { + // todo: this need to set to true or remove it as default is true. set false as chart was giving error when first render + // https://github.com/apexcharts/apexcharts.js/issues/3652 + reactStrictMode: false, + modularizeImports: { + '@mui/material': { + transform: '@mui/material/{{member}}' + }, + '@mui/lab': { + transform: '@mui/lab/{{member}}' + }, + '@mui/icons-material': { + transform: '@mui/icons-material/{{member}}' + } + }, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'flagcdn.com', + pathname: '**' + } + ], + localPatterns: [ + { + pathname: '/assets/**' + } + ] + } +}; + +export default nextConfig; \ No newline at end of file diff --git a/next/package.json b/next/package.json new file mode 100644 index 000000000..77291d4bf --- /dev/null +++ b/next/package.json @@ -0,0 +1,46 @@ +{ + "name": "berry-free-react-material-next-js", + "version": "5.2.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", + "lint:fix": "eslint --fix \"src/**/*.{js,jsx,ts,tsx}\"", + "prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx}\"" + }, + "dependencies": { + "@emotion/cache": "11.14.0", + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "9.1.1", + "@mui/material": "9.1.2", + "@mui/utils": "9.1.1", + "@tabler/icons-react": "3.44.0", + "apexcharts": "5.15.2", + "framer-motion": "12.42.0", + "lodash-es": "4.18.1", + "material-ui-popup-state": "5.3.7", + "next": "16.0.4", + "react": "19.2.7", + "react-apexcharts": "2.1.1", + "react-device-detect": "2.2.3", + "react-dom": "19.2.7", + "simplebar-react": "3.3.2", + "swr": "2.4.2", + "yup": "1.7.1" + }, + "devDependencies": { + "@eslint/js": "10.0.1", + "@next/eslint-plugin-next": "16.0.4", + "eslint": "10.7.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "eslint-plugin-react-hooks": "7.1.1", + "globals": "17.7.0", + "prettier": "3.8.5", + "sass": "1.101.0" + }, + "packageManager": "yarn@4.17.1" +} diff --git a/vite/src/assets/images/auth/auth-pattern-dark.svg b/next/public/assets/images/auth/auth-pattern-dark.svg similarity index 100% rename from vite/src/assets/images/auth/auth-pattern-dark.svg rename to next/public/assets/images/auth/auth-pattern-dark.svg diff --git a/vite/src/assets/images/auth/auth-pattern.svg b/next/public/assets/images/auth/auth-pattern.svg similarity index 100% rename from vite/src/assets/images/auth/auth-pattern.svg rename to next/public/assets/images/auth/auth-pattern.svg diff --git a/vite/src/assets/images/auth/img-a2-grid-dark.svg b/next/public/assets/images/auth/img-a2-grid-dark.svg similarity index 100% rename from vite/src/assets/images/auth/img-a2-grid-dark.svg rename to next/public/assets/images/auth/img-a2-grid-dark.svg diff --git a/vite/src/assets/images/auth/img-a2-grid.svg b/next/public/assets/images/auth/img-a2-grid.svg similarity index 100% rename from vite/src/assets/images/auth/img-a2-grid.svg rename to next/public/assets/images/auth/img-a2-grid.svg diff --git a/vite/src/assets/images/icons/earning.svg b/next/public/assets/images/icons/earning.svg similarity index 100% rename from vite/src/assets/images/icons/earning.svg rename to next/public/assets/images/icons/earning.svg diff --git a/vite/src/assets/images/icons/google.svg b/next/public/assets/images/icons/google.svg similarity index 100% rename from vite/src/assets/images/icons/google.svg rename to next/public/assets/images/icons/google.svg diff --git a/vite/src/assets/images/logo-dark.svg b/next/public/assets/images/logo-dark.svg similarity index 100% rename from vite/src/assets/images/logo-dark.svg rename to next/public/assets/images/logo-dark.svg diff --git a/vite/src/assets/images/logo.svg b/next/public/assets/images/logo.svg similarity index 100% rename from vite/src/assets/images/logo.svg rename to next/public/assets/images/logo.svg diff --git a/vite/src/assets/images/users/user-round.svg b/next/public/assets/images/users/user-round.svg similarity index 100% rename from vite/src/assets/images/users/user-round.svg rename to next/public/assets/images/users/user-round.svg diff --git a/next/public/favicon.ico b/next/public/favicon.ico new file mode 100644 index 000000000..1fc9ce781 Binary files /dev/null and b/next/public/favicon.ico differ diff --git a/next/public/favicon.svg b/next/public/favicon.svg new file mode 100644 index 000000000..72033ff62 --- /dev/null +++ b/next/public/favicon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/next/public/file.svg b/next/public/file.svg new file mode 100644 index 000000000..004145cdd --- /dev/null +++ b/next/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/next/public/globe.svg b/next/public/globe.svg new file mode 100644 index 000000000..567f17b0d --- /dev/null +++ b/next/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/next/public/next.svg b/next/public/next.svg new file mode 100644 index 000000000..5174b28c5 --- /dev/null +++ b/next/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/next/public/vercel.svg b/next/public/vercel.svg new file mode 100644 index 000000000..770539603 --- /dev/null +++ b/next/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/next/public/window.svg b/next/public/window.svg new file mode 100644 index 000000000..b2b2a44f6 --- /dev/null +++ b/next/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/next/src/api/menu.js b/next/src/api/menu.js new file mode 100644 index 000000000..8089224cb --- /dev/null +++ b/next/src/api/menu.js @@ -0,0 +1,41 @@ +import useSWR, { mutate } from 'swr'; +import { useMemo } from 'react'; + +const initialState = { + isDashboardDrawerOpened: false +}; + +const endpoints = { + key: 'api/menu', + master: 'master' +}; + +export function useGetMenuMaster() { + const { data, isLoading } = useSWR(endpoints.key + endpoints.master, () => initialState, { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false + }); + + const memoizedValue = useMemo( + () => ({ + menuMaster: data, + menuMasterLoading: isLoading + }), + [data, isLoading] + ); + + return memoizedValue; +} + +export function handlerDrawerOpen(isDashboardDrawerOpened) { + // to update local state based on key + + mutate( + endpoints.key + endpoints.master, + (currentMenuMaster) => { + return { ...currentMenuMaster, isDashboardDrawerOpened }; + }, + false + ); +} diff --git a/next/src/app/(dashboard)/dashboard/default/page.jsx b/next/src/app/(dashboard)/dashboard/default/page.jsx new file mode 100644 index 000000000..ed95cd9c0 --- /dev/null +++ b/next/src/app/(dashboard)/dashboard/default/page.jsx @@ -0,0 +1,7 @@ +import DefaultDashboard from 'views/dashboard/Default'; + +// ==============================|| PAGE ||============================== // + +export default function DefaultDashboardPage() { + return ; +} diff --git a/next/src/app/(dashboard)/layout.jsx b/next/src/app/(dashboard)/layout.jsx new file mode 100644 index 000000000..8045de26a --- /dev/null +++ b/next/src/app/(dashboard)/layout.jsx @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; + +// project imports +import DashboardLayout from 'layout/MainLayout'; + +// ==============================|| DASHBOARD LAYOUT ||============================== // + +export default function Layout({ children }) { + return {children}; +} + +Layout.propTypes = { children: PropTypes.node }; diff --git a/next/src/app/(dashboard)/sample-page/page.jsx b/next/src/app/(dashboard)/sample-page/page.jsx new file mode 100644 index 000000000..d55c7aa8d --- /dev/null +++ b/next/src/app/(dashboard)/sample-page/page.jsx @@ -0,0 +1,7 @@ +import Sample from 'views/sample-page'; + +// ==============================|| PAGE ||============================== // + +export default function SamplePage() { + return ; +} diff --git a/next/src/app/(dashboard)/utils/util-color/page.jsx b/next/src/app/(dashboard)/utils/util-color/page.jsx new file mode 100644 index 000000000..ff101ea23 --- /dev/null +++ b/next/src/app/(dashboard)/utils/util-color/page.jsx @@ -0,0 +1,7 @@ +import Color from 'views/utilities/Color'; + +// ==============================|| PAGE ||============================== // + +export default function ColorPage() { + return ; +} diff --git a/next/src/app/(dashboard)/utils/util-shadow/page.jsx b/next/src/app/(dashboard)/utils/util-shadow/page.jsx new file mode 100644 index 000000000..cac722743 --- /dev/null +++ b/next/src/app/(dashboard)/utils/util-shadow/page.jsx @@ -0,0 +1,7 @@ +import Shadow from 'views/utilities/Shadow'; + +// ==============================|| PAGE ||============================== // + +export default function ShadowPage() { + return ; +} diff --git a/next/src/app/(dashboard)/utils/util-typography/page.jsx b/next/src/app/(dashboard)/utils/util-typography/page.jsx new file mode 100644 index 000000000..af7a09b83 --- /dev/null +++ b/next/src/app/(dashboard)/utils/util-typography/page.jsx @@ -0,0 +1,7 @@ +import Typography from 'views/utilities/Typography'; + +// ==============================|| PAGE ||============================== // + +export default function TypographyPage() { + return ; +} diff --git a/next/src/app/(minimal)/(auth)/layout.jsx b/next/src/app/(minimal)/(auth)/layout.jsx new file mode 100644 index 000000000..0128dcc2d --- /dev/null +++ b/next/src/app/(minimal)/(auth)/layout.jsx @@ -0,0 +1,9 @@ +import PropTypes from 'prop-types'; + +// ================================|| SIMPLE LAYOUT ||================================ // + +export default function Layout({ children }) { + return <>{children}; +} + +Layout.propTypes = { children: PropTypes.node }; diff --git a/next/src/app/(minimal)/(auth)/login/page.jsx b/next/src/app/(minimal)/(auth)/login/page.jsx new file mode 100644 index 000000000..05a4a26d0 --- /dev/null +++ b/next/src/app/(minimal)/(auth)/login/page.jsx @@ -0,0 +1,7 @@ +import Login from 'views/pages/authentication/Login'; + +// ==============================|| PAGE ||============================== // + +export default function LoginPage() { + return ; +} diff --git a/next/src/app/(minimal)/(auth)/register/page.jsx b/next/src/app/(minimal)/(auth)/register/page.jsx new file mode 100644 index 000000000..78602cbc4 --- /dev/null +++ b/next/src/app/(minimal)/(auth)/register/page.jsx @@ -0,0 +1,7 @@ +import Register from 'views/pages/authentication/Register'; + +// ==============================|| PAGE ||============================== // + +export default function RegisterPage() { + return ; +} diff --git a/next/src/app/(minimal)/layout.jsx b/next/src/app/(minimal)/layout.jsx new file mode 100644 index 000000000..ff6acc9ac --- /dev/null +++ b/next/src/app/(minimal)/layout.jsx @@ -0,0 +1,12 @@ +import PropTypes from 'prop-types'; + +// project imports +import MinimalLayout from 'layout/MinimalLayout'; + +// ================================|| SIMPLE LAYOUT ||================================ // + +export default function Layout({ children }) { + return {children}; +} + +Layout.propTypes = { children: PropTypes.node }; diff --git a/next/src/app/layout.jsx b/next/src/app/layout.jsx new file mode 100644 index 000000000..14fa21447 --- /dev/null +++ b/next/src/app/layout.jsx @@ -0,0 +1,29 @@ +import PropTypes from 'prop-types'; + +import './../scss/style.scss'; + +// project imports +import ProviderWrapper from 'store/ProviderWrapper'; + +export const metadata = { + title: ' Berry - React MUI Admin Dashboard Template', + description: + 'Berry is a fully customizable and powerful admin dashboard template built with React.js, MUI, and Next.js for your next project.' +}; + +// ==============================|| ROOT LAYOUT ||============================== // + +export default function RootLayout({ children }) { + return ( + + + + + + {children} + + + ); +} + +RootLayout.propTypes = { children: PropTypes.node }; diff --git a/next/src/app/loading.jsx b/next/src/app/loading.jsx new file mode 100644 index 000000000..ae6311f5e --- /dev/null +++ b/next/src/app/loading.jsx @@ -0,0 +1,9 @@ +'use client'; + +import Loader from 'ui-component/Loader'; + +// ==============================|| LOADING ||============================== // + +export default function Loading() { + return ; +} diff --git a/next/src/app/page.jsx b/next/src/app/page.jsx new file mode 100644 index 000000000..7ae009746 --- /dev/null +++ b/next/src/app/page.jsx @@ -0,0 +1,7 @@ +import { redirect } from 'next/navigation'; + +// ==============================|| HOME PAGE ||============================== // + +export default function HomePage() { + redirect('/dashboard/default'); +} diff --git a/next/src/config.js b/next/src/config.js new file mode 100644 index 000000000..6cab735ea --- /dev/null +++ b/next/src/config.js @@ -0,0 +1,11 @@ +export const DASHBOARD_PATH = '/sample-page'; +export const DEFAULT_THEME_MODE = 'system'; + +export const CSS_VAR_PREFIX = ''; + +const config = { + fontFamily: `'Roboto', sans-serif`, + borderRadius: 8 +}; + +export default config; diff --git a/next/src/contexts/ConfigContext.jsx b/next/src/contexts/ConfigContext.jsx new file mode 100644 index 000000000..cc37401b4 --- /dev/null +++ b/next/src/contexts/ConfigContext.jsx @@ -0,0 +1,24 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { createContext, useMemo } from 'react'; + +// project imports +import config from 'config'; +import { useLocalStorage } from 'hooks/useLocalStorage'; + +// ==============================|| CONFIG CONTEXT ||============================== // + +export const ConfigContext = createContext(undefined); + +// ==============================|| CONFIG PROVIDER ||============================== // + +export function ConfigProvider({ children }) { + const { state, setState, setField, resetState } = useLocalStorage('berry-config-next-js', config); + + const memoizedValue = useMemo(() => ({ state, setState, setField, resetState }), [state, setField, setState, resetState]); + + return {children}; +} + +ConfigProvider.propTypes = { children: PropTypes.node }; diff --git a/next/src/hooks/useConfig.js b/next/src/hooks/useConfig.js new file mode 100644 index 000000000..1b72cf0d5 --- /dev/null +++ b/next/src/hooks/useConfig.js @@ -0,0 +1,12 @@ +import { use } from 'react'; +import { ConfigContext } from 'contexts/ConfigContext'; + +// ==============================|| CONFIG - HOOKS ||============================== // + +export default function useConfig() { + const context = use(ConfigContext); + + if (!context) throw new Error('useSConfig must be use inside ConfigProvider'); + + return context; +} diff --git a/next/src/hooks/useLocalStorage.js b/next/src/hooks/useLocalStorage.js new file mode 100644 index 000000000..be454fbf5 --- /dev/null +++ b/next/src/hooks/useLocalStorage.js @@ -0,0 +1,46 @@ +'use client'; +import { useState, useEffect, useCallback } from 'react'; + +// ==============================|| HOOKS - LOCAL STORAGE ||============================== // + +export function useLocalStorage(key, defaultValue) { + // Load initial state from localStorage or fallback to default + const readValue = () => { + if (typeof window === 'undefined') return defaultValue; + + try { + const item = localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (err) { + console.warn(`Error reading localStorage key “${key}”:`, err); + return defaultValue; + } + }; + + const [state, setState] = useState(readValue); + + // Sync to localStorage whenever state changes + useEffect(() => { + try { + localStorage.setItem(key, JSON.stringify(state)); + } catch (err) { + console.warn(`Error setting localStorage key “${key}”:`, err); + } + }, [key, state]); + + // Update single field + const setField = useCallback((key, value) => { + setState((prev) => ({ + ...prev, + [key]: value + })); + }, []); + + // Reset to defaults + const resetState = useCallback(() => { + setState(defaultValue); + localStorage.setItem(key, JSON.stringify(defaultValue)); + }, [defaultValue, key]); + + return { state, setState, setField, resetState }; +} diff --git a/next/src/hooks/useMenuCollapse.js b/next/src/hooks/useMenuCollapse.js new file mode 100644 index 000000000..c22d073a6 --- /dev/null +++ b/next/src/hooks/useMenuCollapse.js @@ -0,0 +1,76 @@ +import { useEffect } from 'react'; + +function matchPath(basePath, pathname) { + // Make sure both paths are normalized + const normalizedBase = basePath.replace(/\/+$/, ''); // remove trailing slash + const normalizedPath = pathname.replace(/\/+$/, ''); + + return normalizedPath === normalizedBase || normalizedPath.startsWith(normalizedBase + '/'); +} + +// ==============================|| MENU COLLAPSED - RECURSIVE FUNCTION ||============================== // + +/** + * Recursively traverses menu items to find and open the correct parent menu. + * If a menu item matches the current pathname, it marks the corresponding menu as selected and opens it. + * + * @param {NavItemType[]} items - List of menu items. + * @param {string} pathname - Current route pathname. + * @param {string | undefined} menuId - ID of the menu to be set as selected. + * @param {SetState} setSelected - Function to update the selected menu. + * @param {Dispatch>} setOpen - Function to update the open state. + */ + +function setParentOpenedMenu(items, pathname, menuId, setSelected, setOpen) { + for (const item of items) { + // Base case: match the URL + if (item.url && matchPath(item.url, pathname)) { + setSelected(menuId ?? null); + setOpen(true); + return true; // child matched + } + + // Recurse if children exist + if (item.children?.length) { + const childMatched = setParentOpenedMenu(item.children, pathname, item.id, setSelected, setOpen); + if (childMatched) { + setOpen(true); + setSelected(menuId ?? null); + return true; + } + } + } + return false; // nothing matched in this branch +} + +// ==============================|| MENU COLLAPSED - HOOK ||============================== // + +/** + * Hook to handle menu collapse behavior based on the current route. + * Automatically expands the parent menu of the active route item. + * + * @param {NavItemType} menu - The menu object containing items. + * @param {string} pathname - Current route pathname. + * @param {boolean} miniMenuOpened - Flag indicating if the mini menu is open. + * @param {SetState} setSelected - Function to update selected menu state. + * @param {Dispatch>} setOpen - Function to update menu open state. + * @param {SetState} setAnchorEl - Function to update the anchor element state. + */ + +export default function useMenuCollapse(menu, pathname, miniMenuOpened, setSelected, setOpen, setAnchorEl) { + useEffect(() => { + setOpen(false); // Close the menu initially + + // Reset selection based on menu state + if (!miniMenuOpened) { + setSelected(null); + } else { + setAnchorEl(null); + } + + // If menu has children, determine which should be opened + if (menu.children?.length) { + setParentOpenedMenu(menu.children, pathname, menu.id, setSelected, setOpen); + } + }, [pathname, menu.children, menu.id, miniMenuOpened, setAnchorEl, setOpen, setSelected]); +} diff --git a/next/src/hooks/useScriptRef.js b/next/src/hooks/useScriptRef.js new file mode 100644 index 000000000..a60aac2f8 --- /dev/null +++ b/next/src/hooks/useScriptRef.js @@ -0,0 +1,13 @@ +import { useEffect, useRef } from 'react'; + +// ==============================|| ELEMENT REFERENCE HOOKS ||============================== // + +export default function useScriptRef() { + const scripted = useRef(true); + + useEffect(() => { + scripted.current = false; + }, []); + + return scripted; +} diff --git a/next/src/layout/Customization/BorderRadius.jsx b/next/src/layout/Customization/BorderRadius.jsx new file mode 100644 index 000000000..52682d604 --- /dev/null +++ b/next/src/layout/Customization/BorderRadius.jsx @@ -0,0 +1,54 @@ +'use client'; + +// material-ui +import Grid from '@mui/material/Grid'; +import Slider from '@mui/material/Slider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +// project imports +import useConfig from 'hooks/useConfig'; + +// concat 'px' +function valueText(value) { + return `${value}px`; +} + +export default function BorderRadius() { + const { + state: { borderRadius }, + setField + } = useConfig(); + + const handleChange = (_event, newValue) => { + setField('borderRadius', newValue); + }; + + return ( + + BORDER RADIUS + + + 4px + + + ({ '& .MuiSlider-valueLabel': { color: 'primary.lighter' } })} + /> + + + 24px + + + + ); +} diff --git a/next/src/layout/Customization/FontFamily.jsx b/next/src/layout/Customization/FontFamily.jsx new file mode 100644 index 000000000..509597270 --- /dev/null +++ b/next/src/layout/Customization/FontFamily.jsx @@ -0,0 +1,86 @@ +'use client'; + +// next +import { Inter, Poppins, Roboto } from 'next/font/google'; + +// material-ui +import FormControlLabel from '@mui/material/FormControlLabel'; +import Grid from '@mui/material/Grid'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +// project imports +import useConfig from 'hooks/useConfig'; +import MainCard from 'ui-component/cards/MainCard'; + +const inter = Inter({ subsets: ['latin'], weight: ['400', '500', '600', '700'] }); +const roboto = Roboto({ subsets: ['latin'], weight: ['300', '400', '500', '700'] }); +const poppins = Poppins({ subsets: ['latin'], weight: ['400', '500', '600', '700'] }); + +// ==============================|| CUSTOMIZATION - FONT FAMILY ||============================== // + +export default function FontFamilyPage() { + const { + state: { fontFamily }, + setField + } = useConfig(); + + const handleFontChange = (event) => { + setField('fontFamily', event.target.value); + }; + + const fonts = [ + { + id: 'inter', + value: inter.style.fontFamily, + label: 'Inter' + }, + { + id: 'poppins', + value: poppins.style.fontFamily, + label: 'Poppins' + }, + { + id: 'roboto', + value: roboto.style.fontFamily, + label: 'Roboto' + } + ]; + + return ( + + FONT STYLE + + + {fonts.map((item, index) => ( + + ({ p: 0.75, bgcolor: fontFamily === item.value ? 'primary.lighter' : 'grey.50' })}> + + } + label={ + + {item.label} + + } + /> + + + + ))} + + + + ); +} diff --git a/next/src/layout/Customization/index.jsx b/next/src/layout/Customization/index.jsx new file mode 100644 index 000000000..34340b530 --- /dev/null +++ b/next/src/layout/Customization/index.jsx @@ -0,0 +1,124 @@ +'use client'; + +import PropTypes from 'prop-types'; +import { Activity, useState } from 'react'; + +// material-ui +import { useColorScheme, useTheme } from '@mui/material/styles'; +import Button from '@mui/material/Button'; +import Divider from '@mui/material/Divider'; +import Drawer from '@mui/material/Drawer'; +import Fab from '@mui/material/Fab'; +import Grid from '@mui/material/Grid'; +import IconButton from '@mui/material/IconButton'; +import Stack from '@mui/material/Stack'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import FontFamily from './FontFamily'; +import BorderRadius from './BorderRadius'; + +import { DEFAULT_THEME_MODE } from 'config'; +import MainCard from 'ui-component/cards/MainCard'; +import AnimateButton from 'ui-component/extended/AnimateButton'; +import SimpleBar from 'ui-component/third-party/SimpleBar'; +import useConfig from 'hooks/useConfig'; + +// assets +import { IconSettings, IconPlus } from '@tabler/icons-react'; + +function CustomTabPanel({ children, value, index, ...other }) { + return ( + + ); +} + +export default function Customization() { + const theme = useTheme(); + const { resetState } = useConfig(); + const { setMode } = useColorScheme(); + + // drawer on/off + const [open, setOpen] = useState(false); + const handleToggle = () => { + setOpen(!open); + }; + + const handleReset = () => { + setMode(DEFAULT_THEME_MODE); + resetState(); + }; + + return ( + <> + {/* toggle button */} + + + + + + + + + + + + + + + Theme Customization + + + + + + + + + + + {/* font family */} + + + + + {/* border radius */} + + + + + + + + + + ); +} + +CustomTabPanel.propTypes = { children: PropTypes.node, value: PropTypes.number, index: PropTypes.number, other: PropTypes.any }; diff --git a/next/src/layout/MainLayout/Footer.jsx b/next/src/layout/MainLayout/Footer.jsx new file mode 100644 index 000000000..747d79c72 --- /dev/null +++ b/next/src/layout/MainLayout/Footer.jsx @@ -0,0 +1,61 @@ +import RouterLink from 'next/link'; + +// material-ui +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +export default function Footer() { + return ( + + + © All rights reserved{' '} + + CodedThemes + + + + + License + + + Hire us + + + Terms + + + Figma Design System + + + + ); +} diff --git a/next/src/layout/MainLayout/Header/NotificationSection/NotificationList.jsx b/next/src/layout/MainLayout/Header/NotificationSection/NotificationList.jsx new file mode 100644 index 000000000..428afe8eb --- /dev/null +++ b/next/src/layout/MainLayout/Header/NotificationSection/NotificationList.jsx @@ -0,0 +1,176 @@ +'use client'; +import PropTypes from 'prop-types'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import Chip from '@mui/material/Chip'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import { withAlpha } from 'utils/colorUtils'; + +// assets +import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'; +import MarkunreadMailboxOutlinedIcon from '@mui/icons-material/MarkunreadMailboxOutlined'; +import SendOutlinedIcon from '@mui/icons-material/SendOutlined'; +import StorefrontOutlinedIcon from '@mui/icons-material/StorefrontOutlined'; +const User1 = '/assets/images/users/user-round.svg'; + +function ListItemWrapper({ children }) { + const theme = useTheme(); + + return ( + + {children} + + ); +} + +// ==============================|| NOTIFICATION LIST ITEM ||============================== // + +export default function NotificationList() { + const containerSX = { gap: 2, pl: 7 }; + + return ( + + + + 2 min ago + + } + > + + + + + + + It is a long established fact that a reader will be distracted + + + + + + + + + 2 min ago + + } + > + + + + + + Store Verification Done} /> + + + We have successfully received your request. + + + + + + 2 min ago + + } + > + + + + + + Check Your Mail.} /> + + + All done! Now check your inbox as you're in for a sweet treat! + + + + + + 2 min ago + + } + > + + + + John Doe} /> + + + + Uploaded two file on   + + 21 Jan 2020 + + + + + + demo.jpg + + + + + + + 2 min ago + + } + > + + + + John Doe} /> + + + It is a long established fact that a reader will be distracted + + + + + ); +} + +ListItemWrapper.propTypes = { children: PropTypes.node }; diff --git a/next/src/layout/MainLayout/Header/NotificationSection/index.jsx b/next/src/layout/MainLayout/Header/NotificationSection/index.jsx new file mode 100644 index 000000000..d53f120bb --- /dev/null +++ b/next/src/layout/MainLayout/Header/NotificationSection/index.jsx @@ -0,0 +1,172 @@ +'use client'; + +import { Activity, useEffect, useRef, useState } from 'react'; + +// next +import Link from 'next/link'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Avatar from '@mui/material/Avatar'; +import Button from '@mui/material/Button'; +import CardActions from '@mui/material/CardActions'; +import Chip from '@mui/material/Chip'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Divider from '@mui/material/Divider'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import MainCard from 'ui-component/cards/MainCard'; +import Transitions from 'ui-component/extended/Transitions'; +import NotificationList from './NotificationList'; + +// assets +import { IconBell } from '@tabler/icons-react'; + +// notification status options +const status = [ + { + value: 'all', + label: 'All Notification' + }, + { + value: 'new', + label: 'New' + }, + { + value: 'unread', + label: 'Unread' + }, + { + value: 'other', + label: 'Other' + } +]; + +// ==============================|| NOTIFICATION ||============================== // + +export default function NotificationSection() { + const theme = useTheme(); + const downMD = useMediaQuery(theme.breakpoints.down('md')); + + const [open, setOpen] = useState(false); + const [value, setValue] = useState(''); + + /** + * anchorRef is used on different componets and specifying one type leads to other components throwing an error + * */ + const anchorRef = useRef(null); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + const prevOpen = useRef(open); + useEffect(() => { + if (prevOpen.current === true && open === false) { + anchorRef.current.focus(); + } + prevOpen.current = open; + }, [open]); + + const handleChange = (event) => { + if (event?.target.value) setValue(event?.target.value); + }; + + return ( + <> + + + + + + + {({ TransitionProps }) => ( + + + + + + + + + All Notification + + + + Mark as all read + + + + + + {status.map((option) => ( + + ))} + + + + + + + + + + + + + + + )} + + + ); +} diff --git a/next/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.jsx b/next/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.jsx new file mode 100644 index 000000000..4ca5a688e --- /dev/null +++ b/next/src/layout/MainLayout/Header/ProfileSection/UpgradePlanCard.jsx @@ -0,0 +1,72 @@ +// material-ui +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Link from '@mui/material/Link'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; + +// project imports +import AnimateButton from 'ui-component/extended/AnimateButton'; + +// ==============================|| PROFILE MENU - UPGRADE PLAN CARD ||============================== // + +export default function UpgradePlanCard() { + const cardSX = { + content: '""', + position: 'absolute', + width: 200, + height: 200, + borderColor: 'warning.main' + }; + + return ( + + + + Upgrade your plan + + 70% discount for 1 years
+ subscriptions. +
+ + + + + + + +
+
+
+ ); +} diff --git a/next/src/layout/MainLayout/Header/ProfileSection/index.jsx b/next/src/layout/MainLayout/Header/ProfileSection/index.jsx new file mode 100644 index 000000000..5f496fe00 --- /dev/null +++ b/next/src/layout/MainLayout/Header/ProfileSection/index.jsx @@ -0,0 +1,227 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; + +// material-ui +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Divider from '@mui/material/Divider'; +import InputAdornment from '@mui/material/InputAdornment'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import UpgradePlanCard from './UpgradePlanCard'; +import useConfig from 'hooks/useConfig'; +import MainCard from 'ui-component/cards/MainCard'; +import Transitions from 'ui-component/extended/Transitions'; + +// assets +import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined'; +import { IconLogout, IconSettings, IconUser } from '@tabler/icons-react'; +const User1 = '/assets/images/users/user-round.svg'; + +// ==============================|| PROFILE MENU ||============================== // + +export default function ProfileSection() { + const { + state: { borderRadius } + } = useConfig(); + + const [sdm, setSdm] = useState(true); + const [value, setValue] = useState(''); + const [notification, setNotification] = useState(false); + const [open, setOpen] = useState(false); + + /** + * anchorRef is used on different components and specifying one type leads to other components throwing an error + * */ + const anchorRef = useRef(null); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + }; + + const prevOpen = useRef(open); + useEffect(() => { + if (prevOpen.current === true && open === false) { + anchorRef.current.focus(); + } + + prevOpen.current = open; + }, [open]); + + return ( + <> + + } + label={} + ref={anchorRef} + aria-controls={open ? 'menu-list-grow' : undefined} + aria-haspopup="true" + onClick={handleToggle} + color="primary" + aria-label="user-account" + /> + + {({ TransitionProps }) => ( + + + + {open && ( + + + + + Good Morning, + + Johne Doe + + + Project Admin + + setValue(e.target.value)} + placeholder="Search profile options" + startAdornment={ + + + + } + sx={{ my: 2 }} + aria-describedby="search-helper-text" + slotProps={{ input: { 'aria-label': 'profile options' } }} + /> + + + + + + + + + + Start DND Mode + setSdm(e.target.checked)} name="sdm" size="small" /> + + + Allow Notifications + setNotification(e.target.checked)} name="sdm" size="small" /> + + + + + + + + + + + Account Settings} /> + + + + + + + Social Profile + + + } + /> + + + + + + Logout} /> + + + + + )} + + + + )} + + + ); +} diff --git a/next/src/layout/MainLayout/Header/SearchSection/index.jsx b/next/src/layout/MainLayout/Header/SearchSection/index.jsx new file mode 100644 index 000000000..f25c69cad --- /dev/null +++ b/next/src/layout/MainLayout/Header/SearchSection/index.jsx @@ -0,0 +1,163 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { useState } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import Grid from '@mui/material/Grid'; +import InputAdornment from '@mui/material/InputAdornment'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import Popper from '@mui/material/Popper'; +import Box from '@mui/material/Box'; + +// third party +import PopupState, { bindPopper, bindToggle } from 'material-ui-popup-state'; + +// project imports +import Transitions from 'ui-component/extended/Transitions'; + +// assets +import { IconAdjustmentsHorizontal, IconSearch, IconX } from '@tabler/icons-react'; + +function HeaderAvatar({ children, ref, ...others }) { + const theme = useTheme(); + + return ( + + {children} + + ); +} + +// ==============================|| SEARCH INPUT - MOBILE||============================== // + +function MobileSearch({ value, setValue, popupState }) { + const theme = useTheme(); + + return ( + setValue(e.target.value)} + placeholder="Search" + startAdornment={ + + + + } + endAdornment={ + + + + + + + + + + + } + aria-describedby="search-helper-text" + slotProps={{ input: { 'aria-label': 'weight', sx: { bgcolor: 'transparent', pl: 0.5 } } }} + sx={{ width: '100%', ml: 0.5, px: 2, bgcolor: 'background.paper' }} + /> + ); +} + +// ==============================|| SEARCH INPUT ||============================== // + +export default function SearchSection() { + const [value, setValue] = useState(''); + + return ( + <> + + + {(popupState) => ( + <> + + + + + + + {({ TransitionProps }) => ( + <> + + + + + + + + + + + + + )} + + + )} + + + + setValue(e.target.value)} + placeholder="Search" + startAdornment={ + + + + } + endAdornment={ + + + + + + } + aria-describedby="search-helper-text" + slotProps={{ input: { 'aria-label': 'weight', sx: { bgcolor: 'transparent', pl: 0.5 } } }} + sx={{ width: { md: 250, lg: 434 }, ml: 2, px: 2 }} + /> + + + ); +} + +HeaderAvatar.propTypes = { children: PropTypes.node, ref: PropTypes.any, others: PropTypes.any }; + +MobileSearch.propTypes = { value: PropTypes.string, setValue: PropTypes.func, popupState: PropTypes.any }; diff --git a/next/src/layout/MainLayout/Header/index.jsx b/next/src/layout/MainLayout/Header/index.jsx new file mode 100644 index 000000000..7d2e4fd2c --- /dev/null +++ b/next/src/layout/MainLayout/Header/index.jsx @@ -0,0 +1,66 @@ +'use client'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Avatar from '@mui/material/Avatar'; +import Box from '@mui/material/Box'; + +// project imports +import LogoSection from '../LogoSection'; +import SearchSection from './SearchSection'; +import ProfileSection from './ProfileSection'; +import NotificationSection from './NotificationSection'; + +import { handlerDrawerOpen, useGetMenuMaster } from 'api/menu'; + +// assets +import { IconMenu2 } from '@tabler/icons-react'; + +// ==============================|| MAIN NAVBAR / HEADER ||============================== // + +export default function Header() { + const theme = useTheme(); + const downMD = useMediaQuery(theme.breakpoints.down('md')); + + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + + return ( + <> + {/* logo & toggler button */} + + + + + handlerDrawerOpen(!drawerOpen)} + > + + + + + {/* header search */} + + + + + {/* notification */} + + + {/* profile */} + + + ); +} diff --git a/next/src/layout/MainLayout/LogoSection/index.jsx b/next/src/layout/MainLayout/LogoSection/index.jsx new file mode 100644 index 000000000..553a4439b --- /dev/null +++ b/next/src/layout/MainLayout/LogoSection/index.jsx @@ -0,0 +1,19 @@ +// next +import RouterLink from 'next/link'; + +// material-ui +import Link from '@mui/material/Link'; + +// project imports +import { DASHBOARD_PATH } from 'config'; +import Logo from 'ui-component/Logo'; + +// ==============================|| MAIN LOGO ||============================== // + +export default function LogoSection() { + return ( + + + + ); +} diff --git a/next/src/layout/MainLayout/MainContentStyled.js b/next/src/layout/MainLayout/MainContentStyled.js new file mode 100644 index 000000000..b0198fb7a --- /dev/null +++ b/next/src/layout/MainLayout/MainContentStyled.js @@ -0,0 +1,61 @@ +'use client'; +// material-ui +import { styled } from '@mui/material/styles'; + +// project imports +import { drawerWidth, HEADER_HEIGHT } from 'store/constant'; + +// ==============================|| MAIN LAYOUT - STYLED ||============================== // + +const MainContentStyled = styled('main', { + shouldForwardProp: (prop) => prop !== 'open' && prop !== 'borderRadius' +})(({ theme, open, borderRadius }) => ({ + backgroundColor: theme.vars.palette.grey[100], + minWidth: '1%', + width: '100%', + minHeight: `calc(100vh - ${HEADER_HEIGHT}px)`, + flexGrow: 1, + padding: 20, + marginTop: HEADER_HEIGHT, + marginRight: 20, + borderRadius: `${borderRadius}px`, + borderBottomLeftRadius: 0, + borderBottomRightRadius: 0, + ...(!open && { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shorter + 200 + }), + [theme.breakpoints.up('md')]: { + marginLeft: -(drawerWidth - 72), + width: `calc(100% - ${drawerWidth}px)`, + marginTop: HEADER_HEIGHT + } + }), + ...(open && { + transition: theme.transitions.create('margin', { + easing: theme.transitions.easing.easeOut, + duration: theme.transitions.duration.shorter + 200 + }), + marginLeft: 0, + marginTop: HEADER_HEIGHT, + width: `calc(100% - ${drawerWidth}px)`, + [theme.breakpoints.up('md')]: { + marginTop: HEADER_HEIGHT + } + }), + [theme.breakpoints.down('md')]: { + marginLeft: 20, + padding: 16, + marginTop: HEADER_HEIGHT, + ...(!open && { + width: `calc(100% - ${drawerWidth}px)` + }) + }, + [theme.breakpoints.down('sm')]: { + marginLeft: 10, + marginRight: 10 + } +})); + +export default MainContentStyled; diff --git a/next/src/layout/MainLayout/MenuList/NavCollapse/index.jsx b/next/src/layout/MainLayout/MenuList/NavCollapse/index.jsx new file mode 100644 index 000000000..8d8161481 --- /dev/null +++ b/next/src/layout/MainLayout/MenuList/NavCollapse/index.jsx @@ -0,0 +1,413 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { Activity, useEffect, useRef, useState } from 'react'; + +// next +import { usePathname } from 'next/navigation'; + +// material-ui +import { styled, useTheme } from '@mui/material/styles'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Collapse from '@mui/material/Collapse'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import NavItem from '../NavItem'; +import Transitions from 'ui-component/extended/Transitions'; + +import { useGetMenuMaster } from 'api/menu'; +import useConfig from 'hooks/useConfig'; +import useMenuCollapse from 'hooks/useMenuCollapse'; + +// assets +import { IconChevronDown, IconChevronRight, IconChevronUp } from '@tabler/icons-react'; +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; + +// horizontal-menu - wrapper +const PopperStyled = styled(Popper)(({ theme }) => ({ + overflow: 'visible', + zIndex: 1202, + minWidth: 180, + '&:before': { + content: '""', + display: 'block', + position: 'absolute', + top: 34, + left: -5, + width: 12, + height: 12, + transform: 'translateY(-50%) rotate(45deg)', + zIndex: 120, + borderWidth: '6px', + borderStyle: 'solid', + borderColor: `transparent transparent ${theme.vars.palette.background.paper} ${theme.vars.palette.background.paper}` + }, + '&[data-popper-placement="left-start"]:before': { + left: 'auto', + right: -5, + borderColor: `${theme.vars.palette.background.paper} ${theme.vars.palette.background.paper} transparent transparent` + }, + '&[data-popper-placement="left-end"]:before': { + top: 'auto', + bottom: 15, + left: 'auto', + right: -5, + borderColor: `${theme.vars.palette.background.paper} ${theme.vars.palette.background.paper} transparent transparent` + }, + '&[data-popper-placement="right-end"]:before': { + top: 'auto', + bottom: 15 + } +})); + +export default function NavCollapse({ menu, level, parentId }) { + const theme = useTheme(); + const ref = useRef(null); + + const { + state: { borderRadius } + } = useConfig(); + + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + const isHorizontal = false; + + const [open, setOpen] = useState(false); + const [selected, setSelected] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + + const handleClickMini = (event) => { + setAnchorEl(null); + if (drawerOpen) { + setOpen(!open); + setSelected(!selected ? menu.id : null); + } else { + setAnchorEl(event?.currentTarget); + } + }; + + const handleHover = (event) => { + setAnchorEl(event?.currentTarget); + }; + + const openMini = Boolean(anchorEl); + + const handleMiniClose = () => { + setAnchorEl(null); + }; + + const handleClosePopper = () => { + setOpen(false); + if (!openMini) { + if (!menu.url) { + setSelected(null); + } + } + setAnchorEl(null); + }; + + const pathname = usePathname(); + + // menu collapse for sub-levels + useMenuCollapse(menu, pathname, openMini, setSelected, setOpen, setAnchorEl); + + const [hoverStatus, setHover] = useState(false); + + const compareSize = () => { + const compare = ref.current && ref.current.scrollWidth > ref.current.clientWidth; + setHover(compare); + }; + + useEffect(() => { + compareSize(); + window.addEventListener('resize', compareSize); + window.removeEventListener('resize', compareSize); + }, []); + + useEffect(() => { + if (menu.url === pathname) { + setSelected(menu.id); + setAnchorEl(null); + setOpen(true); + } + }, [pathname, menu]); + + // menu collapse & item + const menus = menu.children?.map((item) => { + switch (item.type) { + case 'collapse': + return ; + case 'item': + return ; + default: + return ( + + Menu Items Error + + ); + } + }); + + const isSelected = selected === menu.id; + + const Icon = menu.icon; + const menuIcon = menu.icon ? ( + + ) : ( + 0 ? 'inherit' : 'medium'} + /> + ); + + const collapseIcon = drawerOpen ? ( + + ) : ( + + ); + + const popperId = openMini ? `collapse-pop-${menu.id}` : undefined; + + return ( + <> + {!isHorizontal ? ( + <> + + + + {menuIcon} + + + {(drawerOpen || (!drawerOpen && level !== 1)) && ( + + + {menu.title} + + } + secondary={ + menu.caption && ( + + {menu.caption} + + ) + } + /> + + )} + + {openMini || open ? ( + collapseIcon + ) : ( + + )} + + + + {({ TransitionProps }) => ( + + + + {menus} + + + + )} + + + + + + + + + {menus} + + + + + + ) : ( + + + {menuIcon} + + + {menu.title} + + } + /> + {openMini ? : } + + + + {({ TransitionProps }) => ( + + + + {menus} + + + + )} + + + + )} + + ); +} + +NavCollapse.propTypes = { menu: PropTypes.any, level: PropTypes.number, parentId: PropTypes.string }; diff --git a/next/src/layout/MainLayout/MenuList/NavGroup/index.jsx b/next/src/layout/MainLayout/MenuList/NavGroup/index.jsx new file mode 100644 index 000000000..c7ad2113b --- /dev/null +++ b/next/src/layout/MainLayout/MenuList/NavGroup/index.jsx @@ -0,0 +1,340 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { Activity, Fragment, useEffect, useState } from 'react'; + +// next +import { usePathname } from 'next/navigation'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import NavCollapse from '../NavCollapse'; +import NavItem from '../NavItem'; + +import useConfig from 'hooks/useConfig'; +import Transitions from 'ui-component/extended/Transitions'; +import { useGetMenuMaster } from 'api/menu'; + +// assets +import { IconChevronDown, IconChevronRight, IconMinusVertical } from '@tabler/icons-react'; + +// ==============================|| SIDEBAR MENU LIST GROUP ||============================== // + +export default function NavGroup({ item, lastItem, remItems, lastItemId, selectedID, setSelectedID }) { + const theme = useTheme(); + const pathname = usePathname(); + + const { + state: { borderRadius } + } = useConfig(); + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + const isHorizontal = false; + + const [anchorEl, setAnchorEl] = useState(null); + const [currentItem, setCurrentItem] = useState(item); + + const openMini = Boolean(anchorEl); + + useEffect(() => { + if (lastItem) { + if (item.id === lastItemId) { + const localItem = { ...item }; + const elements = remItems.map((ele) => ele.elements); + localItem.children = elements.flat(1); + setCurrentItem(localItem); + } else { + setCurrentItem(item); + } + } + }, [item, lastItem, remItems, lastItemId]); + + const checkOpenForParent = (child, id) => { + child.forEach((ele) => { + if (ele.children?.length) { + checkOpenForParent(ele.children, currentItem.id); + } + + if (pathname && pathname.includes('product-details')) { + if (ele.url && ele.url.includes('product-details')) { + setSelectedID(id); + } + } + + if (pathname && pathname.includes('social-profile')) { + if (ele.url && ele.url.includes('social-profile')) { + setSelectedID(id); + } + } + + if (ele.url === pathname) { + setSelectedID(id); + } + }); + }; + + const checkSelectedOnload = (data) => { + const childrens = data.children ? data.children : []; + childrens.forEach((itemCheck) => { + if (itemCheck?.children?.length) { + checkOpenForParent(itemCheck.children, currentItem.id); + } + if (itemCheck?.url === pathname) { + setSelectedID(currentItem.id); + } + }); + + if (pathname && pathname.includes('social-profile')) { + if (data?.url && data?.url.includes('social-profile')) { + setSelectedID(currentItem.id); + } + } + }; + + // keep selected-menu on page load and use for horizontal menu close on change routes + useEffect(() => { + checkSelectedOnload(currentItem); + if (openMini) setAnchorEl(null); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pathname, currentItem]); + + const handleClick = (event) => { + if (!openMini) { + setAnchorEl(event.currentTarget); + } + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const Icon = currentItem?.icon; + const itemIcon = currentItem?.icon ? : null; + + // menu list collapse & items + const items = currentItem.children?.map((menu) => { + switch (menu?.type) { + case 'collapse': + return ; + case 'item': + return ; + default: + return ( + + Menu Items Error + + ); + } + }); + + const moreItems = remItems.map((itemRem, i) => ( + + {itemRem.url ? ( + + ) : ( + + + {itemRem.title} {itemRem.url} + + + )} + {itemRem?.elements?.map((menu) => { + switch (menu?.type) { + case 'collapse': + return ; + case 'item': + return ; + default: + return ( + + Menu Items Error + + ); + } + })} + + )); + + const popperId = openMini ? `group-pop-${item.id}` : undefined; + const isSelected = selectedID === currentItem.id; + + return ( + <> + {!isHorizontal ? ( + <> + + {currentItem.title} + {currentItem.caption && ( + + {currentItem.caption} + + )} + + ) + } + > + {items} + + + {/* group divider */} + + + + + ) : ( + + + + + {currentItem.id === lastItemId ? : itemIcon} + + + + {currentItem.id === lastItemId ? 'More Items' : currentItem.title || null} + + } + /> + {openMini ? : } + + + + {({ TransitionProps }) => ( + + + + + {currentItem.id !== lastItemId ? items : moreItems} + + + + + )} + + + + + )} + + ); +} + +NavGroup.propTypes = { + item: PropTypes.any, + lastItem: PropTypes.number, + remItems: PropTypes.array, + lastItemId: PropTypes.string, + selectedID: PropTypes.oneOfType([PropTypes.any, PropTypes.string]), + setSelectedID: PropTypes.oneOfType([PropTypes.any, PropTypes.string]) +}; diff --git a/next/src/layout/MainLayout/MenuList/NavItem/index.jsx b/next/src/layout/MainLayout/MenuList/NavItem/index.jsx new file mode 100644 index 000000000..8c9f4909d --- /dev/null +++ b/next/src/layout/MainLayout/MenuList/NavItem/index.jsx @@ -0,0 +1,245 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { Activity, useEffect, useRef, useState } from 'react'; + +// next +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import ButtonBase from '@mui/material/ButtonBase'; +import Chip from '@mui/material/Chip'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; + +// project imports +import { handlerDrawerOpen, useGetMenuMaster } from 'api/menu'; +import useConfig from 'hooks/useConfig'; + +// assets +import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'; + +export default function NavItem({ item, level, isParents = false, setSelectedID }) { + const theme = useTheme(); + const downMD = useMediaQuery(theme.breakpoints.down('md')); + const ref = useRef(null); + + const pathname = usePathname(); + const { + state: { borderRadius } + } = useConfig(); + + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + const isHorizontal = false; + const isSelected = pathname === item.url; + + const [hoverStatus, setHover] = useState(false); + + const compareSize = () => { + const compare = ref.current && ref.current.scrollWidth > ref.current.clientWidth; + setHover(compare); + }; + + useEffect(() => { + compareSize(); + window.addEventListener('resize', compareSize); + window.removeEventListener('resize', compareSize); + }, []); + + const Icon = item?.icon; + const itemIcon = item?.icon ? ( + + ) : ( + 0 ? 'inherit' : 'medium'} /> + ); + + let itemTarget = '_self'; + if (item.target) { + itemTarget = '_blank'; + } + + const itemHandler = () => { + if (downMD) handlerDrawerOpen(false); + + if (isParents && setSelectedID) { + setSelectedID(); + } + }; + + return ( + <> + {!isHorizontal ? ( + itemHandler()} + > + + + {itemIcon} + + + + {(drawerOpen || (!drawerOpen && level !== 1)) && ( + + + {item.title} + + } + secondary={ + item.caption && ( + + {item.caption} + + ) + } + /> + + )} + + + + + + ) : ( + 1 ? 'transparent !important' : 'inherit', + py: 1, + pl: 2, + mr: isParents ? 1 : 0 + }} + selected={isSelected} + onClick={() => itemHandler()} + > + + {itemIcon} + + + + {item.title} + + } + secondary={ + + + {item.caption} + + + } + /> + + + + + + )} + + ); +} + +NavItem.propTypes = { item: PropTypes.any, level: PropTypes.number, isParents: PropTypes.bool, setSelectedID: PropTypes.func }; diff --git a/next/src/layout/MainLayout/MenuList/index.jsx b/next/src/layout/MainLayout/MenuList/index.jsx new file mode 100644 index 000000000..60a885d2f --- /dev/null +++ b/next/src/layout/MainLayout/MenuList/index.jsx @@ -0,0 +1,80 @@ +'use client'; +import { Activity, memo, useState } from 'react'; + +import Divider from '@mui/material/Divider'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// project imports +import NavItem from './NavItem'; +import NavGroup from './NavGroup'; +import menuItems from 'menu-items'; + +import { useGetMenuMaster } from 'api/menu'; + +// ==============================|| SIDEBAR MENU LIST ||============================== // + +function MenuList() { + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + + const [selectedID, setSelectedID] = useState(''); + + const lastItem = null; + + let lastItemIndex = menuItems.items.length - 1; + let remItems = []; + let lastItemId; + + if (lastItem && lastItem < menuItems.items.length) { + lastItemId = menuItems.items[lastItem - 1].id; + lastItemIndex = lastItem - 1; + remItems = menuItems.items.slice(lastItem - 1, menuItems.items.length).map((item) => ({ + title: item.title, + elements: item.children, + icon: item.icon, + ...(item.url && { + url: item.url + }) + })); + } + + const navItems = menuItems.items.slice(0, lastItemIndex + 1).map((item, index) => { + switch (item.type) { + case 'group': + if (item.url && item.id !== lastItemId) { + return ( + + setSelectedID('')} /> + + + + + ); + } + + return ( + + ); + default: + return ( + + Menu Items Error + + ); + } + }); + + return {navItems}; +} + +export default memo(MenuList); diff --git a/next/src/layout/MainLayout/Sidebar/MenuCard/index.jsx b/next/src/layout/MainLayout/Sidebar/MenuCard/index.jsx new file mode 100644 index 000000000..64d319e11 --- /dev/null +++ b/next/src/layout/MainLayout/Sidebar/MenuCard/index.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import { memo } from 'react'; + +// material-ui +import Avatar from '@mui/material/Avatar'; +import Card from '@mui/material/Card'; +import LinearProgress, { linearProgressClasses } from '@mui/material/LinearProgress'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import ListItemText from '@mui/material/ListItemText'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; + +// assets +import TableChartOutlinedIcon from '@mui/icons-material/TableChartOutlined'; + +// ==============================|| PROGRESS BAR WITH LABEL ||============================== // + +function LinearProgressWithLabel({ value, ...others }) { + return ( + + + + Progress + + {`${Math.round(value)}%`} + + + + ); +} + +// ==============================|| SIDEBAR - MENU CARD ||============================== // + +function MenuCard() { + return ( + + + + + + + + + + + Get Extra Space + + } + secondary={ 28/23 GB} + /> + + + + + + ); +} + +export default memo(MenuCard); + +LinearProgressWithLabel.propTypes = { value: PropTypes.number, others: PropTypes.any }; diff --git a/next/src/layout/MainLayout/Sidebar/MiniDrawerStyled.jsx b/next/src/layout/MainLayout/Sidebar/MiniDrawerStyled.jsx new file mode 100644 index 000000000..4eeb70f25 --- /dev/null +++ b/next/src/layout/MainLayout/Sidebar/MiniDrawerStyled.jsx @@ -0,0 +1,57 @@ +'use client'; + +// material-ui +import { styled } from '@mui/material/styles'; +import Drawer from '@mui/material/Drawer'; + +// project imports +import { drawerWidth } from 'store/constant'; + +function openedMixin(theme) { + return { + width: drawerWidth, + borderRight: 'none', + zIndex: 1099, + background: theme.vars.palette.background.default, + overflowX: 'hidden', + boxShadow: 'none', + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen + 200 + }) + }; +} + +function closedMixin(theme) { + return { + borderRight: 'none', + zIndex: 1099, + background: theme.vars.palette.background.default, + overflowX: 'hidden', + width: 72, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen + 200 + }) + }; +} + +// ==============================|| DRAWER - MINI STYLED ||============================== // + +const MiniDrawerStyled = styled(Drawer, { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({ + width: drawerWidth, + borderRight: '0px', + flexShrink: 0, + whiteSpace: 'nowrap', + boxSizing: 'border-box', + ...(open && { + ...openedMixin(theme), + '& .MuiDrawer-paper': openedMixin(theme) + }), + ...(!open && { + ...closedMixin(theme), + '& .MuiDrawer-paper': closedMixin(theme) + }) +})); + +export default MiniDrawerStyled; diff --git a/next/src/layout/MainLayout/Sidebar/index.jsx b/next/src/layout/MainLayout/Sidebar/index.jsx new file mode 100644 index 000000000..4e52d2149 --- /dev/null +++ b/next/src/layout/MainLayout/Sidebar/index.jsx @@ -0,0 +1,109 @@ +'use client'; +import { memo, useMemo } from 'react'; + +import useMediaQuery from '@mui/material/useMediaQuery'; +import Chip from '@mui/material/Chip'; +import Drawer from '@mui/material/Drawer'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; + +// project imports +import MenuCard from './MenuCard'; +import MenuList from '../MenuList'; +import LogoSection from '../LogoSection'; +import MiniDrawerStyled from './MiniDrawerStyled'; + +import useConfig from 'hooks/useConfig'; +import { drawerWidth } from 'store/constant'; +import SimpleBar from 'ui-component/third-party/SimpleBar'; + +import { handlerDrawerOpen, useGetMenuMaster } from 'api/menu'; + +// ==============================|| SIDEBAR DRAWER ||============================== // + +function Sidebar() { + const downMD = useMediaQuery((theme) => theme.breakpoints.down('md')); + + const { menuMaster } = useGetMenuMaster(); + const drawerOpen = menuMaster.isDashboardDrawerOpened; + + const { + state: { miniDrawer } + } = useConfig(); + + const logo = useMemo( + () => ( + + + + ), + [] + ); + + const drawer = useMemo(() => { + const drawerContent = ( + <> + + + + + + ); + + let drawerSX = { paddingLeft: '0px', paddingRight: '0px', marginTop: '20px' }; + if (drawerOpen) drawerSX = { paddingLeft: '16px', paddingRight: '16px', marginTop: '0px' }; + + return ( + <> + {downMD ? ( + + + {drawerOpen && drawerContent} + + ) : ( + + + {drawerOpen && drawerContent} + + )} + + ); + }, [downMD, drawerOpen]); + + return ( + + {downMD || (miniDrawer && drawerOpen) ? ( + handlerDrawerOpen(!drawerOpen)} + slotProps={{ + paper: { + sx: { + mt: downMD ? 0 : 11, + zIndex: 1099, + width: drawerWidth, + bgcolor: 'background.default', + color: 'text.primary', + borderRight: 'none' + } + } + }} + ModalProps={{ keepMounted: true }} + color="inherit" + > + {downMD && logo} + {drawer} + + ) : ( + + {logo} + {drawer} + + )} + + ); +} + +export default memo(Sidebar); diff --git a/next/src/layout/MainLayout/index.jsx b/next/src/layout/MainLayout/index.jsx new file mode 100644 index 000000000..3dacfa972 --- /dev/null +++ b/next/src/layout/MainLayout/index.jsx @@ -0,0 +1,73 @@ +'use client'; +import PropTypes from 'prop-types'; + +import { useEffect } from 'react'; + +// material-ui +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Box from '@mui/material/Box'; + +// project imports +import Footer from './Footer'; +import Header from './Header'; +import Sidebar from './Sidebar'; +import MainContentStyled from './MainContentStyled'; +import Customization from '../Customization'; +import Loader from 'ui-component/Loader'; +import Breadcrumbs from 'ui-component/extended/Breadcrumbs'; + +import useConfig from 'hooks/useConfig'; +import { handlerDrawerOpen, useGetMenuMaster } from 'api/menu'; + +// ==============================|| MAIN LAYOUT ||============================== // + +export default function MainLayout({ children }) { + const theme = useTheme(); + const downMD = useMediaQuery(theme.breakpoints.down('md')); + + const { + state: { borderRadius, miniDrawer } + } = useConfig(); + const { menuMaster, menuMasterLoading } = useGetMenuMaster(); + const drawerOpen = menuMaster?.isDashboardDrawerOpened; + + useEffect(() => { + handlerDrawerOpen(!miniDrawer); + }, [miniDrawer]); + + useEffect(() => { + if (downMD) handlerDrawerOpen(false); + }, [downMD]); + + if (menuMasterLoading) return ; + + return ( + + {/* header */} + + +
+ + + + {/* menu / drawer */} + + + {/* main content */} + + + {/* breadcrumb */} + + {children} +