| @@ -0,0 +1,3 @@ | |||
| <svg width="72" height="82" viewBox="0 0 72 82" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
| <path d="M55.9821 20.9284C55.9821 32.1726 46.8691 41.2856 35.625 41.2856C24.3809 41.2856 15.2679 32.1726 15.2679 20.9284C15.2679 9.68588 24.3809 0.571289 35.625 0.571289C46.8691 0.571289 55.9821 9.68588 55.9821 20.9284ZM33.2553 57.6985L27.9911 48.9195H43.2589L37.9947 57.6985L43.2907 77.4036L49.5728 51.7663C61.8507 53.6748 71.25 64.2987 71.25 77.1173C71.25 79.8051 69.0552 81.9999 66.3675 81.9999H4.88571C2.1868 81.9999 0 79.8051 0 77.1173C0 64.2987 9.39769 53.6748 21.6772 51.7663L27.9593 77.4036L33.2553 57.6985Z" fill="#D4D4D4"/> | |||
| </svg> | |||
| @@ -1,100 +1,92 @@ | |||
| import React, { useRef, useState } from "react"; | |||
| import React, { useEffect, useRef, useState } from "react"; | |||
| import PropTypes from "prop-types"; | |||
| import { | |||
| AddFile, | |||
| AddIcon, | |||
| ImageOverlay, | |||
| ImagePickerContainer, | |||
| ImageUploaded, | |||
| Tools, | |||
| } from "./ImagePicker.styled"; | |||
| import { Tooltip } from "@mui/material"; | |||
| import { Trash } from "../Cards/CreateOfferCard/SecondPart/SecondPartCreateOffer.styled"; | |||
| import { Icon } from "../Icon/Icon"; | |||
| import { IconButton } from "../Buttons/IconButton/IconButton"; | |||
| import { ReactComponent as EditIcon } from "../../assets/images/svg/edit.svg"; | |||
| import { ReactComponent as TrashIcon } from "../../assets/images/svg/trash.svg"; | |||
| // import { Input } from "@mui/material"; | |||
| const ImagePicker = (props) => { | |||
| const fileInputRef = useRef(null); | |||
| const [isOpened, setIsOpened] = useState(false); | |||
| const imageRef = useRef(null); | |||
| const [image, setImage] = useState(""); | |||
| const [isEditing, setIsEditing] = useState(false); | |||
| let listener; | |||
| useEffect(() => { | |||
| listener = (event) => { | |||
| if (imageRef.current) { | |||
| if (imageRef.current.contains(event.target)) { | |||
| setIsEditing(true); | |||
| } else { | |||
| setIsEditing(false); | |||
| } | |||
| } | |||
| }; | |||
| window.addEventListener("click", listener); | |||
| return () => window.removeEventListener("click", listener); | |||
| }, [imageRef]); | |||
| const handleChange = () => { | |||
| fileInputRef.current.value = ""; | |||
| fileInputRef.current.click(); | |||
| console.log(fileInputRef.current.click); | |||
| }; | |||
| const handleImage = (event) => { | |||
| console.log("fileEvent: ", event.target.files[0]); | |||
| let reader = new FileReader(); | |||
| reader.readAsDataURL(event.target.files[0]); | |||
| reader.onload = () => { | |||
| console.log(reader.result.toString()); | |||
| props.setImage(reader.result); | |||
| if (props.setImage) props.setImage(reader.result); | |||
| setImage(reader.result); | |||
| }; | |||
| reader.onerror = (error) => { | |||
| console.log(error); | |||
| }; | |||
| reader.onerror = () => {}; | |||
| }; | |||
| // let timeoutObject = { | |||
| // timeoutFunctionSet: false, | |||
| // timeoutFunction: () => {}, | |||
| // }; | |||
| // const showMessage = (event) => { | |||
| // console.log(event); | |||
| // }; | |||
| // const handleMouseEnter = (event) => { | |||
| // timeoutObject.timeoutFunctionSet = true; | |||
| // timeoutObject.timeoutFunction = setTimeout(() => { | |||
| // showMessage(event); | |||
| // }, 1000); | |||
| // }; | |||
| // const handleMouseMove = (event) => { | |||
| // if (timeoutObject.timeoutFunctionSet) { | |||
| // clearTimeout(timeoutObject.timeoutFunction); | |||
| // timeoutObject.timeoutFunction = setTimeout(() => { | |||
| // showMessage(event); | |||
| // }, 1000); | |||
| // } | |||
| // }; | |||
| // const handleMouseLeave = () => { | |||
| // if (timeoutObject.timeoutFunctionSet) { | |||
| // clearTimeout(timeoutObject.timeoutFunction); | |||
| // timeoutObject.timeoutFunctionSet = false; | |||
| // } | |||
| // }; | |||
| const handleOpen = () => { | |||
| setIsOpened(true); | |||
| if (!props.image) setIsOpened(false); | |||
| }; | |||
| const handleClose = () => { | |||
| setIsOpened(false); | |||
| }; | |||
| const handleDelete = () => { | |||
| props.deleteImage(); | |||
| setIsOpened(false) | |||
| } | |||
| if (props.deleteImage) props.deleteImage(); | |||
| setImage(""); | |||
| setIsEditing(false); | |||
| }; | |||
| return ( | |||
| <Tooltip | |||
| open={isOpened} | |||
| onOpen={handleOpen} | |||
| onClose={handleClose} | |||
| enterDelay={500} | |||
| enterNextDelay={500} | |||
| arrow | |||
| title={ | |||
| <Icon style={{width: "50px", height: "42px", paddingTop: "10px"}}> | |||
| <Trash onClick={handleDelete} /> | |||
| </Icon> | |||
| } | |||
| d | |||
| <ImagePickerContainer | |||
| className={props.className} | |||
| onClick={!image ? handleChange : () => {}} | |||
| hasImage={props.image} | |||
| > | |||
| <ImagePickerContainer | |||
| className={props.className} | |||
| onClick={!props.image ? handleChange : () => {}} | |||
| hasImage={props.image} | |||
| > | |||
| <AddFile type="file" ref={fileInputRef} onInput={handleImage} /> | |||
| {props.image ? ( | |||
| <ImageUploaded src={props.image} draggable={false}></ImageUploaded> | |||
| ) : ( | |||
| <AddIcon /> | |||
| )} | |||
| </ImagePickerContainer> | |||
| </Tooltip> | |||
| <AddFile type="file" ref={fileInputRef} onInput={handleImage} /> | |||
| {image ? ( | |||
| <React.Fragment> | |||
| <ImageUploaded src={image} draggable={false} ref={imageRef} /> | |||
| {isEditing && ( | |||
| <React.Fragment> | |||
| <ImageOverlay /> | |||
| <Tools showDeleteIcon={props.showDeleteIcon}> | |||
| <IconButton onClick={handleChange}> | |||
| <EditIcon /> | |||
| </IconButton> | |||
| {props.showDeleteIcon && ( | |||
| <IconButton onClick={handleDelete}> | |||
| <TrashIcon /> | |||
| </IconButton> | |||
| )} | |||
| </Tools> | |||
| </React.Fragment> | |||
| )} | |||
| </React.Fragment> | |||
| ) : ( | |||
| <AddIcon /> | |||
| )} | |||
| {props.children} | |||
| </ImagePickerContainer> | |||
| ); | |||
| }; | |||
| @@ -103,7 +95,8 @@ ImagePicker.propTypes = { | |||
| className: PropTypes.string, | |||
| setImage: PropTypes.func, | |||
| image: PropTypes.func, | |||
| deleteImage: PropTypes.func | |||
| deleteImage: PropTypes.func, | |||
| showDeleteIcon: PropTypes.bool, | |||
| }; | |||
| export default ImagePicker; | |||
| @@ -1,17 +1,20 @@ | |||
| import { Box } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import selectedTheme from "../../themes"; | |||
| import {ReactComponent as Plus} from "../../assets/images/svg/plus.svg"; | |||
| import { ReactComponent as Plus } from "../../assets/images/svg/plus.svg"; | |||
| import { ReactComponent as TrashIcon } from "../../assets/images/svg/trash.svg"; | |||
| export const ImagePickerContainer = styled(Box)` | |||
| flex: 1; | |||
| display: flex; | |||
| flex-basis: 216px; | |||
| flex-basis: 144px; | |||
| flex-grow: 0; | |||
| flex-shrink: 0; | |||
| height: 144px; | |||
| width: 144px; | |||
| margin: 0 9px; | |||
| border-radius: 4px; | |||
| position: relative; | |||
| cursor: pointer; | |||
| background-color: ${selectedTheme.imagePickerBackground}; | |||
| background-image: linear-gradient( | |||
| @@ -41,27 +44,68 @@ export const ImagePickerContainer = styled(Box)` | |||
| &:last-of-type { | |||
| margin-right: 0; | |||
| } | |||
| ${props => props.hasImage && ` | |||
| ${(props) => | |||
| props.hasImage && | |||
| ` | |||
| background-image: none; | |||
| border: 1px solid ${selectedTheme.primaryPurple}; | |||
| `} | |||
| `; | |||
| export const AddIcon = styled(Plus)` | |||
| margin: auto; | |||
| ` | |||
| margin: auto; | |||
| z-index: 1; | |||
| width: 60px; | |||
| height: 60px; | |||
| `; | |||
| export const AddFile = styled.input` | |||
| display: none; | |||
| ` | |||
| display: none; | |||
| `; | |||
| export const ImageUploaded = styled.img` | |||
| width: 216px; | |||
| height: 144px; | |||
| object-fit: scale-down; | |||
| width: 144px; | |||
| height: 144px; | |||
| border-radius: 100px; | |||
| object-fit: scale-down; | |||
| z-index: 1; | |||
| `; | |||
| export const ImageOverlay = styled(Box)` | |||
| position: absolute; | |||
| top: 0; | |||
| left: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| background-color: rgba(0,0,0,0.5); | |||
| z-index: 3; | |||
| overflow: hidden; | |||
| ` | |||
| export const Tooltip = styled(Box)` | |||
| background-color: rgba(255, 255, 255, 0.5); | |||
| width: 100px; | |||
| height: 100px; | |||
| position: absolute; | |||
| left: 0; | |||
| top: 0; | |||
| ` | |||
| background-color: rgba(255, 255, 255, 0.5); | |||
| width: 100px; | |||
| height: 100px; | |||
| position: absolute; | |||
| left: 0; | |||
| top: 0; | |||
| `; | |||
| export const Trash = styled(TrashIcon)` | |||
| cursor: pointer; | |||
| margin: auto; | |||
| width: 22px; | |||
| height: 22px; | |||
| & path { | |||
| stroke: white; | |||
| } | |||
| `; | |||
| export const Tools = styled(Box)` | |||
| position: absolute; | |||
| padding-top: 44px; | |||
| padding-left: ${props => props.showDeleteIcon ? "16px" : "45px"}; | |||
| z-index: 4; | |||
| flex-direction: row; | |||
| display: flex; | |||
| & div { | |||
| background-color: ${selectedTheme.primaryIconBackgroundColor}; | |||
| border-radius: 100px; | |||
| display: flex; | |||
| flex: 1; | |||
| margin: 10px; | |||
| } | |||
| `; | |||
| @@ -1,135 +1,145 @@ | |||
| export default { | |||
| app: { | |||
| title: 'Trampa' | |||
| }, | |||
| refresh: { | |||
| title: 'Jel si aktivan?', | |||
| cta: | |||
| "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.", | |||
| }, | |||
| common: { | |||
| close: 'Zatvori', | |||
| send: "Pošalji", | |||
| sendAgain: "Pošalji ponovo.", | |||
| trademark: 'TM', | |||
| search: 'Pretraga', | |||
| error: 'Greška', | |||
| continue: 'Nastavi', | |||
| labelUsername: 'Username', | |||
| labelEmail: 'Email', | |||
| labelPassword: 'Lozinka', | |||
| labelFirm: "Ime Firme", | |||
| labelPIB: "PIB", | |||
| labelPhone: "Telefon", | |||
| labelLocation: "Lokacija", | |||
| labelWebsite: "Adresa Websajta", | |||
| next: 'Sledeće', | |||
| nextPage: 'Sledeća strana', | |||
| previousPage: 'Prethodna strana', | |||
| back: 'Nazad', | |||
| goBack: 'Idi nazad', | |||
| ok: 'Ok', | |||
| done: 'Gotovo', | |||
| confirm: 'Potvrdi', | |||
| printDownload: 'Print/Download', | |||
| cancel: 'Obustavi', | |||
| remove: 'Izbriši', | |||
| invite: 'Pozovi', | |||
| save: 'Sačuvaj', | |||
| complete: 'Završi', | |||
| download: 'Download', | |||
| yes: 'Da', | |||
| no: 'Ne', | |||
| to: 'do', | |||
| select: 'Izaberi...', | |||
| none: 'Ni jedan', | |||
| date: { | |||
| range: '{{start}} do {{end}}', | |||
| }, | |||
| }, | |||
| login: { | |||
| welcome: 'React template', | |||
| welcomeText: 'Trampa sa kolegama na dohvat ruke', | |||
| dontHaveAccount: "Nemate nalog? ", | |||
| emailFormat: 'Nevalidan format email adrese!', | |||
| emailRequired: 'Email adresa je obavezna!', | |||
| noUsers: 'Ne postoji korisnik sa zadatom email adresom.', | |||
| passwordStrength: 'Your password is {{strength}}.', | |||
| passwordLength: 'Lozinka mora imati najmanje 8 karaktera!', | |||
| signUpRecommendation: 'Registrujte se.', | |||
| email: 'Unesite email adresu kako biste se prijavili', | |||
| logInTitle: 'Uloguj se', | |||
| logIn: 'Uloguj se', | |||
| signUp: 'Registrujte se.', | |||
| usernameRequired: 'Username je obavezan!', | |||
| passwordRequired: 'Lozinka je obavezna!', | |||
| forgotYourPassword: 'Zaboravili ste lozinku?', | |||
| forgotPasswordEmail:'Email', | |||
| useDifferentEmail: 'Iskoristite drugačiju lozinku.', | |||
| wrongCredentials: 'Pogrešan mail ili lozinka!' | |||
| }, | |||
| password: { | |||
| weak: 'slaba', | |||
| average: 'srednja', | |||
| good: 'dobra', | |||
| strong: 'jaka', | |||
| }, | |||
| register: { | |||
| title: "Registruj se", | |||
| descriptionMain: "Trampa sa kolegama na dohvat ruke", | |||
| descriptionFirst: "Email i Lozinka biće Vam primarni način da se ulogujete u aplikaciju", | |||
| descriptionSecond: 'Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije', | |||
| descriptionThird: 'Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije', | |||
| loginText: "Već posedujete nalog?", | |||
| emailFormat: 'Nevalidan format email adrese!', | |||
| emailTaken: "E-mail je zauzet!", | |||
| login: "Ulogujte se.", | |||
| acceptTerms: `Pri klikom na dugme "Registruj se", prihvatate naše`, | |||
| terms: "Uslove Korišćenja", | |||
| success: 'Registracija Uspešna', | |||
| PIBTaken: "PIB je zauzet!", | |||
| welcome: 'Dobro došli na trampu, želimo vam uspešno trampovanje!', | |||
| }, | |||
| forgotPassword: { | |||
| title: 'Povrati lozinku', | |||
| description: 'Molimo vas unesite email sa koji cemo vam poslati link za povratak lozinke', | |||
| label: 'Pošalji email', | |||
| emailRequired: 'Email je obavezan!', | |||
| emailFormat: 'Nevalidan format email adrese!', | |||
| mailSent: "E-Mail poslat!", | |||
| mailSentDescription: "Poslat vam je email sa instrukcijama kako da resetujete lozinku", | |||
| mailNotFound: "Mejl nije povezan ni sa jednim nalogom!", | |||
| notRecievedMail: "Niste dobili email?", | |||
| checkSpam: "U slučaju da Vam ne stigne email, pogledajte <strong>Spam</strong> folder email provajdera", | |||
| forgotPassword: { | |||
| title: 'Zaboravili ste lozinku', | |||
| subtitle: | |||
| 'Odgovorite na tajno pitanje kako biste povratili svoj nalog: ', | |||
| label: 'Obnovite lozinku', | |||
| }, | |||
| }, | |||
| resetPassword: { | |||
| title: "Unesite novu lozinku", | |||
| description: "Poslali ste zahtev za promenu lozinke, molimo Vas da unesete novu željenu lozinku", | |||
| passwordLabel: "Nova Lozinka", | |||
| passwordConfirmLabel: "Potvrdite Lozinku", | |||
| buttonText: "Postavi lozinku" | |||
| }, | |||
| filters: { | |||
| title: "Filteri", | |||
| cancel: "Poništi filtere", | |||
| usefilters: 'Primeni filtere', | |||
| categories: { | |||
| title: "Kategorija", | |||
| placeholder: 'Pretraži kategorije...' | |||
| }, | |||
| subcategories: { | |||
| title: "Podkategorija", | |||
| placeholder: "Pretraži podkategorije..." | |||
| }, | |||
| location: { | |||
| title: "Lokacija", | |||
| placeholder: "Pretraži gradove..." | |||
| } | |||
| } | |||
| } | |||
| app: { | |||
| title: "Trampa", | |||
| }, | |||
| refresh: { | |||
| title: "Jel si aktivan?", | |||
| cta: "You were registered as not active, please confirm that you are active in the next minute, if you don't you will be logged out.", | |||
| }, | |||
| common: { | |||
| close: "Zatvori", | |||
| send: "Pošalji", | |||
| sendAgain: "Pošalji ponovo.", | |||
| trademark: "TM", | |||
| search: "Pretraga", | |||
| error: "Greška", | |||
| continue: "Nastavi", | |||
| labelUsername: "Username", | |||
| labelEmail: "Email", | |||
| labelPassword: "Lozinka", | |||
| labelFirm: "Ime Firme", | |||
| labelPIB: "PIB", | |||
| labelPhone: "Telefon", | |||
| labelLocation: "Lokacija", | |||
| labelWebsite: "Adresa Websajta", | |||
| next: "Sledeće", | |||
| nextPage: "Sledeća strana", | |||
| previousPage: "Prethodna strana", | |||
| back: "Nazad", | |||
| goBack: "Idi nazad", | |||
| ok: "Ok", | |||
| done: "Gotovo", | |||
| confirm: "Potvrdi", | |||
| printDownload: "Print/Download", | |||
| cancel: "Obustavi", | |||
| remove: "Izbriši", | |||
| invite: "Pozovi", | |||
| save: "Sačuvaj", | |||
| complete: "Završi", | |||
| download: "Download", | |||
| yes: "Da", | |||
| no: "Ne", | |||
| to: "do", | |||
| select: "Izaberi...", | |||
| none: "Ni jedan", | |||
| date: { | |||
| range: "{{start}} do {{end}}", | |||
| }, | |||
| }, | |||
| login: { | |||
| welcome: "React template", | |||
| welcomeText: "Trampa sa kolegama na dohvat ruke", | |||
| dontHaveAccount: "Nemate nalog? ", | |||
| emailFormat: "Nevalidan format email adrese!", | |||
| emailRequired: "Email adresa je obavezna!", | |||
| noUsers: "Ne postoji korisnik sa zadatom email adresom.", | |||
| passwordStrength: "Your password is {{strength}}.", | |||
| passwordLength: "Lozinka mora imati najmanje 8 karaktera!", | |||
| signUpRecommendation: "Registrujte se.", | |||
| email: "Unesite email adresu kako biste se prijavili", | |||
| logInTitle: "Uloguj se", | |||
| logIn: "Uloguj se", | |||
| signUp: "Registrujte se.", | |||
| usernameRequired: "Username je obavezan!", | |||
| passwordRequired: "Lozinka je obavezna!", | |||
| forgotYourPassword: "Zaboravili ste lozinku?", | |||
| forgotPasswordEmail: "Email", | |||
| useDifferentEmail: "Iskoristite drugačiju lozinku.", | |||
| wrongCredentials: "Pogrešan mail ili lozinka!", | |||
| }, | |||
| password: { | |||
| weak: "slaba", | |||
| average: "srednja", | |||
| good: "dobra", | |||
| strong: "jaka", | |||
| }, | |||
| register: { | |||
| title: "Registruj se", | |||
| descriptionMain: "Trampa sa kolegama na dohvat ruke", | |||
| descriptionFirst: | |||
| "Email i Lozinka biće Vam primarni način da se ulogujete u aplikaciju", | |||
| descriptionSecond: | |||
| "Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije", | |||
| descriptionThird: | |||
| "Ovaj korak nije obavezan za razgledanje artikla ali za proces trampe je obavezan. Uvek možete popuniti ova polja u podešavanjima naloga kasnije", | |||
| loginText: "Već posedujete nalog?", | |||
| emailFormat: "Nevalidan format email adrese!", | |||
| emailTaken: "E-mail je zauzet!", | |||
| login: "Ulogujte se.", | |||
| acceptTerms: `Pri klikom na dugme "Registruj se", prihvatate naše`, | |||
| terms: "Uslove Korišćenja", | |||
| success: "Registracija Uspešna", | |||
| PIBTaken: "PIB je zauzet!", | |||
| PIBnoOfCharacters: "PIB mora imati 9 karaktera!", | |||
| welcome: "Dobro došli na trampu, želimo vam uspešno trampovanje!", | |||
| imageError: "Slika je obavezna!", | |||
| }, | |||
| forgotPassword: { | |||
| title: "Povrati lozinku", | |||
| description: | |||
| "Molimo vas unesite email sa koji cemo vam poslati link za povratak lozinke", | |||
| label: "Pošalji email", | |||
| emailRequired: "Email je obavezan!", | |||
| emailFormat: "Nevalidan format email adrese!", | |||
| mailSent: "E-Mail poslat!", | |||
| mailSentDescription: | |||
| "Poslat vam je email sa instrukcijama kako da resetujete lozinku", | |||
| mailNotFound: "Mejl nije povezan ni sa jednim nalogom!", | |||
| notRecievedMail: "Niste dobili email?", | |||
| checkSpam: | |||
| "U slučaju da Vam ne stigne email, pogledajte <strong>Spam</strong> folder email provajdera", | |||
| forgotPassword: { | |||
| title: "Zaboravili ste lozinku", | |||
| subtitle: "Odgovorite na tajno pitanje kako biste povratili svoj nalog: ", | |||
| label: "Obnovite lozinku", | |||
| }, | |||
| }, | |||
| resetPassword: { | |||
| title: "Unesite novu lozinku", | |||
| description: | |||
| "Poslali ste zahtev za promenu lozinke, molimo Vas da unesete novu željenu lozinku", | |||
| passwordLabel: "Nova Lozinka", | |||
| passwordConfirmLabel: "Potvrdite Lozinku", | |||
| buttonText: "Postavi lozinku", | |||
| }, | |||
| filters: { | |||
| title: "Filteri", | |||
| cancel: "Poništi filtere", | |||
| usefilters: "Primeni filtere", | |||
| categories: { | |||
| title: "Kategorija", | |||
| placeholder: "Pretraži kategorije...", | |||
| }, | |||
| subcategories: { | |||
| title: "Podkategorija", | |||
| placeholder: "Pretraži podkategorije...", | |||
| }, | |||
| location: { | |||
| title: "Lokacija", | |||
| placeholder: "Pretraži gradove...", | |||
| }, | |||
| }, | |||
| apiErrors: { | |||
| somethingWentWrong: "Greska sa serverom!" | |||
| } | |||
| }; | |||
| @@ -0,0 +1,3 @@ | |||
| export default { | |||
| email: "", | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| email: "", | |||
| password: "", | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| mail: "", | |||
| password: "", | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| nameOfFirm: "", | |||
| PIB: "", | |||
| }; | |||
| @@ -0,0 +1,5 @@ | |||
| export default { | |||
| phoneNumber: "", | |||
| location: "", | |||
| website: "", | |||
| }; | |||
| @@ -0,0 +1,4 @@ | |||
| export default { | |||
| password: "", | |||
| passwordConfirm: "", | |||
| }; | |||
| @@ -51,4 +51,20 @@ export const ErrorMessage = styled(Typography)` | |||
| position: relative; | |||
| top: -7px; | |||
| font-size: 14px; | |||
| ` | |||
| ` | |||
| export const LoginAltText = styled(Typography)` | |||
| font-family: "Poppins"; | |||
| color: ${selectedTheme.primaryText}; | |||
| font-size: 14px; | |||
| padding-right: 6px; | |||
| line-height: 14px; | |||
| `; | |||
| export const LoginTextContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| margin-top: 36px; | |||
| justify-content: center; | |||
| @media (max-height: 800px) { | |||
| margin-top: 26px; | |||
| } | |||
| `; | |||
| @@ -28,7 +28,11 @@ const MailSent = () => { | |||
| const dispatch = useDispatch(); | |||
| useEffect(() => { | |||
| setEmail(location.state.email); | |||
| if (location.state.email) { | |||
| setEmail(location.state.email); | |||
| } else { | |||
| history.push("/login"); | |||
| } | |||
| }, []); | |||
| const navigateLogin = () => { | |||
| @@ -1,7 +1,6 @@ | |||
| import React, { useState } from "react"; | |||
| import { useFormik } from "formik"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import * as Yup from "yup"; | |||
| // import i18next from "i18next"; | |||
| import { ReactComponent as Logo } from "../../assets/images/svg/logo-vertical.svg"; | |||
| import { | |||
| @@ -10,14 +9,19 @@ import { | |||
| ForgotPasswordTitle, | |||
| FormContainer, | |||
| ErrorMessage, | |||
| LoginTextContainer, | |||
| LoginAltText, | |||
| } from "./ForgotPassword.styled"; | |||
| import { TextField } from "../../components/TextFields/TextField/TextField"; | |||
| import { PrimaryButton } from "../../components/Buttons/PrimaryButton/PrimaryButton"; | |||
| import { useHistory } from "react-router-dom"; | |||
| import { NavLink, useHistory } from "react-router-dom"; | |||
| import { FORGOT_PASSWORD_MAIL_SENT } from "../../constants/pages"; | |||
| import selectedTheme from "../../themes"; | |||
| import { useDispatch } from "react-redux"; | |||
| import { forgotPassword } from "../../store/actions/user/userActions"; | |||
| import forgotPasswordValidation from "../../validations/forgotPasswordValidation"; | |||
| import forgotPasswordInitialValues from "../../initialValues/forgotPasswordInitialValues"; | |||
| import Link from "../../components/Link/Link"; | |||
| const ForgotPasswordPage = () => { | |||
| const history = useHistory(); | |||
| @@ -25,12 +29,6 @@ const ForgotPasswordPage = () => { | |||
| const dispatch = useDispatch(); | |||
| const [emailNotFoundStatus, setEmailNotFoundStatus] = useState(false); | |||
| const forgotPasswordValidationSchema = Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(t("forgotPassword.emailRequired")) | |||
| .email(t("forgotPassword.emailFormat")), | |||
| }); | |||
| const handleResponseSuccess = () => { | |||
| history.push({ | |||
| pathname: FORGOT_PASSWORD_MAIL_SENT, | |||
| @@ -52,10 +50,8 @@ const ForgotPasswordPage = () => { | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: "", | |||
| }, | |||
| validationSchema: forgotPasswordValidationSchema, | |||
| initialValues: forgotPasswordInitialValues, | |||
| validationSchema: forgotPasswordValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| @@ -109,6 +105,19 @@ const ForgotPasswordPage = () => { | |||
| > | |||
| {t("common.send")} | |||
| </PrimaryButton> | |||
| <LoginTextContainer> | |||
| <LoginAltText>{t("register.loginText")}</LoginAltText> | |||
| <Link | |||
| to="/login" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| > | |||
| {t("register.login")} | |||
| </Link> | |||
| </LoginTextContainer> | |||
| </FormContainer> | |||
| </ForgotPasswordPageContainer> | |||
| ); | |||
| @@ -32,6 +32,8 @@ import { | |||
| ErrorMessage, | |||
| } from "./Login.styled"; | |||
| import selectedTheme from "../../themes"; | |||
| import loginValidation from "../../validations/loginValidation"; | |||
| import loginInitialValues from "../../initialValues/loginInitialValues"; | |||
| const LoginPage = ({ history }) => { | |||
| const dispatch = useDispatch(); | |||
| @@ -83,16 +85,8 @@ const LoginPage = ({ history }) => { | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: "", | |||
| password: "", | |||
| }, | |||
| validationSchema: Yup.object().shape({ | |||
| email: Yup.string().required(t("login.mailRequired")), | |||
| password: Yup.string() | |||
| .required(t("login.passwordRequired")) | |||
| .min(8, t("login.passwordLength")), | |||
| }), | |||
| initialValues: loginInitialValues, | |||
| validationSchema: loginValidation, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| @@ -68,12 +68,6 @@ const FirstPartOfRegistration = (props) => { | |||
| fullWidth | |||
| /> | |||
| {formik.errors.mail && formik.touched.mail ? ( | |||
| <ErrorMessage>{formik.errors.mail}</ErrorMessage> | |||
| ) : ( | |||
| <></> | |||
| )} | |||
| <TextField | |||
| name="password" | |||
| placeholder={t("common.labelPassword")} | |||
| @@ -89,16 +83,20 @@ const FirstPartOfRegistration = (props) => { | |||
| InputProps={{ | |||
| endAdornment: ( | |||
| <IconButton onClick={handleClickShowPassword}> | |||
| {showPassword ? <VisibilityOn /> : <VisibilityOff />} | |||
| {showPassword ? <VisibilityOff /> : <VisibilityOn />} | |||
| </IconButton> | |||
| ), | |||
| }} | |||
| /> | |||
| {formik.errors.password && formik.touched.password ? ( | |||
| {formik.errors.mail && formik.touched.mail ? ( | |||
| <ErrorMessage>{formik.errors.mail}</ErrorMessage> | |||
| ) : formik.errors.password && formik.touched.password ? ( | |||
| <ErrorMessage>{formik.errors.password}</ErrorMessage> | |||
| ) : ( | |||
| <></> | |||
| )} | |||
| {props.error && <ErrorMessage>{props.errorMessage}</ErrorMessage>} | |||
| <PrimaryButton | |||
| @@ -9,6 +9,10 @@ import { | |||
| ProgressContainer, | |||
| RegisterDescription, | |||
| RegisterTitle, | |||
| ProfileImagePicker, | |||
| ProfilePicture, | |||
| RegisterPageContent, | |||
| ErrorMessage, | |||
| } from "./Register.styled"; | |||
| import { ReactComponent as Logo } from "../../../assets/images/svg/logo-vertical.svg"; | |||
| import { NavLink, useHistory } from "react-router-dom"; | |||
| @@ -28,19 +32,21 @@ const Register = () => { | |||
| const dispatch = useDispatch(); | |||
| const [currentStep, setCurrentStep] = useState(1); | |||
| const [informations, setInformations] = useState({}); // Values of fields typed in all steps | |||
| const [mailError, setMailError] = useState(""); // Wrong mail typed | |||
| const [mailErrorMessage, setMailErrorMessage] = useState(""); // Error message caused by typing wrong mail | |||
| const [PIBError, setPIBError] = useState(""); // Wrong PIB typed | |||
| const [PIBErrorMessage, setPIBErrorMessage] = useState(""); // Error message caused by typing wrong PIB | |||
| const [mailError, setMailError] = useState(""); // Wrong mail typed | |||
| const [mailErrorMessage, setMailErrorMessage] = useState(""); // Error message caused by typing wrong mail | |||
| const [PIBError, setPIBError] = useState(""); // Wrong PIB typed | |||
| const [PIBErrorMessage, setPIBErrorMessage] = useState(""); // Error message caused by typing wrong PIB | |||
| const [imageError, setImageError] = useState(false); // Not picked image | |||
| const handleResponseSuccess = () => { | |||
| history.push(REGISTER_SUCCESSFUL_PAGE); | |||
| }; | |||
| const handleResponseError = (error) => { | |||
| console.log(error); | |||
| const { mail, password, PIB, image } = informations; | |||
| if (error.type === "mail") { | |||
| const { mail } = informations; | |||
| setInformations({}); | |||
| setInformations({image}); | |||
| setCurrentStep(1); | |||
| setMailError(mail); | |||
| if ( | |||
| @@ -52,8 +58,7 @@ const Register = () => { | |||
| setMailErrorMessage(t("register.emailFormat")); | |||
| } | |||
| } else { | |||
| const { mail, password, PIB } = informations; | |||
| setInformations({ mail, password }); | |||
| setInformations({ mail, password, image }); | |||
| setCurrentStep(2); | |||
| setPIBError(PIB.toString()); | |||
| setPIBErrorMessage(t("register.PIBTaken")); | |||
| @@ -70,67 +75,93 @@ const Register = () => { | |||
| if (currentStep !== 3) { | |||
| setCurrentStep((prevState) => prevState + 1); | |||
| } else { | |||
| registerUser({ ...informations, ...values }); | |||
| if (!informations.image) { | |||
| setImageError(true); | |||
| } else { | |||
| registerUser({ ...informations, ...values }); | |||
| } | |||
| } | |||
| setInformations({ ...informations, ...values }); | |||
| }; | |||
| const setImage = (image) => { | |||
| setImageError(false); | |||
| setInformations(prevInfo => ({ | |||
| ...prevInfo, | |||
| image | |||
| })) | |||
| } | |||
| const goStepBack = (stepNumber) => { | |||
| setCurrentStep(stepNumber); | |||
| const { mail, password, image } = informations; | |||
| if (stepNumber === 1) { | |||
| setInformations({}); | |||
| setInformations({image}); | |||
| } | |||
| if (stepNumber === 2) { | |||
| const { mail, password } = informations; | |||
| setInformations({ mail, password }); | |||
| setInformations({ mail, password, image }); | |||
| } | |||
| }; | |||
| return ( | |||
| <RegisterPageContainer currentstep={currentStep}> | |||
| <Logo /> | |||
| <RegisterTitle component="h1" variant="h5"> | |||
| {t("register.title")} | |||
| </RegisterTitle> | |||
| <RegisterDescription component="h1" variant="h6"> | |||
| {t("register.descriptionMain")} | |||
| </RegisterDescription> | |||
| <ProgressContainer> | |||
| <StepProgress | |||
| functions={[() => goStepBack(1), () => goStepBack(2)]} | |||
| current={currentStep} | |||
| numberOfSteps={3} | |||
| /> | |||
| </ProgressContainer> | |||
| {currentStep === 1 && ( | |||
| <FirstPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={mailError} | |||
| errorMessage={mailErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 2 && ( | |||
| <SecondPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={PIBError} | |||
| errorMessage={PIBErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 3 && ( | |||
| <ThirdPartOfRegistration handleSubmit={handleSubmit} /> | |||
| )} | |||
| <LoginTextContainer> | |||
| <LoginAltText>{t("register.loginText")}</LoginAltText> | |||
| <Link to="/login" component={NavLink} underline="hover" align="center"> | |||
| {t("register.login")} | |||
| </Link> | |||
| </LoginTextContainer> | |||
| <RegisterPageContent> | |||
| <Logo /> | |||
| <RegisterTitle component="h1" variant="h5"> | |||
| {t("register.title")} | |||
| </RegisterTitle> | |||
| <RegisterDescription component="h1" variant="h6"> | |||
| {t("register.descriptionMain")} | |||
| </RegisterDescription> | |||
| <ProgressContainer> | |||
| <StepProgress | |||
| functions={[() => goStepBack(1), () => goStepBack(2)]} | |||
| current={currentStep} | |||
| numberOfSteps={3} | |||
| /> | |||
| </ProgressContainer> | |||
| <ProfileImagePicker setImage={setImage} > | |||
| <ProfilePicture /> | |||
| </ProfileImagePicker> | |||
| {currentStep === 1 && ( | |||
| <FirstPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={mailError} | |||
| errorMessage={mailErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 2 && ( | |||
| <SecondPartOfRegistration | |||
| handleSubmit={handleSubmit} | |||
| error={PIBError} | |||
| errorMessage={PIBErrorMessage} | |||
| /> | |||
| )} | |||
| {currentStep === 3 && ( | |||
| <ThirdPartOfRegistration handleSubmit={handleSubmit} /> | |||
| )} | |||
| {imageError && <ErrorMessage>{t("register.imageError")}</ErrorMessage>} | |||
| <LoginTextContainer> | |||
| <LoginAltText>{t("register.loginText")}</LoginAltText> | |||
| <Link | |||
| to="/login" | |||
| component={NavLink} | |||
| underline="hover" | |||
| align="center" | |||
| > | |||
| {t("register.login")} | |||
| </Link> | |||
| </LoginTextContainer> | |||
| </RegisterPageContent> | |||
| <Footer> | |||
| <FooterText> | |||
| @@ -1,9 +1,11 @@ | |||
| import { Box, Container, Typography } from "@mui/material"; | |||
| import styled from "styled-components"; | |||
| import ImagePicker from "../../../components/ImagePicker/ImagePicker"; | |||
| import selectedTheme from "../../../themes"; | |||
| import { ReactComponent as ProfilePictureSVG } from "../../../assets/images/profile-picture.svg"; | |||
| export const RegisterPageContainer = styled(Container)` | |||
| margin-top: 100px; | |||
| margin-top: 72px; | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| @@ -11,6 +13,8 @@ export const RegisterPageContainer = styled(Container)` | |||
| padding: 0; | |||
| flex: 1; | |||
| position: relative; | |||
| transition: 1s all; | |||
| ${props => props.currentstep === 3 && `margin-top: 40px`}; | |||
| @media (max-height: 900px) { | |||
| margin-top: 60px; | |||
| } | |||
| @@ -18,8 +22,8 @@ export const RegisterPageContainer = styled(Container)` | |||
| margin-top: 30px; | |||
| flex: none; | |||
| height: 95vh; | |||
| ${props => props.currentstep === 3 && `height: 105vh`}; | |||
| ${props => props.currentstep === 2 && `height: 100vh`}; | |||
| ${(props) => props.currentstep === 3 && `height: 105vh`}; | |||
| ${(props) => props.currentstep === 2 && `height: 100vh`}; | |||
| } | |||
| `; | |||
| export const RegisterTitle = styled(Typography)` | |||
| @@ -34,7 +38,7 @@ export const RegisterTitle = styled(Typography)` | |||
| color: ${selectedTheme.primaryPurple}; | |||
| margin-top: 34px; | |||
| @media (max-height: 800px) { | |||
| margin-top: 26px; | |||
| margin-top: 26px; | |||
| } | |||
| `; | |||
| export const RegisterDescription = styled(Typography)` | |||
| @@ -51,8 +55,8 @@ export const RegisterDescription = styled(Typography)` | |||
| color: ${selectedTheme.primaryGrayText}; | |||
| margin-bottom: 20px; | |||
| @media (max-height: 800px) { | |||
| margin-bottom: 14px; | |||
| margin-top: 6px; | |||
| margin-bottom: 14px; | |||
| margin-top: 6px; | |||
| } | |||
| `; | |||
| export const FormContainer = styled(Box)` | |||
| @@ -64,7 +68,7 @@ export const LoginAltText = styled(Typography)` | |||
| font-size: 14px; | |||
| padding-right: 6px; | |||
| line-height: 14px; | |||
| ` | |||
| `; | |||
| export const LoginTextContainer = styled(Box)` | |||
| display: flex; | |||
| flex-direction: row; | |||
| @@ -73,11 +77,11 @@ export const LoginTextContainer = styled(Box)` | |||
| @media (max-height: 800px) { | |||
| margin-top: 26px; | |||
| } | |||
| ` | |||
| `; | |||
| export const ProgressContainer = styled(Container)` | |||
| width: 100%; | |||
| padding: 0; | |||
| ` | |||
| width: 100%; | |||
| padding: 0; | |||
| `; | |||
| export const Footer = styled(Box)` | |||
| position: absolute; | |||
| bottom: 36px; | |||
| @@ -88,7 +92,7 @@ export const Footer = styled(Box)` | |||
| @media (max-height: 800px) { | |||
| bottom: 10px; | |||
| } | |||
| ` | |||
| `; | |||
| export const FooterText = styled(Typography)` | |||
| font-family: "Open Sans"; | |||
| color: #505050; | |||
| @@ -98,4 +102,46 @@ export const FooterText = styled(Typography)` | |||
| font-weight: 400; | |||
| padding: 0; | |||
| font-size: 12px; | |||
| `; | |||
| export const ProfileImagePicker = styled(ImagePicker)` | |||
| background: none; | |||
| margin: 36px; | |||
| background: ${selectedTheme.primaryIconBackgroundColor}; | |||
| background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='100' ry='100' stroke='%235A3984FF' stroke-width='2' stroke-dasharray='7%2c 12' stroke-dashoffset='44' stroke-linecap='square'/%3e%3c/svg%3e"); | |||
| border-radius: 100px; | |||
| overflow: hidden; | |||
| position: relative; | |||
| margin-bottom: 5px; | |||
| `; | |||
| export const ProfilePicture = styled(ProfilePictureSVG)` | |||
| position: absolute; | |||
| left: 36px; | |||
| top: 24px; | |||
| z-index: 0; | |||
| `; | |||
| export const RegisterPageContent = styled(Box)` | |||
| display: flex; | |||
| flex-direction: column; | |||
| align-items: center; | |||
| width: 335px; | |||
| padding: 0; | |||
| flex: 1; | |||
| position: relative; | |||
| margin-bottom: 100px; | |||
| @media (max-height: 800px) { | |||
| flex: none; | |||
| height: 95vh; | |||
| ${(props) => props.currentstep === 3 && `height: 105vh`}; | |||
| ${(props) => props.currentstep === 2 && `height: 100vh`}; | |||
| } | |||
| `; | |||
| export const ErrorMessage = styled(Box)` | |||
| color: red; | |||
| font-family: "Open Sans"; | |||
| position: relative; | |||
| top: 5px; | |||
| text-align: left; | |||
| font-size: 14px; | |||
| width: 100%; | |||
| ` | |||
| @@ -36,7 +36,6 @@ export default ({ dispatch }) => | |||
| if (new Date() > new Date(refreshTokenDecoded?.exp * 1000)) { | |||
| dispatch(logoutUser()); | |||
| } | |||
| // If access token is expired, refresh access token | |||
| if (new Date() > new Date(jwtTokenDecoded.exp * 1000)) { | |||
| const axiosResponse = await axios.post(`${baseURL}auth/refresh`, { | |||
| @@ -63,8 +63,9 @@ function* fetchUser({ payload }) { | |||
| if (payload.handleApiResponseError) { | |||
| yield call(payload.handleApiResponseError, e.response.status); | |||
| } | |||
| let errorMessage = yield call(rejectErrorCodeHelper, e); | |||
| if (e.response.status === 401) { | |||
| console.log(e.response.status); | |||
| let errorMessage = yield call(rejectErrorCodeHelper, e.response.status); | |||
| if (e.response.status === 400) { | |||
| errorMessage = i18next.t("login.wrongCredentials", { | |||
| lng: "rs" | |||
| }); | |||
| @@ -4,13 +4,11 @@ import { REGISTER_USER_FETCH } from "../actions/register/registerActionConstants | |||
| function* fetchRegisterUser({ payload }) { | |||
| try { | |||
| const requestData = { | |||
| email: payload.values.mail.toString(), | |||
| password: payload.values.password.toString(), | |||
| roles: [ | |||
| "User", | |||
| ], | |||
| roles: ["User"], | |||
| image: payload.values.image.replace("data:image/png;base64,", ""), | |||
| company: { | |||
| name: payload.values.nameOfFirm.toString(), | |||
| PIB: payload.values.PIB.toString(), | |||
| @@ -1,14 +1,9 @@ | |||
| import i18next from 'i18next'; | |||
| import i18next from "i18next"; | |||
| export const rejectErrorCodeHelper = (error) => { | |||
| if (error?.response?.data?.Errors) { | |||
| const errorCode = error?.response?.data?.Errors[0]?.Code; | |||
| const errorMessage = errorCode | |||
| ? i18next.t(`apiErrors.${errorCode}`) | |||
| : i18next.t('apiErrors.SomethingWentWrong'); | |||
| export const rejectErrorCodeHelper = (errorCode) => { | |||
| const errorMessage = errorCode | |||
| ? i18next.t(`apiErrors.${errorCode}`) | |||
| : i18next.t("apiErrors.SomethingWentWrong"); | |||
| return errorMessage; | |||
| } | |||
| return i18next.t('apiErrors.SomethingWentWrong'); | |||
| return errorMessage; | |||
| }; | |||
| @@ -0,0 +1,8 @@ | |||
| import * as Yup from "yup"; | |||
| import i18n from "../i18n"; | |||
| export default Yup.object().shape({ | |||
| email: Yup.string() | |||
| .required(i18n.t("forgotPassword.emailRequired")) | |||
| .email(i18n.t("forgotPassword.emailFormat")), | |||
| }); | |||
| @@ -0,0 +1,9 @@ | |||
| import * as Yup from "yup"; | |||
| import i18n from "../i18n"; | |||
| export default Yup.object().shape({ | |||
| email: Yup.string().email(i18n.t("login.emailFormat")).required(i18n.t("login.mailRequired")), | |||
| password: Yup.string() | |||
| .required(i18n.t("login.passwordRequired")) | |||
| .min(8, i18n.t("login.passwordLength")), | |||
| }); | |||
| @@ -0,0 +1,10 @@ | |||
| import * as Yup from "yup"; | |||
| import i18n from "../../i18n"; | |||
| export default Yup.object().shape({ | |||
| mail: Yup.string() | |||
| .email(i18n.t("forgotPassword.emailFormat")) | |||
| .required(i18n.t("login.usernameRequired")), | |||
| password: Yup.string() | |||
| .required(i18n.t("login.passwordRequired")) | |||
| .min(8, i18n.t("login.passwordLength")), | |||
| }); | |||
| @@ -0,0 +1,10 @@ | |||
| import * as Yup from "yup"; | |||
| import i18n from "../../i18n"; | |||
| export default Yup.object().shape({ | |||
| nameOfFirm: Yup.string().required(i18n.t("login.usernameRequired")), | |||
| PIB: Yup.number() | |||
| .required(i18n.t("login.passwordRequired")) | |||
| .min(100000000, i18n.t("register.PIBnoOfCharacters")) | |||
| .max(999999999, i18n.t("register.PIBnoOfCharacters")), | |||
| }); | |||
| @@ -0,0 +1,10 @@ | |||
| import * as Yup from "yup"; | |||
| import i18n from "../../i18n"; | |||
| export default Yup.object().shape({ | |||
| phoneNumber: Yup.number().required(i18n.t("login.usernameRequired")), | |||
| location: Yup.string().required(i18n.t("login.passwordRequired")), | |||
| website: Yup.string().matches( | |||
| /^((ftp|http|https):\/\/)?(www.)?(?!.*(ftp|http|https|www.))[a-zA-Z0-9_-]+(\.[a-zA-Z]+)+((\/)[\w#]+)*(\/\w+\?[a-zA-Z0-9_]+=\w+(&[a-zA-Z0-9_]+=\w+)*)?$/gm | |||
| ), | |||
| }); | |||
| @@ -0,0 +1,6 @@ | |||
| import * as Yup from "yup"; | |||
| export default Yup.object().shape({ | |||
| password: Yup.string().required().min(8), | |||
| passwordConfirm: Yup.string().oneOf([Yup.ref("password"), null]), | |||
| }); | |||