refactor: remove unused components and styles; consolidate password handling in new components

- Deleted ProtectedRoute, ContentWrapper, CustomCarousel, CustomContainer, CustomModal, Footer, Header, and Building components as they were no longer needed.
- Removed associated CSS files for the deleted components.
- Introduced PasswordInput and PasswordModal components to handle password input and modal display for protected pastes.
- Updated PastePanel to utilize new PasswordInput and PasswordModal components for better password management.
- Refactored Home component to streamline data fetching and improve readability.
- Enhanced error handling in useData hook and improved session management logic.
This commit is contained in:
2026-03-17 03:57:47 +01:00
parent fcd477e876
commit bf40b235f0
32 changed files with 190 additions and 1047 deletions

View File

@@ -0,0 +1,49 @@
import { useState } from 'react';
import { Form, FloatingLabel, Button } from 'react-bootstrap';
import '@/css/PasswordInput.css';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEye, faEyeSlash, faKey } from '@fortawesome/free-solid-svg-icons';
const PasswordInput = ({ disabled, value, onChange, name = "password" }) => {
const [show, setShow] = useState(false);
const toggleShow = () => setShow(prev => !prev);
return (
<div className="position-relative w-100">
<FloatingLabel
controlId="passwordInput"
label={
<>
<FontAwesomeIcon icon={faKey} className="me-2" />
Contraseña
</>
}
>
<Form.Control
type={show ? "text" : "password"}
name={name}
value={value}
placeholder=""
onChange={onChange}
className="rounded-4 pe-5"
disabled={disabled}
/>
</FloatingLabel>
<Button
variant="link"
className="show-button position-absolute end-0 top-50 translate-middle-y me-2"
onClick={toggleShow}
aria-label="Mostrar contraseña"
tabIndex={-1}
style={{ zIndex: 2 }}
>
<FontAwesomeIcon icon={show ? faEyeSlash : faEye} className='fa-lg' />
</Button>
</div>
);
};
export default PasswordInput;

View File

@@ -0,0 +1,75 @@
import PropTypes from 'prop-types';
import { Modal, Button, Form } from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons';
import { useState } from 'react';
import PasswordInput from '@/components/Pastes/PasswordInput';
import '@/css/PasswordModal.css';
const PasswordModal = ({
show,
onClose,
onSubmit,
error = null,
loading = false
}) => {
const [password, setPassword] = useState("");
const handleSubmit = () => {
if (password.trim() === "") return;
onSubmit(password);
};
return (
<Modal show={show} onHide={onClose} centered>
<Modal.Header
style={{ backgroundColor: "var(--modal-bg)" }}
>
<Modal.Title>
<FontAwesomeIcon icon={faLock} className="me-2" />
Paste protegida
</Modal.Title>
</Modal.Header>
<Modal.Body style={{ backgroundColor: "var(--modal-body-bg)" }}>
<p className="mb-3">
Esta paste está protegida con contraseña. Introduce la clave para continuar.
</p>
<PasswordInput
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose} className='dialog-btn'>
Cancelar
</Button>
<Button
className='dialog-btn'
variant="primary"
onClick={handleSubmit}
disabled={loading || password.trim() === ""}
style={{
backgroundColor: "var(--btn-bg)",
borderColor: "var(--btn-bg)",
color: "var(--btn-text)"
}}
>
{loading ? "Verificando..." : "Acceder"}
</Button>
</Modal.Footer>
</Modal>
);
};
PasswordModal.propTypes = {
show: PropTypes.bool.isRequired,
onClose: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
loading: PropTypes.bool
};
export default PasswordModal;

View File

@@ -1,21 +1,33 @@
import { useState, useEffect, useRef } from "react";
import { Form, Button, Row, Col, FloatingLabel, Alert } from "react-bootstrap";
import { Form, Button, Row, Col, FloatingLabel } from "react-bootstrap";
import '@/css/PastePanel.css';
import PasswordInput from "@/components/Auth/PasswordInput";
import PasswordInput from "@/components/Pastes/PasswordInput";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircle, faCode, faHeader } from "@fortawesome/free-solid-svg-icons";
import CodeEditor from "./CodeEditor";
import PublicPasteItem from "./PublicPasteItem";
import { useParams, useNavigate } from "react-router-dom";
import { useDataContext } from "@/hooks/useDataContext";
import PasswordModal from "@/components/Auth/PasswordModal.jsx";
import { useError } from '@/context/ErrorContext';
import PasswordModal from "@/components/Pastes/PasswordModal.jsx";
import { Client } from "@stomp/stompjs";
import SockJS from 'sockjs-client';
const INITIAL_FORM_DATA = {
title: "",
content: "",
syntax: "",
burnAfter: false,
isPrivate: false,
isRt: false,
password: ""
};
const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnectChange }) => {
const { pasteKey: urlPasteKey, rtKey } = useParams();
const navigate = useNavigate();
const { getData } = useDataContext();
const { showError } = useError();
const activeKey = propKey || urlPasteKey || rtKey;
@@ -26,32 +38,30 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
const [stompClient, setStompClient] = useState(null);
const [connected, setConnected] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [formData, setFormData] = useState({
title: "",
content: "",
syntax: "",
burnAfter: false,
isPrivate: false,
isRt: false,
password: ""
});
const [formData, setFormData] = useState({ ...INITIAL_FORM_DATA });
const lastSavedContent = useRef(formData.content);
const isReadOnly = !!selectedPaste || mode === 'rt';
const isRemoteChange = useRef(false);
// Sincroniza el panel cuando cambia el modo o la clave activa:
// - modo static: intenta cargar la paste seleccionada
// - modo create: reinicia todo el formulario y errores
useEffect(() => {
if (mode === 'static' && activeKey) {
fetchPaste(activeKey);
} else if (mode === 'create') {
setSelectedPaste(null);
setFormData({ title: "", content: "", syntax: "", burnAfter: false, isPrivate: false, password: "" });
setFormData({ ...INITIAL_FORM_DATA });
setFieldErrors({});
setEditorErrors([]);
}
}, [activeKey, mode]);
// Gestiona el ciclo de vida del WebSocket en tiempo real:
// conecta al entrar en modo rt y limpia la conexión al salir.
// Los cambios remotos marcan `isRemoteChange` para no disparar autosave en bucle.
useEffect(() => {
if (mode === 'rt' && activeKey) {
const socketUrl = import.meta.env.MODE === 'production'
@@ -98,6 +108,8 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
}
}, [mode, activeKey]);
// Autosave con debounce en sesiones RT:
// solo guarda cuando el contenido local cambia y evita guardar cambios que vienen del socket.
useEffect(() => {
if (mode === 'rt' && connected && formData.content) {
@@ -132,6 +144,7 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
}
}, [formData.content, mode, connected, activeKey]);
// Actualiza estado local y, si hay sesión RT activa, propaga el cambio al resto de clientes.
const handleChange = (key, value) => {
const updatedData = { ...formData, [key]: value, isRt: mode === 'rt' };
@@ -150,8 +163,16 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
setFieldErrors({});
setEditorErrors([]);
const normalizedTitle = (formData.title ?? "").trim();
const payload = {
...formData,
title: formData.isPrivate
? (normalizedTitle || "Sin título")
: formData.title
};
try {
if (onSubmit) await onSubmit(formData);
if (onSubmit) await onSubmit(payload);
} catch (error) {
if (error.status === 422 && error.errors) {
const newFieldErrors = {};
@@ -171,6 +192,10 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
const handleSelectPaste = (key) => navigate(`/s/${key}`);
// Lookup de paste estática:
// - 403: pide contraseña
// - 404: redirige al inicio
// Se hace en modo silencioso para que no abra el modal global en errores esperados.
const fetchPaste = async (key, pwd = "") => {
const url = import.meta.env.MODE === 'production'
? `https://api.miarma.net/v2/mpaste/pastes/s/${key}`
@@ -179,18 +204,23 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
const headers = pwd ? { "X-Paste-Password": pwd } : {};
try {
const response = await getData(url, null, false, headers, true);
const response = await getData(url, {
params: null,
refresh: false,
headers,
silent: true,
});
if (response) {
setSelectedPaste(response);
setShowPasswordModal(false);
setFormData({
title: response.title ?? "",
...INITIAL_FORM_DATA,
title: (response.title ?? "").trim() || "Sin título",
content: response.content ?? "",
syntax: response.syntax || "plaintext",
burnAfter: response.burnAfter || false,
isPrivate: response.isPrivate || false,
password: ""
isPrivate: response.isPrivate || false
});
}
} catch (error) {
@@ -311,7 +341,7 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
</FloatingLabel>
<div className="d-flex align-items-center ms-1">
{isSaving ? (
{connected && (isSaving ? (
<span className="text-muted" style={{ fontSize: '0.8rem' }}>
<FontAwesomeIcon icon={faCircle} className="pulse-animation me-2" style={{ color: '#ffc107', fontSize: '8px' }} />
Guardando cambios...
@@ -320,7 +350,7 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
<span className="text-success" style={{ fontSize: '0.8rem' }}>
Cambios guardados
</span>
)}
))}
</div>
<Form.Check

View File

@@ -9,7 +9,7 @@ const trimContent = (text, maxLength = 80) => {
const PublicPasteItem = ({ paste, onSelect }) => {
return (
<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 ?? "").trim() || "Sin título"}</h5>
<p className="m-0 text-truncate">{trimContent(paste.content, 100)}</p>
<small className="custom-text-muted">
{new Date(paste.createdAt).toLocaleString()}