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) =>
)
}