From 2e3a8779fc9e7fb6451df46a31a9454d0a6bead4 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: Tue, 4 Nov 2025 14:21:14 +0300 Subject: [PATCH] article form creator --- package.json | 2 +- src/App.tsx | 4 +- src/components/input/Input.tsx | 15 +- src/pages/ArticleEditor.tsx | 106 +++++++++++++ src/pages/Mission.tsx | 2 +- src/redux/slices/auth.ts | 2 +- src/views/articleeditor/Editor.tsx | 144 +++++++----------- src/views/articleeditor/Header.tsx | 31 ++++ src/views/articleeditor/MarckDownPreview.tsx | 43 ++++++ src/views/home/articles/Articles.tsx | 4 +- src/views/home/auth/Login.tsx | 1 + src/views/home/auth/Register.tsx | 1 + src/views/home/contests/ContestItem.tsx | 1 - src/views/home/groups/ModalUpdate.tsx | 2 +- .../mission/statement/MissionSubmissions.tsx | 7 +- .../mission/statement/SubmissionItem.tsx | 6 +- tsconfig.app.tsbuildinfo | 2 +- 17 files changed, 266 insertions(+), 107 deletions(-) create mode 100644 src/views/articleeditor/Header.tsx create mode 100644 src/views/articleeditor/MarckDownPreview.tsx 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} + +
+ ))} +
+
+ + 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) + - `\"img\"/` + - markdown.slice(cursorPos); + const imageUrl = response.data.url; + // Вставляем ссылку на картинку в текст + const cursorPos = (e.target as HTMLTextAreaElement).selectionStart; + const newText = + markdown.slice(0, cursorPos) + + `\"img\"/` + + markdown.slice(cursorPos); - setMarkdown(newText); - } catch (err) { - console.error("Ошибка загрузки изображения:", err); + setMarkdown(newText); + } catch (err) { + console.error("Ошибка загрузки изображения:", err); + } + } } - } - } - }; + }; - return ( -
-
-
-
- -
- {/* Предпросмотр */} -
-
-

👀 Предпросмотр

-
-
- - {markdown} - -
+ return ( +
+ {/* Предпросмотр */} +
+
+

👀 Предпросмотр

+ +
-
-
- {/* Редактор */} -
-
-

📝 Редактор

-