Files
LiquidCode_Frontend/src/pages/ArticleEditor.tsx
Виталий Лавшонок d1a46435c4 add error toasts
2025-12-10 01:33:16 +03:00

275 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useNavigate } from 'react-router-dom';
import Header from '../views/articleeditor/Header';
import MarkdownEditor from '../views/articleeditor/Editor';
import { useEffect, useState } from 'react';
import { PrimaryButton } from '../components/button/PrimaryButton';
import MarkdownPreview from '../views/articleeditor/MarckDownPreview';
import { Input } from '../components/input/Input';
import { useAppDispatch, useAppSelector } from '../redux/hooks';
import {
createArticle,
deleteArticle,
fetchArticleById,
setArticlesStatus,
updateArticle,
} from '../redux/slices/articles';
import { useQuery } from '../hooks/useQuery';
import { ReverseButton } from '../components/button/ReverseButton';
import { cn } from '../lib/cn';
const ArticleEditor = () => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const query = useQuery();
const back = query.get('back') ?? undefined;
const articleId = Number(query.get('articleId') ?? undefined);
const refactor = articleId && !isNaN(articleId);
const [clickSubmit, setClickSubmit] = useState<boolean>(false);
// Достаём данные из redux
const article = useAppSelector(
(state) => state.articles.fetchArticleById.article,
);
const statusCreate = useAppSelector(
(state) => state.articles.createArticle.status,
);
const statusUpdate = useAppSelector(
(state) => state.articles.updateArticle.status,
);
const statusDelete = useAppSelector(
(state) => state.articles.deleteArticle.status,
);
// Локальные состояния
const [code, setCode] = useState<string>(article?.content || '');
const [name, setName] = useState<string>(article?.name || '');
const [tagInput, setTagInput] = useState<string>('');
const [tags, setTags] = useState<string[]>(article?.tags || []);
const [activeEditor, setActiveEditor] = useState<boolean>(false);
// ==========================
// Теги
// ==========================
const addTag = () => {
const newTag = tagInput.trim();
if (newTag && !tags.includes(newTag)) {
setTags([...tags, newTag]);
setTagInput('');
}
};
const removeTag = (tagToRemove: string) => {
setTags(tags.filter((tag) => tag !== tagToRemove));
};
// ==========================
// Эффекты по статусам
// ==========================
useEffect(() => {
if (statusCreate === 'successful') {
dispatch(
setArticlesStatus({ key: 'createArticle', status: 'idle' }),
);
navigate(back ?? '/home/articles');
}
}, [statusCreate]);
useEffect(() => {
if (statusUpdate === 'successful') {
dispatch(
setArticlesStatus({ key: 'updateArticle', status: 'idle' }),
);
navigate(back ?? '/home/articles');
}
}, [statusUpdate]);
useEffect(() => {
if (statusDelete === 'successful') {
dispatch(
setArticlesStatus({ key: 'deleteArticle', status: 'idle' }),
);
navigate(back ?? '/home/articles');
}
}, [statusDelete]);
// ==========================
// Получение статьи
// ==========================
useEffect(() => {
setClickSubmit(false);
if (articleId) {
dispatch(fetchArticleById(articleId));
}
}, [articleId]);
// Обновление локального состояния после загрузки статьи
useEffect(() => {
if (article && refactor) {
setCode(article.content || '');
setName(article.name || '');
setTags(article.tags || []);
}
}, [article]);
const getNameErrorMessage = (): string => {
if (!clickSubmit) return '';
if (name == '') return 'Поле не может быть пустым';
return '';
};
const getContentErrorMessage = (): string => {
if (!clickSubmit) return '';
if (code == '') return 'Поле не может быть пустым';
return '';
};
// ==========================
// Рендер
// ==========================
return (
<div className="h-screen grid grid-rows-[60px,1fr]">
{activeEditor ? (
<Header backClick={() => setActiveEditor(false)} />
) : (
<Header backClick={() => navigate(back ?? '/home/articles')} />
)}
{activeEditor ? (
<MarkdownEditor onChange={setCode} defaultValue={code} />
) : (
<div className="text-liquid-white">
<div className="text-[40px] font-bold">
{refactor
? `Редактирование статьи: \"${article?.name}\"`
: 'Создание статьи'}
</div>
{/* Кнопки действий */}
<div>
{refactor ? (
<div className="flex gap-[20px]">
<PrimaryButton
onClick={() => {
setClickSubmit(true);
dispatch(
updateArticle({
articleId,
name,
tags,
content: code,
}),
);
}}
text="Обновить"
className="mt-[20px]"
disabled={statusUpdate === 'loading'}
/>
<ReverseButton
onClick={() =>
dispatch(deleteArticle(articleId))
}
color="error"
text="Удалить"
className="mt-[20px]"
disabled={statusDelete === 'loading'}
/>
</div>
) : (
<PrimaryButton
onClick={() => {
setClickSubmit(true);
dispatch(
createArticle({
name,
tags,
content: code,
}),
);
}}
text="Опубликовать"
className="mt-[20px]"
disabled={statusCreate === 'loading'}
/>
)}
</div>
{/* Название */}
<Input
defaultState={name}
name="articleName"
autocomplete="articleName"
className="mt-[20px] max-w-[600px]"
type="text"
label="Название"
onChange={setName}
placeholder="Новая статья"
error={getNameErrorMessage()}
/>
{/* Теги */}
<div className="mt-[20px] max-w-[600px]">
<div className="grid grid-cols-[1fr,140px] items-end gap-2">
<Input
name="articleTag"
autocomplete="articleTag"
className="mt-[20px] max-w-[600px]"
type="text"
label="Теги"
onChange={setTagInput}
defaultState={tagInput}
placeholder="arrays"
onKeyDown={(e) => {
if (e.key === 'Enter') addTag();
}}
/>
<PrimaryButton
onClick={addTag}
text="Добавить"
className="h-[40px] w-[140px]"
/>
</div>
<div className="flex flex-wrap gap-[10px] mt-2">
{tags.map((tag) => (
<div
key={tag}
className="flex items-center gap-1 bg-liquid-lighter px-3 py-1 rounded-full"
>
<span>{tag}</span>
<button
onClick={() => removeTag(tag)}
className="text-liquid-red font-bold ml-[5px]"
>
×
</button>
</div>
))}
</div>
</div>
{/* Просмотр и переход в редактор */}
<PrimaryButton
onClick={() => setActiveEditor(true)}
text="Редактировать текст"
className="mt-[20px]"
/>
<div
className={cn(
'text-liquid-red text-[14px] h-auto mt-[5px] whitespace-pre-line ',
getContentErrorMessage() == '' && 'h-0 mt-0',
)}
>
{getContentErrorMessage()}
</div>
<MarkdownPreview
content={code}
className="bg-transparent border-liquid-lighter border-[3px] rounded-[20px] mt-[20px]"
/>
</div>
)}
</div>
);
};
export default ArticleEditor;