First Commit

This commit is contained in:
cesnimda
2026-03-21 11:55:27 +01:00
commit 2e8a29b4d0
1757 changed files with 166084 additions and 0 deletions
@@ -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>
);
}
@@ -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';
@@ -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>
);
}
@@ -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>
);
}
@@ -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 };