diff --git a/package.json b/package.json index 2d9140b..e91a7f4 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "webpack-dev-server": "^5.0.2" }, "dependencies": { + "@ckeditor/ckeditor5-build-classic": "^41.4.2", + "@ckeditor/ckeditor5-react": "^7.0.0", "@fortawesome/fontawesome-svg-core": "^6.5.1", "@fortawesome/free-solid-svg-icons": "^6.5.1", "@fortawesome/react-fontawesome": "^0.2.0", diff --git a/src/assets/styles/articles.module.scss b/src/assets/styles/articles.module.scss new file mode 100644 index 0000000..ff9f769 --- /dev/null +++ b/src/assets/styles/articles.module.scss @@ -0,0 +1,94 @@ +.main { + width: 100%; + height: 100%; + padding: 4% 8%; +} + +.wrapper { + width: 100%; + height: 100%; +} + +.panel { + width: 100%; + height: 10%; + display: flex; + justify-content: space-between; + align-items: end; + padding: 0 5%; +} + +.listArticles { + box-shadow: 0 0 5px 1px rgb(200, 200, 200); + border-radius: 5px; + margin-top: 5%; + height: 70%; + width: 100%; + &__columns { + display: flex; + justify-content: start; + align-items: center; + padding: 0 2%; + height: 15%; + width: 100%; + border-bottom: 1px solid rgb(220, 220, 220); + &__item { + width: 33.3%; + font-size: 15px; + // text-align: center; + font-family: "Montserrat", sans-serif; + } + } + &__forms { + width: 100%; + height: 85%; + overflow-y: auto; + &::-webkit-scrollbar { + width: 7px; + } + &::-webkit-scrollbar-thumb { + background-color: rgb(200, 200, 200); + } + &__item { + display: flex; + justify-content: start; + align-items: center; + padding: 0 2%; + height: 25%; + width: 100%; + font-family: "Montserrat", sans-serif; + position: relative; + &:hover { + background-color: rgba(240, 240, 240, 0.8); + } + &__title { + width: 33.3%; + // text-align: center; + cursor: pointer; + &:hover { + text-decoration: underline; + } + } + &__date { + width: 33.3%; + // text-align: center; + } + &__author { + width: 33.3%; + // text-align: center; + } + i { + position: absolute; + font-size: 15px; + right: 30px; + top: calc(50% - 7px); + cursor: pointer; + } + ul { + li { + cursor: pointer; + } + } + } + } +} \ No newline at end of file diff --git a/src/assets/styles/components/myButton.module.scss b/src/assets/styles/components/myButton.module.scss index 3985573..1f72d0e 100644 --- a/src/assets/styles/components/myButton.module.scss +++ b/src/assets/styles/components/myButton.module.scss @@ -24,4 +24,14 @@ border: 1px solid rgba(0, 0, 0, 0.3); } } + &__transparent { + background-color: rgba(0, 0, 0, 0); + border: 1px solid rgba(0, 0, 0, 0); + transition: 0.3s; + &:hover { + background-color: rgba(180, 180, 180, 0.5); + color: white; + transition: 0.3s; + } + } } \ No newline at end of file diff --git a/src/assets/styles/newArticle.module.scss b/src/assets/styles/newArticle.module.scss new file mode 100644 index 0000000..f725434 --- /dev/null +++ b/src/assets/styles/newArticle.module.scss @@ -0,0 +1,121 @@ +.main { + width: 100%; + height: 100%; + padding: 4% 8%; +} + +.wrapper { + width: 100%; + height: 100%; +} + +.header { + display: flex; + justify-content: space-between; + width: 100%; + height: 8%; + &__listInput { + width: 40%; + height: 100%; + display: flex; + justify-content: space-between; + align-items: center; + &__date { + width: 100%; + height: 100%; + display: flex; + justify-content: space-between; + position: relative; + span { + position: absolute; + font-size: 8px; + font-family: "Montserrat", sans-serif; + top: -40%; + left: 2%; + } + } + &__title { + width: 100%; + height: 100%; + display: flex; + justify-content: space-between; + position: relative; + span { + position: absolute; + font-size: 8px; + font-family: "Montserrat", sans-serif; + top: -40%; + left: 2%; + } + } + } + &__listBtn { + display: flex; + justify-content: space-between; + + width: 30%; + } +} + +.tags { + width: 100%; + margin: 30px 0; + &__wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: start; + &__input { + position: relative; + &__title { + top: -40%; + left: 2px; + position: absolute; + font-size: 8px; + font-family: "Montserrat", sans-serif; + color: rgb(100, 100, 100); + } + } + &__list { + width: 100%; + // height: 100%; + flex-wrap: wrap; + display: flex; + justify-content: start; + align-items: center; + margin-top: 5px; + &__item { + padding: 0 5px; + border-radius: 5px; + border: 1px solid rgb(100, 100, 100); + margin: 1px 3px; + position: relative; + span { + font-size: 13px; + font-family: "Montserrat", sans-serif; + color: rgb(100, 100, 100); + } + &:hover span { + visibility: hidden; + } + &:hover i { + visibility: visible; + } + i { + visibility: hidden; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: rgb(239, 73, 73); + cursor: pointer; + } + } + } + } +} + +.content { + +} \ No newline at end of file diff --git a/src/assets/styles/viewArticle.module.scss b/src/assets/styles/viewArticle.module.scss new file mode 100644 index 0000000..1fdc006 --- /dev/null +++ b/src/assets/styles/viewArticle.module.scss @@ -0,0 +1,159 @@ +.main { + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; +} + +.wrapper { + width: 90%; + height: 90%; +} + +.header { + width: 100%; + height: 10%; + &__wrapper { + width: 100%; + height: 100%; + border: 1px solid rgb(180, 180, 180); + border-bottom: none; + border-radius: 5px 5px 0 0; + padding: 0 5px; + &__article { + width: 100%; + height: 100%; + display: flex; + justify-content: space-between; + align-items: center; + &__title { + width: 20%; + height: 75%; + display: flex; + border-right: 1px solid rgb(180, 180, 180); + justify-content: center; + align-items: center; + position: relative; + &__name { + position: absolute; + top: -20%; + left: 3px; + font-size: 8px; + font-family: "Montserrat", sans-serif; + } + &__text { + font-size: 15px; + font-family: "Montserrat", sans-serif; + } + } + &__tags { + width: 60%; + height: 75%; + display: flex; + justify-content: start; + align-items: center; + flex-wrap: wrap; + padding: 0 5px; + position: relative; + overflow-y: auto; + // &::-webkit-scrollbar { + // width: 10px; + // } + // &::-webkit-scrollbar-thumb { + // background-color: rgb(200, 200, 200); + // } + // &::-webkit-scrollbar-button:single-button { + // background-color: #bbbbbb; + // display: block; + // border-style: solid; + // height: 10px; + // width: 16px; + // } + &__item { + padding: 0 5px; + border-radius: 5px; + border: 1px solid rgb(100, 100, 100); + margin: 1px 3px; + span { + + } + } + } + &__owner { + width: 20%; + height: 75%; + border-left: 1px solid rgb(180, 180, 180); + display: flex; + justify-content: center; + align-items: center; + position: relative; + &__name { + position: absolute; + top: -20%; + right: 3px; + font-size: 8px; + font-family: "Montserrat", sans-serif; + } + &__text { + font-size: 15px; + font-family: "Montserrat", sans-serif; + } + } + } + &__tags { + width: 100%; + height: 50%; + display: flex; + justify-content: start; + align-items: center; + margin-top: 20px; + &__item { + padding: 0 5px; + border-radius: 5px; + border: 1px solid rgb(100, 100, 100); + margin: 1px 3px; + span { + + } + } + } + } +} + +.content { + width: 100%; + height: 90%; + &__wrapper { + width: 100%; + height: 100%; + border: 1px solid rgb(180, 180, 180); + padding: 5px; + // border-radius: 0 0 5px 5px; + overflow: auto; + &::-webkit-scrollbar { + width: 7px; + } + &::-webkit-scrollbar { + height: 7px; + } + &::-webkit-scrollbar-thumb { + background-color: rgb(200, 200, 200); + } + &__article { + max-width: 100%; + height: 100%; + } + } +} + +// .image { +// width: 400px; +// height: 200px; +// border: 4px solid red +// img { +// width: 400px; +// height: 200px; +// aspect-ratio: 1000/700; +// } +// } \ No newline at end of file diff --git a/src/components/MyButton.jsx b/src/components/MyButton.jsx index b9b60c2..bab86a0 100644 --- a/src/components/MyButton.jsx +++ b/src/components/MyButton.jsx @@ -4,7 +4,7 @@ import classes from "../assets/styles/components/myButton.module.scss" const MyButton = (props) => { return ( -
+
+ ) +} + +export default Articles; \ No newline at end of file diff --git a/src/pages/NewArticle.jsx b/src/pages/NewArticle.jsx new file mode 100644 index 0000000..44cf081 --- /dev/null +++ b/src/pages/NewArticle.jsx @@ -0,0 +1,143 @@ +import React, { useState, useContext, useEffect } from "react"; +import { useNavigate, useLocation, useParams } from 'react-router-dom'; +import { useCookies } from "react-cookie"; +import classes from "../assets/styles/newArticle.module.scss"; +import MyButton from "../components/MyButton.jsx"; +import Loading from "../components/Loading.jsx"; +import MyInput from "../components/MyInput.jsx"; +import TextEditor from "../components/TextEditor.jsx"; +import { getArticleApi, editTagsApi, editTitleArticleApi, editArticleApi, addArticleApi } from "../hooks/api/articleApi.js"; + +const NewArticle = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { articleId } = useParams(); + const [loading, setLoading] = useState(false); + + const [title, setTitle] = useState(""); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState("") + const [ownerId, setOwnerId] = useState(""); + + const [contentArticle, setContentArticle] = useState(''); + const [blocks, setBlocks] = useState(''); + + const [cookies, _, __] = useCookies(["user"]); + + useEffect(() => { + async function getArticle() { + const response = await getArticleApi(cookies.token, articleId) + + if (response.status === 200) { + console.log(response) + setTitle(response.data.article.title) + setTags(response.data.article.tags ? response.data.article.tags : []) + setOwnerId(response.data.article.owner_id) + setContentArticle(response.data.blocks ? response.data.blocks[0].data : '') + setBlocks(response.data.blocks ? response.data.blocks : false) + } + } + + getArticle() + }, []) + + async function addTag() { + if (newTag.length > 0 && !tags.find(item => item === newTag)) { + const response = await editTagsApi(cookies.token, articleId, [newTag], false) + + if (response.status === 200) { + setTags([...tags, newTag]) + setNewTag("") + } + } + } + + async function removeTag(item, index) { + const response = await editTagsApi(cookies.token, articleId, [item], true) + + if (response.status === 200) { + const cTags = [...tags] + cTags.splice(index, 1) + setTags(cTags) + } + } + + async function saveArticle() { + const responseTitle = await editTitleArticleApi(cookies.token, articleId, title) + console.log(blocks) + const responseContentArticle = blocks ? + await editArticleApi(cookies.token, articleId, contentArticle) : + await addArticleApi(cookies.token, articleId, contentArticle) + + console.log(responseContentArticle) + + if (responseTitle.status === 200 && responseContentArticle.status === 200) { + navigate("/articles") + } + } + + return ( +
+
+
+
+
+ Дата создания + +
+
+ Название статьи + +
+
+
+ {/* */} + saveArticle()}/> +
+
+
+
+
+ Добавить тэг + + } + class={"main__transparent"} + mainStyle={ + { + position: "absolute", + right: "0", + top: "0", + height: "100%", + } + } + otherStyle={{borderRadius: "0 5px 5px 0", padding: "3px 7px 0 7px"}} + click={() => addTag()} + /> +
+
+ {tags ? tags.map((item, i) => +
+ #{item} + removeTag(item, i)}> +
+ ) : <>} +
+
+
+
+ +
+
+
+ ) +} + +export default NewArticle; \ No newline at end of file diff --git a/src/pages/ViewArticle.jsx b/src/pages/ViewArticle.jsx new file mode 100644 index 0000000..1494db0 --- /dev/null +++ b/src/pages/ViewArticle.jsx @@ -0,0 +1,78 @@ +import React, { useState, useContext, useEffect, useLocation } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { useCookies } from "react-cookie"; +import classes from "../assets/styles/viewArticle.module.scss"; +import { FormsData, TypeAnswerData, answersData, UserData } from "../context"; +import MyButton from "../components/MyButton.jsx"; +import { getArticleApi } from "../hooks/api/articleApi.js"; +import { getListUserApi } from "../hooks/api/profileApi.js"; + + +const ViewArticle = () => { + const navigate = useNavigate(); + // const location = useLocation(); + const { articleId } = useParams(); + + const [title, setTitle] = useState(""); + const [tags, setTags] = useState([]); + const [newTag, setNewTag] = useState("") + const [owner, setOwner] = useState(""); + + const [contentArticle, setContentArticle] = useState(''); + + const [cookies, _, __] = useCookies(["user"]); + + useEffect(() => { + async function getArticle() { + const response = await getArticleApi(cookies.token, articleId) + const user = await getListUserApi(cookies.token) + + if (response.status === 200) { + console.log(response) + setTitle(response.data.article.title) + setTags(response.data.article.tags ? response.data.article.tags : []) + setOwner(user.status === 200 ? user.data.find(item => item.id === response.data.article.owner_id).login : response.data.article.owner_id) + setContentArticle(response.data.blocks ? response.data.blocks[0].data : '') + } + } + + getArticle() + }, []) + + return ( +
+
+
+
+
+
+ Название + {title} +
+
+ {/* Тэги */} + {tags.map(item =>
+ #{item} +
)} +
+
+ Автор + {owner} +
+
+
+
+
+
+
+ +
+
+
+
+
+ ) +} + + +export default ViewArticle \ No newline at end of file diff --git a/src/router/router.js b/src/router/router.js index cbc8b09..2c2c3ff 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -10,6 +10,9 @@ import ViewForm from "../pages/ViewForm.jsx"; import AdminPanel from '../pages/AdminPanel.jsx'; import AnswersForm from '../pages/AnswersForm.jsx'; import TokensForm from '../pages/TokensForm.jsx'; +import Articles from '../pages/Articles.jsx'; +import NewArticle from '../pages/NewArticle.jsx'; +import ViewArticle from '../pages/ViewArticle.jsx'; const router = createBrowserRouter([ { @@ -50,6 +53,18 @@ const router = createBrowserRouter([ { path: "/tokens/:formId", element: + }, + { + path: "/articles", + element: + }, + { + path: "/articles/:articleId/edit", + element: + }, + { + path: "/articles/:articleId/", + element: } ] }