From 9cbfd88a23e9f4d5af38875bd8da9e3e64ed94a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D1=82=D0=B0=D0=BB=D0=B8=D0=B9=20=D0=9B=D0=B0?= =?UTF-8?q?=D0=B2=D1=88=D0=BE=D0=BD=D0=BE=D0=BA?= <114582703+valavshonok@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:46:32 +0300 Subject: [PATCH] group posts --- src/components/input/DateRangeInput.tsx | 4 +- src/redux/slices/groupfeed.ts | 13 +- src/views/articleeditor/Editor.tsx | 31 ++-- src/views/articleeditor/MarckDownPreview.tsx | 7 +- src/views/home/group/posts/ModalCreate.tsx | 72 ++++++++++ src/views/home/group/posts/ModalUpdate.tsx | 140 +++++++++++++++++++ src/views/home/group/posts/PostItem.tsx | 86 ++++++++++++ src/views/home/group/posts/Posts.tsx | 69 +++++---- src/views/home/missions/ModalCreate.tsx | 2 +- 9 files changed, 364 insertions(+), 60 deletions(-) create mode 100644 src/views/home/group/posts/ModalCreate.tsx create mode 100644 src/views/home/group/posts/ModalUpdate.tsx diff --git a/src/components/input/DateRangeInput.tsx b/src/components/input/DateRangeInput.tsx index 1a1c732..be5b2d5 100644 --- a/src/components/input/DateRangeInput.tsx +++ b/src/components/input/DateRangeInput.tsx @@ -27,7 +27,7 @@ const DateRangeInput: React.FC = ({ type="datetime-local" value={startValue} onChange={(e) => onChange('startsAt', e.target.value)} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + className="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
@@ -38,7 +38,7 @@ const DateRangeInput: React.FC = ({ type="datetime-local" value={endValue} onChange={(e) => onChange('endsAt', e.target.value)} - className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + className="mt-1 block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
diff --git a/src/redux/slices/groupfeed.ts b/src/redux/slices/groupfeed.ts index 011267b..05cfdbb 100644 --- a/src/redux/slices/groupfeed.ts +++ b/src/redux/slices/groupfeed.ts @@ -215,7 +215,17 @@ export const deletePost = createAsyncThunk( const postsSlice = createSlice({ name: 'posts', initialState, - reducers: {}, + reducers: { + setGroupFeedStatus: ( + state, + action: PayloadAction<{ key: keyof PostsState; status: Status }>, + ) => { + const { key, status } = action.payload; + if (state[key]) { + (state[key] as any).status = status; + } + }, + }, extraReducers: (builder) => { // fetchGroupPosts builder.addCase(fetchGroupPosts.pending, (state) => { @@ -333,4 +343,5 @@ const postsSlice = createSlice({ }, }); +export const { setGroupFeedStatus } = postsSlice.actions; export const groupFeedReducer = postsSlice.reducer; diff --git a/src/views/articleeditor/Editor.tsx b/src/views/articleeditor/Editor.tsx index 38e4e43..8fd18ad 100644 --- a/src/views/articleeditor/Editor.tsx +++ b/src/views/articleeditor/Editor.tsx @@ -4,18 +4,7 @@ import 'highlight.js/styles/github-dark.css'; import MarkdownPreview from './MarckDownPreview'; -interface MarkdownEditorProps { - defaultValue?: string; - onChange: (value: string) => void; -} - -const MarkdownEditor: FC = ({ - defaultValue, - onChange, -}) => { - const [markdown, setMarkdown] = useState( - defaultValue || - `# 🌙 Добро пожаловать в Markdown-редактор +export const MarkDownPattern = `# 🌙 Добро пожаловать в Markdown-редактор Добро пожаловать в **Markdown-редактор**! Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇 @@ -209,13 +198,29 @@ print(greet("Мир")) **🖤 Конец демонстрации. Спасибо, что используешь Markdown-редактор!** -`, +`; + +interface MarkdownEditorProps { + defaultValue?: string; + onChange: (value: string) => void; +} + +const MarkdownEditor: FC = ({ + defaultValue, + onChange, +}) => { + const [markdown, setMarkdown] = useState( + defaultValue || MarkDownPattern, ); useEffect(() => { onChange(markdown); }, [markdown]); + useEffect(() => { + setMarkdown(defaultValue || MarkDownPattern); + }, [defaultValue]); + // Обработчик вставки const handlePaste = async ( e: React.ClipboardEvent, diff --git a/src/views/articleeditor/MarckDownPreview.tsx b/src/views/articleeditor/MarckDownPreview.tsx index 36f5e38..3cfa04a 100644 --- a/src/views/articleeditor/MarckDownPreview.tsx +++ b/src/views/articleeditor/MarckDownPreview.tsx @@ -30,12 +30,7 @@ const MarkdownPreview: FC = ({ className = '', }) => { return ( -
+
void; +} + +const ModalCreate: FC = ({ active, setActive, groupId }) => { + // const [name, setName] = useState(''); + const [content, setContent] = useState(''); + const status = useAppSelector((state) => state.groupfeed.createPost.status); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (status == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'createPost', status: 'idle' })); + } + }, [status]); + + return ( + +
+
Создать пост
+ +
+ { + setContent(v); + }} + /> +
+
+ { + dispatch( + createPost({ name: '', content, groupId }), + ); + }} + text={status == 'idle' ? 'Опубликовать' : 'Загрузка...'} + disabled={status == 'loading'} + /> + { + setActive(false); + }} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalCreate; diff --git a/src/views/home/group/posts/ModalUpdate.tsx b/src/views/home/group/posts/ModalUpdate.tsx new file mode 100644 index 0000000..55ada02 --- /dev/null +++ b/src/views/home/group/posts/ModalUpdate.tsx @@ -0,0 +1,140 @@ +import { FC, useEffect, useState } from 'react'; +import { Modal } from '../../../../components/modal/Modal'; +import { PrimaryButton } from '../../../../components/button/PrimaryButton'; +import { SecondaryButton } from '../../../../components/button/SecondaryButton'; +import { Input } from '../../../../components/input/Input'; +import { useAppDispatch, useAppSelector } from '../../../../redux/hooks'; +import { createGroup } from '../../../../redux/slices/groups'; +import MarkdownEditor, { MarkDownPattern } from '../../../articleeditor/Editor'; +import { + createPost, + deletePost, + fetchPostById, + setGroupFeedStatus, + updatePost, +} from '../../../../redux/slices/groupfeed'; +import { ReverseButton } from '../../../../components/button/ReverseButton'; +import { cn } from '../../../../lib/cn'; + +interface ModalUpdateProps { + groupId: number; + postId: number; + active: boolean; + setActive: (value: boolean) => void; +} + +const ModalUpdate: FC = ({ + active, + setActive, + groupId, + postId, +}) => { + // const [name, setName] = useState(''); + const [content, setContent] = useState(''); + const status = useAppSelector((state) => state.groupfeed.updatePost.status); + const statusDelete = useAppSelector( + (state) => state.groupfeed.deletePost.status, + ); + const { post, status: statusPost } = useAppSelector( + (state) => state.groupfeed.fetchPostById, + ); + const dispatch = useAppDispatch(); + + useEffect(() => { + if (status == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'updatePost', status: 'idle' })); + } + }, [status]); + + useEffect(() => { + if (statusDelete == 'successful') { + setActive(false); + dispatch(setGroupFeedStatus({ key: 'deletePost', status: 'idle' })); + } + }, [statusDelete]); + + useEffect(() => { + dispatch(fetchPostById({ groupId, postId })); + }, [postId]); + + return ( + +
+
+ Обновить пост #{post?.id} +
+
+
Загрузка...
+
+
+ { + setContent(v); + }} + /> +
+
+ { + dispatch( + updatePost({ + name: '', + content, + groupId, + postId, + }), + ); + }} + text={status == 'idle' ? 'Сохранить' : 'Загрузка...'} + disabled={ + status == 'loading' || statusPost != 'successful' + } + /> + { + dispatch(deletePost({ groupId, postId })); + }} + color="error" + text={ + statusDelete == 'idle' ? 'Удалить' : 'Загрузка...' + } + disabled={ + statusDelete == 'loading' || + statusPost != 'successful' + } + /> + { + setActive(false); + }} + text="Отмена" + /> +
+
+
+ ); +}; + +export default ModalUpdate; diff --git a/src/views/home/group/posts/PostItem.tsx b/src/views/home/group/posts/PostItem.tsx index e69de29..98e34d9 100644 --- a/src/views/home/group/posts/PostItem.tsx +++ b/src/views/home/group/posts/PostItem.tsx @@ -0,0 +1,86 @@ +import { FC } from 'react'; +import { useAppSelector } from '../../../../redux/hooks'; +import MarkdownPreview from '../../../articleeditor/MarckDownPreview'; +import { Edit } from '../../../../assets/icons/input'; + +function convertDate(isoString: string) { + const date = new Date(isoString); + + const dd = String(date.getUTCDate()).padStart(2, '0'); + const mm = String(date.getUTCMonth() + 1).padStart(2, '0'); + const yyyy = date.getUTCFullYear(); + + const hh = String(date.getUTCHours()).padStart(2, '0'); + const min = String(date.getUTCMinutes()).padStart(2, '0'); + + return `${dd}.${mm}.${yyyy} ${hh}:${min}`; +} + +interface PostItemProps { + id: number; + groupId: number; + authorId: number; + authorUsername: string; + name: string; + content: string; + createdAt: string; + updatedAt: string; + isAdmin: boolean; + setModalUpdateActive: (v: boolean) => void; + setUpdatePostId: (v: number) => void; +} + +export const PostItem: FC = ({ + id, + groupId, + authorId, + authorUsername, + name, + content, + createdAt, + updatedAt, + isAdmin, + setModalUpdateActive, + setUpdatePostId, +}) => { + const members = useAppSelector( + (state) => state.groups.fetchGroupById.group?.members, + ); + const member = members?.find((m) => m.userId === authorId); + + return ( +
+
+
+
+
{authorUsername}
+
+ {member ? member.role : 'роль не найдена'} +
+
+
+
+ {convertDate(createdAt)} +
+
+ + {isAdmin && ( +
{ + setUpdatePostId(id); + setModalUpdateActive(true); + }} + > + +
+ )} +
+ +
+ +
+
+ ); +}; diff --git a/src/views/home/group/posts/Posts.tsx b/src/views/home/group/posts/Posts.tsx index 3043190..e5cc012 100644 --- a/src/views/home/group/posts/Posts.tsx +++ b/src/views/home/group/posts/Posts.tsx @@ -6,6 +6,9 @@ import { SearchInput } from '../../../../components/input/SearchInput'; import { setMenuActiveGroupPage } from '../../../../redux/slices/store'; import { fetchGroupById } from '../../../../redux/slices/groups'; import { SecondaryButton } from '../../../../components/button/SecondaryButton'; +import ModalCreate from './ModalCreate'; +import { PostItem } from './PostItem'; +import ModalUpdate from './ModalUpdate'; interface PostsProps { groupId: number; @@ -14,15 +17,16 @@ interface PostsProps { export const Posts: FC = ({ groupId }) => { const dispatch = useAppDispatch(); + const [modalCreateActive, setModalCreateActive] = useState(false); + const [modalUpdateActive, setModalUpdateActive] = useState(false); + const [updatePostId, setUpdatePostId] = useState(0); const [isAdmin, setIsAdmin] = useState(false); const { pages, status } = useAppSelector( (state) => state.groupfeed.fetchPosts, ); const { id: userId } = useAppSelector((state) => state.auth); - const { group, status: statusGroup } = useAppSelector( - (state) => state.groups.fetchGroupById, - ); + const { group } = useAppSelector((state) => state.groups.fetchGroupById); // Загружаем только первую страницу useEffect(() => { @@ -58,12 +62,12 @@ export const Posts: FC = ({ groupId }) => { placeholder="Поиск сообщений" /> {isAdmin && ( -
+
{ - // setModalActive(true); + setModalCreateActive(true); }} - text="Добавить задачу" + text="Создать пост" />
)} @@ -75,42 +79,33 @@ export const Posts: FC = ({ groupId }) => { {status == 'successful' && page0?.items && page0.items.length > 0 ? ( -
- {page0.items.map((post) => ( -
-
- ID: {post.id} -
-
- Название: {post.name} -
-
- Содержимое: {post.content} -
-
- Автор: {post.authorUsername} -
-
- Автор ID: {post.authorId} -
-
- Group ID: {post.groupId} -
-
- Создан: {post.createdAt} -
-
- Обновлён: {post.updatedAt} -
-
+
+ {page0.items.map((post, i) => ( + ))}
) : status === 'successful' ? (
Постов пока нет
) : null} + + + +
); }; diff --git a/src/views/home/missions/ModalCreate.tsx b/src/views/home/missions/ModalCreate.tsx index e93fce6..c36fa3a 100644 --- a/src/views/home/missions/ModalCreate.tsx +++ b/src/views/home/missions/ModalCreate.tsx @@ -97,7 +97,7 @@ const ModalCreate: FC = ({ active, setActive }) => {
-