From 0b6d5a93fe16d26ce7e00f57f768a1ba4ff40ca9 Mon Sep 17 00:00:00 2001 From: "ns.potapov" Date: Tue, 9 Jan 2024 12:00:24 +0400 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D1=81=D1=82=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data.json | 21 ++-- package-lock.json | 78 +++++++++++++ package.json | 6 +- src/components/post/post.jsx | 49 ++++++-- .../postinputform/postinputform.jsx | 95 +++++---------- src/hooks/PostHooks.js | 22 ---- src/hooks/PostInputFormHook.js | 110 ++++++++++++++++++ src/main.jsx | 2 + src/pages/FeedPage.jsx | 8 +- src/pages/UserProfilePage.jsx | 7 +- 10 files changed, 283 insertions(+), 115 deletions(-) create mode 100644 src/hooks/PostInputFormHook.js diff --git a/data.json b/data.json index 38ad4b4..f01a4af 100644 --- a/data.json +++ b/data.json @@ -38,13 +38,6 @@ } ], "posts": [ - { - "id": 1, - "userId": 1, - "pubDate": "Jan 08 2024 16:03:17 GMT+0400 (GMT+04:00)", - "image": null, - "html": "Привет, друзья! 👫\nСегодня хочу поделиться с вами одним интересным событием, которое произошло со мной на днях. 🚴‍♂️\nЯ недавно записался на участие в велопробеге, который будет проходить через месяц в нашем городе. 🌐\nЭто будет не просто гонка, а целое приключение, так как маршрут проложен через самые живописные места нашей области. 🌲🌳🏞️\nНадеюсь, что смогу преодолеть все дистанции и получить массу положительных эмоций! 😁💪\nА вы когда-нибудь участвовали в подобных мероприятиях? Расскажите в комментариях! 💬" - }, { "id": 2, "userId": 2, @@ -52,13 +45,6 @@ "image": "https://bye-bye-calories.ru/wp-content/uploads/4/2/3/4234c2bc5dd8f7087701a819ea20464b.jpeg", "html": "Сегодня я хочу поделиться с вами своим любимым рецептом. Я готовлю это блюдо уже много лет и всегда получаю комплименты от гостей. Вот ингредиенты, которые вам понадобятся:\n– 400 г куриного филе\n– 200 г шампиньонов\n– 1 луковица\n– 3 зубчика чеснока\n– 100 мл сливок\n– соль, перец по вкусу\n– растительное масло для жарки\nНарежьте куриное филе небольшими кусочками и обжарьте на растительном масле до золотистого цвета. Затем добавьте нарезанный лук и чеснок, обжаривайте еще пару минут. Добавьте нарезанные шампиньоны и жарьте до тех пор, пока вся жидкость не испарится. Влейте сливки, посолите и поперчите по вкусу, перемешайте и тушите на медленном огне еще 5-7 минут. Ваше блюдо готово! Подавайте с гарниром на ваш выбор. Приятного аппетита!" }, - { - "id": 3, - "userId": 1, - "pubDate": "Jan 08 2024 11:03:17 GMT+0400 (GMT+04:00)", - "image": "https://primelens.ru/800/600/https/pixnio.com/free-images/2017/08/12/2017-08-12-18-17-42.jpg", - "html": "Привет, друзья!\nСегодня я хочу рассказать вам о моем увлечении, которое стало для меня настоящим хобби. \nЭто фотография. \nЯ начал заниматься этим искусством около года назад и с тех пор не могу остановиться. \nМне нравится находить красивые места для съемки, экспериментировать с ракурсами и светом. \nНадеюсь, мои работы вдохновят и вас! \nДелитесь своими фотографиями в комментариях, буду рад посмотреть." - }, { "id": 8, "userId": 3, @@ -100,6 +86,13 @@ "pubDate": "Jun 08 2023 18:03:17 GMT+0400 (GMT+04:00)", "image": "https://blog.smarthealthshop.com/wp-content/uploads/2019/01/5-Ways-Healthy-Cooking-Classes-Can-Help-With-Your-Diet.jpg", "html": "Недавно я увлекся новым хобби - кулинарией. И это было просто потрясающе! Я научился готовить множество новых блюд, от простых закусок до сложных десертов. А самое главное - я смог порадовать свою семью и друзей своими кулинарными шедеврами. Если вы тоже хотите освоить это искусство, не бойтесь экспериментировать и пробовать новые рецепты. Ведь кулинария - это не только вкусно, но и очень увлекательно!" + }, + { + "userId": 1, + "html": "Привет, друзья! 👫\nСегодня хочу поделиться с вами одним интересным событием, которое произошло со мной на днях. 🚴‍♂️\nЯ недавно записался на участие в велопробеге, который будет проходить через месяц в нашем городе. 🌐\nЭто будет не просто гонка, а целое приключение, так как маршрут проложен через самые живописные места нашей области. 🌲🌳🏞️\nНадеюсь, что смогу преодолеть все дистанции и получить массу положительных эмоций! 😁💪\nА вы когда-нибудь участвовали в подобных мероприятиях? Расскажите в комментариях! 💬", + "image": null, + "pubDate": "2024-01-09T07:46:30.695Z", + "id": 10 } ], "subscribes": [ diff --git a/package-lock.json b/package-lock.json index 1b9c1cd..bfe10fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,9 @@ "react-bootstrap": "^2.9.2", "react-bootstrap-icons": "^1.10.3", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-router-dom": "^6.19.0", + "react-textarea-autosize": "^8.5.3", "universal-cookie": "^6.1.1" }, "devDependencies": { @@ -3758,6 +3760,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.13.tgz", + "integrity": "sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -6219,6 +6229,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6268,6 +6293,22 @@ "react-dom": ">=16.8" } }, + "node_modules/react-textarea-autosize": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -7359,6 +7400,43 @@ "node": ">=0.10.0" } }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index 01a5fd3..4fc9513 100644 --- a/package.json +++ b/package.json @@ -14,15 +14,17 @@ "prod": "npm-run-all build --parallel serve" }, "dependencies": { + "axios": "^1.6.1", "bootstrap": "^5.3.2", "prop-types": "^15.8.1", "react": "^18.2.0", "react-bootstrap": "^2.9.2", "react-bootstrap-icons": "^1.10.3", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "react-router-dom": "^6.19.0", - "universal-cookie": "^6.1.1", - "axios": "^1.6.1" + "react-textarea-autosize": "^8.5.3", + "universal-cookie": "^6.1.1" }, "devDependencies": { "@types/react": "^18.2.15", diff --git a/src/components/post/post.jsx b/src/components/post/post.jsx index bc9d30d..61ab58e 100644 --- a/src/components/post/post.jsx +++ b/src/components/post/post.jsx @@ -1,22 +1,47 @@ import { PropTypes } from 'prop-types'; import { Link } from 'react-router-dom'; -import { Trash } from 'react-bootstrap-icons'; +import { Trash, Pencil } from 'react-bootstrap-icons'; import './post.css'; import { getUserAvatarImg, getUserLink } from '../../utils/user'; import { useCurrentUser } from '../../hooks/UserHooks'; import PostsApiService from '../../services/PostsApiService'; +import toast from 'react-hot-toast'; const options = { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }; -const Post = ({ post, handlePostsChange }) => { +const Post = ({ post, handlePostsChange, postInputFormState }) => { const currentUser = useCurrentUser(); + if (!postInputFormState) return; + + const { postImage, + setPostImage, + onImageChange, + postText, + setPostText, + onTextChange, + postId, + setPostId, + imageCheck, + onFormSubmit, + postInputForm, + handlePostInputFormChange, + setImage } = postInputFormState; + const onPostDelete = async () => { await PostsApiService.delete(post.id); + toast.success("Пост удален"); handlePostsChange(); }; + const onPostEdit = async () => { + setPostId(post.id); + setPostText(post.html); + setImage(post.image, false); + window.scrollTo(0, 0); + }; + return ( <>
@@ -34,11 +59,18 @@ const Post = ({ post, handlePostsChange }) => {
- { - currentUser.id == post.userId ? - - : '' - } +
+ { + currentUser.id == post.userId ? + + : '' + } + { + currentUser.id == post.userId ? + + : '' + } +
@@ -72,7 +104,8 @@ const Post = ({ post, handlePostsChange }) => { Post.propTypes = { post: PropTypes.object, - handlePostsChange: PropTypes.func + handlePostsChange: PropTypes.func, + postInputFormState: PropTypes.object }; export default Post; \ No newline at end of file diff --git a/src/components/postinputform/postinputform.jsx b/src/components/postinputform/postinputform.jsx index 338b751..3974c8a 100644 --- a/src/components/postinputform/postinputform.jsx +++ b/src/components/postinputform/postinputform.jsx @@ -1,65 +1,29 @@ import './postinputform.css'; import { PropTypes } from 'prop-types'; import { Camera } from 'react-bootstrap-icons'; -import { useCurrentUser } from '../../hooks/UserHooks'; -import { useState } from 'react'; -import { getBase64FromFile } from '../../utils/base64'; -import PostsApiService from '../../services/PostsApiService'; +import TextareaAutosize from 'react-textarea-autosize'; -const PostInputForm = ({ handlePostsChange }) => { - const user = useCurrentUser(); - - const [postImage, setPostImage] = useState(null); - - const onImageChange = async (event) => { - if (event.target.files.length != 0) { - setPostImage(await getBase64FromFile(event.target.files[0])); - } - else { - setPostImage(null); - } - document.getElementById('check-title-image').checked = true; - }; - - const onPublish = async () => { - const postInputText = document.getElementById('post-editor'); - const postInputImage = document.getElementById('selected-title-image'); - const postImageChanged = document.getElementById('check-title-image').checked; - const postId = document.getElementById('post-input-id').value; - - const postImage = postInputImage ? postInputImage.src : null; - - let post = { - userId: user.id, - pubDate: new Date(), - html: postInputText.value - }; - - if (postImageChanged) { - post = { - ...post, - image: postImage - } - } - - if (postId) { - await PostsApiService.update(postId, post); - } - else { - await PostsApiService.create(post); - } - - postInputText.value = ''; - setPostImage(null); - handlePostsChange(); - }; +const PostInputForm = ({ postInputFormState }) => { + const { postImage, + setPostImage, + onImageChange, + postText, + setPostText, + onTextChange, + postId, + setPostId, + imageCheck, + onFormSubmit, + postInputForm, + handlePostInputFormChange, + onFormCanceled } = postInputFormState; return ( <> - - + +
- +
- -
- + +
: - -
+ } ); }; PostInputForm.propTypes = { - handlePostsChange: PropTypes.func + postInputFormState: PropTypes.object }; export default PostInputForm; \ No newline at end of file diff --git a/src/hooks/PostHooks.js b/src/hooks/PostHooks.js index fc17684..b8d5662 100644 --- a/src/hooks/PostHooks.js +++ b/src/hooks/PostHooks.js @@ -67,25 +67,3 @@ export const usePosts = () => { handlePostsChange: handlePostsChange, }; }; - -export const usePostInputForm = (postId) => { - const [postsRefresh, setPostsRefresh] = useState(false); - const [posts, setPosts] = useState([]); - const handlePostsChange = () => setPostsRefresh(!postsRefresh); - - const getPosts = async () => { - let expand = "?_expand=user"; - const data = await PostsApiService.getAll(expand); - setPosts(data ?? []); - }; - - useEffect(() => { - getPosts(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [postsRefresh, postId]); - - return { - posts: posts, - handlePostsChange: handlePostsChange, - }; -}; diff --git a/src/hooks/PostInputFormHook.js b/src/hooks/PostInputFormHook.js new file mode 100644 index 0000000..6ed3aee --- /dev/null +++ b/src/hooks/PostInputFormHook.js @@ -0,0 +1,110 @@ +import { useState } from "react"; +import { getBase64FromFile } from "../utils/base64"; +import PostsApiService from "../services/PostsApiService"; +import toast from "react-hot-toast"; + +export const usePostInputForm = (user, handlePostsChange) => { + const [postInputFormRefresh, setPostInputFormRefresh] = useState(false); + const refreshForm = () => setPostInputFormRefresh(!postInputFormRefresh); + const [postImage, setPostImage] = useState(null); + const [postText, setPostText] = useState(""); + const [imageCheck, setImageCheck] = useState(false); + const [postId, setPostId] = useState(null); + + const postInputForm = { + textInput: document.getElementById("post-editor"), + imageInput: document.getElementById("input-title-image"), + selectedImage: document.getElementById("selected-title-image") + ? document.getElementById("selected-title-image").src + : null, + imageCheck: document.getElementById("check-title-image"), + postIdInput: document.getElementById("post-input-id"), + }; + + const handlePostInputFormChange = () => + setPostInputFormRefresh(!postInputFormRefresh); + + const setImage = async (image, isBlob = true) => { + if (image) { + if (!isBlob) { + setPostImage(image); + } else { + setPostImage(await getBase64FromFile(image)); + } + } else { + setPostImage(null); + } + setImageCheck(true); + }; + + const onImageChange = async (event) => { + await setImage( + event.target.files.length != 0 ? event.target.files[0] : null + ); + }; + + const onTextChange = (event) => { + setPostText(event.target.value); + }; + + const onFormCanceled = () => { + toast.error("Изменения сброшены"); + onFormClear(); + }; + + const onFormClear = () => { + setPostText(""); + setPostImage(null); + setPostId(null); + handlePostsChange(); + refreshForm(); + }; + + const onFormSubmit = async () => { + let post = { + userId: user.id, + html: postText, + }; + + if (imageCheck) { + post = { + ...post, + image: postImage, + }; + } + + if (postText.trim() == "" && postImage == null) { + toast.error("Заполните пост"); + } else { + if (postId) { + const postObj = await PostsApiService.get(postId); + post.pubDate = postObj.pubDate; + await PostsApiService.update(postId, post); + toast.success("Изменения сохранены"); + } else { + post.pubDate = new Date(); + await PostsApiService.create(post); + toast.success("Пост опубликован"); + } + + onFormClear(); + } + }; + + return { + postImage, + setPostImage, + onImageChange, + postText, + setPostText, + onTextChange, + postId, + setPostId, + imageCheck, + onFormSubmit, + postInputForm, + handlePostInputFormChange, + setImage, + onFormCanceled, + }; +}; diff --git a/src/main.jsx b/src/main.jsx index 26e7150..fb05d91 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -11,6 +11,7 @@ import App from './App.jsx' import './index.css' import { Person, LayoutTextSidebarReverse, ChatText, People } from 'react-bootstrap-icons'; import AuthPage from './pages/AuthPage.jsx' +import { Toaster } from 'react-hot-toast' const routes = [ { @@ -66,6 +67,7 @@ const router = createBrowserRouter([ ReactDOM.createRoot(document.getElementById('root')).render( + , ) diff --git a/src/pages/FeedPage.jsx b/src/pages/FeedPage.jsx index cbadf1f..76b9b16 100644 --- a/src/pages/FeedPage.jsx +++ b/src/pages/FeedPage.jsx @@ -6,24 +6,28 @@ import { useRequireAuthenticated } from "../hooks/AuthHooks"; import { usePosts } from "../hooks/PostHooks"; import { useUserSubscribes } from "../hooks/SubscribeHook"; import { useCurrentUser } from "../hooks/UserHooks"; +import { usePostInputForm } from "../hooks/PostInputFormHook"; const FeedPage = () => { useRequireAuthenticated(); let { posts, handlePostsChange } = usePosts(); const currentUser = useCurrentUser(); + const postInputFormState = usePostInputForm(currentUser, handlePostsChange); const { subs } = useUserSubscribes(currentUser.id); const subsIds = subs.map((sub) => sub.userId); posts = posts.filter((post) => subsIds.includes(post.userId) || post.userId == currentUser.id).sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); + if (postInputFormState === undefined) return; + return ( <>
- +
{ posts.length == 0 ? 'Нет постов' : - posts.map((post, index) => ) + posts.map((post, index) => ) }
diff --git a/src/pages/UserProfilePage.jsx b/src/pages/UserProfilePage.jsx index 30a4a0e..b7ac059 100644 --- a/src/pages/UserProfilePage.jsx +++ b/src/pages/UserProfilePage.jsx @@ -8,6 +8,7 @@ import { useCurrentUser } from "../hooks/UserHooks"; import { useUserPosts } from "../hooks/PostHooks"; import { useParams } from "react-router-dom"; import { useUserByUsername } from "../hooks/UserHooks"; +import { usePostInputForm } from "../hooks/PostInputFormHook"; const UserProfilePage = () => { useRequireAuthenticated(); @@ -19,6 +20,8 @@ const UserProfilePage = () => { let { userPosts, handlePostsChange } = useUserPosts(user.id); + const postInputFormState = usePostInputForm(currentUser, handlePostsChange); + userPosts = userPosts.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate)); if (params.username) { document.title = user.name + ' ' + user.surname; @@ -41,11 +44,11 @@ const UserProfilePage = () => {
{ - user.id == currentUser.id ? : '' + user.id == currentUser.id ? : '' }
{ - userPosts.map((post, index) => ) + userPosts.map((post, index) => ) }