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,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>
);
}