group posts
This commit is contained in:
@@ -27,7 +27,7 @@ const DateRangeInput: React.FC<DateRangeInputProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -38,7 +38,7 @@ const DateRangeInput: React.FC<DateRangeInputProps> = ({
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<MarkdownEditorProps> = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const [markdown, setMarkdown] = useState<string>(
|
||||
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<MarkdownEditorProps> = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
}) => {
|
||||
const [markdown, setMarkdown] = useState<string>(
|
||||
defaultValue || MarkDownPattern,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onChange(markdown);
|
||||
}, [markdown]);
|
||||
|
||||
useEffect(() => {
|
||||
setMarkdown(defaultValue || MarkDownPattern);
|
||||
}, [defaultValue]);
|
||||
|
||||
// Обработчик вставки
|
||||
const handlePaste = async (
|
||||
e: React.ClipboardEvent<HTMLTextAreaElement>,
|
||||
|
||||
@@ -30,12 +30,7 @@ const MarkdownPreview: FC<MarkdownPreviewProps> = ({
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex-1 bg-[#161b22] rounded-lg shadow-lg p-6',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex-1 bg-[#161b22] rounded-lg p-6', className)}>
|
||||
<div className="prose prose-invert max-w-none h-full overflow-auto pr-4 medium-scrollbar">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
|
||||
72
src/views/home/group/posts/ModalCreate.tsx
Normal file
72
src/views/home/group/posts/ModalCreate.tsx
Normal 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;
|
||||
140
src/views/home/group/posts/ModalUpdate.tsx
Normal file
140
src/views/home/group/posts/ModalUpdate.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user