Files
siren/ui/src/components/Topbar/index.tsx
2023-10-18 22:52:59 -04:00

398 lines
12 KiB
TypeScript

'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import './topbar.css';
import {
Anchor,
Avatar,
Button,
Card,
Checkbox,
Container,
Group,
Menu,
Modal,
Paper,
PasswordInput,
Text,
TextInput,
Title,
UnstyledButton
} from '@mantine/core';
import Cookies from 'js-cookie';
import { useEffect, useState } from 'react';
import { useForm } from '@mantine/form';
import { login, register, logout, me, refresh } from '@/api/auth';
import { User } from '@/api/auth.types';
import { useToggle } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
interface HeaderItem {
name: string;
link: string;
role?: string;
}
const headerItems: HeaderItem[] = [
{
name: 'Races',
link: '/races'
},
{
name: 'Classes',
link: '/classes'
},
{
name: 'Feats',
link: '/feats'
},
{
name: 'Options & Features',
link: '/options'
},
{
name: 'Backgrounds',
link: '/backgrounds'
},
{
name: 'Items',
link: '/items'
},
{
name: 'Spells',
link: '/spells'
},
{
name: 'Management',
link: '/management',
role: 'admin'
}
];
export default function Topbar() {
const pathName = usePathname();
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
const [headers, setHeaders] = useState<HeaderItem[]>([]);
const [user, setUser] = useState<User | undefined>(undefined);
useEffect(() => {
if (Cookies.get('logged_in')) {
me().then((response) => {
if (response) {
setUser(response.user);
}
});
} else {
refresh(true).then((response) => {
if (response) {
setUser(response.user);
} else {
setUser(undefined);
}
});
}
}, []);
useEffect(() => {
const h: HeaderItem[] = [];
headerItems.forEach((item) => {
if (item.role == undefined || user?.role == item.role) {
h.push(item);
}
setHeaders(h);
});
}, [user]);
return (
<>
<nav className='navbar'>
<div className='left'>
<Link href={'/'} className='title'>
Siren
</Link>
<div className='header-items'>
{headers.map((item) => (
<Link className={`header-item ${pathName == item.link && 'active'}`} href={item.link} key={item.name}>
{item.name}
</Link>
))}
</div>
</div>
<div className='user-section'>
{user ? (
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
<Menu.Target>
<UnstyledButton className='user user-button'>
<Group>
<Avatar />
<div style={{ flex: 1 }}>
<Text size='sm' fw={500}>
{user.first_name} {user.last_name}
</Text>
<Text c='dimmed' size='xs'>
{user.role}
</Text>
</div>
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>
<Card>
<Card.Section h={140} style={{}} />
<Avatar size={80} radius={80} mx={'auto'} mt={-30} />
<Text ta='center' fz='lg' fw={500} mt='sm'>
{user.first_name} {user.last_name}
</Text>
<Text ta='center' fz='sm' c='dimmed'>
{user.role}
</Text>
<Button
fullWidth
radius='md'
mt='xl'
size='md'
variant='default'
onClick={async () => {
const response = await logout();
if (response?.status == 200) {
Cookies.remove('logged_in');
setUser(undefined);
}
}}
>
Logout
</Button>
</Card>
</Menu.Dropdown>
</Menu>
) : (
<Group className='user'>
<Button onClick={() => toggle('login')}>Login</Button>
<Button variant='outline' onClick={() => toggle('register')}>
Sign up
</Button>
</Group>
)}
</div>
</nav>
<LoginModal type={modalType} toggle={toggle} setUser={setUser} />
</>
);
}
interface LoginModalProps {
type?: string;
toggle: any;
setUser: (user: User) => void;
}
function LoginModal({ type, toggle, setUser }: LoginModalProps) {
function passwordValidator(value: string) {
if (value.trim().length < 10) {
return 'Password must be at least 10 characters';
}
if (value.trim().length >= 128) {
return 'Password must be at most 128 characters';
}
if (!/(\d)/.test(value)) {
return 'Password must contain at least one number';
}
if (!/[a-z]/.test(value)) {
return 'Password must contain at least one lowercase letter';
}
if (!/[A-Z]/.test(value)) {
return 'Password must contain at least one uppercase letter';
}
if (!/[!@#$%^&*]/.test(value)) {
return 'Password must contain at least one special character';
}
if (/(.)\1\1/.test(value)) {
return 'Password must not contain more than 2 repeating characters';
}
return null;
}
function emailValidator(value: string) {
if (value.trim().length == 0) {
return 'Email is required';
}
if (!/^\S+@\S+$/.test(value)) {
return 'Invalid email';
}
return null;
}
const form = useForm({
initialValues: {
firstName: '',
lastName: '',
email: '',
password: '',
remember: false
},
validate: {
firstName: (value) => (value.trim().length > 0 ? null : 'First name is required'),
lastName: (value) => (value.trim().length > 0 ? null : 'Last name is required'),
email: emailValidator,
password: passwordValidator
}
});
function onClose() {
toggle(undefined);
if (!form.values.remember) {
form.reset();
}
}
return (
<Modal opened={type !== undefined} onClose={onClose} withCloseButton={false}>
{type == 'reset' ? (
<Container>
<Title ta='center'>Reset password</Title>
<Text c='dimmed' size='sm' ta='center' mt={5}>
Enter your email and we will send you a link to reset your password.{' '}
<Anchor size='sm' component='a' onClick={() => toggle('login')}>
Go Back
</Anchor>
</Text>
<Paper withBorder shadow='md' p={30} mt={30} radius='md'>
<form onSubmit={form.onSubmit(async (values) => console.log(values))}>
<TextInput label='Email' placeholder='you@example.com' required {...form.getInputProps('email')} />
<Button type='submit' fullWidth mt='xl'>
Reset password
</Button>
</form>
</Paper>
</Container>
) : type == 'register' ? (
<Container>
<Title ta='center'>Create account</Title>
<Text c='dimmed' size='sm' ta='center' mt={5}>
Already have an account?{' '}
<Anchor size='sm' component='a' onClick={() => toggle('login')}>
Sign in
</Anchor>
</Text>
<Paper withBorder shadow='md' p={30} mt={30} radius='md'>
<form
onSubmit={form.onSubmit(async (values) => {
const id = notifications.show({
loading: true,
title: `Creating account`,
message: `Please wait...`,
autoClose: false,
withCloseButton: false
});
const registerResponse = await register({
first_name: values.firstName,
last_name: values.lastName,
email: values.email,
password: values.password
});
if (registerResponse) {
const loginResponse = await login(values.email, values.password);
if (loginResponse) {
setUser(loginResponse.user);
onClose();
notifications.update({
id,
title: `Account created`,
message: `Welcome ${loginResponse.user.first_name}!`,
color: 'green',
autoClose: 2000,
loading: false
});
} else {
notifications.update({
id,
title: `Unable to Login`,
message: `Please try again.`,
color: 'red',
autoClose: 2000,
loading: false
});
}
} else {
notifications.update({
id,
title: `Unable to Register`,
message: `Please try again.`,
color: 'error',
autoClose: 2000,
loading: false
});
}
})}
>
<TextInput label='First name' placeholder='John' required {...form.getInputProps('firstName')} />
<TextInput label='Last name' placeholder='Smith' required mt='md' {...form.getInputProps('lastName')} />
<TextInput label='Email' placeholder='you@example.com' required {...form.getInputProps('email')} />
<PasswordInput
label='Password'
description='Passwords must be at least 10 characters long, contain at least one number, one uppercase letter, one lowercase letter, and one special character.'
placeholder='Your password'
required
mt='md'
{...form.getInputProps('password')}
/>
<Button type='submit' fullWidth mt='xl'>
Sign up
</Button>
</form>
</Paper>
</Container>
) : (
<Container>
<Title ta='center'>Welcome back!</Title>
<Text c='dimmed' size='sm' ta='center' mt={5}>
Do not have an account yet?{' '}
<Anchor size='sm' component='a' onClick={() => toggle('register')}>
Create account
</Anchor>
</Text>
<Paper withBorder shadow='md' p={30} mt={30} radius='md'>
<form
onSubmit={form.onSubmit(async (values) => {
const response = await login(values.email, values.password);
if (response) {
setUser(response.user);
onClose();
} else {
notifications.show({
title: `Unable to Login`,
message: `Please try again.`,
color: 'red',
autoClose: 2000
});
}
})}
>
<TextInput label='Email' placeholder='you@example.com' required {...form.getInputProps('email')} />
<PasswordInput
label='Password'
placeholder='Your password'
required
mt='md'
{...form.getInputProps('password')}
/>
<Group justify='space-between' mt='lg'>
<Checkbox label='Remember me' {...form.getInputProps('remember')} />
<Anchor component='a' size='sm' onClick={() => toggle('reset')}>
Forgot password?
</Anchor>
</Group>
<Button type='submit' fullWidth mt='xl'>
Sign in
</Button>
</form>
</Paper>
</Container>
)}
</Modal>
);
}