Compare commits
2 Commits
589215b2bc
...
69140e6da1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69140e6da1 | ||
|
|
4d0c4d3f26 |
4524
package-lock.json
generated
Normal file
4524
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"apiConfig": {
|
"apiConfig": {
|
||||||
"baseUrl": "https://api.miarma.net/mpaste",
|
"baseUrl": "http://localhost:8081/v2/mpaste",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"pastes": {
|
"pastes": {
|
||||||
"all": "/raw/v1/pastes",
|
"all": "/pastes",
|
||||||
"byId": "/raw/v1/pastes/:paste_id",
|
"byId": "/:pasteId",
|
||||||
"byKey": "/v1/pastes/:paste_key"
|
"byKey": "/:pasteKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"apiConfig": {
|
"apiConfig": {
|
||||||
"baseUrl": "https://api.miarma.net/mpaste",
|
"baseUrl": "https://api.miarma.net/v2/mpaste",
|
||||||
"endpoints": {
|
"endpoints": {
|
||||||
"pastes": {
|
"pastes": {
|
||||||
"all": "/raw/v1/pastes",
|
"all": "/pastes",
|
||||||
"byId": "/raw/v1/pastes/:paste_id",
|
"byId": "/:pasteId",
|
||||||
"byKey": "/v1/pastes/:paste_key"
|
"byKey": "/:pasteKey"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function App() {
|
|||||||
<div className="fill d-flex flex-column">
|
<div className="fill d-flex flex-column">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/:paste_key" element={<Home />} />
|
<Route path="/:pasteKey" element={<Home />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,23 +1,29 @@
|
|||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { useTheme } from "@/hooks/useTheme";
|
import { useTheme } from "@/hooks/useTheme";
|
||||||
import { useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { loader } from '@monaco-editor/react';
|
import * as monaco from "monaco-editor";
|
||||||
|
|
||||||
loader.config({
|
const CodeEditor = ({ className = "", syntax, readOnly, onChange, value, editorErrors = [] }) => {
|
||||||
'vs/nls': {
|
|
||||||
availableLanguages: { '*': 'es' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const editorRef = useRef(null);
|
const editorRef = useRef(null);
|
||||||
|
|
||||||
const onMount = (editor) => {
|
useEffect(() => {
|
||||||
editorRef.current = editor;
|
if (!editorRef.current) return;
|
||||||
editor.focus();
|
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 (
|
return (
|
||||||
<div className={`code-editor ${className}`}>
|
<div className={`code-editor ${className}`}>
|
||||||
@@ -25,7 +31,7 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
|
|||||||
language={syntax || "plaintext"}
|
language={syntax || "plaintext"}
|
||||||
value={value || ""}
|
value={value || ""}
|
||||||
theme={theme === "dark" ? "vs-dark" : "vs-light"}
|
theme={theme === "dark" ? "vs-dark" : "vs-light"}
|
||||||
onChange={(value) => onChange?.(value)}
|
onChange={onChange}
|
||||||
onMount={onMount}
|
onMount={onMount}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
@@ -37,10 +43,6 @@ const CodeEditor = ({ className = "", syntax, readOnly, onChange, value }) => {
|
|||||||
scrollbar: { verticalScrollbarSize: 0 },
|
scrollbar: { verticalScrollbarSize: 0 },
|
||||||
wordWrap: "on",
|
wordWrap: "on",
|
||||||
formatOnPaste: true,
|
formatOnPaste: true,
|
||||||
suggest: {
|
|
||||||
showFields: true,
|
|
||||||
showFunctions: true,
|
|
||||||
},
|
|
||||||
readOnly: readOnly || false,
|
readOnly: readOnly || false,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -54,6 +56,7 @@ CodeEditor.propTypes = {
|
|||||||
readOnly: PropTypes.bool,
|
readOnly: PropTypes.bool,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
|
editorErrors: PropTypes.array,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CodeEditor;
|
export default CodeEditor;
|
||||||
@@ -11,78 +11,79 @@ import { useDataContext } from "@/hooks/useDataContext";
|
|||||||
import PasswordModal from "@/components/Auth/PasswordModal.jsx";
|
import PasswordModal from "@/components/Auth/PasswordModal.jsx";
|
||||||
|
|
||||||
const PastePanel = ({ onSubmit, publicPastes }) => {
|
const PastePanel = ({ onSubmit, publicPastes }) => {
|
||||||
const { paste_key } = useParams();
|
const { pasteKey } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { getData } = useDataContext();
|
const { getData } = useDataContext();
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
const [content, setContent] = useState("");
|
const [formData, setFormData] = useState({
|
||||||
const [syntax, setSyntax] = useState("");
|
title: "",
|
||||||
const [burnAfter, setBurnAfter] = useState(false);
|
content: "",
|
||||||
const [isPrivate, setIsPrivate] = useState(false);
|
syntax: "",
|
||||||
const [password, setPassword] = useState("");
|
burnAfter: false,
|
||||||
|
isPrivate: false,
|
||||||
|
password: ""
|
||||||
|
});
|
||||||
|
|
||||||
const [selectedPaste, setSelectedPaste] = useState(null);
|
const [selectedPaste, setSelectedPaste] = useState(null);
|
||||||
const [error, setError] = useState(null);
|
const [editorErrors, setEditorErrors] = useState([]);
|
||||||
|
const [fieldErrors, setFieldErrors] = useState({});
|
||||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const paste = {
|
setFieldErrors({});
|
||||||
title,
|
setEditorErrors([]);
|
||||||
content,
|
|
||||||
syntax,
|
|
||||||
burn_after: burnAfter,
|
|
||||||
is_private: isPrivate,
|
|
||||||
password: password || null,
|
|
||||||
};
|
|
||||||
if (onSubmit) onSubmit(paste);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectPaste = async (key) => {
|
try {
|
||||||
navigate(`/${key}`);
|
if (onSubmit) await onSubmit(formData);
|
||||||
};
|
} catch (error) {
|
||||||
|
if (error.status === 422 && error.errors) {
|
||||||
const fetchPaste = async (key, pwd = "") => {
|
const newFieldErrors = {};
|
||||||
const url = `https://api.miarma.net/mpaste/v1/pastes/${key}`;
|
Object.entries(error.errors).forEach(([field, msg]) => {
|
||||||
const { data, error } = await getData(url, {}, {
|
if (field === "content") {
|
||||||
'X-Paste-Password': pwd
|
setEditorErrors([{ lineNumber: 1, message: msg }]);
|
||||||
});
|
} else {
|
||||||
|
newFieldErrors[field] = msg;
|
||||||
if (error) {
|
}
|
||||||
if (error?.status === 401) {
|
});
|
||||||
setShowPasswordModal(true);
|
setFieldErrors(newFieldErrors);
|
||||||
return;
|
|
||||||
} else {
|
} else {
|
||||||
setError(error);
|
showError(error);
|
||||||
setSelectedPaste(null);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setError(null);
|
|
||||||
setSelectedPaste(data);
|
|
||||||
setTitle(data.title);
|
|
||||||
setContent(data.content);
|
|
||||||
setSyntax(data.syntax || "plaintext");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSelectPaste = async (key) => navigate(`/${key}`);
|
||||||
if (paste_key) fetchPaste(paste_key);
|
|
||||||
}, [paste_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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3">
|
<div className="paste-panel border-0 flex-fill d-flex flex-column min-h-0 p-3">
|
||||||
{error &&
|
|
||||||
<Alert variant="danger" onClose={() => setError(null)} dismissible>
|
|
||||||
<strong>
|
|
||||||
<span className="text-danger">{
|
|
||||||
error.status == 404 ? "404: Paste no encontrada." :
|
|
||||||
"Ha ocurrido un error al cargar la paste."
|
|
||||||
}</span>
|
|
||||||
</strong>
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Form onSubmit={handleSubmit} className="flex-fill d-flex flex-column min-h-0">
|
<Form onSubmit={handleSubmit} className="flex-fill d-flex flex-column min-h-0">
|
||||||
<Row className="g-3 flex-fill min-h-0">
|
<Row className="g-3 flex-fill min-h-0">
|
||||||
<Col xs={12} lg={2} className="order-last order-lg-first d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
<Col xs={12} lg={2} className="order-last order-lg-first d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
||||||
@@ -93,7 +94,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
{publicPastes && publicPastes.length > 0 ? (
|
{publicPastes && publicPastes.length > 0 ? (
|
||||||
publicPastes.map((paste) => (
|
publicPastes.map((paste) => (
|
||||||
<PublicPasteItem
|
<PublicPasteItem
|
||||||
key={paste.paste_key}
|
key={paste.pasteKey}
|
||||||
paste={paste}
|
paste={paste}
|
||||||
onSelect={handleSelectPaste}
|
onSelect={handleSelectPaste}
|
||||||
/>
|
/>
|
||||||
@@ -108,10 +109,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
<Col xs={12} lg={7} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
<Col xs={12} lg={7} className="d-flex flex-column flex-fill min-h-0 overflow-hidden">
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
className="flex-fill custom-border rounded-4 overflow-hidden pt-4 pe-4"
|
className="flex-fill custom-border rounded-4 overflow-hidden pt-4 pe-4"
|
||||||
syntax={syntax}
|
syntax={formData.syntax}
|
||||||
readOnly={!!selectedPaste}
|
readOnly={!!selectedPaste}
|
||||||
onChange={selectedPaste ? undefined : setContent}
|
onChange={(val) => handleChange("content", val)}
|
||||||
value={content}
|
value={formData.content ?? ""}
|
||||||
|
editorErrors={editorErrors}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@@ -129,10 +131,11 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
disabled={!!selectedPaste}
|
disabled={!!selectedPaste}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Título de la paste"
|
value={formData.title}
|
||||||
value={title}
|
onChange={(e) => handleChange("title", e.target.value)}
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
isInvalid={!!fieldErrors.title}
|
||||||
/>
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">{fieldErrors.title}</Form.Control.Feedback>
|
||||||
</FloatingLabel>
|
</FloatingLabel>
|
||||||
|
|
||||||
<FloatingLabel
|
<FloatingLabel
|
||||||
@@ -146,8 +149,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
>
|
>
|
||||||
<Form.Select
|
<Form.Select
|
||||||
disabled={!!selectedPaste}
|
disabled={!!selectedPaste}
|
||||||
value={syntax}
|
value={formData.syntax}
|
||||||
onChange={(e) => setSyntax(e.target.value)}
|
onChange={(e) => handleChange("syntax", e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="">Sin resaltado</option>
|
<option value="">Sin resaltado</option>
|
||||||
<option value="javascript">JavaScript</option>
|
<option value="javascript">JavaScript</option>
|
||||||
@@ -186,8 +189,8 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
disabled={!!selectedPaste}
|
disabled={!!selectedPaste}
|
||||||
id="burnAfter"
|
id="burnAfter"
|
||||||
label="volátil"
|
label="volátil"
|
||||||
checked={burnAfter}
|
checked={formData.burnAfter}
|
||||||
onChange={(e) => setBurnAfter(e.target.checked)}
|
onChange={(e) => handleChange("burnAfter", e.target.checked)}
|
||||||
className="ms-1 d-flex gap-2 align-items-center"
|
className="ms-1 d-flex gap-2 align-items-center"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -196,13 +199,13 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
disabled={!!selectedPaste}
|
disabled={!!selectedPaste}
|
||||||
id="isPrivate"
|
id="isPrivate"
|
||||||
label="privado"
|
label="privado"
|
||||||
checked={isPrivate}
|
checked={formData.isPrivate}
|
||||||
onChange={(e) => setIsPrivate(e.target.checked)}
|
onChange={(e) => handleChange("isPrivate", e.target.checked)}
|
||||||
className="ms-1 d-flex gap-2 align-items-center"
|
className="ms-1 d-flex gap-2 align-items-center"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isPrivate && (
|
{formData.isPrivate && (
|
||||||
<PasswordInput onChange={(e) => setPassword(e.target.value)} />
|
<PasswordInput onChange={(e) => handleChange("password", e.target.value)} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="d-flex justify-content-end">
|
<div className="d-flex justify-content-end">
|
||||||
@@ -224,7 +227,7 @@ const PastePanel = ({ onSubmit, publicPastes }) => {
|
|||||||
onClose={() => setShowPasswordModal(false)}
|
onClose={() => setShowPasswordModal(false)}
|
||||||
onSubmit={(pwd) => {
|
onSubmit={(pwd) => {
|
||||||
setShowPasswordModal(false);
|
setShowPasswordModal(false);
|
||||||
fetchPaste(paste_key, pwd); // reintentas con la pass
|
fetchPaste(pasteKey, pwd);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ const trimContent = (text, maxLength = 80) => {
|
|||||||
|
|
||||||
const PublicPasteItem = ({ paste, onSelect }) => {
|
const PublicPasteItem = ({ paste, onSelect }) => {
|
||||||
return (
|
return (
|
||||||
<div className="public-paste-item p-2 mb-2 rounded custom-border" style={{ cursor: "pointer" }} onClick={() => onSelect(paste.paste_key)}>
|
<div className="public-paste-item p-2 mb-2 rounded custom-border" style={{ cursor: "pointer" }} onClick={() => onSelect(paste.pasteKey)}>
|
||||||
<h5 className="m-0">{paste.title}</h5>
|
<h5 className="m-0">{paste.title}</h5>
|
||||||
<p className="m-0 text-truncate">{trimContent(paste.content, 100)}</p>
|
<p className="m-0 text-truncate">{trimContent(paste.content, 100)}</p>
|
||||||
<small className="custom-text-muted">
|
<small className="custom-text-muted">
|
||||||
{new Date(paste.created_at).toLocaleString()}
|
{new Date(paste.createdAt).toLocaleString()}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -22,7 +22,7 @@ PublicPasteItem.propTypes = {
|
|||||||
paste: PropTypes.shape({
|
paste: PropTypes.shape({
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
content: PropTypes.string.isRequired,
|
content: PropTypes.string.isRequired,
|
||||||
created_at: PropTypes.string.isRequired,
|
createdAt: PropTypes.string.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import { useState, useEffect, createContext } from "react";
|
|
||||||
import createAxiosInstance from "@/api/axiosInstance";
|
|
||||||
import { useConfig } from "@/hooks/useConfig";
|
|
||||||
|
|
||||||
export const AuthContext = createContext();
|
|
||||||
|
|
||||||
export const AuthProvider = ({ children }) => {
|
|
||||||
const axios = createAxiosInstance();
|
|
||||||
const { config } = useConfig();
|
|
||||||
|
|
||||||
const [user, setUser] = useState(() => JSON.parse(localStorage.getItem("user")) || null);
|
|
||||||
const [token, setToken] = useState(() => localStorage.getItem("token"));
|
|
||||||
const [authStatus, setAuthStatus] = useState("checking");
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!config) return;
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
setAuthStatus("unauthenticated");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const BASE_URL = config.apiConfig.baseUrl;
|
|
||||||
const VALIDATE_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.validateToken}`;
|
|
||||||
|
|
||||||
const checkAuth = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(VALIDATE_URL, {
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
});
|
|
||||||
if (res.status === 200) {
|
|
||||||
setAuthStatus("authenticated");
|
|
||||||
} else {
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error validando token:", err);
|
|
||||||
logout();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkAuth();
|
|
||||||
}, [token, config]);
|
|
||||||
|
|
||||||
const login = async (formData) => {
|
|
||||||
setError(null);
|
|
||||||
const BASE_URL = config.apiConfig.baseUrl;
|
|
||||||
const LOGIN_URL = `${BASE_URL}${config.apiConfig.endpoints.auth.login}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.post(LOGIN_URL, formData);
|
|
||||||
const { token, member, tokenTime } = res.data.data;
|
|
||||||
|
|
||||||
localStorage.setItem("token", token);
|
|
||||||
localStorage.setItem("user", JSON.stringify(member));
|
|
||||||
localStorage.setItem("tokenTime", tokenTime);
|
|
||||||
|
|
||||||
setToken(token);
|
|
||||||
setUser(member);
|
|
||||||
setAuthStatus("authenticated");
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Error al iniciar sesión:", err);
|
|
||||||
|
|
||||||
let message = "Ha ocurrido un error inesperado.";
|
|
||||||
|
|
||||||
if (err.response) {
|
|
||||||
const { status, data } = err.response;
|
|
||||||
|
|
||||||
if (status === 400) {
|
|
||||||
message = "Usuario o contraseña incorrectos.";
|
|
||||||
} else if (status === 403) {
|
|
||||||
message = "Tu cuenta está inactiva o ha sido suspendida.";
|
|
||||||
} else if (status === 404) {
|
|
||||||
message = "Usuario no encontrado.";
|
|
||||||
} else if (data?.message) {
|
|
||||||
message = data.message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setError(message);
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
localStorage.clear();
|
|
||||||
setUser(null);
|
|
||||||
setToken(null);
|
|
||||||
setAuthStatus("unauthenticated");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{ user, token, authStatus, login, logout, error }}>
|
|
||||||
{children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -4,8 +4,8 @@ import { useData } from "../hooks/useData";
|
|||||||
|
|
||||||
export const DataContext = createContext();
|
export const DataContext = createContext();
|
||||||
|
|
||||||
export const DataProvider = ({ config, children }) => {
|
export const DataProvider = ({ config, onError, children }) => {
|
||||||
const data = useData(config);
|
const data = useData(config, onError);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataContext.Provider value={data}>
|
<DataContext.Provider value={data}>
|
||||||
|
|||||||
36
src/context/ErrorContext.jsx
Normal file
36
src/context/ErrorContext.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createContext, useState, useContext } from 'react';
|
||||||
|
import NotificationModal from '../components/NotificationModal';
|
||||||
|
|
||||||
|
const ErrorContext = createContext();
|
||||||
|
|
||||||
|
export const ErrorProvider = ({ children }) => {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
const showError = (err) => {
|
||||||
|
setError({
|
||||||
|
title: err.status ? `Error ${err.status}` : "Error",
|
||||||
|
message: err.message,
|
||||||
|
variant: 'danger'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeError = () => setError(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorContext.Provider value={{ showError }}>
|
||||||
|
{children}
|
||||||
|
{error && (
|
||||||
|
<NotificationModal
|
||||||
|
show={true}
|
||||||
|
onClose={closeError}
|
||||||
|
title={error.title}
|
||||||
|
message={error.message}
|
||||||
|
variant='danger'
|
||||||
|
buttons={[{ label: "Aceptar", variant: "danger", onClick: closeError }]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ErrorContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useError = () => useContext(ErrorContext);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
export const useData = (config) => {
|
export const useData = (config, onError) => {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [dataLoading, setLoading] = useState(true);
|
const [dataLoading, setLoading] = useState(true);
|
||||||
const [dataError, setError] = useState(null);
|
const [dataError, setError] = useState(null);
|
||||||
@@ -13,10 +13,59 @@ export const useData = (config) => {
|
|||||||
}
|
}
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
const getAuthHeaders = () => ({
|
const getAuthHeaders = (isFormData = false) => {
|
||||||
"Content-Type": "application/json",
|
const token = localStorage.getItem("token");
|
||||||
"Authorization": `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
});
|
const headers = {};
|
||||||
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
|
||||||
|
if (!isFormData) {
|
||||||
|
headers["Content-Type"] = "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAxiosError = (err) => {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
const data = err.response.data;
|
||||||
|
|
||||||
|
if (data.status === 422 && data.errors) {
|
||||||
|
return {
|
||||||
|
status: 422,
|
||||||
|
errors: data.errors,
|
||||||
|
path: data.path ?? null,
|
||||||
|
timestamp: data.timestamp ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: data.status ?? err.response.status,
|
||||||
|
error: data.error ?? null,
|
||||||
|
message: data.message ?? err.response.statusText ?? "Error desconocido",
|
||||||
|
path: data.path ?? null,
|
||||||
|
timestamp: data.timestamp ?? null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.request) {
|
||||||
|
return {
|
||||||
|
status: null,
|
||||||
|
error: "Network Error",
|
||||||
|
message: "No se pudo conectar al servidor",
|
||||||
|
path: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: null,
|
||||||
|
error: "Client Error",
|
||||||
|
message: err.message || "Error desconocido",
|
||||||
|
path: null,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const fetchData = useCallback(async () => {
|
const fetchData = useCallback(async () => {
|
||||||
const current = configRef.current;
|
const current = configRef.current;
|
||||||
@@ -30,104 +79,62 @@ export const useData = (config) => {
|
|||||||
headers: getAuthHeaders(),
|
headers: getAuthHeaders(),
|
||||||
params: current.params,
|
params: current.params,
|
||||||
});
|
});
|
||||||
setData(response.data.data);
|
setData(response.data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.response?.data?.message || err.message);
|
const error = handleAxiosError(err);
|
||||||
|
setError(error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (config?.baseUrl) {
|
if (config?.baseUrl) fetchData();
|
||||||
fetchData();
|
|
||||||
}
|
|
||||||
}, [config, fetchData]);
|
}, [config, fetchData]);
|
||||||
|
|
||||||
const getData = async (url, params = {}, headers = {}) => {
|
const requestWrapper = async (method, endpoint, payload = null, refresh = false) => {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(url, {
|
const isFormData = payload instanceof FormData;
|
||||||
headers: { ...getAuthHeaders(), ...headers },
|
const headers = getAuthHeaders(isFormData);
|
||||||
params,
|
const cfg = { headers };
|
||||||
});
|
let response;
|
||||||
return { data: response.data.data, error: null };
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
data: null,
|
|
||||||
error: {
|
|
||||||
status: err.response?.data?.status || err.response?.status,
|
|
||||||
message: err.response?.data?.message || err.message,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const postData = async (endpoint, payload) => {
|
if (method === "get") {
|
||||||
const headers = {
|
if (payload) cfg.params = payload;
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
response = await axios.get(endpoint, cfg);
|
||||||
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
|
} else if (method === "delete") {
|
||||||
};
|
if (payload) cfg.data = payload;
|
||||||
const response = await axios.post(endpoint, payload, { headers });
|
response = await axios.delete(endpoint, cfg);
|
||||||
await fetchData();
|
} else {
|
||||||
return response.data.data;
|
response = await axios[method](endpoint, payload, cfg);
|
||||||
};
|
|
||||||
|
|
||||||
const postDataValidated = async (endpoint, payload) => {
|
|
||||||
try {
|
|
||||||
const headers = {
|
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
|
||||||
...(payload instanceof FormData ? {} : { "Content-Type": "application/json" }),
|
|
||||||
};
|
|
||||||
const response = await axios.post(endpoint, payload, { headers });
|
|
||||||
return { data: response.data.data, errors: null };
|
|
||||||
} catch (err) {
|
|
||||||
const raw = err.response?.data?.message;
|
|
||||||
let parsed = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return { data: null, errors: { general: raw || err.message } };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { data: null, errors: parsed };
|
if (refresh) await fetchData();
|
||||||
|
return response.data;
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
const error = handleAxiosError(err);
|
||||||
|
|
||||||
|
if (error.status !== 422 && onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const putData = async (endpoint, payload) => {
|
const clearError = () => setError(null);
|
||||||
const response = await axios.put(endpoint, payload, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteData = async (endpoint) => {
|
|
||||||
const response = await axios.delete(endpoint, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteDataWithBody = async (endpoint, payload) => {
|
|
||||||
const response = await axios.delete(endpoint, {
|
|
||||||
headers: getAuthHeaders(),
|
|
||||||
data: payload,
|
|
||||||
});
|
|
||||||
await fetchData();
|
|
||||||
return response.data.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
dataLoading,
|
dataLoading,
|
||||||
dataError,
|
dataError,
|
||||||
getData,
|
clearError,
|
||||||
postData,
|
getData: (url, params, refresh = true) => requestWrapper("get", url, params, refresh),
|
||||||
postDataValidated,
|
postData: (url, body, refresh = true) => requestWrapper("post", url, body, refresh),
|
||||||
putData,
|
putData: (url, body, refresh = true) => requestWrapper("put", url, body, refresh),
|
||||||
deleteData,
|
deleteData: (url, refresh = true) => requestWrapper("delete", url, null, refresh),
|
||||||
deleteDataWithBody,
|
deleteDataWithBody: (url, body, refresh = true) => requestWrapper("delete", url, body, refresh)
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,28 +5,28 @@ import { createRoot } from 'react-dom/client'
|
|||||||
import App from '@/App.jsx'
|
import App from '@/App.jsx'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { ThemeProvider } from '@/context/ThemeContext'
|
import { ThemeProvider } from '@/context/ThemeContext'
|
||||||
import { AuthProvider } from '@/context/AuthContext'
|
|
||||||
import { ConfigProvider } from '@/context/ConfigContext.jsx'
|
import { ConfigProvider } from '@/context/ConfigContext.jsx'
|
||||||
|
|
||||||
/* CSS */
|
/* CSS */
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css'
|
import 'bootstrap/dist/css/bootstrap.min.css'
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js'
|
||||||
import "slick-carousel/slick/slick.css";
|
import "slick-carousel/slick/slick.css";
|
||||||
import "slick-carousel/slick/slick-theme.css";
|
import "slick-carousel/slick/slick-theme.css";
|
||||||
import '@/css/index.css'
|
import '@/css/index.css'
|
||||||
import { SearchProvider } from './context/SearchContext'
|
import { SearchProvider } from './context/SearchContext'
|
||||||
|
import { ErrorProvider } from './context/ErrorContext'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<AuthProvider>
|
<ErrorProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<SearchProvider>
|
<SearchProvider>
|
||||||
<App />
|
<App />
|
||||||
</SearchProvider>
|
</SearchProvider>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</AuthProvider>
|
</ErrorProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
|||||||
@@ -7,22 +7,21 @@ import { useState } from 'react';
|
|||||||
import { DataProvider } from '@/context/DataContext';
|
import { DataProvider } from '@/context/DataContext';
|
||||||
import NotificationModal from '@/components/NotificationModal';
|
import NotificationModal from '@/components/NotificationModal';
|
||||||
import { useSearch } from "@/context/SearchContext";
|
import { useSearch } from "@/context/SearchContext";
|
||||||
|
import { useError } from '@/context/ErrorContext';
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { config, configLoading } = useConfig();
|
const { config, configLoading } = useConfig();
|
||||||
|
const { showError } = useError();
|
||||||
|
|
||||||
if (configLoading) return <p><LoadingIcon /></p>;
|
if (configLoading) return <p><LoadingIcon /></p>;
|
||||||
|
|
||||||
const reqConfig = {
|
const reqConfig = {
|
||||||
baseUrl: `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.pastes.all}`,
|
baseUrl: `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.pastes.all}`,
|
||||||
params: {
|
params: {}
|
||||||
_sort: 'created_at',
|
|
||||||
_order: 'desc',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataProvider config={reqConfig}>
|
<DataProvider config={reqConfig} onError={showError}>
|
||||||
<HomeContent reqConfig={reqConfig} />
|
<HomeContent reqConfig={reqConfig} />
|
||||||
</DataProvider>
|
</DataProvider>
|
||||||
);
|
);
|
||||||
@@ -45,8 +44,8 @@ const HomeContent = ({ reqConfig }) => {
|
|||||||
const handleSubmit = async (paste) => {
|
const handleSubmit = async (paste) => {
|
||||||
try {
|
try {
|
||||||
const createdPaste = await postData(reqConfig.baseUrl, paste);
|
const createdPaste = await postData(reqConfig.baseUrl, paste);
|
||||||
if (createdPaste && createdPaste.is_private) {
|
if (createdPaste && createdPaste.isPrivate) {
|
||||||
setKey(createdPaste.paste_key);
|
setKey(createdPaste.pasteKey);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
|||||||
Reference in New Issue
Block a user