Работает редактирование постов

This commit is contained in:
Никита Потапов 2024-01-09 12:00:24 +04:00
parent 47e7bb64f1
commit 0b6d5a93fe
10 changed files with 283 additions and 115 deletions

View File

@ -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": [

78
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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 (
<>
<div className="post mb-2 mb-sm-4 w-100 d-flex flex-column border rounded-2" id={'post-' + post.id}>
@ -34,11 +59,18 @@ const Post = ({ post, handlePostsChange }) => {
</div>
</div>
</Link>
{
currentUser.id == post.userId ? <a onClick={onPostDelete} title='Удалить пост'>
<Trash className='fs-6' fill='red' />
</a> : ''
}
<div className='d-flex'>
{
currentUser.id == post.userId ? <a className='me-3' onClick={onPostEdit} title='Редактировать пост'>
<Pencil className='fs-6' fill='yellow' />
</a> : ''
}
{
currentUser.id == post.userId ? <a onClick={onPostDelete} title='Удалить пост'>
<Trash className='fs-6' fill='red' />
</a> : ''
}
</div>
</div>
<div className="post-body">
@ -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;

View File

@ -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 (
<>
<textarea placeholder="Что нового?" id="post-editor" className="border rounded-2 mb-2 p-2"></textarea>
<input id='post-input-id' className='d-none' type='number' />
<TextareaAutosize placeholder="Что нового?" id="post-editor" onChange={onTextChange} defaultValue={postText} className="border rounded-2 mb-2 p-2" />
<input id='post-input-id' className='d-none' readOnly value={postId ?? ''} type='number' />
<div id="title-image-block">
<input id="check-title-image" type="checkbox" style={{ display: "none" }} />
<input id="check-title-image" type="checkbox" readOnly value={imageCheck} style={{ display: "none" }} />
<input onChange={onImageChange} id="input-title-image" className='border' name="titleImage" accept="image/*" type="file" />
<label id="title-image-preview" htmlFor="input-title-image" title="Добавить изображение" className="border rounded-2 mb-2">
{
@ -68,23 +32,24 @@ const PostInputForm = ({ handlePostsChange }) => {
</label>
</div>
<button onClick={onPublish} className="btn btn-primary mb-2" id="post-publication-button">
Опубликовать
</button>
<div className="d-none d-flex mb-2 w-100" id="edit-block">
<button className="btn btn-danger me-2 w-100" id="edit-post-button-cancel">
Отмена
{
postId != null ? <div className="d-flex mb-2 w-100" id="edit-block">
<button onClick={onFormCanceled} className="btn btn-danger me-2 w-100" id="edit-post-button-cancel">
Отмена
</button>
<button onClick={onFormSubmit} className="btn btn-success w-100" id="edit-post-button-accept">
Применить
</button>
</div > : <button onClick={onFormSubmit} className="btn btn-primary mb-2" id="post-publication-button">
Опубликовать
</button>
<button className="btn btn-success w-100" id="edit-post-button-accept">
Применить
</button>
</div >
}
</>
);
};
PostInputForm.propTypes = {
handlePostsChange: PropTypes.func
postInputFormState: PropTypes.object
};
export default PostInputForm;

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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(
<React.StrictMode>
<Toaster />
<RouterProvider router={router} />
</React.StrictMode>,
)

View File

@ -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 (
<>
<Wrapper>
<Center>
<PostInputForm handlePostsChange={handlePostsChange} />
<PostInputForm postInputFormState={postInputFormState} />
<div className="posts-wrapper mt-3">
{
posts.length == 0 ? 'Нет постов' :
posts.map((post, index) => <Post post={post} key={index} handlePostsChange={handlePostsChange} />)
posts.map((post, index) => <Post post={post} key={index} handlePostsChange={handlePostsChange} postInputFormState={postInputFormState} />)
}
</div>
</Center>

View File

@ -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 = () => {
<Center>
<UserProfileBlock user={user} />
{
user.id == currentUser.id ? <PostInputForm handlePostsChange={handlePostsChange} /> : ''
user.id == currentUser.id ? <PostInputForm handlePostsChange={handlePostsChange} postInputFormState={postInputFormState} /> : ''
}
<div className="posts-wrapper mt-3">
{
userPosts.map((post, index) => <Post post={post} key={index} handlePostsChange={handlePostsChange} />)
userPosts.map((post, index) => <Post post={post} key={index} handlePostsChange={handlePostsChange} postInputFormState={postInputFormState} />)
}
</div>
</Center>