DONE: first init

This commit is contained in:
ISMAIL MASSERAN
2026-07-20 16:10:22 +08:00
commit 3189e3a1e3
272 changed files with 48853 additions and 0 deletions
@@ -0,0 +1,30 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export default function AuthGuard({ children, roles }) {
const navigate = useNavigate();
useEffect(() => {
const storedUserData = localStorage.getItem('userData');
const user = storedUserData ? JSON.parse(storedUserData) : null;
if (!user) {
navigate('/login');
return;
}
const hasNecessaryRole = roles.some(role => user.roles.find(userRole => userRole === role));
if (!hasNecessaryRole) {
navigate('/');
return;
}
}, []);
return (
<>
{ children }
</>
);
}
@@ -0,0 +1,10 @@
import { useContext } from 'react';
import { UserContext } from '../../context/UserContext.jsx';
export default function CanAccess( {children, roles} ){
const {user, setUser} = useContext(UserContext);
const hasRole = user && user.roles && user.roles.some(userRole => roles.find(role => role === userRole));
return hasRole ? children : null;
}
@@ -0,0 +1,59 @@
header.main-header {
display: flex;
justify-content: space-around;
align-items: center;
margin: 0 0 0 -2%;
background-color: #d3e2f8;
width: 101vw;
}
.header-logo {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
margin-right: auto;
margin-left: 2%;
}
.header-logo-png {
width: 200px;
height: 60px;
}
.header-logout-btn {
background-color: var(--blue);
color: white;
justify-content: center; /* Center content horizontally */
align-items: center;
border-radius: 6px;
border: none;
box-shadow: 3px 8px 10px 0 rgba(0,0,0,0.2);
cursor: pointer;
display: flex;
height: 40px;
width: 130px;
font-size: 20px;
margin-bottom: 2px;
}
.header-logout-btn:hover {
background-color: var(--light-blue);
transition-duration: 300ms;
}
.header-profile {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
margin-left: 20px;
margin-right: 2%;
margin-bottom: 0.5%;
}
.header-profile-png {
width: 46px;
height: 46px;
}
@@ -0,0 +1,62 @@
import { useContext } from 'react';
import { Button } from 'react-bootstrap';
import { useNavigate } from 'react-router-dom';
import { lastPathPart } from '../../utils/StringUtils.js';
import profileImage from '../../../assets/profile-user.png'
import './Header.css';
import { UserContext } from '../../context/UserContext.jsx';
export default function Header() {
const navigate = useNavigate();
const { user, setUser } = useContext(UserContext);
function handleLogout() {
localStorage.removeItem('userData');
localStorage.removeItem('token');
setUser(null);
navigate('/');
}
const path = window.location.pathname;
const showBackButton = !!user
&& '/' !== path
&& '/login' !== path
&& 'home' !== lastPathPart(window.location.href);
const goHome = () => {
if (user?.tenantCode) {
navigate(`/${user.tenantCode}/home`);
} else {
navigate('/');
}
};
return (
<>
<header className="main-header">
<h2 className="header-logo" onClick={ goHome }>BBQMS</h2>
<div className="header-logout">
{ !!user && (
<button className="header-logout-btn" onClick={ handleLogout }>
Logout
</button>
) }
</div>
<div className="header-profile" onClick={ () => navigate('/profile') }>
<img src={ profileImage } className="header-profile-png" alt="Profile image" />
</div>
</header>
{ showBackButton && (
<Button variant="secondary"
className="mt-2 px-4"
onClick={ goHome }>
Back
</Button>
) }
</>
);
}
@@ -0,0 +1,31 @@
.button-hp{
height: 80%;
display: inline-block;
margin-top: 50%;
box-shadow: 3px 6px 10px 0 rgba(0,0,0,0.2);
cursor: pointer;
}
.card-hp{
width: 14%;
border-radius: 10px;
float: left;
margin-right: 4%;
margin-left: 6%;
margin-top: 6%;
}
.card-title-hp{
text-align: center;
color: #334257;
height: 10%;
max-height: 10%;
}
.card-text-hp{
text-align: center;
color: #334257;
}
.button-container-hp{
text-align: center;
}
@@ -0,0 +1,21 @@
import React from "react";
import { useNavigate, useParams } from 'react-router-dom';
import './HomePageCard.css'
export default function HomePageCard( {title, backgroundColor, buttonColor, url} ) {
const navigate = useNavigate();
return (
<div>
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: backgroundColor, height: '200px'}}>
<div className="card-body">
<h5 className="card-title card-title-hp" style={{color: 'white'}}>{title}</h5>
<p className="card-text card-text-hp" style={{color: 'white'}}></p>
<div className="button-container-hp">
<button className="btn btn-primary button-hp"
style={{backgroundColor: buttonColor}} onClick ={ () => navigate(url) } >Open</button>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,21 @@
import React from "react";
import './HomePageCard.css'
export default function HomePageCardLight() {
return (
<div>
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: '#548CA8'}}>
<div className="card-body">
<h5 className="card-title card-title-hp" style={{color: 'white'}}>Card title</h5>
<p className="card-text card-text-hp" style={{color: 'white'}}>Some quick example text to build on
the card title and make up the bulk of
the card's content.</p>
<div className="button-container-hp">
<a href="#" className="btn btn-primary button-hp"
style={{backgroundColor: '#476072', border: '1px solid #548CA8'}}>Go somewhere</a>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,53 @@
#auth-container *{
box-sizing: border-box;
}
#auth-container{
font-family: "Poppins", sans-serif;
min-height: 100vh;
display: grid;
place-items: center;
}
#authForm {
border: 1px solid black;
padding: 40px;
width: 1000px;
background-color: white;
}
.inputs {
display: grid;
gap: 20px;
grid-template-columns: repeat(6, 1fr);
margin-bottom: 20px;
}
.inputs > * {
border: 1px solid black;
width: 100%;
padding: 40px;
text-align: center;
font-size: 40px;
line-height: 1;
}
.button-auth {
width: 100%;
margin-top: 20px;
padding: 24px;
background-color: #548CA8;
border: none;
color: white;
font-size: 32px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
.button-auth:hover {
background-color: #334257;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
@@ -0,0 +1,243 @@
import React, { useEffect, useRef, useReducer, useContext } from "react";
import "./LoginAuth.css";
import { SERVER_URL } from "../../constants.js";
import { fetchData } from '../../fetching/Fetch.js';
import { UserContext } from '../../context/UserContext.jsx';
import { useNavigate } from "react-router-dom";
function doSubmit(submittedValues) {
console.log(`Submitted: ${submittedValues.join("")}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
}
function clampIndex(index) {
if (index > 6) {
return 6;
} else if (index < 0) {
return 0;
} else {
return index;
}
}
function reducer(state, action) {
switch (action.type) {
case "INPUT":
return {
...state,
inputValues: [
...state.inputValues.slice(0, action.payload.index),
action.payload.value,
...state.inputValues.slice(action.payload.index + 1)
],
focusedIndex: clampIndex(state.focusedIndex + 1)
};
case "BACK":
return {
...state,
focusedIndex: clampIndex(state.focusedIndex - 1)
};
case "PASTE":
return {
...state,
inputValues: state.inputValues.map(
(_, index) => action.payload.pastedValue[index] || ""
)
};
case "FOCUS":
return {
...state,
focusedIndex: action.payload.focusedIndex
};
case "VERIFY":
return {
...state,
status: "pending"
};
case "VERIFY_SUCCESS":
return {
...state,
status: "idle"
};
case "RESET_INPUTS":
return {
...state,
inputValues: Array(6).fill(""),
focusedIndex: 0,
status: "idle"
};
default:
throw new Error("unknown action");
}
}
const initialState = {
inputValues: Array(6).fill(""),
focusedIndex: 0,
status: "idle"
};
export default function LoginAuth() {
const { user, setUser } = useContext(UserContext);
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
reducer,
initialState
);
function handleInput(index, value) {
dispatch({ type: "INPUT", payload: { index, value } });
}
function handleBack() {
dispatch({ type: "BACK" });
}
function handlePaste(pastedValue) {
dispatch({ type: "PASTE", payload: { pastedValue } });
if (pastedValue.length === 6) {
dispatch({ type: "VERIFY" });
doSubmit(pastedValue.split("")).then(() =>
dispatch({ type: "VERIFY_SUCCESS" })
);
}
}
function handleFocus(focusedIndex) {
dispatch({ type: "FOCUS", payload: { focusedIndex } });
}
async function handleSubmit(e) {
//e.preventDefault();
dispatch({ type: "VERIFY" });
try {
const storedUserData = localStorage.getItem('userData');
const userData = storedUserData ? JSON.parse(storedUserData) : null;
if (!userData) {
throw new Error("Email not found in localStorage");
}
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
const { data, success } = await fetchData(url, 'POST', {
code: inputValues.join(""),
email: userData.email
});
if (success) {
localStorage.setItem('token', data.token);
setUser(data.userData);
navigate(`/${ data.userData.tenantCode }/home`);
} else {
throw new Error("Code could not be verified. It is incorrect or expired.");
}
} catch (error) {
alert("Code could not be verified. It is incorrect or expired.");
resetInputs();
}
}
function resetInputs() {
dispatch({ type: "RESET_INPUTS" });
}
let navigate = useNavigate();
function routeChange() {
handleSubmit({});
}
return (
<div id="auth-container">
<form id="authForm" onSubmit={handleSubmit}>
<div className="inputs">
{inputValues.map((value, index) => {
return (
<Input
key={index}
index={index}
value={value}
onChange={handleInput}
onBackspace={handleBack}
onPaste={handlePaste}
isFocused={index === focusedIndex}
onFocus={handleFocus}
isDisabled={status === "pending"}
/>
);
})}
</div>
<button className="button-auth" disabled={status === "pending"} onClick={routeChange}>
{status === "pending" ? "VERIFYING..." : "VERIFY"}
</button>
</form>
</div>
);
}
function Input({
index,
value,
onChange,
onPaste,
onBackspace,
isFocused,
onFocus,
isDisabled
}) {
const ref = useRef();
useEffect(() => {
requestAnimationFrame(() => {
if (ref.current !== document.activeElement && isFocused) {
ref.current.focus();
}
});
}, [isFocused]);
function handleChange(e) {
onChange(index, e.target.value);
}
function handlePaste(e) {
onPaste(e.clipboardData.getData("text"));
}
function handleKeyDown(e) {
if (e.key === "Backspace") {
onBackspace();
}
}
function handleFocus(e) {
e.target.setSelectionRange(0, 1);
onFocus(index);
}
return (
<input
ref={ref}
type="text"
value={value}
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
maxLength="1"
onFocus={handleFocus}
disabled={isDisabled}
/>
);
}
@@ -0,0 +1,82 @@
#login-form {
position: relative;
width: 420px;
max-width: 100%;
margin: 150px auto 50px;
background-color: #334257;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
#login-form h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
#login-form form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
#login-form form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: gray;
font-family: "Poppins", sans-serif;
}
#login-form form input[type="text"],
#login-form form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
#login-form form input[type="submit"] {
width: 100%;
margin-top: 10px;
padding: 12px;
background-color: #548CA8;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
#login-form form input[type="submit"]:hover {
background-color: #334257;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
#registration{
text-align: center;
}
.form-group {
margin-bottom: 20px;
position: relative;
}
.error {
color: red;
font-size: 12px;
position: absolute;
bottom: -20px;
left: 0;
width: 100%;
}
@@ -0,0 +1,144 @@
import { GoogleLogin } from '@react-oauth/google';
import React, { useState } from "react";
import validator from "validator";
import "./LoginForm.css";
import LoginAuth from "../LoginAuth/LoginAuth";
import { Route, Routes, useNavigate, Link } from "react-router-dom";
import { SERVER_URL } from "../../constants.js";
const LoginForm = () => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [isSubmitted, setIsSubmitted] = useState(false);
const navigate = useNavigate();
async function handleGoogleLogin(credentialResponse) {
const response = await fetch(`${SERVER_URL}/api/v1/auth/login/oauth2/google`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
googleToken: credentialResponse.credential
})
});
if (!response.ok) {
setError("Error while trying to log in with Google.");
return;
}
const data = await response.json();
localStorage.setItem('userData', JSON.stringify(data));
navigate('/');
}
const handleSubmit = async (event) => {
event.preventDefault();
console.log("username: " + username);
console.log("password: " + password);
if (!username.trim()) {
setError("Username is required");
return;
}
if (!validator.isEmail(username) && !validator.isMobilePhone(username, "any")) {
setError("Invalid username format. Please enter a valid email or phone number.");
return;
}
if (!password.trim()) {
setError("Password is required.");
return;
}
try {
const response = await fetch(`${SERVER_URL}/api/v1/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: username,
password: password
}),
});
const data = await response.json();
localStorage.setItem('userData', JSON.stringify(data));
if (response.ok) {
setIsSubmitted(true);
navigate('/');
} else if (response.status === 403) {
setError("Your credentials are incorrect.");
}
} catch (error) {
console.error('Error:', error);
setError("An error occurred. Please try again.");
}
};
const handleUsernameChange = (event) => {
setUsername(event.target.value);
setError("");
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
setError("");
};
if (isSubmitted) {
return (
<Routes>
<Route path='/' element={<LoginAuth />} />
</Routes>
)
}
return (
<div id="login-form">
<h1>LOGIN</h1>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Email or phone number:</label>
<input
type="text"
id="username"
name="username"
value={username}
onChange={handleUsernameChange}
/>
{error && (error.includes("Username") || error.includes("Invalid")) && <p className="error">{error}</p>}
{error && (error.includes("credentials")) && <p className="error">{error}</p>}
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
type="password"
id="password"
name="password"
value={password}
onChange={handlePasswordChange}
/>
{error && error.includes("Password") && <p className="error">{error}</p>}
</div>
<input type="submit" value="Submit" />
<p id="registration">
Not registered? <Link to="/registration">Create an account</Link>
</p>
<div style={{
display: 'flex',
justifyContent: 'center'
}}>
</div>
</form>
</div>
);
};
export default LoginForm;
@@ -0,0 +1,66 @@
.auth-container-reg *{
box-sizing: border-box;
}
.auth-container-reg{
font-family: "Poppins", sans-serif;
min-height: 100vh;
display: grid;
place-items: center;
}
.authForm-reg {
border: 1px solid black;
padding: 0px 40px 40px 40px;
width: 1000px;
background-color: white;
}
.QRCodeContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
margin-bottom: 20px;
}
.QRCodeContainer h1 {
margin-bottom: 30px;
}
.inputs-reg {
display: grid;
gap: 20px;
grid-template-columns: repeat(6, 1fr);
margin-bottom: 20px;
}
.inputs-reg > * {
border: 1px solid black;
width: 100%;
padding: 40px;
text-align: center;
font-size: 40px;
line-height: 1;
}
button {
width: 100%;
margin-top: 20px;
padding: 24px;
background-color: #548CA8;
border: none;
color: white;
font-size: 32px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
button:hover {
background-color: #334257;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
@@ -0,0 +1,236 @@
import React, { useEffect, useRef, useReducer, useState } from "react";
import "./RegistrationAuth.css";
import { SERVER_URL } from "../../constants";
function doSubmit(submittedValues) {
console.log(`Submitted: ${submittedValues.join("")}`);
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 1500);
});
}
function clampIndex(index) {
if (index > 6) {
return 6;
} else if (index < 0) {
return 0;
} else {
return index;
}
}
function reducer(state, action) {
switch (action.type) {
case "INPUT":
return {
...state,
inputValues: [
...state.inputValues.slice(0, action.payload.index),
action.payload.value,
...state.inputValues.slice(action.payload.index + 1)
],
focusedIndex: clampIndex(state.focusedIndex + 1)
};
case "BACK":
return {
...state,
focusedIndex: clampIndex(state.focusedIndex - 1)
};
case "PASTE":
return {
...state,
inputValues: state.inputValues.map(
(_, index) => action.payload.pastedValue[index] || ""
)
};
case "FOCUS":
return {
...state,
focusedIndex: action.payload.focusedIndex
};
case "VERIFY":
return {
...state,
status: "pending"
};
case "VERIFY_SUCCESS":
return {
...state,
status: "idle"
};
case "RESET_INPUTS":
return {
...state,
inputValues: Array(6).fill(""),
focusedIndex: 0,
status: "idle"
};
default:
throw new Error("unknown action");
}
}
const initialState = {
inputValues: Array(6).fill(""),
focusedIndex: 0,
status: "idle"
};
export default function RefistrationAuth( {qrCode, email} ) {
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
reducer,
initialState
);
function handleInput(index, value) {
dispatch({ type: "INPUT", payload: { index, value } });
}
function handleBack() {
dispatch({ type: "BACK" });
}
function handlePaste(pastedValue) {
dispatch({ type: "PASTE", payload: { pastedValue } });
if (pastedValue.length === 6) {
dispatch({ type: "VERIFY" });
doSubmit(pastedValue.split("")).then(() =>
dispatch({ type: "VERIFY_SUCCESS" })
);
}
}
function handleFocus(focusedIndex) {
dispatch({ type: "FOCUS", payload: { focusedIndex } });
}
function handleSubmit(e) {
e.preventDefault();
dispatch({ type: "VERIFY" });
doSubmit(inputValues).then(() => dispatch({ type: "VERIFY_SUCCESS" }));
}
const checkInputValues = async () =>{
const enteredValues = inputValues.join("");
const requestBody = {
code: enteredValues,
email: email
}
try{
const response = await fetch(SERVER_URL + '/api/v1/auth/tfa', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
if (response.ok) {
dispatch({ type: "VERIFY_SUCCESS" });
} else {
throw new Error("Code could not be verified. It is incorrect or expired.");
}
} catch (error) {
alert("Code could not be verified. It is incorrect.");
resetInputs();
}
}
function resetInputs() {
dispatch({ type: "RESET_INPUTS" });
}
return (
<div className="auth-container-reg">
<form className="authForm-reg" onSubmit={handleSubmit}>
<div className="QRCodeContainer">
<h1>Scan QR code</h1>
<img src={qrCode} height='300px'></img>
</div>
<div className="inputs-reg">
{inputValues.map((value, index) => {
return (
<Input
key={index}
index={index}
value={value}
onChange={handleInput}
onBackspace={handleBack}
onPaste={handlePaste}
isFocused={index === focusedIndex}
onFocus={handleFocus}
isDisabled={status === "pending"}
/>
);
})}
</div>
<button onClick={checkInputValues} disabled={status === "pending"}>
{status === "pending" ? "VERIFYING..." : "VERIFY"}
</button>
</form>
</div>
);
}
function Input({
index,
value,
onChange,
onPaste,
onBackspace,
isFocused,
onFocus,
isDisabled
}) {
const ref = useRef();
useEffect(() => {
requestAnimationFrame(() => {
if (ref.current !== document.activeElement && isFocused) {
ref.current.focus();
}
});
}, [isFocused]);
function handleChange(e) {
onChange(index, e.target.value);
}
function handlePaste(e) {
onPaste(e.clipboardData.getData("text"));
}
function handleKeyDown(e) {
if (e.key === "Backspace") {
onBackspace();
}
}
function handleFocus(e) {
e.target.setSelectionRange(0, 1);
onFocus(index);
}
return (
<input
ref={ref}
type="text"
value={value}
onChange={handleChange}
onPaste={handlePaste}
onKeyDown={handleKeyDown}
maxLength="1"
onFocus={handleFocus}
disabled={isDisabled}
/>
);
}
@@ -0,0 +1,75 @@
.login-form-reg {
position: relative;
width: 420px;
max-width: 100%;
margin: 150px auto 50px;
background-color: #334257;
border-radius: 10px;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
font-family: "Poppins", sans-serif;
}
.login-form-reg h1 {
text-align: center;
margin: 0;
padding: 20px 0;
font-size: 28px;
font-weight: bold;
color: white;
}
.login-form-reg form {
padding: 20px;
background-color: white;
border-radius: 10px;
font-family: "Poppins", sans-serif;
}
.login-form-reg form label {
display: block;
margin-bottom: 8px;
font-size: 14px;
color: gray;
font-family: "Poppins", sans-serif;
}
.login-form-reg form input[type="text"],
.login-form-reg form input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid lightgray;
border-radius: 5px;
font-size: 16px;
box-sizing: border-box;
margin-bottom: 20px;
}
.login-form-reg form input[type="submit"] {
width: 100%;
margin-top: 10px;
padding: 12px;
background-color: #548CA8;
border: none;
color: white;
font-size: 16px;
font-weight: bold;
border-radius: 5px;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: background-color 0.3s ease;
}
.login-form-reg form input[type="submit"]:hover {
background-color: #334257;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
}
.login{
text-align: center;
}
.error-reg{
color: red;
font-size: 12px;
margin-top: 5px;
}
@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { Route, Routes, useNavigate, Link } from "react-router-dom";
import { useFormik } from 'formik';
import * as yup from 'yup';
import "./RegistrationForm.css";
import RegistrationAuth from '../RegistrationAuth/RegistrationAuth';
import { SERVER_URL } from '../../constants';
const userSchema = yup.object().shape({
email: yup.string().email("Please enter a valid email").required("Email is required"),
password: yup.string().min(4).max(10).required("Password is required")
});
export default function RegistrationForm() {
const navigate = useNavigate();
const [nextPage, setNextPage] = useState(false);
const [qrCode, setQrCode] = useState("");
const [email, setEmail] = useState("");
const formik = useFormik({
initialValues: {
email: '',
password: ''
},
validationSchema: userSchema,
onSubmit: async (values, { setFieldError }) => {
try {
const isValid = await userSchema.isValid(values);
if (isValid) {
const response = await fetch(SERVER_URL + '/api/v1/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: values.email,
password: values.password,
phone_number: "00000"
})
});
if (response.ok) {
const body = await response.json();
const responseBody = await fetch(SERVER_URL + `/api/v1/auth/tfa?email=${body.email}`);
const responseData = await responseBody.json();
setQrCode(responseData.qrCode);
setEmail(body.email);
setNextPage(true);
} else if (response.status === 400) {
setFieldError('email', 'Email already in use');
} else {
console.error('Failed to register:', response.statusText);
}
}
} catch (error) {
console.error('Error fetching data:', error);
}
}
});
if (nextPage) {
return (
<Routes>
<Route path='*' element={<RegistrationAuth qrCode={qrCode} email={email} />} />
</Routes>
);
}
return (
<div className="login-form-reg">
<h1>REGISTER ACCOUNT</h1>
<form onSubmit={formik.handleSubmit}>
<label htmlFor="email">Email or phone number:</label>
<input
id="email"
name="email"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.email}
/>
{formik.touched.email && formik.errors.email ? (
<p className="error-reg">{formik.errors.email}</p>
) : null}
<label htmlFor="password">Password:</label>
<input
id="password"
name="password"
type="password"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.password}
/>
{formik.touched.password && formik.errors.password ? (
<p className="error-reg">{formik.errors.password}</p>
) : null}
<input type="submit" value="Create new account"/>
<p className="login">
Already have an account? <Link to="/login">Login</Link>
</p>
</form>
</div>
);
}
@@ -0,0 +1,19 @@
import { useSortContext } from '../../context/SortContext.jsx';
import { useSortSearchState } from '../../hooks/sortParamsHooks.jsx';
export function SortableHeader({ columnName, children }) {
const { sort, updateSort } = useSortSearchState(columnName);
const sortContext = useSortContext();
function handleSort() {
const newSort = updateSort();
sortContext.onSort(newSort);
}
return (
<th style={{ cursor: 'pointer' }}
onClick={ handleSort }>
{ children }
</th>
)
}
@@ -0,0 +1,72 @@
import { useState } from 'react';
import { createClassName } from '../../utils/StringUtils.js'
export function TimePicker({ title, onChange, className, defaultHour, defaultMinute }) {
const [ hours, setHours ] = useState(defaultHour ?? '');
const [ minutes, setMinutes ] = useState(defaultMinute ?? '');
function handleChange(hours, minutes) {
onChange({
hour: hours && hours !== '' ? parseInt(hours) : defaultHour ?? 0,
minutes: minutes && minutes !== '' ? parseInt(minutes) : defaultMinute ?? 0
});
}
function handleHourChange(value) {
setHours(value);
handleChange(value, minutes);
}
function handleMinuteChange(value) {
setMinutes(value);
handleChange(hours, value);
}
return (
<div className={ createClassName([ 'd-inline-flex flex-column gap-1', className ]) }>
<div>{ title }</div>
<div className="d-inline-flex gap-2 align-items-center">
<TimePart value={ hours }
placeholder="HH"
min={ 0 }
max={ 23 }
onChange={ handleHourChange } />
:
<TimePart value={ minutes }
placeholder="MM"
min={ 0 }
max={ 59 }
onChange={ handleMinuteChange } />
</div>
</div>
)
}
function TimePart({ value, placeholder, onChange, min, max }) {
function handleChange(newValueString) {
if (newValueString !== '') {
if (isNaN(newValueString)) {
onChange('');
return;
}
const newValue = parseInt(newValueString);
if (newValue <= max && newValue >= min) {
onChange(newValue)
}
} else {
onChange('');
}
}
return (
<input className="border py-1 px-2 text-center rounded-1"
value={ value && value !== '' && parseInt(value) }
placeholder={ placeholder }
onChange={ e => handleChange(e.target.value) }
size={ 3 }
min={ min }
max={ max } />
)
}