group posts

This commit is contained in:
Виталий Лавшонок
2025-11-16 19:46:32 +03:00
parent b949837e13
commit 9cbfd88a23
9 changed files with 364 additions and 60 deletions

View File

@@ -0,0 +1,72 @@
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 from '../../../articleeditor/Editor';
import {
createPost,
setGroupFeedStatus,
} from '../../../../redux/slices/groupfeed';
interface ModalCreateProps {
groupId: number;
active: boolean;
setActive: (value: boolean) => void;
}
const ModalCreate: FC<ModalCreateProps> = ({ active, setActive, groupId }) => {
// const [name, setName] = useState<string>('');
const [content, setContent] = useState<string>('');
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 (
<Modal
className="bg-liquid-background h-[calc(100vh-30%)] border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white overflow-hidden"
onOpenChange={setActive}
open={active}
backdrop="blur"
>
<div className="max-w-[1400px] h-full overflow-hidden">
<div className="font-bold text-[30px]">Создать пост</div>
<div className="h-[calc(100%-45px-60px)]">
<MarkdownEditor
onChange={(v) => {
setContent(v);
}}
/>
</div>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton
onClick={() => {
dispatch(
createPost({ name: '', content, groupId }),
);
}}
text={status == 'idle' ? 'Опубликовать' : 'Загрузка...'}
disabled={status == 'loading'}
/>
<SecondaryButton
onClick={() => {
setActive(false);
}}
text="Отмена"
/>
</div>
</div>
</Modal>
);
};
export default ModalCreate;

View File

@@ -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<ModalUpdateProps> = ({
active,
setActive,
groupId,
postId,
}) => {
// const [name, setName] = useState<string>('');
const [content, setContent] = useState<string>('');
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 (
<Modal
className="bg-liquid-background h-[calc(100vh-30%)] border-liquid-lighter border-[2px] p-[25px] rounded-[20px] text-liquid-white overflow-hidden"
onOpenChange={setActive}
open={active}
backdrop="blur"
>
<div className="max-w-[1400px] h-full overflow-hidden transition-all duratoin-300">
<div className="font-bold text-[30px]">
Обновить пост #{post?.id}
</div>
<div
className={cn(
' absolute z-10 h-[calc(100%-100px)] w-[calc(100%-50px)] flex items-center justify-center text-transparent transition-all pointer-events-none ',
statusPost == 'loading' && 'text-liquid-white',
)}
>
<div>Загрузка...</div>
</div>
<div
className={cn(
'h-[calc(100%-45px-60px)] opacity-50 pointer-events-none transition-all ',
statusPost == 'successful' &&
'text-liquid-white pointer-events-auto opacity-100',
)}
>
<MarkdownEditor
defaultValue={
statusPost == 'successful'
? post?.content
: MarkDownPattern
}
onChange={(v) => {
setContent(v);
}}
/>
</div>
<div className="flex flex-row w-full items-center justify-end mt-[20px] gap-[20px]">
<PrimaryButton
onClick={() => {
dispatch(
updatePost({
name: '',
content,
groupId,
postId,
}),
);
}}
text={status == 'idle' ? 'Сохранить' : 'Загрузка...'}
disabled={
status == 'loading' || statusPost != 'successful'
}
/>
<ReverseButton
onClick={() => {
dispatch(deletePost({ groupId, postId }));
}}
color="error"
text={
statusDelete == 'idle' ? 'Удалить' : 'Загрузка...'
}
disabled={
statusDelete == 'loading' ||
statusPost != 'successful'
}
/>
<SecondaryButton
onClick={() => {
setActive(false);
}}
text="Отмена"
/>
</div>
</div>
</Modal>
);
};
export default ModalUpdate;

View File

@@ -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<PostItemProps> = ({
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 (
<div className="rounded-[10px] flex flex-col gap-[20px]">
<div className="h-[40px] w-full flex gap-[10px] relative">
<div className="h-[40px] w-[40px] bg-[#D9D9D9] rounded-[10px]"></div>
<div className=" leading-[20px] font-bold text-[16px] ">
<div>{authorUsername} </div>
<div className="text-liquid-light">
{member ? member.role : 'роль не найдена'}
</div>
</div>
<div className=" leading-[20px] font-bold text-[16px] ">
<div className="text-liquid-light">
{convertDate(createdAt)}
</div>
</div>
{isAdmin && (
<div
className=" h-[40px] w-[40px] absolute top-0 right-0 flex items-center justify-center cursor-pointer
rounded-[10px] hover:bg-liquid-lighter transition-all duration-300 active:scale-90"
onClick={() => {
setUpdatePostId(id);
setModalUpdateActive(true);
}}
>
<img src={Edit} />
</div>
)}
</div>
<div>
<MarkdownPreview className="bg-transparent" content={content} />
</div>
</div>
);
};

View File

@@ -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<PostsProps> = ({ groupId }) => {
const dispatch = useAppDispatch();
const [modalCreateActive, setModalCreateActive] = useState<boolean>(false);
const [modalUpdateActive, setModalUpdateActive] = useState<boolean>(false);
const [updatePostId, setUpdatePostId] = useState<number>(0);
const [isAdmin, setIsAdmin] = useState<boolean>(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<PostsProps> = ({ groupId }) => {
placeholder="Поиск сообщений"
/>
{isAdmin && (
<div className=" h-[40px] w-[200px] absolute top-0 right-0 flex items-center">
<div className=" h-[40px] w-[180px] absolute top-0 right-0 flex items-center">
<SecondaryButton
onClick={() => {
// setModalActive(true);
setModalCreateActive(true);
}}
text="Добавить задачу"
text="Создать пост"
/>
</div>
)}
@@ -75,42 +79,33 @@ export const Posts: FC<PostsProps> = ({ groupId }) => {
{status == 'successful' &&
page0?.items &&
page0.items.length > 0 ? (
<div className="space-y-4">
{page0.items.map((post) => (
<div
key={post.id}
className="border border-gray-700 rounded p-3"
>
<div>
<b>ID:</b> {post.id}
</div>
<div>
<b>Название:</b> {post.name}
</div>
<div>
<b>Содержимое:</b> {post.content}
</div>
<div>
<b>Автор:</b> {post.authorUsername}
</div>
<div>
<b>Автор ID:</b> {post.authorId}
</div>
<div>
<b>Group ID:</b> {post.groupId}
</div>
<div>
<b>Создан:</b> {post.createdAt}
</div>
<div>
<b>Обновлён:</b> {post.updatedAt}
</div>
</div>
<div className="flex flex-col gap-[20px]">
{page0.items.map((post, i) => (
<PostItem
{...post}
key={i}
isAdmin={isAdmin}
setModalUpdateActive={setModalUpdateActive}
setUpdatePostId={setUpdatePostId}
/>
))}
</div>
) : status === 'successful' ? (
<div>Постов пока нет</div>
) : null}
<ModalCreate
active={modalCreateActive}
setActive={setModalCreateActive}
groupId={groupId}
/>
<ModalUpdate
active={modalUpdateActive}
setActive={setModalUpdateActive}
groupId={groupId}
postId={updatePostId}
/>
</div>
);
};

View File

@@ -97,7 +97,7 @@ const ModalCreate: FC<ModalCreateProps> = ({ active, setActive }) => {
<div className="mt-4">
<label className="block mb-2">Файл задачи</label>
<label className="cursor-pointer inline-flex items-center justify-center px-4 py-2 bg-liquid-lighter hover:bg-liquid-dark transition-colors rounded-[10px] text-liquid-white font-medium shadow-md">
<label className="cursor-pointer inline-flex items-center justify-center px-4 py-2 bg-liquid-lighter hover:bg-liquid-dark transition-colors rounded-[10px] text-liquid-white font-medium">
{file ? file.name : 'Выбрать файл'}
<input
type="file"