added api, validation and protected routing

This commit is contained in:
kuwsh1n 2024-03-18 19:04:41 +03:00
parent 7b1dab7002
commit 6817f5bf9b
13 changed files with 309 additions and 91 deletions

View File

@ -1,6 +1,7 @@
import React, { useState } from "react";
import React, { useState, useEffect, useLayoutEffect } from "react";
import { Outlet, useNavigate } from "react-router-dom";
import { FormsData, UserData, TypeAnswerData } from "./context";
import { globalRender } from "./router/protectedRouting.js";
import classes from "./assets/styles/app.module.scss"
import NavBar from "./components/NavBar.jsx";
import 'bootstrap/dist/css/bootstrap.min.css';
@ -28,6 +29,8 @@ const App = () => {
{id: 8, text: 'Дата', typeTag: InputDate}
]);
// useEffect(() => globalRender(window.location.pathname, user, navigate));
return (
<UserData.Provider value={{ user, setUser }}>
<FormsData.Provider value={{ forms, setForms }}>

View File

@ -26,8 +26,9 @@
width: 100%;
&__columns {
display: flex;
justify-content: space-around;
justify-content: start;
align-items: center;
padding: 0 2%;
height: 15%;
width: 100%;
border-bottom: 1px solid rgb(220, 220, 220);
@ -48,8 +49,9 @@
}
&__item {
display: flex;
justify-content: space-around;
justify-content: start;
align-items: center;
padding: 0 2%;
height: 25%;
width: 100%;
font-family: "Montserrat", sans-serif;
@ -58,7 +60,7 @@
background-color: rgba(240, 240, 240, 0.8);
}
&__title {
width: 33.3%;
// width: 33.3%;
text-align: center;
cursor: pointer;
&:hover {
@ -80,6 +82,11 @@
top: calc(50% - 7px);
cursor: pointer;
}
ul {
li {
cursor: pointer;
}
}
}
}
}

View File

@ -4,7 +4,13 @@ import classes from "../assets/styles/components/myInput.module.scss"
const MyInput = (props) => {
return (
<div className={classes.main} style={{...props.otherMainStyle}}>
<input type={props.type} placeholder={props.placeholder} style={{...props.otherInputStyle}} onChange={(e) => props.change(e.target.value)} value={props.value}/>
<input
type={props.type}
placeholder={props.placeholder}
style={{...props.otherInputStyle}}
onChange={(e) => props.change(e.target.value)}
value={props.value}
/>
</div>
)
}

View File

@ -6,12 +6,12 @@ const NavBar = ({navigate, auth, setAuth}) => {
<div className={classes.main}>
<div className={classes.wrapper}>
<div className={classes.menu}>
{auth ?
<div className={classes.menu__authorized}>
<span onClick={() => navigate("/")}>Главная</span>
<span onClick={() => navigate("/forms")}>Мои формы</span>
</div> :
<div></div>}
{auth ?
<span onClick={() => navigate("/forms")}>Мои формы</span> :
<span></span>}
</div>
</div>
<div className={classes.profile}>
{auth ?

View File

@ -0,0 +1,43 @@
import { totalRegisterValidate } from "../validation/enterAccountValidate.js";
async function logIn(email, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({
email: "senya.bogachev@mail.ru",
phone: "89110128244",
name: "Арсений",
surname: "Богачев",
patronymic: "Валерьевич"
})
}
else {
reject("Error")
}
}, 1000)
}).catch((error) => {
console.log(error)
})
};
async function completeRegistration(data) {
const validate = totalRegisterValidate(data)
return new Promise((resolve, reject) => {
setTimeout(() => {
if (validate.status) {
console.log("Отправляем данные на бэк ->", data)
resolve({
status: 201,
data: data
})
}
else {
reject(validate.message)
}
}, 1000)
})
};
export { logIn, completeRegistration };

65
src/hooks/api/formApi.js Normal file
View File

@ -0,0 +1,65 @@
function saveAnswersApi(id, answers) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({
id: id,
answers: answers
})
}
else {
reject("Error")
}
}, 1000)
})
}
function removeFormApi(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({
id: id
})
}
else {
reject("Error")
}
}, 200)
})
}
function updateFormByFormsApi(id, name, questions) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({
id: id,
name: name,
questions: questions
})
}
else {
reject("Error")
}
}, 1000)
})
}
function saveFormApi(name, questions) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (true) {
resolve({
name: name,
questions: questions
})
}
else {
reject("Error")
}
}, 1000)
})
}
export { saveAnswersApi, saveFormApi, updateFormByFormsApi, removeFormApi }

View File

@ -0,0 +1,31 @@
const constructorAnswerValidate = (state, messageReject = "Ошибка", messageResolve = undefined) => {
return state ?
{
status: true,
message: messageResolve
} :
{
status: false,
message: messageReject
}
}
const totalRegisterValidate = (data) => {
const listValidation = [
constructorAnswerValidate(data.name.length, "Обязательное поле."),
constructorAnswerValidate(data.surname.length, "Обязательное поле."),
constructorAnswerValidate(data.email.length, "Обязательное поле."),
constructorAnswerValidate(data.phone.length, "Обязательное поле."),
constructorAnswerValidate(data.password === data.repiedPassword, "Введенные пароли не совпадают."),
constructorAnswerValidate(data.password < 8, "Пароль должен иметь более 8 символов.")
]
for (let value of listValidation) {
if (!value.status) {
return value
}
}
return { status: true }
}
export { totalRegisterValidate, constructorAnswerValidate }

View File

@ -4,6 +4,7 @@ import classes from "../assets/styles/enterAccount.module.scss";
import MyInput from "../components/MyInput.jsx";
import MyButton from "../components/MyButton.jsx";
import { UserData } from "../context";
import { logIn, completeRegistration } from "../hooks/api/enterAccountApi.js";
const EnterAccount = () => {
const [enter, setEnter] = useState("login");
@ -20,31 +21,38 @@ const EnterAccount = () => {
const {user, setUser} = useContext(UserData);
function cleanState() {
setEmail();
setPhone();
setName();
setSurname();
setPatronymic();
setPassword();
setRepiedPassword();
}
setEmail("");
setPhone("");
setName("");
setSurname("");
setPatronymic("");
setPassword("");
setRepiedPassword("");
};
function selectTag(tag) {
setEnter(tag);
cleanState();
};
function createUser() {
if (password === repiedPassword) {
setUser({
email: email,
phone: phone,
name: name,
surname: surname,
patronymic: patronymic,
password: password
})
cleanState();
navigate("/");
}
else {
console.log('Error')
}
completeRegistration({
email: email,
phone: phone,
name: name,
surname: surname,
patronymic: patronymic,
password: password,
repiedPassword: repiedPassword
}).then((resolve, reject) => {
if (resolve.status === 201) {
setUser(resolve.data)
cleanState();
navigate("/");
}
}).catch((error) => {
console.log(error)
})
};
return (
@ -52,10 +60,10 @@ const EnterAccount = () => {
<div className={classes.wrapper}>
<div className={classes.tabs}>
<ul class="nav nav-tabs">
<li class="nav-item" onClick={() => setEnter("login")}>
<li class="nav-item" onClick={() => selectTag("login")}>
<a className={enter === "login" ? "nav-link active" : "nav-link"} aria-current="page">Авторизация</a>
</li>
<li class="nav-item" onClick={() => setEnter("register")}>
<li class="nav-item" onClick={() => selectTag("register")}>
<a className={enter === "register" ? "nav-link active" : "nav-link"}>Регитрация</a>
</li>
</ul>
@ -67,11 +75,15 @@ const EnterAccount = () => {
<h3>Войти в аккаунт</h3>
</div>
<div className={classes.content__wrapper__login__body}>
<MyInput placeholder={"Email"} otherMainStyle={{width: "100%", height: "20%"}} otherInputStyle={{width: "100%"}}/>
<MyInput type={"password"} placeholder={"Пароль"} otherMainStyle={{width: "100%", height: "20%"}} otherInputStyle={{width: "100%"}}/>
<MyInput placeholder={"Email"} otherMainStyle={{width: "100%", height: "20%"}} otherInputStyle={{width: "100%"}} value={email} change={setEmail}/>
<MyInput type={"password"} placeholder={"Пароль"} otherMainStyle={{width: "100%", height: "20%"}} otherInputStyle={{width: "100%"}} value={password} change={setPassword}/>
</div>
<div className={classes.content__wrapper__login__footer}>
<MyButton text={"Войти"} otherStyle={{height: "50%", width: "20%"}}/>
<MyButton
text={"Войти"}
otherStyle={{height: "50%", width: "20%"}}
click={() => logIn(email, password).then((resolve, reject) => setUser(resolve))}
/>
</div>
</div> :
<div className={classes.content__wrapper__register}>

View File

@ -1,22 +1,23 @@
import React, { useState, useContext } from "react";
import React, { useState, useContext, useEffect } from "react";
import { useNavigate } from 'react-router-dom';
import classes from "../assets/styles/forms.module.scss"
import MyButton from "../components/MyButton.jsx";
import MyInput from "../components/MyInput.jsx";
import { FormsData } from "../context";
import { FormsData, UserData } from "../context";
import { removeFormApi } from "../hooks/api/formApi.js";
const Forms = () => {
const navigate = useNavigate()
const navigate = useNavigate();
const {forms, setForms} = useContext(FormsData);
const {user, setUser} = useContext(UserData);
const [stateLoading, setStateLoading] = useState(false);
const response = ms => {
return new Promise(r => setTimeout(() => r('response end'), ms))
}
};
function createForm() {
setStateLoading(true);
// Эмуляция пост запроса
response(1000)
.then((r) => {
console.log(r);
@ -24,16 +25,27 @@ const Forms = () => {
navigate("/forms/edit");
}
)
}
};
function editForm(item) {
navigate("/forms/edit", {
state: {
id: item.id,
data: item.listAnswer
data: item.questions
}
});
}
};
function removeForm(id) {
removeFormApi(id)
.then((resolve, _) => {
console.log(resolve);
setForms([...forms.filter(item => {
item.id !== id
})]);
})
.catch(error => console.log(error));
};
return (
<div className={classes.main}>
@ -49,19 +61,20 @@ const Forms = () => {
<div className={classes.listForms}>
<div className={classes.listForms__columns}>
<div className={classes.listForms__columns__item}>Название</div>
<div className={classes.listForms__columns__item}>Ответы</div>
<div className={classes.listForms__columns__item}>Изменения</div>
{/* <div className={classes.listForms__columns__item}>Ответы</div>
<div className={classes.listForms__columns__item}>Изменения</div> */}
</div>
<div className={classes.listForms__forms}>
{forms.map((item, i) =>
<div className={classes.listForms__forms__item} key={i}>
<div className={classes.listForms__forms__item__title} onClick={() => editForm(item)}>{item.title}</div>
<div className={classes.listForms__forms__item__answers}>{item.datetime}</div>
<div className={classes.listForms__forms__item__update}>{item.update}</div>
{/* <div className={classes.listForms__forms__item__answers}>{item.datetime}</div>
<div className={classes.listForms__forms__item__update}>{item.update}</div> */}
<i class="fa-solid fa-ellipsis-vertical" id="action" data-bs-toggle="dropdown"></i>
<ul class="dropdown-menu" aria-labelledby="action">
<li><a class="dropdown-item" onClick={() => navigate(`/forms/${item.id}/`)}>Открыть</a></li>
<li><a class="dropdown-item" onClick={() => navigator.clipboard.writeText(`http://localhost:3000/forms/${item.id}/`)}>Скопировать ссылку</a></li>
<li><a class="dropdown-item" onClick={() => removeForm(item.id)}>Удалить</a></li>
</ul>
</div>
)}

View File

@ -5,6 +5,7 @@ import MyButton from "../components/MyButton.jsx";
import AnswerModal from "../components/AnswerModal.jsx";
import PreviewModal from "../components/PreviewModal.jsx"
import { FormsData, TypeAnswerData } from "../context";
import { saveFormApi, updateFormByFormsApi } from "../hooks/api/formApi.js";
const NewForm = () => {
const navigate = useNavigate();
@ -29,11 +30,11 @@ const NewForm = () => {
const [newForm, setNewForm] = useState(location.state ? location.state.data : []);
const [stateModal, setStateModal] = useState(false)
const [stateModal, setStateModal] = useState(false);
function removeAnswerByForm(id) {
setNewForm([...newForm.filter(item => item.id !== id)]);
}
};
function cleanStates() {
setStateModal(false)
@ -45,7 +46,7 @@ const NewForm = () => {
setCurrentTypeAnswer("");
setCurrentOptionAnswer("")
setMandatory(false);
}
};
function addOptionAnswer(text) {
setOptionAnswer([...optionAnswer, {
@ -53,7 +54,7 @@ const NewForm = () => {
text: currentOptionAnswer
}]);
setCurrentOptionAnswer("");
}
};
function editAnswerByForm(id) {
const obj = newForm.find(item => item.id === id);
@ -65,7 +66,7 @@ const NewForm = () => {
setOptionAnswer(obj.optionAnswer);
setMandatory(obj.mandatory);
setStateModal(id);
}
};
function updateAnswerByForm() {
setNewForm(newForm.map(item => {
@ -81,7 +82,7 @@ const NewForm = () => {
return item
}))
cleanStates()
}
};
function saveStates() {
setNewForm([...newForm, {
@ -95,38 +96,43 @@ const NewForm = () => {
file: file
}]);
cleanStates();
}
};
function updateFormByForms() {
setForms(
forms.map(item => {
if (item.id === location.state.id) {
item.title = 'Новая форма',
item.datetime = 'Без изменений',
item.update = '01/01/24',
item.listAnswer = newForm
}
return item
updateFormByFormsApi(location.state.id, "Новая форма", newForm)
.then((resolve, _) => {
console.log(resolve);
setForms(
forms.map(item => {
if (item.id === location.state.id) {
item.title = "Новая форма",
item.questions = newForm
}
return item
})
);
cleanStates();
navigate("/forms");
})
)
cleanStates();
navigate("/forms");
}
};
function saveForm() {
setForms(
[...forms, {
id: nextID(forms),
title: 'Новая форма',
datetime: 'Без изменений',
update: '01/01/24',
listAnswer: newForm,
answers: []
}]
);
cleanStates();
navigate("/forms");
}
saveFormApi("Новая форма", newForm)
.then((resolve, reject) => {
console.log(resolve);
setForms(
[...forms, {
id: nextID(forms),
title: "Новая форма",
questions: newForm,
answers: []
}]
);
cleanStates();
navigate("/forms");
})
.catch(error => console.log(error));
};
return (
<div className={classes.main}>

View File

@ -4,6 +4,7 @@ import classes from "../assets/styles/viewForm.module.scss";
import { FormsData, TypeAnswerData } from "../context";
import GeneratingFormFields from "../components/GeneratingFormFields.jsx";
import MyButton from "../components/MyButton.jsx";
import { saveAnswersApi } from "../hooks/api/formApi.js";
const ViewForm = () => {
const navigate = useNavigate();
@ -16,9 +17,9 @@ const ViewForm = () => {
};
const [answers, setAnswers] = useState(
newForm() ? newForm().listAnswer.map(item => (
newForm() ? newForm().questions.map(item => (
{id: item.id, answer: []}
)) : ""
)) : []
);
function updateAnswersForm(value, id) {
@ -32,6 +33,16 @@ const ViewForm = () => {
)
};
function saveAnswers() {
saveAnswersApi(formId, answers)
.then((resolve, _) => {
console.log(resolve)
setAnswers([]);
navigate("/");
})
.catch((error) => console.log(error));
}
return (
<div className={classes.main}>
{newForm() ?
@ -46,12 +57,15 @@ const ViewForm = () => {
</div>
</div>
<div className={classes.form__content}>
<GeneratingFormFields newForm={newForm().listAnswer} listTypeAnswer={listTypeAnswer} answers={answers} updateAnswersForm={updateAnswersForm}/>
<GeneratingFormFields newForm={newForm().questions} listTypeAnswer={listTypeAnswer} answers={answers} updateAnswersForm={updateAnswersForm}/>
</div>
</div>
<div className={classes.footer}>
<MyButton text={"Отправить"}/>
<MyButton text={"Отмена"} backgroundColor={"rgb(180, 180, 180)"} click={() => {}}/>
<MyButton text={"Отправить"} click={saveAnswers}/>
<MyButton text={"Отмена"} backgroundColor={"rgb(180, 180, 180)"} click={() => {
setAnswers([]);
navigate("/");
}}/>
</div>
</div> :
<div className={classes.wrapper}>

View File

@ -0,0 +1,18 @@
const protectedUrl = {
notAuthorized: [
"/forms",
"/forms/edit",
"/profile"
],
notRights: [
]
}
function globalRender(url, user, navigate) {
if (!user && protectedUrl.notAuthorized.some(item => item === url)) {
navigate("/enter")
}
}
export { globalRender }

View File

@ -21,19 +21,19 @@ const router = createBrowserRouter([
element: <Forms/>,
},
{
path: '/forms/edit/',
path: "/forms/edit",
element: <NewForm/>
},
{
path: '/enter',
path: "/enter",
element: <EnterAccount/>
},
{
path: '/profile',
path: "/profile",
element: <Profile/>
},
{
path: '/forms/:formId',
path: "/forms/:formId",
element: <ViewForm/>
}
]