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

View File

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

View File

@ -4,7 +4,13 @@ import classes from "../assets/styles/components/myInput.module.scss"
const MyInput = (props) => { const MyInput = (props) => {
return ( return (
<div className={classes.main} style={{...props.otherMainStyle}}> <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> </div>
) )
} }

View File

@ -6,12 +6,12 @@ const NavBar = ({navigate, auth, setAuth}) => {
<div className={classes.main}> <div className={classes.main}>
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div className={classes.menu}> <div className={classes.menu}>
{auth ?
<div className={classes.menu__authorized}> <div className={classes.menu__authorized}>
<span onClick={() => navigate("/")}>Главная</span> <span onClick={() => navigate("/")}>Главная</span>
<span onClick={() => navigate("/forms")}>Мои формы</span> {auth ?
</div> : <span onClick={() => navigate("/forms")}>Мои формы</span> :
<div></div>} <span></span>}
</div>
</div> </div>
<div className={classes.profile}> <div className={classes.profile}>
{auth ? {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 MyInput from "../components/MyInput.jsx";
import MyButton from "../components/MyButton.jsx"; import MyButton from "../components/MyButton.jsx";
import { UserData } from "../context"; import { UserData } from "../context";
import { logIn, completeRegistration } from "../hooks/api/enterAccountApi.js";
const EnterAccount = () => { const EnterAccount = () => {
const [enter, setEnter] = useState("login"); const [enter, setEnter] = useState("login");
@ -20,31 +21,38 @@ const EnterAccount = () => {
const {user, setUser} = useContext(UserData); const {user, setUser} = useContext(UserData);
function cleanState() { function cleanState() {
setEmail(); setEmail("");
setPhone(); setPhone("");
setName(); setName("");
setSurname(); setSurname("");
setPatronymic(); setPatronymic("");
setPassword(); setPassword("");
setRepiedPassword(); setRepiedPassword("");
} };
function selectTag(tag) {
setEnter(tag);
cleanState();
};
function createUser() { function createUser() {
if (password === repiedPassword) { completeRegistration({
setUser({
email: email, email: email,
phone: phone, phone: phone,
name: name, name: name,
surname: surname, surname: surname,
patronymic: patronymic, patronymic: patronymic,
password: password password: password,
}) repiedPassword: repiedPassword
}).then((resolve, reject) => {
if (resolve.status === 201) {
setUser(resolve.data)
cleanState(); cleanState();
navigate("/"); navigate("/");
} }
else { }).catch((error) => {
console.log('Error') console.log(error)
} })
}; };
return ( return (
@ -52,10 +60,10 @@ const EnterAccount = () => {
<div className={classes.wrapper}> <div className={classes.wrapper}>
<div className={classes.tabs}> <div className={classes.tabs}>
<ul class="nav nav-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> <a className={enter === "login" ? "nav-link active" : "nav-link"} aria-current="page">Авторизация</a>
</li> </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> <a className={enter === "register" ? "nav-link active" : "nav-link"}>Регитрация</a>
</li> </li>
</ul> </ul>
@ -67,11 +75,15 @@ const EnterAccount = () => {
<h3>Войти в аккаунт</h3> <h3>Войти в аккаунт</h3>
</div> </div>
<div className={classes.content__wrapper__login__body}> <div className={classes.content__wrapper__login__body}>
<MyInput placeholder={"Email"} 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%"}}/> <MyInput type={"password"} placeholder={"Пароль"} otherMainStyle={{width: "100%", height: "20%"}} otherInputStyle={{width: "100%"}} value={password} change={setPassword}/>
</div> </div>
<div className={classes.content__wrapper__login__footer}> <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> : </div> :
<div className={classes.content__wrapper__register}> <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 { useNavigate } from 'react-router-dom';
import classes from "../assets/styles/forms.module.scss" import classes from "../assets/styles/forms.module.scss"
import MyButton from "../components/MyButton.jsx"; import MyButton from "../components/MyButton.jsx";
import MyInput from "../components/MyInput.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 Forms = () => {
const navigate = useNavigate() const navigate = useNavigate();
const {forms, setForms} = useContext(FormsData); const {forms, setForms} = useContext(FormsData);
const {user, setUser} = useContext(UserData);
const [stateLoading, setStateLoading] = useState(false); const [stateLoading, setStateLoading] = useState(false);
const response = ms => { const response = ms => {
return new Promise(r => setTimeout(() => r('response end'), ms)) return new Promise(r => setTimeout(() => r('response end'), ms))
} };
function createForm() { function createForm() {
setStateLoading(true); setStateLoading(true);
// Эмуляция пост запроса
response(1000) response(1000)
.then((r) => { .then((r) => {
console.log(r); console.log(r);
@ -24,16 +25,27 @@ const Forms = () => {
navigate("/forms/edit"); navigate("/forms/edit");
} }
) )
} };
function editForm(item) { function editForm(item) {
navigate("/forms/edit", { navigate("/forms/edit", {
state: { state: {
id: item.id, 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 ( return (
<div className={classes.main}> <div className={classes.main}>
@ -49,19 +61,20 @@ const Forms = () => {
<div className={classes.listForms}> <div className={classes.listForms}>
<div className={classes.listForms__columns}> <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 className={classes.listForms__columns__item}>Изменения</div> */}
</div> </div>
<div className={classes.listForms__forms}> <div className={classes.listForms__forms}>
{forms.map((item, i) => {forms.map((item, i) =>
<div className={classes.listForms__forms__item} key={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__title} onClick={() => editForm(item)}>{item.title}</div>
<div className={classes.listForms__forms__item__answers}>{item.datetime}</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__update}>{item.update}</div> */}
<i class="fa-solid fa-ellipsis-vertical" id="action" data-bs-toggle="dropdown"></i> <i class="fa-solid fa-ellipsis-vertical" id="action" data-bs-toggle="dropdown"></i>
<ul class="dropdown-menu" aria-labelledby="action"> <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={() => 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={() => navigator.clipboard.writeText(`http://localhost:3000/forms/${item.id}/`)}>Скопировать ссылку</a></li>
<li><a class="dropdown-item" onClick={() => removeForm(item.id)}>Удалить</a></li>
</ul> </ul>
</div> </div>
)} )}

View File

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

View File

@ -4,6 +4,7 @@ import classes from "../assets/styles/viewForm.module.scss";
import { FormsData, TypeAnswerData } from "../context"; import { FormsData, TypeAnswerData } from "../context";
import GeneratingFormFields from "../components/GeneratingFormFields.jsx"; import GeneratingFormFields from "../components/GeneratingFormFields.jsx";
import MyButton from "../components/MyButton.jsx"; import MyButton from "../components/MyButton.jsx";
import { saveAnswersApi } from "../hooks/api/formApi.js";
const ViewForm = () => { const ViewForm = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@ -16,9 +17,9 @@ const ViewForm = () => {
}; };
const [answers, setAnswers] = useState( const [answers, setAnswers] = useState(
newForm() ? newForm().listAnswer.map(item => ( newForm() ? newForm().questions.map(item => (
{id: item.id, answer: []} {id: item.id, answer: []}
)) : "" )) : []
); );
function updateAnswersForm(value, id) { 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 ( return (
<div className={classes.main}> <div className={classes.main}>
{newForm() ? {newForm() ?
@ -46,12 +57,15 @@ const ViewForm = () => {
</div> </div>
</div> </div>
<div className={classes.form__content}> <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> </div>
<div className={classes.footer}> <div className={classes.footer}>
<MyButton text={"Отправить"}/> <MyButton text={"Отправить"} click={saveAnswers}/>
<MyButton text={"Отмена"} backgroundColor={"rgb(180, 180, 180)"} click={() => {}}/> <MyButton text={"Отмена"} backgroundColor={"rgb(180, 180, 180)"} click={() => {
setAnswers([]);
navigate("/");
}}/>
</div> </div>
</div> : </div> :
<div className={classes.wrapper}> <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/>, element: <Forms/>,
}, },
{ {
path: '/forms/edit/', path: "/forms/edit",
element: <NewForm/> element: <NewForm/>
}, },
{ {
path: '/enter', path: "/enter",
element: <EnterAccount/> element: <EnterAccount/>
}, },
{ {
path: '/profile', path: "/profile",
element: <Profile/> element: <Profile/>
}, },
{ {
path: '/forms/:formId', path: "/forms/:formId",
element: <ViewForm/> element: <ViewForm/>
} }
] ]