diff --git a/src/components/Pastes/CodeEditor.jsx b/src/components/Pastes/CodeEditor.jsx index 9d9b846..ab5c8e6 100644 --- a/src/components/Pastes/CodeEditor.jsx +++ b/src/components/Pastes/CodeEditor.jsx @@ -1,16 +1,29 @@ import Editor from "@monaco-editor/react"; import { useTheme } from "@/hooks/useTheme"; -import { useRef } from "react"; +import { useEffect, useRef } from "react"; import PropTypes from "prop-types"; +import * as monaco from "monaco-editor"; -const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => { +const CodeEditor = ({ className = "", syntax, readOnly, onChange, value, editorErrors = [] }) => { const { theme } = useTheme(); const editorRef = useRef(null); - const onMount = (editor) => { - editorRef.current = editor; - editor.focus(); - } + useEffect(() => { + if (!editorRef.current) return; + const model = editorRef.current.getModel(); + if (!model) return; + + monaco.editor.setModelMarkers(model, "owner", editorErrors.map(err => ({ + startLineNumber: err.lineNumber, + startColumn: 1, + endLineNumber: err.lineNumber, + endColumn: model.getLineLength(err.lineNumber) + 1, + message: err.message, + severity: monaco.MarkerSeverity.Error + }))); + }, [editorErrors]); + + const onMount = (editor) => { editorRef.current = editor; editor.focus(); } return (
@@ -18,7 +31,7 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => { language={syntax || "plaintext"} value={value || ""} theme={theme === "dark" ? "vs-dark" : "vs-light"} - onChange={(value) => onChange?.(value)} + onChange={onChange} onMount={onMount} options={{ minimap: { enabled: false }, @@ -30,10 +43,6 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => { scrollbar: { verticalScrollbarSize: 0 }, wordWrap: "on", formatOnPaste: true, - suggest: { - showFields: true, - showFunctions: true, - }, readOnly: readOnly || false, }} /> @@ -47,6 +56,7 @@ CodeEditor.propTypes = { readOnly: PropTypes.bool, onChange: PropTypes.func, value: PropTypes.string, + editorErrors: PropTypes.array, }; -export default CodeEditor; +export default CodeEditor; \ No newline at end of file diff --git a/src/components/Pastes/PastePanel.jsx b/src/components/Pastes/PastePanel.jsx index e6590c4..e9cfad8 100644 --- a/src/components/Pastes/PastePanel.jsx +++ b/src/components/Pastes/PastePanel.jsx @@ -14,78 +14,76 @@ const PastePanel = ({ onSubmit, publicPastes }) => { const { pasteKey } = useParams(); const navigate = useNavigate(); const { getData } = useDataContext(); - const [title, setTitle] = useState(""); - const [content, setContent] = useState(""); - const [syntax, setSyntax] = useState(""); - const [burnAfter, setBurnAfter] = useState(false); - const [isPrivate, setIsPrivate] = useState(false); - const [password, setPassword] = useState(""); + + const [formData, setFormData] = useState({ + title: "", + content: "", + syntax: "", + burnAfter: false, + isPrivate: false, + password: "" + }); + const [selectedPaste, setSelectedPaste] = useState(null); - const [error, setError] = useState(null); + const [editorErrors, setEditorErrors] = useState([]); + const [fieldErrors, setFieldErrors] = useState({}); const [showPasswordModal, setShowPasswordModal] = useState(false); - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - const paste = { - title, - content, - syntax, - burnAfter: burnAfter, - isPrivate: isPrivate, - password: password || null, - }; - if (onSubmit) onSubmit(paste); - }; + setFieldErrors({}); + setEditorErrors([]); - const handleSelectPaste = async (key) => { - navigate(`/${key}`); - }; - - const fetchPaste = async (key, pwd = "") => { - const url = import.meta.env.MODE === 'production' ? - `https://api.miarma.net/v2/mpaste/pastes/${key}` : - `http://localhost:8081/v2/mpaste/pastes/${key}`; - - const { data, error } = await getData(url, {}, false, { - 'X-Paste-Password': pwd - }); - - if (error) { - if (error?.status === 401) { - setShowPasswordModal(true); - return; + try { + if (onSubmit) await onSubmit(formData); + } catch (error) { + if (error.status === 422 && error.errors) { + const newFieldErrors = {}; + Object.entries(error.errors).forEach(([field, msg]) => { + if (field === "content") { + setEditorErrors([{ lineNumber: 1, message: msg }]); + } else { + newFieldErrors[field] = msg; + } + }); + setFieldErrors(newFieldErrors); } else { - setError(error); - setSelectedPaste(null); - return; + showError(error); } } - - setError(null); - setSelectedPaste(data); - setTitle(data.title); - setContent(data.content); - setSyntax(data.syntax || "plaintext"); }; - useEffect(() => { - if (pasteKey) fetchPaste(pasteKey); - }, [pasteKey]); + const handleSelectPaste = async (key) => navigate(`/${key}`); + + const fetchPaste = async (key, pwd = "") => { + const url = import.meta.env.MODE === 'production' + ? `https://api.miarma.net/v2/mpaste/pastes/${key}` + : `http://localhost:8081/v2/mpaste/pastes/${key}`; + + const data = await getData(url, { password: pwd }, false); + + if (!data) return; + + setSelectedPaste(data); + setFormData({ + title: data.title ?? "", + content: data.content ?? "", + syntax: data.syntax || "plaintext", + burnAfter: data.burnAfter || false, + isPrivate: data.isPrivate || false, + password: "" + }); + }; + + useEffect(() => { if (pasteKey) fetchPaste(pasteKey); }, [pasteKey]); + + const handleChange = (key, value) => { + setFormData(prev => ({ ...prev, [key]: value })); + }; return ( <>
- {error && - setError(null)} dismissible> - - { - error.status == 404 ? "404: Paste no encontrada." : - "Ha ocurrido un error al cargar la paste." - } - - - } -
@@ -111,10 +109,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => { handleChange("content", val)} + value={formData.content ?? ""} + editorErrors={editorErrors} /> @@ -132,10 +131,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => { setTitle(e.target.value)} + value={formData.title} + onChange={(e) => handleChange("title", e.target.value)} + isInvalid={!!fieldErrors.title} /> + {fieldErrors.title} { > setSyntax(e.target.value)} + value={formData.syntax} + onChange={(e) => handleChange("syntax", e.target.value)} > @@ -189,8 +189,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => { disabled={!!selectedPaste} id="burnAfter" label="volátil" - checked={burnAfter} - onChange={(e) => setBurnAfter(e.target.checked)} + checked={formData.burnAfter} + onChange={(e) => handleChange("burnAfter", e.target.checked)} className="ms-1 d-flex gap-2 align-items-center" /> @@ -199,13 +199,13 @@ const PastePanel = ({ onSubmit, publicPastes }) => { disabled={!!selectedPaste} id="isPrivate" label="privado" - checked={isPrivate} - onChange={(e) => setIsPrivate(e.target.checked)} + checked={formData.isPrivate} + onChange={(e) => handleChange("isPrivate", e.target.checked)} className="ms-1 d-flex gap-2 align-items-center" /> - {isPrivate && ( - setPassword(e.target.value)} /> + {formData.isPrivate && ( + handleChange("password", e.target.value)} /> )}
@@ -227,7 +227,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => { onClose={() => setShowPasswordModal(false)} onSubmit={(pwd) => { setShowPasswordModal(false); - fetchPaste(pasteKey, pwd); // reintentas con la pass + fetchPaste(pasteKey, pwd); }} /> diff --git a/src/hooks/useData.js b/src/hooks/useData.js index 43783f8..9ad7365 100644 --- a/src/hooks/useData.js +++ b/src/hooks/useData.js @@ -92,10 +92,10 @@ export const useData = (config, onError) => { if (config?.baseUrl) fetchData(); }, [config, fetchData]); - const requestWrapper = async (method, endpoint, payload = null, refresh = false, extraHeaders = {}) => { + const requestWrapper = async (method, endpoint, payload = null, refresh = false) => { try { const isFormData = payload instanceof FormData; - const headers = { ...getAuthHeaders(isFormData), ...extraHeaders }; + const headers = getAuthHeaders(isFormData); const cfg = { headers }; let response; @@ -131,7 +131,7 @@ export const useData = (config, onError) => { dataLoading, dataError, clearError, - getData: (url, params, refresh = true, headers = {}) => requestWrapper("get", url, params, refresh, headers), + getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh), postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh), putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh), deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),