First Commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @next
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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 { 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 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={NextLink}
|
||||
underline="hover"
|
||||
variant="caption"
|
||||
href="#"
|
||||
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,166 @@
|
||||
'use client';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// @next
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
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 { emailSchema, passwordSchema, firstNameSchema, lastNameSchema } from '@/utils/validation-schema/common';
|
||||
|
||||
// @icons
|
||||
import { IconEye, IconEyeOff } from '@tabler/icons-react';
|
||||
|
||||
/*************************** 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,55 @@
|
||||
'use client';
|
||||
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 GetImagePath from '@/utils/GetImagePath';
|
||||
import { SocialTypes } from '@/enum';
|
||||
|
||||
/*************************** SOCIAL BUTTON - DATA ***************************/
|
||||
|
||||
const authButtons = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: '/assets/images/social/google.svg',
|
||||
title: 'Sign in with Google'
|
||||
},
|
||||
{
|
||||
label: 'Facebook',
|
||||
icon: '/assets/images/social/facebook.svg',
|
||||
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,50 @@
|
||||
// @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';
|
||||
import { NextLink } from '@/components/routes';
|
||||
|
||||
/*************************** AUTH - COPYRIGHT ***************************/
|
||||
|
||||
export default function Copyright() {
|
||||
const copyrightSX = { display: { xs: 'none', sm: 'flex' } };
|
||||
|
||||
const linkProps = {
|
||||
component: NextLink,
|
||||
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}>
|
||||
© 2024 {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">
|
||||
© 2026 {branding.brandName}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user