diff --git a/package-lock.json b/package-lock.json index c160b94..cfc4eb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "axios": "^1.6.8", "framer-motion": "^11.0.8", "markdown-to-jsx": "^7.4.1", + "notistack": "^3.0.1", "nprogress": "^0.2.0", "react": "^18.2.0", "react-avatar-editor": "^13.0.2", @@ -3504,6 +3505,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3946,6 +3955,35 @@ "node": ">=0.10.0" } }, + "node_modules/notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/notistack/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", @@ -7210,6 +7248,12 @@ "slash": "^3.0.0" } }, + "goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "requires": {} + }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -7539,6 +7583,22 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, + "notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "requires": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } + } + }, "nprogress": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", diff --git a/package.json b/package.json index 7df2ec0..8917389 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "axios": "^1.6.8", "framer-motion": "^11.0.8", "markdown-to-jsx": "^7.4.1", + "notistack": "^3.0.1", "nprogress": "^0.2.0", "react": "^18.2.0", "react-avatar-editor": "^13.0.2", diff --git a/src/app.tsx b/src/app.tsx index 161e9b0..8f90578 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -5,6 +5,7 @@ import { AppNavBar, AppFooter } from 'component' import { Fallback } from 'page' import { AuthProvider } from 'auth/auth-provider' import { SettingProvider } from 'component/setting' +import { SnackProvider } from 'hook' import ErrorBoundary from 'util/error-boundary' import Router from 'route' import withRoot from 'withroot' @@ -22,11 +23,13 @@ function App() { - - }> - - - + + + }> + + + + diff --git a/src/auth/auth-provider.tsx b/src/auth/auth-provider.tsx index aa5fc31..0f1fad9 100644 --- a/src/auth/auth-provider.tsx +++ b/src/auth/auth-provider.tsx @@ -1,14 +1,15 @@ -import { LOCAL_STORAGE } from 'constant' +import { COLOR, LOCAL_STORAGE } from 'constant' import React, { FC, createContext, useEffect, useState, useMemo, useCallback } from 'react' import { createAsyncThunk } from '@reduxjs/toolkit' import localStorageSpace from 'util/local-storage-space' import storage from 'redux-persist/lib/storage' import { setInitial, setCredential } from 'store/slice/auth' import { useLoginMutation, useLogoutMutation } from 'store/slice/auth/endpoint' +import { useSelector, dispatch } from 'store' import { isValidToken, setSession } from 'auth/utility' import { AuthPath } from 'route/path' import { RESPONSE } from 'constant' -import { useSelector, dispatch } from 'store' +import { snack } from 'hook' export const AuthContext = createContext<{ isAuthenticated: boolean @@ -32,30 +33,10 @@ export const AuthProvider: FC = ({ children }) => { // const [isAuthenticated, setIsAuthenticated] = useState(false) const { user, isAuthenticated } = useSelector((state: RootState) => state.auth || {}) const [logoutCall] = useLogoutMutation() - const [loginCall, { isLoading }] = useLoginMutation() + const [loginCall, { isLoading, error }] = useLoginMutation() const storageAvailable = useMemo(() => localStorageSpace(), []) - const token = storageAvailable ? localStorage.getItem(LOCAL_STORAGE.TOKEN) : '' - - if (token && isValidToken(token)) { - setSession(token) - } - - // useEffect(() => { - // const expirationTime = localStorage.getItem('expirationTime') - // const user = JSON.parse(localStorage.getItem(LOCAL_STORAGE.USER) || '{}') - - // if (user.user !== null) { - // dispatch(setCredential({ user: null, isAuthenticated: false, token: null, isInitialized: false })) - // localStorage.removeItem('expirationTime') - // } else { - // console.log('user', user) - // setIsAuthenticated(false) - // dispatch(setCredential({ user: null, isAuthenticated: false, token: null, isInitialized: false })) - // } - // }, [dispatch]) - const initialize = useCallback(async () => { try { const token = storageAvailable ? localStorage.getItem(LOCAL_STORAGE.TOKEN) : '' @@ -116,13 +97,26 @@ export const AuthProvider: FC = ({ children }) => { const login = useCallback( async (credentials: { email: string; password: string; name?: string; token?: string }) => { const { email, password } = credentials - const res = await loginCall({ + + if (error) { + snack(error || RESPONSE.error.INVALID_CREDENTIAL) + dispatch(setCredential({ isAuthenticated: false, ...(credentials || {}) })) + throw new Error(RESPONSE.error.INVALID_CREDENTIAL) + } + + const res = (await loginCall({ email, password - }).unwrap() + }).unwrap()) as any - dispatch(setCredential({ isAuthenticated: true, ...(res || {}) })) - // setIsAuthenticated(true) + if (res && res.success && res.user) { + snack(RESPONSE.success.LOGIN, { variant: COLOR.SUCCESS }) + dispatch(setCredential({ isAuthenticated: true, ...res })) + } else { + snack(RESPONSE.error.LOGIN_UNABLE, { variant: COLOR.ERROR }) + dispatch(setCredential({ isAuthenticated: false, ...(res || {}) })) + throw new Error(RESPONSE.error.INVALID_CREDENTIAL) + } }, [dispatch] ) diff --git a/src/component/navbar/account-popover.tsx b/src/component/navbar/account-popover.tsx index 14a486b..03f2916 100644 --- a/src/component/navbar/account-popover.tsx +++ b/src/component/navbar/account-popover.tsx @@ -3,6 +3,7 @@ import { useNavigate, Link as RouterLink } from 'react-router-dom' import { alpha, useTheme } from '@mui/material/styles' import { Box, Divider, Dialog, Typography, Stack, MenuItem, Link } from '@mui/material' import { useAuthContext } from 'auth' +import { useSnackbar } from 'hook/use-snack' import { DefaultAvatar, Avatar } from 'component/avatar' import { MenuPopover } from 'component/menu-popover' import { MotionButton } from 'component/motion' @@ -22,7 +23,7 @@ export default function AccountPopover({ user }: { user: any }) { const email = user?.email const displayName = user?.firstname + ' ' + user?.lastname - // const { enqueueSnackbar } = useSnackbar() + const { enqueueSnackbar: snack } = useSnackbar() const { themeMode, themeStretch, themeContrast, onResetSetting } = useSettingContext() @@ -39,8 +40,10 @@ export default function AccountPopover({ user }: { user: any }) { if (logout) { logout() } + snack('Logout Successful') handleClosePopover() } catch (error) { + snack('Unable to logout', { variant: 'error' }) console.error(error) // enqueueSnackbar('Unable to logout', { variant: 'error' }) } diff --git a/src/config/icon-directory.ts b/src/config/icon-directory.ts index 91fa2c6..4b136e6 100644 --- a/src/config/icon-directory.ts +++ b/src/config/icon-directory.ts @@ -20,8 +20,11 @@ function _getWebIcon(icon: string) { const ICON_WEB = { ALERT_OUTLINE: _getWebIcon('alert-triangle-outline'), ARROW_FORWARD: _getWebIcon('arrow-ios-forward'), + CHEVRON_RIGHT: _getWebIcon('chevron-right-outline'), CLOSE: _getWebIcon('close-fill'), + CHECKMARK_CIRCLE: _getWebIcon('checkmark-circle-outline'), CONTRAST_BOX: _getWebIcon('contrast-box'), + ERROR_OUTLINE: _getWebIcon('alert-circle-outline'), ERROR: _getWebIcon('alert-circle'), INFO: _getWebIcon('info'), @@ -63,9 +66,12 @@ export enum ICON_WEB_NAME { // @web ALERT_OUTLINE = 'ALERT_OUTLINE', ARROW_FORWARD = 'ARROW_FORWARD', + CHECKMARK_CIRCLE = 'CHECKMARK_CIRCLE', + CHEVRON_RIGHT = 'CHEVRON_RIGHT', CLOSE = 'CLOSE', CONTRAST_BOX = 'CONTRAST_BOX', ERROR = 'ERROR', + ERROR_OUTLINE = 'ERROR_OUTLINE', EYE_OFF = 'EYE_OFF', EYE_HIDE = 'EYE_HIDE', INFO = 'INFO', diff --git a/src/constant/response.ts b/src/constant/response.ts index 7b2c15d..fc14eaa 100644 --- a/src/constant/response.ts +++ b/src/constant/response.ts @@ -57,6 +57,7 @@ const RESPONSE = { INVALID_TOKEN: 'Invalid token', NOT_OWNER: (user: string, course: string) => `User ${user} is unauthorized to update course ${course}`, ROLE_NOT_ALLOWED: (data: string) => `Current role ${data} is unauthorized to access this route`, + LOGIN_UNABLE: 'Unable to login', parseErr: (err: any) => `Error parsing JSON: ${err}`, NotInstance: 'This class cannot be instantiated', PASSWORD_MATCH: 'Passwords do not match', diff --git a/src/constant/size.ts b/src/constant/size.ts index da272ca..e7b91d1 100644 --- a/src/constant/size.ts +++ b/src/constant/size.ts @@ -1,7 +1,7 @@ export enum SIZE { SMALL = 'small', MEDIUM = 'medium', - LARGE = 'large', + LARGE = 'large' } type SizeMapType = 'small' | 'medium' | 'large' @@ -9,7 +9,7 @@ type SizeMapType = 'small' | 'medium' | 'large' export const SIZE_MAP: Record = { [SIZE.SMALL]: 'small', [SIZE.MEDIUM]: 'medium', - [SIZE.LARGE]: 'large', + [SIZE.LARGE]: 'large' } export type SizeType = { @@ -25,5 +25,5 @@ export const SIZE_TYPE: SizeType = { sm: 'sm', md: 'md', lg: 'lg', - xl: 'xl', + xl: 'xl' } diff --git a/src/hook/index.ts b/src/hook/index.ts index e572931..524b422 100644 --- a/src/hook/index.ts +++ b/src/hook/index.ts @@ -1,3 +1,4 @@ +export { default as useLocalStorage } from './use-local-storage' +export * from './use-snack' export { useResponsive } from './use-responsive' export * from './use-icon' -export { default as useLocalStorage } from './use-local-storage' diff --git a/src/hook/use-snack/index.ts b/src/hook/use-snack/index.ts new file mode 100644 index 0000000..1c0631e --- /dev/null +++ b/src/hook/use-snack/index.ts @@ -0,0 +1,3 @@ +export * from 'notistack' +export { enqueueSnackbar as snack } from 'notistack' +export { default as SnackProvider } from './use-snack' diff --git a/src/hook/use-snack/style.tsx b/src/hook/use-snack/style.tsx new file mode 100644 index 0000000..e98c303 --- /dev/null +++ b/src/hook/use-snack/style.tsx @@ -0,0 +1,42 @@ +import { m } from 'framer-motion' +import { Box } from '@mui/material' +import { styled } from '@mui/material/styles' +import { MaterialDesignContent } from 'notistack' + +export const SSnackContent = styled(MaterialDesignContent)(({ theme }) => ({ + '&.notistack-MuiContent': { + backgroundColor: theme.palette.common.white, + display: 'flex', + flexDirection: 'row', + objectFit: 'cover', + justifyContent: 'space-evenly', + fontSize: theme.typography.overline.fontSize, + color: theme.palette.common.black, + padding: theme.spacing(2) + }, + '&.notistack-MuiContent-error': { + backgroundColor: theme.palette.error.dark, + flexDirection: 'row', + justifyContent: 'space-between', + color: theme.palette.common.white, + padding: 0 + }, + '&.notistack-MuiContent-success': { + flexDirection: 'row', + padding: 0 + }, + '&.notistack-MuiContent-warning': { + backgroundColor: theme.palette.common.white, + flexDirection: 'row', + padding: 0 + } +})) + +export const SSnackIconMDiv = styled(Box)(({ theme, color }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + color: theme.palette.grey[500], + width: 30, + height: 30 +})) diff --git a/src/hook/use-snack/use-snack.tsx b/src/hook/use-snack/use-snack.tsx new file mode 100644 index 0000000..35d50c2 --- /dev/null +++ b/src/hook/use-snack/use-snack.tsx @@ -0,0 +1,68 @@ +import { Fragment } from 'react' +import { m, AnimatePresence } from 'framer-motion' +import { SnackbarProvider as NotistackProvider, useSnackbar } from 'notistack' +import { Slide, IconButton } from '@mui/material' +import { useIcon, ICON_NAME } from 'hook' +import { ICON_WEB_NAME } from 'config' +import { KEY, COLOR as COLOR_CONSTANT } from 'constant' +import { SSnackContent, SSnackIconMDiv } from './style' +import { Iconify } from 'component/iconify' + +interface SnackProviderProps { + children: React.ReactNode +} + +export default function SnackProvider({ children }: SnackProviderProps) { + const { closeSnackbar } = useSnackbar() + + const onClose = (key: any) => () => { + closeSnackbar(key) + } + + const { Icon, iconSrc: closeSrc } = useIcon(ICON_NAME.CHEVRON_RIGHT) + + return ( + + , + success: , + warning: , + error: + }} + action={(key) => ( + + + + )}> + {children} + + + ) +} + +interface SnackIconProps { + icon: ICON_WEB_NAME + color: any +} + +function SnackIcon({ icon, color }: SnackIconProps) { + const { Icon, iconSrc } = useIcon(icon) + return ( + + + + ) +} diff --git a/src/page/auth/log-in/log-in.tsx b/src/page/auth/log-in/log-in.tsx index 3d3b601..b604ec5 100644 --- a/src/page/auth/log-in/log-in.tsx +++ b/src/page/auth/log-in/log-in.tsx @@ -1,19 +1,19 @@ import { useState, useEffect, Fragment, ChangeEvent, BaseSyntheticEvent } from 'react' import { useNavigate } from 'react-router-dom' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import * as Yup from 'yup' +import { useAuthContext } from 'auth' import { useForm, Controller } from 'react-hook-form' import { yupResolver } from '@hookform/resolvers/yup' import { FormField, FormProvider } from 'component/form' -import { useAuthContext } from 'auth' import { Box, FormControlLabel, Checkbox } from '@mui/material' import { AppForm, FormButtonRedir, email, required } from 'component/form' import { MotionContainer } from 'component/motion' import { Meta } from 'component/meta' -import { Snack } from 'component/snack' import { AuthBranding } from 'section/auth' -import { AuthPath, RootPath } from 'route/path' +import { AuthPath } from 'route/path' import { FORM } from 'section/auth' +import { useSnackbar } from 'hook/use-snack' import { LABEL, KEY, LOCAL_STORAGE, RESPONSE, COLOR } from 'constant' import withRoot from 'withroot' @@ -22,6 +22,7 @@ function LogIn() { const [remember, setRemember] = useState(false) const { login } = useAuthContext() const navigate = useNavigate() + const { enqueueSnackbar: snack } = useSnackbar() const { isAuthenticated } = useSelector((state: { auth: { isAuthenticated: boolean } }) => state.auth) const loginSchema = Yup.object().shape({ @@ -59,29 +60,38 @@ function LogIn() { const onSubmit = async (data: any) => { try { - console.log('data : ', data) if (login) { await login(data) - } + // navigate(RootPath.ROOT_PARAM) - if (remember) { - const userInfo = { - email: data.email, - remember - } + if (remember) { + const userInfo = { + email: data.email, + remember + } - localStorage.setItem(LOCAL_STORAGE.USER_INFO, JSON.stringify(userInfo)) + localStorage.setItem(LOCAL_STORAGE.USER_INFO, JSON.stringify(userInfo)) + } else { + localStorage.removeItem(LOCAL_STORAGE.USER_INFO) + } } else { - localStorage.removeItem(LOCAL_STORAGE.USER_INFO) - } - - if (isAuthenticated) { - navigate(RootPath.ROOT_PARAM) + snack(RESPONSE.error.INVALID_CREDENTIAL, { + variant: COLOR.ERROR + }) + throw new Error(RESPONSE.error.INVALID_CREDENTIAL) } } catch (error: any) { console.error('error : ', error || '') reset() - setError(KEY.EMAIL, { message: error.message }) + if (error.message === RESPONSE.error.INVALID_CREDENTIAL) { + setError(KEY.EMAIL, { message: RESPONSE.error.EMAIL_INVALID }) + setError(KEY.PASSWORD, { message: RESPONSE.error.PASSWORD_INVALID }) + } else { + snack(error.message, { + variant: COLOR.ERROR + }) + setError(KEY.EMAIL, { message: error.message }) + } } } @@ -90,22 +100,18 @@ function LogIn() { - + + + - + - - + + + + + + { return ( diff --git a/src/theme/palette.ts b/src/theme/palette.ts index 4a6f91b..42bd919 100644 --- a/src/theme/palette.ts +++ b/src/theme/palette.ts @@ -1,5 +1,6 @@ import { alpha } from '@mui/material/styles' import { KEY } from 'constant' +import { COMMON as COMMON_COLOR } from './theme' const BRAND = { background: '#63738114', @@ -28,9 +29,19 @@ const PRIMARY = { contrastText: '#F2EED8' } +const ERROR = { + lighter: '#FCEBEB', + light: '#E45D5D', + main: '#DD3535', + dark: '#9B2525', + darker: '#581515', + contrastText: '#F2EED8' +} + const COMMON = { - common: { black: '#000', white: '#F2EED8' }, + common: COMMON_COLOR, primary: PRIMARY, + error: ERROR, grey: GREY, divider: alpha(GREY[500], 0.24), action: { diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 958aa07..7514d3f 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -1,5 +1,5 @@ import { createTheme } from '@mui/material/styles' -import { green, grey, red } from '@mui/material/colors' +import { blue, green, grey, red } from '@mui/material/colors' import { Theme } from '@mui/material/styles' import ComponentOverride from './override' import { ASSET } from 'config' @@ -11,41 +11,41 @@ const rawTheme = createTheme({ primary: { light: '#D3D3D3', main: '#000009', - dark: '#1E1E1f', + dark: '#1E1E1f' }, secondary: { light: '#FFF5F8', main: '#FFD500', - dark: '#E62958', + dark: '#E62958' }, warning: { light: '#FFF3E0', main: '#FFC071', - dark: '#FFB25E', + dark: '#FFB25E' }, error: { light: red[50], main: red[500], - dark: red[700], + dark: red[700] }, success: { light: green[50], main: green[500], - dark: green[700], + dark: green[700] }, text: { primary: '#172B4D', - secondary: '#6B778C', - }, + secondary: '#6B778C' + } }, typography: { fontFamily: "'Poppins', sans-serif", fontSize: 14, fontWeightLight: 300, - fontWeightRegular: 400, + fontWeightRegular: 400 }, shape: { - borderRadius: 2, + borderRadius: 2 }, components: { MuiButton: { @@ -53,18 +53,18 @@ const rawTheme = createTheme({ root: { boxShadow: 'none', '&:hover': { - boxShadow: 'none', - }, - }, - }, - }, - }, + boxShadow: 'none' + } + } + } + } + } }) const fontHeader = { color: rawTheme.palette.text.primary, fontWeight: rawTheme.typography.fontWeightRegular, - fontFamily: 'Poppins, sans-serif', + fontFamily: 'Poppins, sans-serif' } const BRAND = { @@ -72,15 +72,19 @@ const BRAND = { secondary: '#FFD500', warning: '#FFC071', error: red[500], - success: green[500], + success: green[500] } -const COMMON = { +export const COMMON = { light: '#D3D3D3', main: '#D9D9D9', + yellow: '#FFD500', + blue: blue[500], + red: red[500], + green: green[500], dark: '#1E1E1F', black: '#000000', - white: '#FFFFFF', + white: '#F2EED8' } const theme = { @@ -93,15 +97,15 @@ const theme = { placeholder: grey[200], light: '#F5F5F5', main: '#D4D3D3', - dark: '#D9D9D9', + dark: '#D9D9D9' }, common: COMMON, brand: BRAND, mode: 'light', - backgroundImage: ASSET.PATTERN_BG, + backgroundImage: ASSET.PATTERN_BG }, shape: { - borderRadius: 2, + borderRadius: 2 }, typography: { ...rawTheme.typography, @@ -110,58 +114,69 @@ const theme = { ...rawTheme.typography.h1, ...fontHeader, letterSpacing: 0, - fontSize: 80, + fontSize: 80 }, h1: { ...rawTheme.typography.h1, ...fontHeader, letterSpacing: 0, - fontSize: 60, + fontSize: 60 }, h2: { ...rawTheme.typography.h2, ...fontHeader, - fontSize: 48, + fontSize: 48 }, h3: { ...rawTheme.typography.h3, ...fontHeader, - fontSize: 42, + fontSize: 42 }, h4: { ...rawTheme.typography.h4, ...fontHeader, - fontSize: 36, + fontSize: 36 }, h5: { ...rawTheme.typography.h5, fontSize: 20, - fontWeight: rawTheme.typography.fontWeightLight, + fontWeight: rawTheme.typography.fontWeightLight }, h6: { ...rawTheme.typography.h6, ...fontHeader, - fontSize: 18, + fontSize: 18 }, h7: { ...rawTheme.typography.h6, ...fontHeader, - fontSize: 15, + fontSize: 15 }, subtitle1: { ...rawTheme.typography.subtitle1, - fontSize: 18, + fontSize: 18 + }, + overline: { + ...rawTheme.typography.overline, + fontSize: 16, + textTransform: 'uppercase' + }, + body0: { + ...rawTheme.typography.body2, + fontWeight: rawTheme.typography.fontWeightRegular, + fontSize: 20, + textTransform: 'uppercase' }, body1: { ...rawTheme.typography.body2, fontWeight: rawTheme.typography.fontWeightRegular, - fontSize: 16, + fontSize: 16 }, body2: { ...rawTheme.typography.body1, - fontSize: 14, - }, - }, + fontSize: 14 + } + } } ComponentOverride(theme as ThemeType) diff --git a/src/theme/typography.ts b/src/theme/typography.ts new file mode 100644 index 0000000..cf530ef --- /dev/null +++ b/src/theme/typography.ts @@ -0,0 +1,130 @@ +import { SizeType } from 'constant' + +export function remToPx(value: any) { + return Math.round(parseFloat(value) * 16) +} + +export function pxToRem(value: any) { + return `${value / 16}rem` +} + +export function responsiveFontSizes({ xs, sm, md, lg, xl }: ISize) { + return { + '@media (min-width:600px)': { + fontSize: pxToRem(sm) + }, + '@media (min-width:900px)': { + fontSize: pxToRem(md) + }, + '@media (min-width:1200px)': { + fontSize: pxToRem(lg) + } + } +} + +const FONT_PRIMARY = 'Yantramanav, Arimo, Calibri' +const FONT_SECONDARY = 'Arimo' + +const typography = { + fontFamily: FONT_PRIMARY, + fontWeightLight: 200, + fontWeightRegular: 400, + fontWeightMedium: 600, + fontWeightBold: 700, + h0: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(48), + ...responsiveFontSizes({ sm: 60, md: 72, lg: 120 }) + }, + h1: { + fontWeight: 800, + lineHeight: 80 / 64, + fontSize: pxToRem(40), + ...responsiveFontSizes({ sm: 52, md: 58, lg: 64 }) + }, + h2: { + fontWeight: 800, + lineHeight: 64 / 48, + fontSize: pxToRem(32), + ...responsiveFontSizes({ sm: 48, md: 50, lg: 52 }) + }, + h3: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(24), + ...responsiveFontSizes({ sm: 26, md: 30, lg: 32 }) + }, + h4: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(20), + ...responsiveFontSizes({ sm: 20, md: 24, lg: 24 }) + }, + h5: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(18), + ...responsiveFontSizes({ sm: 19, md: 20, lg: 20 }) + }, + h6: { + fontWeight: 700, + lineHeight: 28 / 18, + fontSize: pxToRem(17), + ...responsiveFontSizes({ sm: 18, md: 18, lg: 18 }) + }, + subtitle0: { + fontFamily: FONT_SECONDARY, + fontWeight: 600, + lineHeight: 1.5, + fontSize: pxToRem(24) + }, + subtitle1: { + fontFamily: FONT_SECONDARY, + fontWeight: 600, + lineHeight: 1.5, + fontSize: pxToRem(16) + }, + subtitle2: { + fontFamily: FONT_SECONDARY, + fontWeight: 600, + lineHeight: 22 / 14, + fontSize: pxToRem(14) + }, + body1: { + fontFamily: FONT_SECONDARY, + lineHeight: 1.5, + fontSize: pxToRem(18) + }, + body2: { + fontFamily: FONT_SECONDARY, + lineHeight: 22 / 14, + fontSize: pxToRem(14) + }, + caption: { + fontFamily: FONT_SECONDARY, + lineHeight: 1.5, + fontSize: pxToRem(12) + }, + overline: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(12), + textTransform: 'uppercase' + }, + overline2: { + fontWeight: 700, + lineHeight: 1.5, + fontSize: pxToRem(16), + textTransform: 'uppercase' + }, + button: { + fontFamily: FONT_SECONDARY, + fontWeight: 700, + lineHeight: 24 / 14, + fontSize: pxToRem(14), + textTransform: 'capitalize' + } +} + +export default typography diff --git a/src/types/global.d.ts b/src/types/global.d.ts index be01789..f2fdf6f 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -87,6 +87,14 @@ declare global { message?: string } + interface ISize { + xs?: number + sm: number + md: number + lg: number + xl?: number + } + type VERTICAL = KEY.TOP | KEY.CENTER | KEY.BOTTOM type HORIZONTAL = KEY.LEFT | KEY.CENTER | KEY.RIGHT type COLOR = 'default' | 'inherit' | 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'