From bf40b235f067f693554b6392530e3e73f126ebf1 Mon Sep 17 00:00:00 2001
From: Jose
Date: Tue, 17 Mar 2026 03:57:47 +0100
Subject: [PATCH] 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.
---
src/components/AnimatedDropdown.jsx | 92 ------------
src/components/AnimatedDropend.jsx | 122 ----------------
src/components/Auth/IfAuthenticated.jsx | 8 --
src/components/Auth/IfNotAuthenticated.jsx | 8 --
src/components/Auth/IfRole.jsx | 13 --
src/components/Auth/LoginForm.jsx | 120 ----------------
src/components/Auth/ProtectedRoute.jsx | 18 ---
src/components/ContentWrapper.jsx | 15 --
src/components/CustomCarousel.jsx | 47 ------
src/components/CustomContainer.jsx | 15 --
src/components/CustomModal.jsx | 26 ----
src/components/Footer.jsx | 56 --------
src/components/Header.jsx | 19 ---
.../{Auth => Pastes}/PasswordInput.jsx | 0
.../{Auth => Pastes}/PasswordModal.jsx | 5 +-
src/components/Pastes/PastePanel.jsx | 70 ++++++---
src/components/Pastes/PublicPasteItem.jsx | 2 +-
src/context/ConfigContext.jsx | 26 +++-
src/context/DataContext.jsx | 6 +-
src/css/AnimatedDropdown.css | 28 ----
src/css/CustomCarousel.css | 11 --
src/css/Footer.css | 136 ------------------
src/css/Header.css | 43 ------
src/css/LoginForm.css | 53 -------
src/hooks/useAuth.js | 10 +-
src/hooks/useConfig.js | 10 +-
src/hooks/useData.js | 84 ++++++++---
src/hooks/useSessionRenewal.jsx | 103 -------------
src/pages/Building.jsx | 17 ---
src/pages/Home.jsx | 44 +++---
src/util/alertHelpers.jsx | 15 --
vite.config.js | 15 ++
32 files changed, 190 insertions(+), 1047 deletions(-)
delete mode 100644 src/components/AnimatedDropdown.jsx
delete mode 100644 src/components/AnimatedDropend.jsx
delete mode 100644 src/components/Auth/IfAuthenticated.jsx
delete mode 100644 src/components/Auth/IfNotAuthenticated.jsx
delete mode 100644 src/components/Auth/IfRole.jsx
delete mode 100644 src/components/Auth/LoginForm.jsx
delete mode 100644 src/components/Auth/ProtectedRoute.jsx
delete mode 100644 src/components/ContentWrapper.jsx
delete mode 100644 src/components/CustomCarousel.jsx
delete mode 100644 src/components/CustomContainer.jsx
delete mode 100644 src/components/CustomModal.jsx
delete mode 100644 src/components/Footer.jsx
delete mode 100644 src/components/Header.jsx
rename src/components/{Auth => Pastes}/PasswordInput.jsx (100%)
rename src/components/{Auth => Pastes}/PasswordModal.jsx (93%)
delete mode 100644 src/css/AnimatedDropdown.css
delete mode 100644 src/css/CustomCarousel.css
delete mode 100644 src/css/Footer.css
delete mode 100644 src/css/Header.css
delete mode 100644 src/css/LoginForm.css
delete mode 100644 src/hooks/useSessionRenewal.jsx
delete mode 100644 src/pages/Building.jsx
delete mode 100644 src/util/alertHelpers.jsx
diff --git a/src/components/AnimatedDropdown.jsx b/src/components/AnimatedDropdown.jsx
deleted file mode 100644
index c8c0e08..0000000
--- a/src/components/AnimatedDropdown.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import { useState, useRef, useEffect, cloneElement } from 'react';
-import { Button } from 'react-bootstrap';
-import { AnimatePresence, motion as _motion } from 'framer-motion';
-import '../css/AnimatedDropdown.css';
-
-const AnimatedDropdown = ({
- trigger,
- icon,
- variant = "secondary",
- className = "",
- buttonStyle = "",
- show,
- onToggle,
- onMouseEnter,
- onMouseLeave,
- children
-}) => {
- const isControlled = show !== undefined;
- const [open, setOpen] = useState(false);
- const triggerRef = useRef(null);
- const dropdownRef = useRef(null);
-
- const actualOpen = isControlled ? show : open;
-
- const toggle = () => {
- const newState = !actualOpen;
- if (!isControlled) setOpen(newState);
- onToggle?.(newState);
- };
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (
- dropdownRef.current &&
- !dropdownRef.current.contains(e.target) &&
- !triggerRef.current?.contains(e.target)
- ) {
- if (!isControlled) setOpen(false);
- onToggle?.(false);
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, [isControlled, onToggle]);
-
- const triggerElement = trigger
- ? (typeof trigger === "function"
- ? trigger({ onClick: toggle, ref: triggerRef })
- : cloneElement(trigger, { onClick: toggle, ref: triggerRef }))
- : (
-
- {icon}
-
- );
-
- const dropdownClasses = `dropdown-menu show shadow rounded-4 px-2 py-2 ${className}`;
-
- return (
-
- {triggerElement}
-
-
- {actualOpen && (
- <_motion.div
- ref={dropdownRef}
- initial={{ opacity: 0, scale: 0.98 }}
- animate={{ opacity: 1, scale: 1 }}
- exit={{ opacity: 0, scale: 0.98 }}
- transition={{ duration: 0.15 }}
- className={dropdownClasses}
- >
- {typeof children === "function" ? children({ closeDropdown: () => toggle(false) }) : children}
-
- )}
-
-
- );
-};
-
-export default AnimatedDropdown;
diff --git a/src/components/AnimatedDropend.jsx b/src/components/AnimatedDropend.jsx
deleted file mode 100644
index 63e98e5..0000000
--- a/src/components/AnimatedDropend.jsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import { useState, useRef, useEffect, cloneElement } from 'react';
-import { Button } from 'react-bootstrap';
-import { AnimatePresence, motion as _motion } from 'framer-motion';
-import '@/css/AnimatedDropdown.css';
-
-const AnimatedDropend = ({
- trigger,
- icon,
- variant = "secondary",
- className = "",
- buttonStyle = "",
- show,
- onToggle,
- onMouseEnter,
- onMouseLeave,
- children
-}) => {
- const isControlled = show !== undefined;
- const [open, setOpen] = useState(false);
- const triggerRef = useRef(null);
- const dropdownRef = useRef(null);
-
- const actualOpen = isControlled ? show : open;
-
- const toggle = (forceValue) => {
- const newState = typeof forceValue === "boolean" ? forceValue : !actualOpen;
- if (!isControlled) setOpen(newState);
- onToggle?.(newState);
- };
-
- useEffect(() => {
- const handleClickOutside = (e) => {
- if (
- dropdownRef.current &&
- !dropdownRef.current.contains(e.target) &&
- !triggerRef.current?.contains(e.target)
- ) {
- if (!isControlled) setOpen(false);
- onToggle?.(false);
- }
- };
-
- document.addEventListener("mousedown", handleClickOutside);
- return () => document.removeEventListener("mousedown", handleClickOutside);
- }, [isControlled, onToggle]);
-
- const handleMouseEnter = () => {
- if (!isControlled) setOpen(true);
- onToggle?.(true);
- onMouseEnter?.();
- };
-
- const handleMouseLeave = () => {
- if (!isControlled) setOpen(false);
- onToggle?.(false);
- onMouseLeave?.();
- };
-
- const triggerElement = trigger
- ? (typeof trigger === "function"
- ? trigger({
- onClick: e => {
- e.stopPropagation();
- toggle();
- },
- ref: triggerRef
- })
- : cloneElement(trigger, {
- onClick: e => {
- e.stopPropagation();
- toggle();
- },
- ref: triggerRef
- }))
- : (
-
- {icon}
-
- );
-
- const dropdownClasses = `dropdown-menu show shadow rounded-4 px-2 py-2 ${className}`;
-
- return (
-
- {triggerElement}
-
-
- {actualOpen && (
- <_motion.div
- ref={dropdownRef}
- initial={{ opacity: 0, x: -10 }}
- animate={{ opacity: 1, x: 0 }}
- exit={{ opacity: 0, x: -10 }}
- transition={{ duration: 0.15 }}
- className={dropdownClasses}
- style={{
- position: 'absolute',
- top: '0',
- left: '100%',
- zIndex: 1000,
- whiteSpace: 'nowrap'
- }}
- >
- {typeof children === "function" ? children({ closeDropdown: () => toggle(false) }) : children}
-
- )}
-
-
- );
-};
-
-export default AnimatedDropend;
diff --git a/src/components/Auth/IfAuthenticated.jsx b/src/components/Auth/IfAuthenticated.jsx
deleted file mode 100644
index 9504174..0000000
--- a/src/components/Auth/IfAuthenticated.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useAuth } from "@/hooks/useAuth.js";
-
-const IfAuthenticated = ({ children }) => {
- const { authStatus } = useAuth();
- return authStatus === "authenticated" ? children : null;
-};
-
-export default IfAuthenticated;
diff --git a/src/components/Auth/IfNotAuthenticated.jsx b/src/components/Auth/IfNotAuthenticated.jsx
deleted file mode 100644
index 53593c1..0000000
--- a/src/components/Auth/IfNotAuthenticated.jsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useAuth } from "@/hooks/useAuth.js";
-
-const IfNotAuthenticated = ({ children }) => {
- const { authStatus } = useAuth();
- return authStatus === "unauthenticated" ? children : null;
-};
-
-export default IfNotAuthenticated;
diff --git a/src/components/Auth/IfRole.jsx b/src/components/Auth/IfRole.jsx
deleted file mode 100644
index 336791a..0000000
--- a/src/components/Auth/IfRole.jsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { useAuth } from "@/hooks/useAuth.js";
-
-const IfRole = ({ roles, children }) => {
- const { user, authStatus } = useAuth();
-
- if (authStatus !== "authenticated") return null;
-
- const userRole = user?.role;
-
- return roles.includes(userRole) ? children : null;
-};
-
-export default IfRole;
diff --git a/src/components/Auth/LoginForm.jsx b/src/components/Auth/LoginForm.jsx
deleted file mode 100644
index fd707cd..0000000
--- a/src/components/Auth/LoginForm.jsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faUser } from '@fortawesome/free-solid-svg-icons';
-import { Form, Button, Alert, FloatingLabel, Row, Col } from 'react-bootstrap';
-import PasswordInput from '@/components/Auth/PasswordInput.jsx';
-
-import { useContext, useState } from "react";
-import { Link, useNavigate } from "react-router-dom";
-import { AuthContext } from "@/context/AuthContext.jsx";
-
-import CustomContainer from '@/components/CustomContainer.jsx';
-import ContentWrapper from '@/components/ContentWrapper.jsx';
-
-import '@/css/LoginForm.css';
-
-const LoginForm = () => {
- const { login, error } = useContext(AuthContext);
- const navigate = useNavigate();
-
- const [formState, setFormState] = useState({
- emailOrUserName: "",
- password: "",
- keepLoggedIn: false
- });
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setFormState((prev) => ({ ...prev, [name]: value }));
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
-
- const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formState.emailOrUserName);
-
- const loginBody = {
- password: formState.password,
- keepLoggedIn: Boolean(formState.keepLoggedIn),
- };
-
- if (isEmail) {
- loginBody.email = formState.emailOrUserName;
- } else {
- loginBody.userName = formState.emailOrUserName;
- }
-
- try {
- await login(loginBody);
- navigate("/");
- } catch (err) {
- console.error("Error de login:", err.message);
- }
- };
-
- return (
-
-
-
-
-
- );
-};
-
-
-export default LoginForm;
diff --git a/src/components/Auth/ProtectedRoute.jsx b/src/components/Auth/ProtectedRoute.jsx
deleted file mode 100644
index 911d951..0000000
--- a/src/components/Auth/ProtectedRoute.jsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Navigate } from "react-router-dom";
-import { useAuth } from "@/hooks/useAuth.js";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faSpinner } from "@fortawesome/free-solid-svg-icons";
-
-const ProtectedRoute = ({ minimumRoles, children }) => {
- const { authStatus } = useAuth();
-
- if (authStatus === "checking") return ; // o un loader si quieres
- if (authStatus === "unauthenticated") return ;
- if (authStatus === "authenticated" && minimumRoles) {
- const userRole = JSON.parse(localStorage.getItem("user"))?.role;
- if (!minimumRoles.includes(userRole)) return ;
- }
- return children;
-};
-
-export default ProtectedRoute;
diff --git a/src/components/ContentWrapper.jsx b/src/components/ContentWrapper.jsx
deleted file mode 100644
index 22312ea..0000000
--- a/src/components/ContentWrapper.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import PropTypes from 'prop-types';
-
-const ContentWrapper = ({ children }) => {
- return (
-
- {children}
-
- );
-}
-
-ContentWrapper.propTypes = {
- children: PropTypes.node.isRequired,
-}
-
-export default ContentWrapper;
\ No newline at end of file
diff --git a/src/components/CustomCarousel.jsx b/src/components/CustomCarousel.jsx
deleted file mode 100644
index 014e839..0000000
--- a/src/components/CustomCarousel.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import Slider from 'react-slick';
-import '@/css/CustomCarousel.css';
-
-const CustomCarousel = ({ images }) => {
- const settings = {
- dots: false,
- infinite: true,
- speed: 500,
- slidesToShow: 2,
- slidesToScroll: 1,
- arrows: false,
- autoplay: true,
- autoplaySpeed: 3000,
- responsive: [
- {
- breakpoint: 768, // móviles
- settings: {
- slidesToShow: 1,
- arrows: false,
- autoplay: true,
- autoplaySpeed: 3000,
- dots: false,
- infinite: true,
- speed: 500
- }
- }
- ]
- };
-
- return (
-
-
- {images.map((src, index) => (
-
-
-
- ))}
-
-
- );
-};
-
-export default CustomCarousel;
diff --git a/src/components/CustomContainer.jsx b/src/components/CustomContainer.jsx
deleted file mode 100644
index 6d14ffb..0000000
--- a/src/components/CustomContainer.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import PropTypes from 'prop-types';
-
-const CustomContainer = ({ children }) => {
- return (
-
- {children}
-
- );
-}
-
-CustomContainer.propTypes = {
- children: PropTypes.node.isRequired,
-}
-
-export default CustomContainer;
\ No newline at end of file
diff --git a/src/components/CustomModal.jsx b/src/components/CustomModal.jsx
deleted file mode 100644
index 4ab5139..0000000
--- a/src/components/CustomModal.jsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { faXmark } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Modal, Button } from "react-bootstrap";
-
-const CustomModal = ({ show, onClose, title, children }) => {
- return (
-
-
- {title}
-
-
-
-
-
- {children}
-
-
- );
-}
-
-export default CustomModal;
\ No newline at end of file
diff --git a/src/components/Footer.jsx b/src/components/Footer.jsx
deleted file mode 100644
index d05bc76..0000000
--- a/src/components/Footer.jsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
-import '@/css/Footer.css';
-import { faGithub } from '@fortawesome/free-brands-svg-icons';
-
-const Footer = () => {
- const [heart, setHeart] = useState('💜');
-
- useEffect(() => {
- const hearts = ["❤️", "💛", "🧡", "💚", "💙", "💜"];
- const randomHeart = () => hearts[Math.floor(Math.random() * hearts.length)];
-
- const interval = setInterval(() => {
- setHeart(randomHeart());
- }, 3000);
-
- return () => clearInterval(interval);
- }, []);
-
- return (
-
- );
-};
-
-export default Footer;
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
deleted file mode 100644
index 56e400a..0000000
--- a/src/components/Header.jsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import '@/css/Header.css';
-import { Link } from 'react-router-dom';
-
-const Header = () => {
-
- return (
-
-
-
-
-
Tu página web
-
-
-
-
- );
-}
-
-export default Header;
\ No newline at end of file
diff --git a/src/components/Auth/PasswordInput.jsx b/src/components/Pastes/PasswordInput.jsx
similarity index 100%
rename from src/components/Auth/PasswordInput.jsx
rename to src/components/Pastes/PasswordInput.jsx
diff --git a/src/components/Auth/PasswordModal.jsx b/src/components/Pastes/PasswordModal.jsx
similarity index 93%
rename from src/components/Auth/PasswordModal.jsx
rename to src/components/Pastes/PasswordModal.jsx
index fd1a5b0..96b3572 100644
--- a/src/components/Auth/PasswordModal.jsx
+++ b/src/components/Pastes/PasswordModal.jsx
@@ -3,8 +3,7 @@ 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/Auth/PasswordInput';
-import { renderErrorAlert } from '@/util/alertHelpers';
+import PasswordInput from '@/components/Pastes/PasswordInput';
import '@/css/PasswordModal.css';
const PasswordModal = ({
@@ -37,8 +36,6 @@ const PasswordModal = ({
Esta paste está protegida con contraseña. Introduce la clave para continuar.
- {renderErrorAlert(error)}
-
setPassword(e.target.value)}
diff --git a/src/components/Pastes/PastePanel.jsx b/src/components/Pastes/PastePanel.jsx
index bb3b001..e436ac8 100644
--- a/src/components/Pastes/PastePanel.jsx
+++ b/src/components/Pastes/PastePanel.jsx
@@ -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
- {isSaving ? (
+ {connected && (isSaving ? (
Guardando cambios...
@@ -320,7 +350,7 @@ const PastePanel = ({ onSubmit, publicPastes, mode, pasteKey: propKey, onConnect
Cambios guardados
- )}
+ ))}
{
const PublicPasteItem = ({ paste, onSelect }) => {
return (
onSelect(paste.pasteKey)}>
-
{paste.title}
+
{(paste.title ?? "").trim() || "Sin título"}
{trimContent(paste.content, 100)}
{new Date(paste.createdAt).toLocaleString()}
diff --git a/src/context/ConfigContext.jsx b/src/context/ConfigContext.jsx
index 25b59bc..f85b62e 100644
--- a/src/context/ConfigContext.jsx
+++ b/src/context/ConfigContext.jsx
@@ -9,22 +9,36 @@ export const ConfigProvider = ({ children }) => {
const [configError, setError] = useState(null);
useEffect(() => {
+ let isMounted = true;
+
const fetchConfig = async () => {
try {
- const response = import.meta.env.MODE === 'production'
- ? await fetch("/config/settings.prod.json")
- : await fetch("/config/settings.dev.json");
+ const settingsPath = import.meta.env.MODE === 'production'
+ ? "/config/settings.prod.json"
+ : "/config/settings.dev.json";
+
+ const response = await fetch(settingsPath);
if (!response.ok) throw new Error("Error al cargar settings.*.json");
const json = await response.json();
- setConfig(json);
+ if (isMounted) {
+ setConfig(json);
+ }
} catch (err) {
- setError(err.message);
+ if (isMounted) {
+ setError(err.message || "Error al cargar configuración");
+ }
} finally {
- setLoading(false);
+ if (isMounted) {
+ setLoading(false);
+ }
}
};
fetchConfig();
+
+ return () => {
+ isMounted = false;
+ };
}, []);
return (
diff --git a/src/context/DataContext.jsx b/src/context/DataContext.jsx
index 1bd5334..05cc4c5 100644
--- a/src/context/DataContext.jsx
+++ b/src/context/DataContext.jsx
@@ -7,10 +7,12 @@ export const DataContext = createContext();
export const DataProvider = ({ config, children }) => {
const { showError } = useError();
- const data = useData(config, showError);
+ // Centraliza errores HTTP para que cualquier consumidor de DataContext
+ // tenga comportamiento homogéneo sin repetir wiring en cada página.
+ const dataApi = useData(config, showError);
return (
-
+
{children}
);
diff --git a/src/css/AnimatedDropdown.css b/src/css/AnimatedDropdown.css
deleted file mode 100644
index ba10c6e..0000000
--- a/src/css/AnimatedDropdown.css
+++ /dev/null
@@ -1,28 +0,0 @@
-.dropdown-menu .dropdown-divider {
- border-top: 1px solid var(--divider-color);
-}
-
-.dropdown-menu {
- background-color: var(--bg-color) !important;
- color: var(--text-color) !important;
-}
-
-.dropdown-menu.show {
- background-color: var(--navbar-bg) !important;
- box-shadow: 0 5px 10px var(--shadow-color);
-}
-
-.dropdown-item {
- background-color: var(--navbar-bg) !important;
- color: var(--navbar-dropdown-item-color);
- cursor: pointer;
-}
-
-.dropdown-item:hover {
- background-color: var(--navbar-bg) !important;
- color: var(--secondary-color) !important;
-}
-
-.disabled.text-muted {
- color: var(--muted-color) !important;
-}
\ No newline at end of file
diff --git a/src/css/CustomCarousel.css b/src/css/CustomCarousel.css
deleted file mode 100644
index 4e44fca..0000000
--- a/src/css/CustomCarousel.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.carousel-img-wrapper {
- padding: 0.5rem;
-}
-
-.carousel-img {
- width: 100%;
- height: auto;
- border-radius: 1rem;
- max-height: 60vh;
- object-fit: cover;
-}
diff --git a/src/css/Footer.css b/src/css/Footer.css
deleted file mode 100644
index 95fd13e..0000000
--- a/src/css/Footer.css
+++ /dev/null
@@ -1,136 +0,0 @@
-.footer {
- background-color: var(--navbar-bg);
- color: var(--fg-color);
- border-top: 2px solid var(--border-color);
- font-size: 1rem;
- box-shadow: 0 -2px 8px var(--shadow-color);
- position: relative;
- z-index: 1;
-}
-
-.footer-title,
-.footer h6#devd {
- font-family: "Product Sans";
- font-size: 1.8rem;
- font-weight: 700;
- margin-bottom: 1rem;
- color: var(--primary-color);
-}
-
-.footer-columns {
- display: flex;
- flex-direction: column;
- gap: 2rem;
- margin-bottom: 2rem;
- background-color: var(--bg-hover-color);
- padding: 1.5rem;
- border-radius: 1rem;
- box-shadow: 0 4px 10px rgba(0,0,0,0.04);
-}
-
-@media (min-width: 768px) {
- .footer-columns {
- flex-direction: row;
- justify-content: space-between;
- align-items: flex-start;
- }
-}
-
-.footer-column {
- flex: 1;
- min-width: 200px;
-}
-
-.footer-column h5 {
- font-size: 1.1rem;
- margin-bottom: 0.75rem;
- font-weight: 600;
- color: var(--fg-color);
-}
-
-.footer-column ul {
- list-style: none;
- padding: 0;
- margin: 0;
-}
-
-.footer-column ul li {
- margin-bottom: 0.5rem;
-}
-
-.footer-column ul li a {
- color: var(--fg-color);
- text-decoration: none;
-
-}
-
-.footer-column ul li a:hover {
- color: var(--primary-color);
- text-shadow: 0 0 4px currentColor;
-}
-
-.footer-bottom {
- font-size: 0.9rem;
- opacity: 0.85;
- text-align: center;
- border-top: 1px solid var(--divider-color);
-}
-
-.footer-bottom a {
- font-weight: 600;
- text-decoration: none;
- color: var(--fg-color);
-
-}
-
-.footer-bottom a:hover {
- text-shadow: 0 0 5px currentColor;
- color: var(--primary-color);
-}
-
-.contact-info {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
- padding: 1rem;
- border-radius: 1rem;
- font-size: 0.95rem;
- background-color: var(--contact-info-bg);
- color: var(--primary-color);
- box-shadow: 0 4px 10px var(--shadow-color);
-
-}
-
-.contact-info a {
- font-weight: 600;
- text-decoration: none;
- color: var(--primary-color);
- display: inline-flex;
- align-items: center;
- gap: 0.5rem;
-
-}
-
-.contact-info a:hover {
- transform: translateX(8px);
- color: var(--secondary-color);
-}
-
-.contact-info .fa-icon {
- font-size: 1.2rem;
- color: var(--primary-color);
-}
-
-.heart-anim {
- display: inline-block;
- animation: heartbeat 1.5s infinite ease-in-out;
-}
-
-@keyframes heartbeat {
- 0%, 100% {
- transform: scale(1);
- }
- 50% {
- transform: scale(1.15);
- }
-}
diff --git a/src/css/Header.css b/src/css/Header.css
deleted file mode 100644
index 3d51fa8..0000000
--- a/src/css/Header.css
+++ /dev/null
@@ -1,43 +0,0 @@
-/* ================================
- HEADER - ESTILO BASE
-================================== */
-
-.bg-img {
- background-image: url('/images/bg.png');
- background-size: cover;
- background-position: center;
- background-repeat: no-repeat;
-}
-
-/*.mask {
- background-color: var(--header-mask-color);
- backdrop-filter: blur(4px);
- -webkit-backdrop-filter: blur(4px);
-
-}
-*/
-
-.header-title {
- font-family: 'Product Sans';
- font-size: 3em;
- font-weight: bolder;
- color: var(--text-color);
-
-}
-
-.shadowed {
- text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5);
-}
-
-
-/* ================================
- RESPONSIVE HEADER TITLE
-================================== */
-
-@media (max-width: 768px) {
- .header-title {
- font-size: 2em;
- padding: 0 1rem;
- text-align: center;
- }
-}
diff --git a/src/css/LoginForm.css b/src/css/LoginForm.css
deleted file mode 100644
index e6d933d..0000000
--- a/src/css/LoginForm.css
+++ /dev/null
@@ -1,53 +0,0 @@
-/* ================================
- LOGIN - CARD CONTAINER (VISUAL)
-================================== */
-
-.login-card {
- background-color: var(--login-bg) !important;
- color: var(--text-color);
- box-shadow: 0 0 10px var(--shadow-color);
-
-}
-
-/* ================================
- INPUTS VISUALES
- ================================== */
-
-input.form-control {
- background-color: var(--input-bg);
- color: var(--input-text);
- border: 1px solid var(--input-border);
-
-}
-
-/* ================================
- LABELS PERSONALIZADAS
- ================================== */
-
-.form-floating>label {
- font-family: 'Product Sans';
- font-size: 1.1em;
- color: var(--label-color);
-}
-
-.form-floating>label::after {
- background-color: transparent !important;
-}
-
-/* ================================
- BOTÓN VISUAL
- ================================== */
-
-.login-button {
- font-family: 'Product Sans' !important;
- font-size: 1.3em !important;
- font-weight: bold !important;
- background-color: var(--login-btn-bg) !important;
- color: var(--login-btn-text) !important;
-
-}
-
-.login-button:hover {
- background-color: var(--login-btn-hover) !important;
- color: var(--login-btn-text-hover) !important;
-}
\ No newline at end of file
diff --git a/src/hooks/useAuth.js b/src/hooks/useAuth.js
index ad9a9f7..86604bd 100644
--- a/src/hooks/useAuth.js
+++ b/src/hooks/useAuth.js
@@ -1,4 +1,12 @@
import { useContext } from "react";
import { AuthContext } from "@/context/AuthContext";
-export const useAuth = () => useContext(AuthContext);
+export const useAuth = () => {
+ const authContext = useContext(AuthContext);
+
+ if (!authContext) {
+ throw new Error("useAuth debe usarse dentro de AuthProvider");
+ }
+
+ return authContext;
+};
diff --git a/src/hooks/useConfig.js b/src/hooks/useConfig.js
index 024ee84..43e072a 100644
--- a/src/hooks/useConfig.js
+++ b/src/hooks/useConfig.js
@@ -1,4 +1,12 @@
import { useContext } from "react";
import { ConfigContext } from "@/context/ConfigContext.jsx";
-export const useConfig = () => useContext(ConfigContext);
+export const useConfig = () => {
+ const configContext = useContext(ConfigContext);
+
+ if (!configContext) {
+ throw new Error("useConfig debe usarse dentro de ConfigProvider");
+ }
+
+ return configContext;
+};
diff --git a/src/hooks/useData.js b/src/hooks/useData.js
index 193fb20..dfd9cd3 100644
--- a/src/hooks/useData.js
+++ b/src/hooks/useData.js
@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback, useRef } from "react";
+import { useState, useEffect, useCallback } from "react";
import axios from "axios";
export const useData = (config, onError) => {
@@ -6,8 +6,6 @@ export const useData = (config, onError) => {
const [dataLoading, setLoading] = useState(true);
const [dataError, setError] = useState(null);
- const configString = JSON.stringify(config);
-
const getAuthHeaders = (isFormData = false) => {
const token = localStorage.getItem("token");
const headers = {};
@@ -17,14 +15,20 @@ export const useData = (config, onError) => {
};
const handleAxiosError = (err) => {
- const errorData = {
+ return {
status: err.response?.status || (err.request ? "Network Error" : "Client Error"),
message: err.response?.data?.message || err.message || "Error desconocido",
errors: err.response?.data?.errors || null
};
- return errorData;
};
+ const isExpectedPasteLookupError = (baseUrl, status) => {
+ const isPasteLookup = baseUrl?.includes("/pastes/");
+ return isPasteLookup && [403, 404, 500].includes(status);
+ };
+
+ // Carga inicial ligada al `config` del contexto.
+ // En lookup de pastes, algunos estados son esperados y no deben disparar error global.
const fetchData = useCallback(async () => {
if (!config?.baseUrl) return;
@@ -39,25 +43,25 @@ export const useData = (config, onError) => {
setData(response.data);
} catch (err) {
const error = handleAxiosError(err);
- const isPasteLookup = config.baseUrl.includes('/pastes/');
-
- if (isPasteLookup && (error.status === 403 || error.status === 404 || error.status === 500)) {
- console.log("Not in DB, assuming real-time...");
- setError(error);
- } else {
- if (onError) onError(error);
- setError(error);
- }
+ if (!isExpectedPasteLookupError(config.baseUrl, error.status) && onError) onError(error);
+ setError(error);
} finally {
setLoading(false);
}
- }, [configString, onError]);
+ }, [config?.baseUrl, config?.params, onError]);
useEffect(() => {
fetchData();
}, [fetchData]);
- const requestWrapper = async (method, endpoint, payload = null, refresh = false, extraHeaders = {}, silent = false) => {
+ // Wrapper único para peticiones CRUD.
+ // Usa objeto de opciones para mantener llamadas claras y evitar errores por orden de argumentos.
+ const requestWrapper = async (method, endpoint, {
+ payload = null,
+ refresh = false,
+ extraHeaders = {},
+ silent = false,
+ } = {}) => {
try {
const isFormData = payload instanceof FormData;
@@ -98,9 +102,49 @@ export const useData = (config, onError) => {
return {
data, dataLoading, dataError,
- getData: (url, params, refresh = true, h = {}, silent = false) => requestWrapper("get", url, params, refresh, h, silent),
- postData: (url, body, refresh = true, silent = false) => requestWrapper("post", url, body, refresh, silent),
- putData: (url, body, refresh = true, silent = false) => requestWrapper("put", url, body, refresh, silent),
- deleteData: (url, refresh = true, silent = false) => requestWrapper("delete", url, null, refresh, silent),
+ getData: (url, paramsOrOptions, refresh = true, h = {}, silent = false) => {
+ const isOptionsObject =
+ paramsOrOptions &&
+ typeof paramsOrOptions === "object" &&
+ !Array.isArray(paramsOrOptions) &&
+ ("params" in paramsOrOptions || "refresh" in paramsOrOptions || "headers" in paramsOrOptions || "silent" in paramsOrOptions);
+
+ if (isOptionsObject) {
+ const {
+ params = null,
+ refresh: optionsRefresh = true,
+ headers = {},
+ silent: optionsSilent = false,
+ } = paramsOrOptions;
+
+ return requestWrapper("get", url, {
+ payload: params,
+ refresh: optionsRefresh,
+ extraHeaders: headers,
+ silent: optionsSilent,
+ });
+ }
+
+ return requestWrapper("get", url, {
+ payload: paramsOrOptions,
+ refresh,
+ extraHeaders: h,
+ silent,
+ });
+ },
+ postData: (url, body, refresh = true, silent = false) => requestWrapper("post", url, {
+ payload: body,
+ refresh,
+ silent,
+ }),
+ putData: (url, body, refresh = true, silent = false) => requestWrapper("put", url, {
+ payload: body,
+ refresh,
+ silent,
+ }),
+ deleteData: (url, refresh = true, silent = false) => requestWrapper("delete", url, {
+ refresh,
+ silent,
+ }),
};
};
\ No newline at end of file
diff --git a/src/hooks/useSessionRenewal.jsx b/src/hooks/useSessionRenewal.jsx
deleted file mode 100644
index 4566619..0000000
--- a/src/hooks/useSessionRenewal.jsx
+++ /dev/null
@@ -1,103 +0,0 @@
-import { useEffect, useState } from "react";
-import { parseJwt } from "../util/tokenUtils.js";
-import NotificationModal from "../components/NotificationModal.jsx";
-import axios from "axios";
-import { useAuth } from "./useAuth.js";
-import { useConfig } from "./useConfig.js";
-
-const useSessionRenewal = () => {
- const { logout } = useAuth();
- const { config } = useConfig();
-
- const [showModal, setShowModal] = useState(false);
- const [alreadyWarned, setAlreadyWarned] = useState(false);
-
- useEffect(() => {
- const interval = setInterval(() => {
- const token = localStorage.getItem("token");
- const decoded = parseJwt(token);
-
- if (!token || !decoded?.exp) return;
-
- const now = Date.now();
- const expTime = decoded.exp * 1000;
- const timeLeft = expTime - now;
-
- if (timeLeft <= 60000 && timeLeft > 0 && !alreadyWarned) {
- setShowModal(true);
- setAlreadyWarned(true);
- }
-
- if (timeLeft <= 0) {
- clearInterval(interval);
- logout();
- }
- }, 10000); // revisa cada 10 segundos
-
- return () => clearInterval(interval);
- }, [alreadyWarned, logout]);
-
- const handleRenew = async () => {
- const token = localStorage.getItem("token");
- const decoded = parseJwt(token);
- const now = Date.now();
- const expTime = decoded?.exp * 1000;
-
- if (!token || !decoded || now > expTime) {
- logout();
- return;
- }
-
- try {
- const response = await axios.get(
- `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.auth.refreshToken}`,
- null,
- {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- }
- );
-
- const newToken = response.data.data.token;
- localStorage.setItem("token", newToken);
- setShowModal(false);
- setAlreadyWarned(false);
- } catch (err) {
- console.error("Error renovando sesión:", err);
- logout();
- }
- };
-
- const modal = showModal && (
- {
- setShowModal(false);
- logout();
- }}
- title="¿Quieres seguir conectado?"
- message="Tu sesión está a punto de expirar. ¿Quieres renovarla 1 hora más?"
- variant="info"
- buttons={[
- {
- label: "Renovar sesión",
- variant: "success",
- onClick: handleRenew,
- },
- {
- label: "Cerrar sesión",
- variant: "danger",
- onClick: () => {
- logout();
- setShowModal(false);
- },
- },
- ]}
- />
- );
-
- return { modal };
-};
-
-export default useSessionRenewal;
diff --git a/src/pages/Building.jsx b/src/pages/Building.jsx
deleted file mode 100644
index 5a2c4dc..0000000
--- a/src/pages/Building.jsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { useLocation } from 'react-router-dom';
-import '@/css/Building.css';
-
-export default function Building() {
- const location = useLocation();
-
- if (location.pathname === '/') return null;
-
- return (
-
-
🚧
-
Esta página está en construcción
-
Estamos trabajando para traértela pronto
-
-
- );
-}
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 3f681b5..db31b5b 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -7,63 +7,53 @@ import { useState, useMemo } from 'react';
import { DataProvider } from '@/context/DataContext';
import NotificationModal from '@/components/NotificationModal';
import { useSearch } from "@/context/SearchContext";
-import { useError } from '@/context/ErrorContext';
-import { useParams } from 'react-router-dom';
-import { useLocation } from 'react-router-dom';
+import { useLocation, useParams } from 'react-router-dom';
const Home = ({ mode, onConnectChange }) => {
const { pasteKey, rtKey } = useParams();
const { config, configLoading } = useConfig();
- const { showError } = useError();
const location = useLocation();
+ const isStaticMode = mode === 'static';
- const currentKey = mode === 'static' ? pasteKey : rtKey;
+ const currentKey = isStaticMode ? pasteKey : rtKey;
- const reqConfig = useMemo(() => {
+ const requestConfig = useMemo(() => {
if (!config?.apiConfig?.baseUrl) return null;
const baseApi = `${config.apiConfig.baseUrl}${config.apiConfig.endpoints.pastes.all}`;
- if (mode === 'static' && currentKey) {
- return {
- baseUrl: `${baseApi}/s/${currentKey}`,
- params: {}
- };
- }
-
return {
baseUrl: baseApi,
params: {}
};
- }, [config, mode, currentKey]);
+ }, [config]);
if (configLoading) return
;
- if (mode === 'static' && !reqConfig?.baseUrl?.includes('/s/')) {
- return
;
- }
-
return (
-
-
+
+
);
};
-const HomeContent = ({ reqConfig, mode, pasteKey, onConnectChange }) => {
+const HomeContent = ({ requestConfig, mode, pasteKey, onConnectChange }) => {
const { data, dataLoading, postData } = useDataContext();
const [createdKey, setCreatedKey] = useState(null);
const { searchTerm } = useSearch();
+ const isStaticMode = mode === 'static';
- if (mode === 'static' && dataLoading) return
;
+ const filteredPublicPastes = useMemo(() => {
+ if (!Array.isArray(data)) return [];
+ const normalizedSearchTerm = (searchTerm ?? "").toLowerCase();
+ return data.filter((paste) => (paste.title ?? "").toLowerCase().includes(normalizedSearchTerm));
+ }, [data, searchTerm]);
- const filtered = (data && Array.isArray(data)) ? data.filter(paste =>
- paste.title.toLowerCase().includes((searchTerm ?? "").toLowerCase())
- ) : [];
+ if (isStaticMode && dataLoading) return
;
const handleSubmit = async (paste, isAutosave = false) => {
try {
- const createdPaste = await postData(reqConfig.baseUrl, paste);
+ const createdPaste = await postData(requestConfig.baseUrl, paste);
if (!isAutosave && createdPaste && !paste.pasteKey) {
setCreatedKey(createdPaste.pasteKey);
}
@@ -76,7 +66,7 @@ const HomeContent = ({ reqConfig, mode, pasteKey, onConnectChange }) => {
<>
{
- const { className = 'alert alert-danger alert-dismissible py-1 px-2 small', role = 'alert' } = options;
-
- if (!error) return null;
-
- return (
-
- {typeof error === 'string' ? error : 'An unexpected error occurred.'}
-
- );
-};
-
-export const resetErrorIfEditEnds = (editMode, setError) => {
- if (!editMode) setError(null);
-};
diff --git a/vite.config.js b/vite.config.js
index 3617408..0460669 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -16,5 +16,20 @@ export default defineConfig({
},
define: {
global: 'window'
+ },
+ build: {
+ rollupOptions: {
+ output: {
+ manualChunks(id) {
+ if (id.includes('node_modules')) {
+ if (id.includes('monaco-editor')) {
+ return 'monaco';
+ }
+ return 'vendor';
+ }
+ }
+ }
+ },
+ chunkSizeWarningLimit: 1500,
}
})