diff --git a/package.json b/package.json index d07fb1b..946dd15 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --host", - "build": "tsc && vite build", + "build": "tsc -b && vite build", "lint": "eslint .", "preview": "vite preview" }, diff --git a/src/App.tsx b/src/App.tsx index c6135f4..d9a3403 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import Home from "./pages/Home"; import Mission from "./pages/Mission"; import UploadMissionForm from "./views/mission/UploadMissionForm"; import MarkdownEditor from "./views/articleeditor/Editor"; +import ArticleEditor from "./pages/ArticleEditor"; function App() { return ( @@ -16,8 +17,9 @@ function App() { } /> } /> + } /> }/> - {}}/>} /> + {console.log(value)}}/>} /> diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx index 5568a0f..42fa814 100644 --- a/src/components/input/Input.tsx +++ b/src/components/input/Input.tsx @@ -14,6 +14,7 @@ interface inputProps { onChange: (state: string) => void; defaultState?: string; autocomplete?: string; + onKeyDown?: (e: React.KeyboardEvent) => void; } export const Input: React.FC = ({ @@ -27,12 +28,14 @@ export const Input: React.FC = ({ onChange, defaultState = "", name = "", - autocomplete="", + autocomplete = "", + onKeyDown, }) => { const [value, setValue] = React.useState(defaultState); const [visible, setVIsible] = React.useState(type != "password"); React.useEffect(() => onChange(value), [value]); + React.useEffect(() => setValue(defaultState), [defaultState]); @@ -59,12 +62,18 @@ export const Input: React.FC = ({ placeholder={placeholder} onChange={(e) => { setValue(e.target.value); - }} /> + }} + onKeyDown={(e: React.KeyboardEvent) => { + if (onKeyDown) + onKeyDown(e); + } + } + /> { type == "password" && { setVIsible(!visible); - }}/> + }} /> } diff --git a/src/pages/ArticleEditor.tsx b/src/pages/ArticleEditor.tsx index e69de29..0f1d163 100644 --- a/src/pages/ArticleEditor.tsx +++ b/src/pages/ArticleEditor.tsx @@ -0,0 +1,106 @@ +import { Route, Routes, useNavigate } from "react-router-dom"; +import Header from '../views/articleeditor/Header'; +import MarkdownEditor from "../views/articleeditor/Editor"; +import { useState } from "react"; +import { PrimaryButton } from "../components/button/PrimaryButton"; +import MarkdownPreview from "../views/articleeditor/MarckDownPreview"; +import { Input } from "../components/input/Input"; + + +const ArticleEditor = () => { + const [code, setCode] = useState(""); + const [name, setName] = useState(""); + const navigate = useNavigate(); + + + const [tagInput, setTagInput] = useState(""); + const [tags, setTags] = useState([]); + + 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)); + }; + + return ( + + + + } /> + } /> + + + + + + } /> + + Создание статьи + + + { + console.log({ + name: name, + tags: tags, + text: code, + }) + + }} text="Опубликовать" className="mt-[20px]" /> + + + { setName(v) }} placeholder="Новая статья" /> + + + {/* Блок для тегов */} + + + + { setTagInput(v) }} + defaultState={tagInput} + placeholder="arrays" + onKeyDown={(e) => { + console.log(e.key); + if (e.key == "Enter") + addTag(); + } + } + /> + + + + + {tags.map(tag => ( + + {tag} + removeTag(tag)} className="text-liquid-red font-bold ml-[5px]">× + + ))} + + + + navigate("editor")} text="Редактировать текст" className="mt-[20px]" /> + + + } /> + + + ); +}; + +export default ArticleEditor; diff --git a/src/pages/Mission.tsx b/src/pages/Mission.tsx index 9b46f48..e44f0c3 100644 --- a/src/pages/Mission.tsx +++ b/src/pages/Mission.tsx @@ -1,6 +1,6 @@ import { useParams, Navigate } from 'react-router-dom'; import CodeEditor from '../views/mission/codeeditor/CodeEditor'; -import Statement, { StatementData } from '../views/mission/statement/Statement'; +import Statement from '../views/mission/statement/Statement'; import { PrimaryButton } from '../components/button/PrimaryButton'; import { useEffect, useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; diff --git a/src/redux/slices/auth.ts b/src/redux/slices/auth.ts index 41a31ce..64d4bee 100644 --- a/src/redux/slices/auth.ts +++ b/src/redux/slices/auth.ts @@ -80,7 +80,7 @@ export const fetchWhoAmI = createAsyncThunk( // AsyncThunk: Загрузка токенов из localStorage export const loadTokensFromLocalStorage = createAsyncThunk( "auth/loadTokens", - async (_, { dispatch }) => { + async (_, { }) => { const jwt = localStorage.getItem("jwt"); const refreshToken = localStorage.getItem("refreshToken"); diff --git a/src/views/articleeditor/Editor.tsx b/src/views/articleeditor/Editor.tsx index a68d60f..b7a2645 100644 --- a/src/views/articleeditor/Editor.tsx +++ b/src/views/articleeditor/Editor.tsx @@ -1,34 +1,17 @@ import { FC, useEffect, useState } from "react"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import rehypeHighlight from "rehype-highlight"; -import rehypeRaw from "rehype-raw"; -import rehypeSanitize from "rehype-sanitize"; import axios from "../../axios"; import "highlight.js/styles/github-dark.css"; -import Header from "../mission/statement/Header"; -import { defaultSchema } from "hast-util-sanitize"; - -const schema = { - ...defaultSchema, - attributes: { - ...defaultSchema.attributes, - div: [ - ...(defaultSchema.attributes?.div || []), - ["style"] // разрешаем атрибут style на div - ] - } -}; +import MarkdownPreview from "./MarckDownPreview"; interface MarkdownEditorProps { - defaultValue?: string; - onChange: (value: string) => void; + defaultValue?: string; + onChange: (value: string) => void; } const MarkdownEditor: FC = ({ defaultValue, onChange }) => { - const [markdown, setMarkdown] = useState(defaultValue || `# 🌙 Добро пожаловать в Markdown-редактор + const [markdown, setMarkdown] = useState(defaultValue || `# 🌙 Добро пожаловать в Markdown-редактор Добро пожаловать в **Markdown-редактор**! Здесь ты можешь писать в формате Markdown и видеть результат **в реальном времени** 👇 @@ -224,87 +207,72 @@ print(greet("Мир")) `); - useEffect(() => { - onChange(markdown); - }, [markdown]); + useEffect(() => { + onChange(markdown); + }, [markdown]); - // Обработчик вставки - const handlePaste = async (e: React.ClipboardEvent) => { - const items = e.clipboardData.items; + // Обработчик вставки + const handlePaste = async (e: React.ClipboardEvent) => { + const items = e.clipboardData.items; - for (const item of items) { - if (item.type.startsWith("image/")) { - e.preventDefault(); // предотвращаем вставку картинки как текста + for (const item of items) { + if (item.type.startsWith("image/")) { + e.preventDefault(); // предотвращаем вставку картинки как текста - const file = item.getAsFile(); - if (!file) return; + const file = item.getAsFile(); + if (!file) return; - const formData = new FormData(); - formData.append("file", file); + const formData = new FormData(); + formData.append("file", file); - try { - const response = await axios.post("/media/upload", formData, { - headers: { "Content-Type": "multipart/form-data" }, - }); + try { + const response = await axios.post("/media/upload", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); - const imageUrl = response.data.url; - // Вставляем ссылку на картинку в текст - const cursorPos = (e.target as HTMLTextAreaElement).selectionStart; - const newText = - markdown.slice(0, cursorPos) + - `` + - markdown.slice(cursorPos); + const imageUrl = response.data.url; + // Вставляем ссылку на картинку в текст + const cursorPos = (e.target as HTMLTextAreaElement).selectionStart; + const newText = + markdown.slice(0, cursorPos) + + `` + + markdown.slice(cursorPos); - setMarkdown(newText); - } catch (err) { - console.error("Ошибка загрузки изображения:", err); + setMarkdown(newText); + } catch (err) { + console.error("Ошибка загрузки изображения:", err); + } + } } - } - } - }; + }; - return ( - - - - - - - {/* Предпросмотр */} - - - 👀 Предпросмотр - - - - {markdown} - - + return ( + + {/* Предпросмотр */} + + + 👀 Предпросмотр + + - - - {/* Редактор */} - - - 📝 Редактор - setMarkdown(e.target.value)} - onPaste={handlePaste} // <-- вот сюда обработчик вставки - className="flex-1 w-full bg-[#0d1117] text-gray-200 border border-gray-700 + {/* Редактор */} + + + 📝 Редактор + setMarkdown(e.target.value)} + onPaste={handlePaste} // <-- вот сюда обработчик вставки + className="flex-1 w-full bg-[#0d1117] text-gray-200 border border-gray-700 rounded-lg p-5 font-mono text-sm resize-none focus:outline-none focus:ring-2 medium-scrollbar" - placeholder="Пиши в формате Markdown..." - /> - + placeholder="Пиши в формате Markdown..." + /> + + - - - ); + ); }; export default MarkdownEditor; diff --git a/src/views/articleeditor/Header.tsx b/src/views/articleeditor/Header.tsx new file mode 100644 index 0000000..084ce77 --- /dev/null +++ b/src/views/articleeditor/Header.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { arrowLeft } from "../../assets/icons/header"; +import { Logo } from "../../assets/logos"; +import { useNavigate } from "react-router-dom"; + +interface HeaderProps { + backUrl?: string; +} + + +const Header: React.FC = ({ + backUrl="/home/articles", +}) => { + const navigate = useNavigate(); + return ( + + { navigate("/home") }} /> + + { navigate(backUrl) }} /> + + {/* + { navigate(`/mission/${missionId - 1}`) }} /> + {missionId} + { navigate(`/mission/${missionId + 1}`) }} /> + */} + + + ); +}; + +export default Header; diff --git a/src/views/articleeditor/MarckDownPreview.tsx b/src/views/articleeditor/MarckDownPreview.tsx new file mode 100644 index 0000000..556b22e --- /dev/null +++ b/src/views/articleeditor/MarckDownPreview.tsx @@ -0,0 +1,43 @@ +import { FC } from "react"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import rehypeHighlight from "rehype-highlight"; +import rehypeRaw from "rehype-raw"; +import rehypeSanitize from "rehype-sanitize"; +import "highlight.js/styles/github-dark.css"; + +import { defaultSchema } from "hast-util-sanitize"; +import { cn } from "../../lib/cn"; + +const schema = { + ...defaultSchema, + attributes: { + ...defaultSchema.attributes, + div: [ + ...(defaultSchema.attributes?.div || []), + ["style"] // разрешаем атрибут style на div + ] + } +}; + +interface MarkdownPreviewProps { + content: string; + className?: string; +} + +const MarkdownPreview: FC = ({ content, className="" }) => { + return ( + + + + {content} + + + + ); +}; + +export default MarkdownPreview; diff --git a/src/views/home/articles/Articles.tsx b/src/views/home/articles/Articles.tsx index de5f1d4..f2a4bc9 100644 --- a/src/views/home/articles/Articles.tsx +++ b/src/views/home/articles/Articles.tsx @@ -3,6 +3,7 @@ import { SecondaryButton } from "../../../components/button/SecondaryButton"; import { useAppDispatch } from "../../../redux/hooks"; import ArticleItem from "./ArticleItem"; import { setMenuActivePage } from "../../../redux/slices/store"; +import { useNavigate } from "react-router-dom"; export interface Article { @@ -15,6 +16,7 @@ export interface Article { const Articles = () => { const dispatch = useAppDispatch(); + const navigate = useNavigate(); const articles: Article[] = [ { @@ -142,7 +144,7 @@ const Articles = () => { Статьи { }} + onClick={() => {navigate("/article/create")}} text="Создать статью" className="absolute right-0" /> diff --git a/src/views/home/auth/Login.tsx b/src/views/home/auth/Login.tsx index e1d5432..e8b5a44 100644 --- a/src/views/home/auth/Login.tsx +++ b/src/views/home/auth/Login.tsx @@ -26,6 +26,7 @@ const Login = () => { // После успешного логина useEffect(() => { dispatch(setMenuActivePage("account")) + console.log(submitClicked); }, []); useEffect(() => { diff --git a/src/views/home/auth/Register.tsx b/src/views/home/auth/Register.tsx index 4f39ef7..612f8a7 100644 --- a/src/views/home/auth/Register.tsx +++ b/src/views/home/auth/Register.tsx @@ -35,6 +35,7 @@ const Register = () => { if (jwt) { navigate("/home"); } + console.log(submitClicked); }, [jwt]); const handleRegister = () => { diff --git a/src/views/home/contests/ContestItem.tsx b/src/views/home/contests/ContestItem.tsx index 78319cf..e026f60 100644 --- a/src/views/home/contests/ContestItem.tsx +++ b/src/views/home/contests/ContestItem.tsx @@ -1,6 +1,5 @@ import { cn } from "../../../lib/cn"; import { Account } from "../../../assets/icons/auth"; -import { registerUser } from "../../../redux/slices/auth"; import { PrimaryButton } from "../../../components/button/PrimaryButton"; import { ReverseButton } from "../../../components/button/ReverseButton"; diff --git a/src/views/home/groups/ModalUpdate.tsx b/src/views/home/groups/ModalUpdate.tsx index 347e0db..fe7b39e 100644 --- a/src/views/home/groups/ModalUpdate.tsx +++ b/src/views/home/groups/ModalUpdate.tsx @@ -4,7 +4,7 @@ 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, deleteGroup, updateGroup } from "../../../redux/slices/groups"; +import { deleteGroup, updateGroup } from "../../../redux/slices/groups"; interface ModalUpdateProps { active: boolean; diff --git a/src/views/mission/statement/MissionSubmissions.tsx b/src/views/mission/statement/MissionSubmissions.tsx index e058ee1..e918ee8 100644 --- a/src/views/mission/statement/MissionSubmissions.tsx +++ b/src/views/mission/statement/MissionSubmissions.tsx @@ -1,10 +1,7 @@ import SubmissionItem from "./SubmissionItem"; -import { SecondaryButton } from "../../../components/button/SecondaryButton"; -import { useAppDispatch, useAppSelector } from "../../../redux/hooks"; +import { useAppSelector } from "../../../redux/hooks"; import { FC, useEffect } from "react"; -import { setMenuActivePage } from "../../../redux/slices/store"; -import { useNavigate } from "react-router-dom"; -import { fetchMissions } from "../../../redux/slices/missions"; + export interface Mission { diff --git a/src/views/mission/statement/SubmissionItem.tsx b/src/views/mission/statement/SubmissionItem.tsx index 49c215f..12757ea 100644 --- a/src/views/mission/statement/SubmissionItem.tsx +++ b/src/views/mission/statement/SubmissionItem.tsx @@ -1,6 +1,6 @@ import { cn } from "../../../lib/cn"; -import { IconError, IconSuccess } from "../../../assets/icons/missions"; -import { useNavigate } from "react-router-dom"; +// import { IconError, IconSuccess } from "../../../assets/icons/missions"; +// import { useNavigate } from "react-router-dom"; export interface SubmissionItemProps { id: number; @@ -43,7 +43,7 @@ const SubmissionItem: React.FC = ({ type, status, }) => { - const navigate = useNavigate(); + // const navigate = useNavigate(); return (