Overhaul refactor. Still things in progress
This commit is contained in:
212
ui-old/src/components/Header/HeaderModal.tsx
Normal file
212
ui-old/src/components/Header/HeaderModal.tsx
Normal file
@@ -0,0 +1,212 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Modal,
|
||||
Container,
|
||||
Title,
|
||||
Anchor,
|
||||
Paper,
|
||||
TextInput,
|
||||
Button,
|
||||
PasswordInput,
|
||||
Group,
|
||||
Checkbox,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
interface HeaderModalProps {
|
||||
type?: string;
|
||||
toggle: any;
|
||||
login: ({ email, password }: { email: string, password: string }) => Promise<boolean>;
|
||||
register: ({ firstName, lastName, email, password }: { firstName: string, lastName: string, email: string, password: string }) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function HeaderModal({ type, toggle, login, register }: HeaderModalProps) {
|
||||
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';
|
||||
}
|
||||
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 registerForm = useForm({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
const loginForm = useForm({
|
||||
initialValues: {
|
||||
email: Cookies.get('email') || '',
|
||||
password: '',
|
||||
remember: Cookies.get('remember') === 'true'
|
||||
}
|
||||
});
|
||||
|
||||
const resetForm = useForm({
|
||||
initialValues: {
|
||||
email: ''
|
||||
}
|
||||
});
|
||||
|
||||
function onClose() {
|
||||
toggle(undefined);
|
||||
registerForm.reset();
|
||||
resetForm.reset();
|
||||
if (!loginForm.values.remember) {
|
||||
loginForm.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={resetForm.onSubmit(async (values) => console.log(values))}>
|
||||
<TextInput label='Email' placeholder='you@example.com' required {...resetForm.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={registerForm.onSubmit(async (values) => {
|
||||
const success = await register(values);
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<TextInput label='First name' placeholder='John' required {...registerForm.getInputProps('firstName')} />
|
||||
<TextInput
|
||||
label='Last name'
|
||||
placeholder='Smith'
|
||||
required
|
||||
mt='md'
|
||||
{...registerForm.getInputProps('lastName')}
|
||||
/>
|
||||
<TextInput
|
||||
label='Email'
|
||||
placeholder='you@example.com'
|
||||
required
|
||||
{...registerForm.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'
|
||||
{...registerForm.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={loginForm.onSubmit(async (values) => {
|
||||
Cookies.set('remember', 'true', { expires: 365 });
|
||||
if (values.remember) {
|
||||
Cookies.set('email', values.email, { expires: 365 });
|
||||
} else {
|
||||
Cookies.remove('email');
|
||||
}
|
||||
const success = await login(values);
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
})}
|
||||
>
|
||||
<TextInput label='Email' placeholder='you@example.com' required {...loginForm.getInputProps('email')} />
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
placeholder='Your password'
|
||||
required
|
||||
mt='md'
|
||||
{...loginForm.getInputProps('password')}
|
||||
/>
|
||||
<Group justify='space-between' mt='lg'>
|
||||
<Checkbox label='Remember me' defaultChecked={loginForm.values.remember} {...loginForm.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>
|
||||
);
|
||||
}
|
||||
114
ui-old/src/components/Header/UserMenu.tsx
Normal file
114
ui-old/src/components/Header/UserMenu.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { User } from "@/api/auth.types";
|
||||
import { setPicture } from "@/api/users";
|
||||
import {
|
||||
Menu,
|
||||
UnstyledButton,
|
||||
Group,
|
||||
Avatar,
|
||||
Card,
|
||||
FileButton,
|
||||
Grid,
|
||||
Button,
|
||||
Text
|
||||
} from "@mantine/core";
|
||||
import Link from "next/link";
|
||||
import { SetterOrUpdater } from "recoil";
|
||||
import './styles.css';
|
||||
|
||||
interface UserMenuProps {
|
||||
user: User;
|
||||
profilePicture: File | undefined;
|
||||
setProfilePicture: SetterOrUpdater<File | undefined>;
|
||||
toggle: (type: string) => void;
|
||||
logout: () => Promise<void>;
|
||||
}
|
||||
|
||||
export default function UserMenu({ user, profilePicture, setProfilePicture, logout, toggle }: UserMenuProps) {
|
||||
|
||||
return (
|
||||
<Menu shadow='md' width={200} openDelay={100} closeDelay={400}>
|
||||
<Menu.Target>
|
||||
<UnstyledButton>
|
||||
<Group>
|
||||
<Avatar src={profilePicture ? URL.createObjectURL(profilePicture) : undefined} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<Text size='sm' fw={500}>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text c='dimmed' size='xs' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown p={0}>
|
||||
<Card>
|
||||
<Card.Section h={140} style={{ backgroundColor: '#4481e3' }} />
|
||||
<FileButton
|
||||
onChange={(payload) => {
|
||||
if (payload) {
|
||||
setPicture(payload).then((response) => {
|
||||
if (response) {
|
||||
setProfilePicture(payload);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
accept='image/png,image/jpeg,image/svg+xml,image/webp,image/gif,image/apng,image/avif'
|
||||
multiple={false}
|
||||
>
|
||||
{(props) => (
|
||||
<Avatar
|
||||
{...props}
|
||||
component='button'
|
||||
size={80}
|
||||
radius={80}
|
||||
mx={'auto'}
|
||||
mt={-30}
|
||||
style={{ cursor: 'pointer' }}
|
||||
bg={profilePicture ? 'transparent' : 'white'}
|
||||
src={profilePicture ? URL.createObjectURL(profilePicture) : undefined}
|
||||
/>
|
||||
)}
|
||||
</FileButton>
|
||||
<Text ta='center' fz='lg' fw={500} mt='sm'>
|
||||
{user.first_name} {user.last_name}
|
||||
</Text>
|
||||
<Text ta='center' fz='sm' c='dimmed' style={{ textTransform: 'uppercase' }}>
|
||||
{user.role}
|
||||
</Text>
|
||||
<Grid mt='xl'>
|
||||
<Grid.Col span={6}>
|
||||
<Link href='/profile'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Profile
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={6}>
|
||||
<Button
|
||||
fullWidth
|
||||
radius='md'
|
||||
size='xs'
|
||||
variant='default'
|
||||
onClick={logout}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</Grid.Col>
|
||||
{user.role == 'admin' && (
|
||||
<Grid.Col span={12}>
|
||||
<Link href='/admin'>
|
||||
<Button fullWidth radius='md' size='xs' variant='default'>
|
||||
Administration
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Col>
|
||||
)}
|
||||
</Grid>
|
||||
</Card>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
111
ui-old/src/components/Header/index.tsx
Normal file
111
ui-old/src/components/Header/index.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import { getAirport, getAirports } from '@/api/airport';
|
||||
import { Autocomplete, Button, Group, UnstyledButton } from '@mantine/core';
|
||||
import { SetterOrUpdater, useRecoilState } from 'recoil';
|
||||
import { useToggle } from '@mantine/hooks';
|
||||
import { HeaderModal } from './HeaderModal';
|
||||
import { coordinatesState } from '@/state/map';
|
||||
import { User } from '@/api/auth.types';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { FaMoon } from "react-icons/fa6";
|
||||
import { FaSun } from "react-icons/fa6";
|
||||
import UserMenu from './UserMenu';
|
||||
import './styles.css';
|
||||
|
||||
interface HeaderProps {
|
||||
user: User | undefined;
|
||||
profilePicture: File | undefined;
|
||||
setProfilePicture: SetterOrUpdater<File | undefined>;
|
||||
login: ({ email, password }: { email: string, password: string }) => Promise<boolean>;
|
||||
logout: () => Promise<void>;
|
||||
register: ({ firstName, lastName, email, password }: { firstName: string, lastName: string, email: string, password: string }) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export default function Header({ user, profilePicture, setProfilePicture, login, logout, register }: HeaderProps) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [airports, setAirports] = useState<{ key: string; value: string; label: string }[]>([]);
|
||||
const [modalType, toggle] = useToggle([undefined, 'login', 'register', 'reset']);
|
||||
const [_, setCoordinates] = useRecoilState(coordinatesState);
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
async function onChange(value: string) {
|
||||
setSearchValue(value);
|
||||
const airportData = await getAirports({ icaos: [value], name: value });
|
||||
setAirports(
|
||||
airportData.data.map((airport) => ({
|
||||
key: airport.icao,
|
||||
value: airport.icao,
|
||||
label: `${airport.icao} - ${airport.name}`
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
async function onClick(value: string) {
|
||||
setSearchValue('');
|
||||
// Get current path
|
||||
if (pathname == '/') {
|
||||
const airport = await getAirport({ icao: value });
|
||||
if (airport) {
|
||||
setCoordinates({ lat: airport.data.latitude, lon: airport.data.longitude });
|
||||
}
|
||||
} else {
|
||||
router.push(`/airport/${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<nav className='navbar'>
|
||||
<div className='left'>
|
||||
<Link href={'/'} className='title'>
|
||||
<span>Aviation Weather</span>
|
||||
</Link>
|
||||
<div className='search'>
|
||||
<Autocomplete
|
||||
radius='xl'
|
||||
placeholder='Search Airports...'
|
||||
data={airports}
|
||||
limit={10}
|
||||
value={searchValue}
|
||||
onChange={onChange}
|
||||
onOptionSubmit={onClick}
|
||||
onBlur={() => setSearchValue('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='right'>
|
||||
<UnstyledButton style={{ paddingRight: '1em', margin: 'auto' }}>
|
||||
<FaMoon />
|
||||
{/* <FaSun /> */}
|
||||
</UnstyledButton>
|
||||
{user ? (
|
||||
<UserMenu
|
||||
user={user}
|
||||
profilePicture={profilePicture}
|
||||
setProfilePicture={setProfilePicture}
|
||||
toggle={toggle}
|
||||
logout={logout}
|
||||
/>
|
||||
) : (
|
||||
<Group className='user'>
|
||||
<Button onClick={() => toggle('login')}>Login</Button>
|
||||
<Button variant='outline' onClick={() => toggle('register')}>
|
||||
Sign up
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
<HeaderModal
|
||||
type={modalType}
|
||||
toggle={toggle}
|
||||
login={login}
|
||||
register={register}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
ui-old/src/components/Header/styles.css
Normal file
27
ui-old/src/components/Header/styles.css
Normal file
@@ -0,0 +1,27 @@
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 46px;
|
||||
background-color: #242d3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-left: 2em;
|
||||
padding-right: 2em;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2em;
|
||||
}
|
||||
Reference in New Issue
Block a user