First Commit
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import Link from '@mui/material/Link';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @third-party
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
// @project
|
||||
import { APP_DEFAULT_PATH } from '@/config';
|
||||
import RouterLink from '@/components/Link';
|
||||
import { useRouter } from '@/utils/navigation';
|
||||
import { emailSchema, passwordSchema } from '@/utils/validation-schema/common';
|
||||
|
||||
// @icons
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons-react';
|
||||
|
||||
// Mock user credentials
|
||||
const userCredentials = [
|
||||
{ title: 'Super Admin', email: 'super_admin@saasable.io', password: 'Super@123' },
|
||||
{ title: 'Admin', email: 'admin@saasable.io', password: 'Admin@123' },
|
||||
{ title: 'User', email: 'user@saasable.io', password: 'User@123' }
|
||||
];
|
||||
|
||||
function isChildObjectContained(parent, child) {
|
||||
return Object.entries(child).every(([key, value]) => parent.hasOwnProperty(key) && parent[key] === value);
|
||||
}
|
||||
|
||||
/*************************** AUTH - LOGIN ***************************/
|
||||
|
||||
export default function AuthLogin({ inputSx }) {
|
||||
const router = useRouter();
|
||||
const theme = useTheme();
|
||||
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [loginError, setLoginError] = useState('');
|
||||
|
||||
// Initialize react-hook-form
|
||||
const {
|
||||
register,
|
||||
watch,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors }
|
||||
} = useForm({ defaultValues: { email: 'super_admin@saasable.io', password: 'Super@123' } });
|
||||
|
||||
const formData = watch();
|
||||
|
||||
// Handle form submission
|
||||
const onSubmit = (formData) => {
|
||||
setIsProcessing(true);
|
||||
setLoginError('');
|
||||
|
||||
router.push(APP_DEFAULT_PATH);
|
||||
};
|
||||
|
||||
const commonIconProps = { size: 16, color: theme.vars.palette.grey[700] };
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" sx={{ gap: 1, mb: 2 }}>
|
||||
{userCredentials.map((credential) => (
|
||||
<Button
|
||||
key={credential.title}
|
||||
variant="outlined"
|
||||
color={isChildObjectContained(credential, formData) ? 'primary' : 'secondary'}
|
||||
sx={{ flex: 1 }}
|
||||
onClick={() => {
|
||||
reset({ email: credential.email, password: credential.password });
|
||||
}}
|
||||
>
|
||||
{credential.title}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Stack sx={{ gap: 2 }}>
|
||||
<Box>
|
||||
<InputLabel>Email</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('email', emailSchema)}
|
||||
placeholder="example@saasable.io"
|
||||
fullWidth
|
||||
error={Boolean(errors.email)}
|
||||
sx={inputSx}
|
||||
/>
|
||||
{errors.email?.message && <FormHelperText error>{errors.email.message}</FormHelperText>}
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('password', passwordSchema)}
|
||||
type={isPasswordVisible ? 'text' : 'password'}
|
||||
placeholder="Enter your password"
|
||||
fullWidth
|
||||
error={Boolean(errors.password)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end" sx={{ cursor: 'pointer' }} onClick={() => setIsPasswordVisible(!isPasswordVisible)}>
|
||||
{isPasswordVisible ? <IconEye {...commonIconProps} /> : <IconEyeOff {...commonIconProps} />}
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={inputSx}
|
||||
/>
|
||||
<Stack direction="row" sx={{ alignItems: 'center', justifyContent: errors.password ? 'space-between' : 'flex-end', width: 1 }}>
|
||||
{errors.password?.message && <FormHelperText error>{errors.password.message}</FormHelperText>}
|
||||
<Link
|
||||
component={RouterLink}
|
||||
underline="hover"
|
||||
variant="caption"
|
||||
to="#"
|
||||
textAlign="right"
|
||||
sx={{ '&:hover': { color: 'primary.dark' }, mt: 0.75 }}
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={isProcessing}
|
||||
endIcon={isProcessing && <CircularProgress color="secondary" size={16} />}
|
||||
sx={{ minWidth: 120, mt: { xs: 1, sm: 4 }, '& .MuiButton-endIcon': { ml: 1 } }}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
|
||||
{loginError && (
|
||||
<Alert sx={{ mt: 2 }} severity="error" variant="filled" icon={false}>
|
||||
{loginError}
|
||||
</Alert>
|
||||
)}
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
AuthLogin.propTypes = { inputSx: PropTypes.any };
|
||||
@@ -0,0 +1,164 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import FormHelperText from '@mui/material/FormHelperText';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
import InputLabel from '@mui/material/InputLabel';
|
||||
import OutlinedInput from '@mui/material/OutlinedInput';
|
||||
|
||||
// @third-party
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
// @project
|
||||
import Contact from '@/components/Contact';
|
||||
import { useRouter } from '@/utils/navigation';
|
||||
import { emailSchema, passwordSchema, firstNameSchema, lastNameSchema } from '@/utils/validation-schema/common';
|
||||
|
||||
// @icons
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons-react';
|
||||
|
||||
// @types
|
||||
|
||||
/*************************** AUTH - REGISTER ***************************/
|
||||
|
||||
export default function AuthRegister({ inputSx }) {
|
||||
const router = useRouter();
|
||||
|
||||
const theme = useTheme();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [registerError, setRegisterError] = useState('');
|
||||
|
||||
// Initialize react-hook-form
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
watch,
|
||||
control,
|
||||
setValue,
|
||||
formState: { errors }
|
||||
} = useForm({ defaultValues: { dialcode: '+1' } });
|
||||
|
||||
const password = useRef({});
|
||||
password.current = watch('password', '');
|
||||
|
||||
// Handle form submission
|
||||
const onSubmit = (formData) => {
|
||||
setIsProcessing(true);
|
||||
setRegisterError('');
|
||||
router.push('/auth/login');
|
||||
};
|
||||
|
||||
const commonIconProps = { size: 16, color: theme.vars.palette.grey[700] };
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||
<Grid container rowSpacing={2.5} columnSpacing={1.5}>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<InputLabel>First Name</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('firstname', firstNameSchema)}
|
||||
placeholder="Enter first name"
|
||||
fullWidth
|
||||
error={Boolean(errors.firstname)}
|
||||
sx={{ ...inputSx }}
|
||||
/>
|
||||
{errors.firstname?.message && <FormHelperText error>{errors.firstname?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6 }}>
|
||||
<InputLabel>Last Name</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('lastname', lastNameSchema)}
|
||||
placeholder="Enter last name"
|
||||
fullWidth
|
||||
error={Boolean(errors.lastname)}
|
||||
sx={{ ...inputSx }}
|
||||
/>
|
||||
{errors.lastname?.message && <FormHelperText error>{errors.lastname?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<InputLabel>Email</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('email', emailSchema)}
|
||||
placeholder="example@saasable.io"
|
||||
fullWidth
|
||||
error={Boolean(errors.email)}
|
||||
sx={{ ...inputSx }}
|
||||
/>
|
||||
{errors.email?.message && <FormHelperText error>{errors.email?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<InputLabel>Contact</InputLabel>
|
||||
<Contact
|
||||
fullWidth
|
||||
dialCode={watch('dialcode')}
|
||||
onCountryChange={(data) => setValue('dialcode', data.dialCode)}
|
||||
control={control}
|
||||
isError={Boolean(errors.contact)}
|
||||
/>
|
||||
{errors.contact?.message && <FormHelperText error>{errors.contact?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
|
||||
<Grid size={12}>
|
||||
<InputLabel>Password</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('password', passwordSchema)}
|
||||
type={isOpen ? 'text' : 'password'}
|
||||
placeholder="Enter password"
|
||||
fullWidth
|
||||
autoComplete="new-password"
|
||||
error={Boolean(errors.password)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end" sx={{ cursor: 'pointer' }} onClick={() => setIsOpen(!isOpen)}>
|
||||
{isOpen ? <IconEye {...commonIconProps} /> : <IconEyeOff {...commonIconProps} />}
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={inputSx}
|
||||
/>
|
||||
{errors.password?.message && <FormHelperText error>{errors.password?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
<Grid size={12}>
|
||||
<InputLabel>Confirm Password</InputLabel>
|
||||
<OutlinedInput
|
||||
{...register('confirmPassword', { validate: (value) => value === password.current || 'The passwords do not match' })}
|
||||
type={isConfirmOpen ? 'text' : 'password'}
|
||||
placeholder="Enter confirm password"
|
||||
fullWidth
|
||||
error={Boolean(errors.confirmPassword)}
|
||||
endAdornment={
|
||||
<InputAdornment position="end" sx={{ cursor: 'pointer' }} onClick={() => setIsConfirmOpen(!isConfirmOpen)}>
|
||||
{isConfirmOpen ? <IconEye {...commonIconProps} /> : <IconEyeOff {...commonIconProps} />}
|
||||
</InputAdornment>
|
||||
}
|
||||
sx={inputSx}
|
||||
/>
|
||||
{errors.confirmPassword?.message && <FormHelperText error>{errors.confirmPassword?.message}</FormHelperText>}
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={isProcessing}
|
||||
endIcon={isProcessing && <CircularProgress color="secondary" size={16} />}
|
||||
sx={{ minWidth: 120, mt: { xs: 2, sm: 4 }, '& .MuiButton-endIcon': { ml: 1 } }}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
{registerError && (
|
||||
<Alert sx={{ mt: 2 }} severity="error" variant="filled" icon={false}>
|
||||
{registerError}
|
||||
</Alert>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
AuthRegister.propTypes = { inputSx: PropTypes.any };
|
||||
@@ -0,0 +1,50 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @mui
|
||||
import Button from '@mui/material/Button';
|
||||
import CardMedia from '@mui/material/CardMedia';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import { SocialTypes } from '@/enum';
|
||||
import GetImagePath from '@/utils/GetImagePath';
|
||||
|
||||
// @assets
|
||||
import googleIcon from '@/assets/images/social/google.svg';
|
||||
import facebookIcon from '@/assets/images/social/facebook.svg';
|
||||
|
||||
/*************************** SOCIAL BUTTON - DATA ***************************/
|
||||
|
||||
const authButtons = [
|
||||
{ label: 'Google', icon: googleIcon, title: 'Sign in with Google' },
|
||||
{ label: 'Facebook', icon: facebookIcon, title: 'Sign in with Facebook' }
|
||||
];
|
||||
|
||||
/*************************** AUTH - SOCIAL ***************************/
|
||||
|
||||
export default function AuthSocial({ type = SocialTypes.VERTICAL, buttonSx }) {
|
||||
return (
|
||||
<Stack direction={type === SocialTypes.VERTICAL ? 'column' : 'row'} sx={{ gap: 1 }}>
|
||||
{authButtons.map((item, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
size="small"
|
||||
color="secondary"
|
||||
sx={{ ...(type === SocialTypes.HORIZONTAL && { '.MuiButton-startIcon': { m: 0 } }), ...buttonSx }}
|
||||
startIcon={<CardMedia component="img" src={GetImagePath(item.icon)} sx={{ width: 16, height: 16 }} alt={item.label} />}
|
||||
>
|
||||
{type === SocialTypes.VERTICAL && (
|
||||
<Typography variant="caption1" sx={{ textTransform: 'none' }}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
AuthSocial.propTypes = { type: PropTypes.any, SocialTypes: PropTypes.any, VERTICAL: PropTypes.any, buttonSx: PropTypes.any };
|
||||
@@ -0,0 +1,49 @@
|
||||
// @mui
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Link from '@mui/material/Link';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import branding from '@/branding.json';
|
||||
|
||||
/*************************** AUTH - COPYRIGHT ***************************/
|
||||
|
||||
export default function Copyright() {
|
||||
const copyrightSX = { display: { xs: 'none', sm: 'flex' } };
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
const linkProps = {
|
||||
variant: 'caption',
|
||||
color: 'text.secondary',
|
||||
target: '_blank',
|
||||
underline: 'hover',
|
||||
sx: { '&:hover': { color: 'primary.main' } }
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack sx={{ gap: 1, width: 'fit-content', mx: 'auto' }}>
|
||||
<Stack direction="row" sx={{ justifyContent: 'center', gap: { xs: 1, sm: 1.5 }, textAlign: 'center' }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={copyrightSX}>
|
||||
© {currentYear} {branding.brandName}
|
||||
</Typography>
|
||||
<Divider orientation="vertical" flexItem sx={copyrightSX} />
|
||||
<Link {...linkProps} href="https://saasable.io/privacy-policy">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
<Link {...linkProps} href="https://mui.com/store/terms/">
|
||||
Terms & Conditions
|
||||
</Link>
|
||||
</Stack>
|
||||
|
||||
<Box sx={{ textAlign: 'center', display: { xs: 'block', sm: 'none' } }}>
|
||||
<Divider sx={{ marginBottom: 1 }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
© {currentYear} {branding.brandName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
// @mui
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
|
||||
/*************************** COLOR - CARD ***************************/
|
||||
|
||||
export default function ColorBox({ value, color, muiLabel, figmaLabel, figmaValue, main = false }) {
|
||||
return (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<MainCard
|
||||
sx={{
|
||||
p: 1.5,
|
||||
borderRadius: 4,
|
||||
...(main && { border: '1px dashed', borderColor: 'primary.main' }),
|
||||
...(muiLabel === 'grey.100' && { bgcolor: 'grey.200' })
|
||||
}}
|
||||
>
|
||||
<MainCard sx={{ py: 3, borderRadius: 3, bgcolor: value, color }}>
|
||||
<Stack sx={{ gap: 0.75, alignItems: 'center', textAlign: 'center' }}>
|
||||
<Typography variant="h3">{value}</Typography>
|
||||
<Typography>{figmaLabel}</Typography>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
<Stack direction="row" sx={{ justifyContent: 'space-between', mt: 1.5 }}>
|
||||
<Typography variant="subtitle1">{muiLabel}</Typography>
|
||||
<Typography sx={{ color: 'text.secondary' }}>{figmaValue}</Typography>
|
||||
</Stack>
|
||||
</MainCard>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ColorBox.propTypes = {
|
||||
value: PropTypes.string,
|
||||
color: PropTypes.string,
|
||||
muiLabel: PropTypes.string,
|
||||
figmaLabel: PropTypes.string,
|
||||
figmaValue: PropTypes.string,
|
||||
main: PropTypes.bool
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ColorBox } from './ColorBox';
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
|
||||
// @project
|
||||
import OverviewCard from '@/components/cards/OverviewCard';
|
||||
import { getRadiusStyles } from '@/utils/getRadiusStyles';
|
||||
|
||||
// @assets
|
||||
import { IconArrowDown, IconArrowUp } from '@tabler/icons-react';
|
||||
|
||||
/*************************** CARDS - BORDER WITH RADIUS ***************************/
|
||||
|
||||
export function applyBorderWithRadius(radius, theme) {
|
||||
return {
|
||||
overflow: 'hidden',
|
||||
'--Grid-borderWidth': '1px',
|
||||
borderTop: 'var(--Grid-borderWidth) solid',
|
||||
borderLeft: 'var(--Grid-borderWidth) solid',
|
||||
borderColor: 'divider',
|
||||
'& > div': {
|
||||
overflow: 'hidden',
|
||||
borderRight: 'var(--Grid-borderWidth) solid',
|
||||
borderBottom: 'var(--Grid-borderWidth) solid',
|
||||
borderColor: 'divider',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
'&:nth-of-type(1)': getRadiusStyles(radius, 'topLeft'),
|
||||
'&:nth-of-type(2)': getRadiusStyles(radius, 'topRight'),
|
||||
'&:nth-of-type(3)': getRadiusStyles(radius, 'bottomLeft'),
|
||||
'&:nth-of-type(4)': getRadiusStyles(radius, 'bottomRight')
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'&:first-of-type': getRadiusStyles(radius, 'topLeft', 'bottomLeft'),
|
||||
'&:last-of-type': getRadiusStyles(radius, 'topRight', 'bottomRight')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*************************** OVERVIEW CARD -DATA ***************************/
|
||||
|
||||
const overviewAnalytics = [
|
||||
{
|
||||
title: 'Unique Visitors',
|
||||
value: '23,876',
|
||||
compare: 'Compare to last week',
|
||||
chip: {
|
||||
label: '24.5%',
|
||||
avatar: <IconArrowUp />
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Page View',
|
||||
value: '30,450',
|
||||
compare: 'Compare to last week',
|
||||
chip: {
|
||||
label: '20.5%',
|
||||
avatar: <IconArrowUp />
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Events',
|
||||
value: '34,789',
|
||||
compare: 'Compare to last week',
|
||||
chip: {
|
||||
label: '20.5%',
|
||||
color: 'error',
|
||||
avatar: <IconArrowDown />
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Live Visitor',
|
||||
value: '45,687',
|
||||
compare: 'Compare to last week',
|
||||
chip: {
|
||||
label: '24.5%',
|
||||
avatar: <IconArrowUp />
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
/*************************** OVERVIEW - CARDS ***************************/
|
||||
|
||||
export default function AnalyticsOverviewCard() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Grid container sx={{ borderRadius: 4, boxShadow: theme.vars.customShadows.section, ...applyBorderWithRadius(16, theme) }}>
|
||||
{overviewAnalytics.map((item, index) => (
|
||||
<Grid key={index} size={{ xs: 6, sm: 6, md: 3 }}>
|
||||
<OverviewCard {...{ ...item, cardProps: { sx: { border: 'none', borderRadius: 0, boxShadow: 'none' } } }} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { LineChart } from '@mui/x-charts/LineChart';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// @project
|
||||
import MainCard from '@/components/MainCard';
|
||||
import Legend from '@/components/third-party/chart/Legend';
|
||||
import { TabsType, ViewMode } from '@/enum';
|
||||
|
||||
/*************************** CHART - DATA ***************************/
|
||||
|
||||
const yearlyPoints = [
|
||||
new Date(2014, 0, 1),
|
||||
new Date(2014, 2, 1),
|
||||
new Date(2014, 5, 1),
|
||||
new Date(2014, 8, 1),
|
||||
new Date(2014, 11, 1),
|
||||
new Date(2015, 0, 1),
|
||||
new Date(2015, 2, 1),
|
||||
new Date(2015, 5, 1),
|
||||
new Date(2015, 8, 1),
|
||||
new Date(2015, 11, 1),
|
||||
new Date(2016, 0, 1),
|
||||
new Date(2016, 2, 1),
|
||||
new Date(2016, 5, 1),
|
||||
new Date(2016, 8, 1),
|
||||
new Date(2016, 11, 1),
|
||||
new Date(2017, 0, 1),
|
||||
new Date(2017, 2, 1),
|
||||
new Date(2017, 5, 1),
|
||||
new Date(2017, 8, 1),
|
||||
new Date(2017, 11, 1),
|
||||
new Date(2018, 0, 1),
|
||||
new Date(2018, 2, 1),
|
||||
new Date(2018, 5, 1),
|
||||
new Date(2018, 8, 1),
|
||||
new Date(2018, 11, 1),
|
||||
new Date(2019, 0, 1),
|
||||
new Date(2019, 2, 1),
|
||||
new Date(2019, 5, 1),
|
||||
new Date(2019, 8, 1),
|
||||
new Date(2019, 11, 1),
|
||||
new Date(2020, 0, 1),
|
||||
new Date(2020, 2, 1),
|
||||
new Date(2020, 5, 1),
|
||||
new Date(2020, 8, 1),
|
||||
new Date(2020, 11, 1),
|
||||
new Date(2021, 0, 1),
|
||||
new Date(2021, 2, 1),
|
||||
new Date(2021, 5, 1),
|
||||
new Date(2021, 8, 1),
|
||||
new Date(2021, 11, 1),
|
||||
new Date(2022, 0, 1),
|
||||
new Date(2022, 2, 1),
|
||||
new Date(2022, 5, 1),
|
||||
new Date(2022, 8, 1),
|
||||
new Date(2022, 11, 1),
|
||||
new Date(2023, 0, 1),
|
||||
new Date(2023, 2, 1),
|
||||
new Date(2023, 5, 1),
|
||||
new Date(2023, 8, 1),
|
||||
new Date(2023, 11, 1)
|
||||
];
|
||||
|
||||
const yearlyData = {
|
||||
pageViewData: [
|
||||
190, 230, 240, 230, 240, 230, 250, 270, 300, 320, 340, 360, 400, 420, 450, 470, 490, 500, 480, 450, 420, 380, 420, 380, 190, 230, 240,
|
||||
230, 240, 230, 250, 270, 300, 320, 400, 440, 480, 520, 540, 580, 620, 640, 680, 720, 650, 680, 720, 840, 950, 800
|
||||
],
|
||||
uniqueVisitorData: [
|
||||
900, 920, 930, 860, 840, 820, 800, 840, 860, 840, 800, 780, 760, 790, 740, 710, 670, 650, 690, 750, 780, 760, 730, 680, 650, 620, 500,
|
||||
470, 430, 400, 380, 360, 340, 320, 300, 280, 260, 240, 220, 260, 300, 340, 380, 420, 460, 360, 450, 520, 450, 600
|
||||
]
|
||||
};
|
||||
|
||||
const monthlyPoints = [
|
||||
new Date(2023, 0, 1),
|
||||
new Date(2023, 0, 15),
|
||||
new Date(2023, 0, 31),
|
||||
new Date(2023, 1, 1),
|
||||
new Date(2023, 1, 15),
|
||||
new Date(2023, 1, 28),
|
||||
new Date(2023, 2, 1),
|
||||
new Date(2023, 2, 15),
|
||||
new Date(2023, 2, 31),
|
||||
new Date(2023, 3, 1),
|
||||
new Date(2023, 3, 15),
|
||||
new Date(2023, 3, 30),
|
||||
new Date(2023, 4, 1),
|
||||
new Date(2023, 4, 15),
|
||||
new Date(2023, 4, 31),
|
||||
new Date(2023, 5, 1),
|
||||
new Date(2023, 5, 15),
|
||||
new Date(2023, 5, 30),
|
||||
new Date(2023, 6, 1),
|
||||
new Date(2023, 6, 15),
|
||||
new Date(2023, 6, 31),
|
||||
new Date(2023, 7, 1),
|
||||
new Date(2023, 7, 15),
|
||||
new Date(2023, 7, 31),
|
||||
new Date(2023, 8, 1),
|
||||
new Date(2023, 8, 15),
|
||||
new Date(2023, 8, 30),
|
||||
new Date(2023, 9, 1),
|
||||
new Date(2023, 9, 15),
|
||||
new Date(2023, 9, 31),
|
||||
new Date(2023, 10, 1),
|
||||
new Date(2023, 10, 15),
|
||||
new Date(2023, 10, 30),
|
||||
new Date(2023, 11, 1),
|
||||
new Date(2023, 11, 15),
|
||||
new Date(2023, 11, 31)
|
||||
];
|
||||
|
||||
const monthlyData = {
|
||||
pageViewData: [
|
||||
190, 230, 240, 230, 300, 230, 250, 270, 230, 320, 340, 450, 400, 420, 485, 470, 490, 500, 480, 450, 420, 380, 420, 380, 400, 600, 575,
|
||||
540, 550, 520, 580, 570, 600, 600, 720, 780
|
||||
],
|
||||
uniqueVisitorData: [
|
||||
900, 920, 930, 860, 840, 820, 800, 840, 860, 840, 800, 780, 760, 790, 740, 710, 670, 650, 690, 750, 780, 760, 730, 680, 650, 680, 630,
|
||||
510, 460, 460, 405, 460, 415, 430, 410, 500
|
||||
]
|
||||
};
|
||||
|
||||
const dailyPoints = [
|
||||
new Date(2024, 0, 1),
|
||||
new Date(2024, 0, 2),
|
||||
new Date(2024, 0, 3),
|
||||
new Date(2024, 0, 4),
|
||||
new Date(2024, 0, 5),
|
||||
new Date(2024, 0, 6),
|
||||
new Date(2024, 0, 7)
|
||||
];
|
||||
|
||||
const dailyData = {
|
||||
pageViewData: [10, 5, 12, 8, 35, 14, 30],
|
||||
uniqueVisitorData: [15, 20, 22, 18, 21, 30, 38]
|
||||
};
|
||||
|
||||
const timeFilter = ['Daily', 'Monthly', 'Yearly'];
|
||||
|
||||
const valueFormatter = (date, view) => {
|
||||
switch (view) {
|
||||
case ViewMode.DAILY:
|
||||
return date.toLocaleDateString('en-us', { weekday: 'short' });
|
||||
case ViewMode.MONTHLY:
|
||||
return date.toLocaleDateString('en-US', { month: 'short' });
|
||||
case ViewMode.YEARLY:
|
||||
default:
|
||||
return date.getFullYear().toString();
|
||||
}
|
||||
};
|
||||
|
||||
const tickInterval = (date, view) => {
|
||||
switch (view) {
|
||||
case ViewMode.MONTHLY:
|
||||
return date.getDate() === 15;
|
||||
case ViewMode.YEARLY:
|
||||
return date.getMonth() === 5;
|
||||
case ViewMode.DAILY:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const dataMap = { [ViewMode.MONTHLY]: monthlyData, [ViewMode.DAILY]: dailyData, [ViewMode.YEARLY]: yearlyData };
|
||||
|
||||
/*************************** CHART - 1 ***************************/
|
||||
|
||||
export default function Chart1() {
|
||||
const theme = useTheme();
|
||||
|
||||
const [view, setView] = useState(ViewMode.MONTHLY);
|
||||
const [visibilityOption, setVisibilityOption] = useState({
|
||||
page_views: true,
|
||||
unique_visitor: true
|
||||
});
|
||||
|
||||
const handleViewChange = (_event, newValue) => {
|
||||
setView(newValue);
|
||||
};
|
||||
|
||||
const toggleVisibility = (id) => {
|
||||
setVisibilityOption((prev) => ({ ...prev, [id]: !prev[id] }));
|
||||
};
|
||||
|
||||
const seriesData = [
|
||||
{
|
||||
id: 'page_views',
|
||||
data: dataMap[view].pageViewData,
|
||||
color: theme.vars.palette.primary.light,
|
||||
visible: visibilityOption['page_views'],
|
||||
label: 'Page View'
|
||||
},
|
||||
{
|
||||
id: 'unique_visitor',
|
||||
data: dataMap[view].uniqueVisitorData,
|
||||
color: theme.vars.palette.primary.main,
|
||||
visible: visibilityOption['unique_visitor'],
|
||||
label: 'Unique Visitor'
|
||||
}
|
||||
];
|
||||
|
||||
const visibleSeries = seriesData.filter((s) => s.visible);
|
||||
const lagendItems = seriesData.map((series) => ({ label: series.label, color: series.color, visible: series.visible, id: series.id }));
|
||||
|
||||
const xData = view === ViewMode.MONTHLY ? monthlyPoints : view === ViewMode.DAILY ? dailyPoints : yearlyPoints;
|
||||
|
||||
// Dynamic styles for visible series
|
||||
const dynamicSeriesStyles = visibleSeries.reduce((acc, series) => {
|
||||
acc[`& .MuiAreaElement-series-${series.id}`] = { fill: `url(#${series.id})`, opacity: series.id === 'page_views' ? 0 : 0.15 };
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<MainCard>
|
||||
<Stack sx={{ gap: 3 }}>
|
||||
<Stack direction="row" sx={{ alignItems: 'end', justifyContent: 'space-between', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Stack sx={{ gap: 0.5 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 400 }}>
|
||||
Analysis
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'grey.700' }}>
|
||||
Analyze user engagement and improve your product with real-time analytics.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Tabs value={view} onChange={handleViewChange} aria-label="filter tabs" type={TabsType.SEGMENTED} sx={{ width: 'fit-content' }}>
|
||||
{timeFilter.map((filter, index) => (
|
||||
<Tab label={filter} value={filter} key={index} />
|
||||
))}
|
||||
</Tabs>
|
||||
</Stack>
|
||||
|
||||
<Legend items={lagendItems} onToggle={toggleVisibility} />
|
||||
</Stack>
|
||||
|
||||
<LineChart
|
||||
series={visibleSeries.map((series) => ({ ...series, showMark: false, curve: 'linear', area: true }))}
|
||||
height={261}
|
||||
grid={{ horizontal: true }}
|
||||
margin={{ top: 25, right: 0, bottom: -4, left: 0 }}
|
||||
xAxis={[
|
||||
{
|
||||
data: xData,
|
||||
scaleType: 'point',
|
||||
disableLine: true,
|
||||
disableTicks: true,
|
||||
valueFormatter: (value) => valueFormatter(value, view),
|
||||
tickInterval: (time) => tickInterval(time, view)
|
||||
}
|
||||
]}
|
||||
yAxis={[{ scaleType: 'linear', disableLine: true, disableTicks: true, label: 'Visits' }]}
|
||||
hideLegend
|
||||
sx={{ '& .MuiLineElement-root': { strokeDasharray: '0', strokeWidth: 2 }, ...dynamicSeriesStyles }}
|
||||
>
|
||||
<defs>
|
||||
{visibleSeries.map((series, index) => (
|
||||
<linearGradient id={series.id} key={index} gradientTransform="rotate(90)">
|
||||
<stop offset="10%" stopColor={series.color} stopOpacity={1} />
|
||||
<stop offset="86%" stopColor={series.color} stopOpacity={0.1} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
</LineChart>
|
||||
</MainCard>
|
||||
);
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
// @mui
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import Grid from '@mui/material/Grid';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Tab from '@mui/material/Tab';
|
||||
import Tabs from '@mui/material/Tabs';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// @project
|
||||
import ProgressCard from '@/components/cards/ProgressCard';
|
||||
import { TabsType } from '@/enum';
|
||||
import { getRadiusStyles } from '@/utils/getRadiusStyles';
|
||||
|
||||
/*************************** TABS - DATA ***************************/
|
||||
|
||||
const sevenDaysData = [
|
||||
{ title: 'Direct', value: '16,890', progress: { value: 45 } },
|
||||
{ title: 'Google.com', value: '4,909', progress: { value: 56 } },
|
||||
{ title: 'Remix.com', value: '550', progress: { value: 74 } },
|
||||
{ title: 'dev.to', value: '140', progress: { value: 25 } },
|
||||
{ title: 'acpc.api.ic.io', value: '8,675', progress: { value: 45 } },
|
||||
{ title: 'wewe.uv.us', value: '4,900', progress: { value: 95 } }
|
||||
];
|
||||
|
||||
const monthData = [
|
||||
{ title: 'Direct', value: '67,560', progress: { value: 75 } },
|
||||
{ title: 'Google.com', value: '19,636', progress: { value: 45 } },
|
||||
{ title: 'Remix.com', value: '2,220', progress: { value: 10 } },
|
||||
{ title: 'dev.to', value: '560', progress: { value: 89 } },
|
||||
{ title: 'acpc.api.ic.io', value: '34,700', progress: { value: 95 } },
|
||||
{ title: 'wewe.uv.us', value: '19,600', progress: { value: 74 } }
|
||||
];
|
||||
|
||||
const yearData = [
|
||||
{ title: 'Direct', value: '8,10,720', progress: { value: 52 } },
|
||||
{ title: 'Google.com', value: '2,35,632', progress: { value: 45 } },
|
||||
{ title: 'Remix.com', value: '26,640', progress: { value: 85 } },
|
||||
{ title: 'dev.to', value: '6,720', progress: { value: 42 } },
|
||||
{ title: 'acpc.api.ic.io', value: '4,16,400', progress: { value: 55 } },
|
||||
{ title: 'wewe.uv.us', value: '2,35,200', progress: { value: 45 } }
|
||||
];
|
||||
|
||||
const routesData = [
|
||||
{ title: 'Home', value: '16,890', progress: { value: 15 } },
|
||||
{ title: 'Pricing', value: '4,909', progress: { value: 78 } },
|
||||
{ title: 'Change-log', value: '550', progress: { value: 25 } },
|
||||
{ title: 'Feature', value: '140', progress: { value: 47 } },
|
||||
{ title: 'Service', value: '8,675', progress: { value: 20 } },
|
||||
{ title: 'Pricing', value: '4,900', progress: { value: 74 } }
|
||||
];
|
||||
|
||||
const pageData = [
|
||||
{ title: 'Home', value: '67,560', progress: { value: 45 } },
|
||||
{ title: 'Pricing', value: '19,636', progress: { value: 25 } },
|
||||
{ title: 'Change-log', value: '2,220', progress: { value: 74 } },
|
||||
{ title: 'Feature', value: '560', progress: { value: 44 } },
|
||||
{ title: 'Service', value: '34,700', progress: { value: 41 } },
|
||||
{ title: 'Pricing', value: '19,600', progress: { value: 95 } }
|
||||
];
|
||||
|
||||
const affiliateData = [
|
||||
{ title: 'No-Refference', value: '16,890', progress: { value: 44 } },
|
||||
{ title: 'Medium', value: '4,909', progress: { value: 90 } },
|
||||
{ title: 'remaixblock.com', value: '550', progress: { value: 20 } },
|
||||
{ title: 'remaix-pge-block-hero', value: '140', progress: { value: 85 } },
|
||||
{ title: 'remaix-pge-block-banner', value: '8,675', progress: { value: 75 } },
|
||||
{ title: 'dev.io', value: '4,900', progress: { value: 78 } }
|
||||
];
|
||||
|
||||
const campaignData = [
|
||||
{ title: 'No-Refference', value: '67,560', progress: { value: 25 } },
|
||||
{ title: 'Medium', value: '19,636', progress: { value: 74 } },
|
||||
{ title: 'remaixblock.com', value: '2,220', progress: { value: 65 } },
|
||||
{ title: 'remaix-pge-block-hero', value: '560', progress: { value: 45 } },
|
||||
{ title: 'remaix-pge-block-banner', value: '34,700', progress: { value: 85 } },
|
||||
{ title: 'dev.io', value: '19,600', progress: { value: 47 } }
|
||||
];
|
||||
|
||||
const marketingData = [
|
||||
{ title: 'No-Refference', value: '8,10,720', progress: { value: 41 } },
|
||||
{ title: 'Medium', value: '2,35,632', progress: { value: 35 } },
|
||||
{ title: 'remaixblock.com', value: '26,640', progress: { value: 55 } },
|
||||
{ title: 'remaix-pge-block-hero', value: '6,720', progress: { value: 75 } },
|
||||
{ title: 'remaix-pge-block-banner', value: '4,16,400', progress: { value: 100 } },
|
||||
{ title: 'dev.io', value: '2,35,200', progress: { value: 20 } }
|
||||
];
|
||||
|
||||
/*************************** TABS - A11Y ***************************/
|
||||
|
||||
function a11yProps(value) {
|
||||
return { value: value, id: `simple-tab-${value}`, 'aria-controls': `simple-tabpanel-${value}` };
|
||||
}
|
||||
|
||||
/*************************** TABS - PANEL ***************************/
|
||||
|
||||
function TabPanel({ children, value, index, ...other }) {
|
||||
return (
|
||||
<div role="tabpanel" hidden={value !== index} id={`simple-tabpanel-${index}`} aria-labelledby={`simple-tab-${index}`} {...other}>
|
||||
{value === index && <Box sx={{ pt: 1.5 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*************************** TABS - CONTENT ***************************/
|
||||
|
||||
function TabContent({ data }) {
|
||||
return (
|
||||
<Stack sx={{ gap: 1.25 }}>
|
||||
{data.map((item, index) => (
|
||||
<ProgressCard key={index} {...item} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/*************************** CARDS - BORDER WITH RADIUS ***************************/
|
||||
|
||||
export function applyBorderWithRadius(radius, theme) {
|
||||
return {
|
||||
overflow: 'hidden',
|
||||
'--Grid-borderWidth': '1px',
|
||||
borderTop: 'var(--Grid-borderWidth) solid',
|
||||
borderLeft: 'var(--Grid-borderWidth) solid',
|
||||
borderColor: 'divider',
|
||||
'& > div': {
|
||||
overflow: 'hidden',
|
||||
borderRight: 'var(--Grid-borderWidth) solid',
|
||||
borderBottom: 'var(--Grid-borderWidth) solid',
|
||||
borderColor: 'divider',
|
||||
[theme.breakpoints.only('xs')]: {
|
||||
'&:first-of-type': getRadiusStyles(radius, 'topLeft', 'topRight'),
|
||||
'&:last-of-type': getRadiusStyles(radius, 'bottomLeft', 'bottomRight')
|
||||
},
|
||||
[theme.breakpoints.between('sm', 'md')]: {
|
||||
'&:nth-of-type(1)': getRadiusStyles(radius, 'topLeft'),
|
||||
'&:nth-of-type(2)': getRadiusStyles(radius, 'topRight'),
|
||||
'&:nth-of-type(3)': getRadiusStyles(radius, 'bottomLeft', 'bottomRight')
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
'&:first-of-type': getRadiusStyles(radius, 'topLeft', 'bottomLeft'),
|
||||
'&:last-of-type': getRadiusStyles(radius, 'topRight', 'bottomRight')
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*************************** CARDS - TOP REFERRERS ***************************/
|
||||
export default function TopReferrers() {
|
||||
const theme = useTheme();
|
||||
const [httpReferrers, setHttpReferrers] = useState('days');
|
||||
const [pages, setPages] = useState('routes');
|
||||
const [sources, setSources] = useState('affiliate');
|
||||
|
||||
// Separate handleChange functions
|
||||
const handleHTTPReferrers = (_event, newValue) => {
|
||||
setHttpReferrers(newValue);
|
||||
};
|
||||
|
||||
const handlePages = (_event, newValue) => {
|
||||
setPages(newValue);
|
||||
};
|
||||
|
||||
const handleSources = (_event, newValue) => {
|
||||
setSources(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Grid container sx={{ borderRadius: 4, boxShadow: theme.vars.customShadows.section, ...applyBorderWithRadius(16, theme) }}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Stack sx={{ gap: 2.5, p: 3 }}>
|
||||
<Typography variant="subtitle1">Top HTTP Referrers</Typography>
|
||||
<Box>
|
||||
<Tabs
|
||||
variant="fullWidth"
|
||||
value={httpReferrers}
|
||||
onChange={handleHTTPReferrers}
|
||||
aria-label="basic tabs example"
|
||||
type={TabsType.SEGMENTED}
|
||||
>
|
||||
<Tab label="Last 7 days" {...a11yProps('days')} />
|
||||
<Tab label="Last Month" {...a11yProps('month')} />
|
||||
<Tab label="Last Year" {...a11yProps('year')} />
|
||||
</Tabs>
|
||||
<TabPanel value={httpReferrers} index="days">
|
||||
<TabContent data={sevenDaysData} />
|
||||
</TabPanel>
|
||||
<TabPanel value={httpReferrers} index="month">
|
||||
<TabContent data={monthData} />
|
||||
</TabPanel>
|
||||
<TabPanel value={httpReferrers} index="year">
|
||||
<TabContent data={yearData} />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4 }}>
|
||||
<Stack sx={{ gap: 2.5, p: 3 }}>
|
||||
<Typography variant="subtitle1">Top Pages</Typography>
|
||||
<Box>
|
||||
<Tabs variant="fullWidth" value={pages} onChange={handlePages} aria-label="basic tabs example" type={TabsType.SEGMENTED}>
|
||||
<Tab label="Routes" {...a11yProps('routes')} />
|
||||
<Tab label="Pages" {...a11yProps('pages')} />
|
||||
</Tabs>
|
||||
<TabPanel value={pages} index="routes">
|
||||
<TabContent data={routesData} />
|
||||
</TabPanel>
|
||||
<TabPanel value={pages} index="pages">
|
||||
<TabContent data={pageData} />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Stack sx={{ gap: 2.5, p: 3 }}>
|
||||
<Typography variant="subtitle1">Top Sources</Typography>
|
||||
<Box>
|
||||
<Tabs variant="fullWidth" value={sources} onChange={handleSources} aria-label="basic tabs example" type={TabsType.SEGMENTED}>
|
||||
<Tab label="Affiliate" {...a11yProps('affiliate')} />
|
||||
<Tab label="Campaign" {...a11yProps('campaign')} />
|
||||
<Tab label="Marketing" {...a11yProps('marketing')} />
|
||||
</Tabs>
|
||||
<TabPanel value={sources} index="affiliate">
|
||||
<TabContent data={affiliateData} />
|
||||
</TabPanel>
|
||||
<TabPanel value={sources} index="campaign">
|
||||
<TabContent data={campaignData} />
|
||||
</TabPanel>
|
||||
<TabPanel value={sources} index="marketing">
|
||||
<TabContent data={marketingData} />
|
||||
</TabPanel>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TabPanel.propTypes = {
|
||||
children: PropTypes.any,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
index: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
||||
other: PropTypes.any
|
||||
};
|
||||
|
||||
TabContent.propTypes = { data: PropTypes.array };
|
||||
Reference in New Issue
Block a user