DataScope26/WEB
0
1import { useAuth } from "../context/AuthContext.jsx";2import { useTheme } from "../context/ThemeContext.jsx";3import { useNavigate } from "react-router-dom";4import { useState, useRef, useEffect, useCallback } from "react";5import "./Settings.css";6import perfilGenerico from "../assets/perfilGenerico.png";7import ButtonPrimary from "./ButtonPrimary.jsx";8import ButtonAlert from "./ButtonAlert.jsx";9import { Loader, Sun, Moon, Pencil, Camera, AlertTriangle, Save, Trash2 } from "lucide-react";10 11// Tamaño (en px) del área de recorte cuadrada que se muestra en el popup12const CROP_SIZE = 280;13 14// Tamaño (en px) del canvas de salida que se envía al backend15const OUTPUT_SIZE = 500;16 17export default function Settings() {18 const { user, logout, updateUserData, uploadPfp, deleteProfile, changePassword, changeEmail } = useAuth();19 const { theme, setTheme } = useTheme();20 const navigate = useNavigate();21 22 const [username, setUsername] = useState(user.username || "");23 const [email, setEmail] = useState(user.email || "");24 const [originalUsername, setOriginalUsername] = useState(user.username || "");25 const [isEditingUsername, setIsEditingUsername] = useState(false);26 const usernameInputRef = useRef(null);27 28 const [usernameError, setUsernameError] = useState("");29 const [emailError, setEmailError] = useState("");30 const [message, setMessage] = useState("");31 32 useEffect(() => {33 const nextUsername = user.username || "";34 setUsername(nextUsername);35 setOriginalUsername(nextUsername);36 37 if (!isEditingUsername) {38 setUsername(nextUsername);39 }40 }, [user.username, isEditingUsername]);41 42 useEffect(() => {43 if (isEditingUsername) {44 usernameInputRef.current?.focus();45 usernameInputRef.current?.select();46 }47 }, [isEditingUsername]);48 49 // --- Estado del popup de foto de perfil ---50 const [showPfpPopup, setShowPfpPopup] = useState(false);51 const [selectedImageUrl, setSelectedImageUrl] = useState(null);52 const [scale, setScale] = useState(1);53 const [minScale, setMinScale] = useState(1);54 const [offset, setOffset] = useState({ x: 0, y: 0 });55 const [isSaving, setIsSaving] = useState(false);56 const [showDeleteModal, setShowDeleteModal] = useState(false);57 58 const fileInputRef = useRef(null);59 const canvasRef = useRef(null);60 const imgElRef = useRef(null);61 const isDraggingRef = useRef(false);62 const lastPosRef = useRef({ x: 0, y: 0 });63 64 // =========================65 // VALIDACIONES66 // =========================67 68 const validateUsername = (value) => {69 if (!value.trim()) {70 return "El nombre no puede estar vacío.";71 }72 73 if (value.trim().length > 50) {74 return "El nombre no puede superar los 50 caracteres.";75 }76 77 if (!/[A-Za-zÀ-ÿ]/.test(value.trim())) {78 return "El nombre debe contener al menos una letra.";79 }80 81 if (!/^[a-zA-ZÀ-ÿ0-9 _-]+$/.test(value.trim())) {82 return "El nombre solo puede contener letras, números, espacios, guiones y guiones bajos.";83 }84 85 return "";86 };87 88 const validateEmail = (value) => {89 const normalizedValue = value.trim();90 91 if (!normalizedValue) {92 return "El correo electrónico no puede estar vacío.";93 }94 95 if (!/[A-Za-z]/.test(normalizedValue)) {96 return "El email debe tener al menos una letra.";97 }98 99 const parts = normalizedValue.split("@");100 101 if (parts.length !== 2) {102 return "Falta el símbolo @.";103 }104 105 const [localPart, domain] = parts;106 107 const localPartRegex =108 /^[A-Za-z0-9](?:[A-Za-z0-9._%+-]*[A-Za-z0-9])?$/;109 110 if (!localPartRegex.test(localPart)) {111 return "Parte del correo antes del @ inválida.";112 }113 114 const domainRegex = /^[a-zA-Z]{2,10}\.[A-Za-z]{2,5}(\.[A-Za-z]{2,3})?$/;115 116 if (!domainRegex.test(domain)) {117 return "Dominio del correo inválido.";118 }119 120 return "";121 };122 123 const handleUsernameChange = (e) => {124 const value = e.target.value;125 126 setUsername(value);127 setUsernameError(validateUsername(value));128 setMessage("");129 };130 131 const validateProfileFields = () => {132 const usernameErr = validateUsername(username);133 const emailErr = validateEmail(email);134 135 setUsernameError(usernameErr);136 setEmailError(emailErr);137 138 if (usernameErr || emailErr) {139 setMessage("");140 return false;141 }142 143 return true;144 };145 146 const handleUsernameSave = () => {147 const trimmedValue = username.trim();148 const validationError = validateUsername(trimmedValue);149 150 if (validationError) {151 setUsernameError(validationError);152 return;153 }154 155 setUsernameError("");156 updateUserData(trimmedValue);157 setOriginalUsername(trimmedValue);158 setIsEditingUsername(false);159 setMessage("");160 };161 162 const handleUsernameRevert = () => {163 setUsername(originalUsername);164 setUsernameError("");165 setIsEditingUsername(false);166 setMessage("");167 };168 169 const handleUsernameKeyDown = (e) => {170 if (e.key === "Enter") {171 e.preventDefault();172 handleUsernameSave();173 }174 };175 176 // =========================177 // PERFIL178 // =========================179 180 181 182 const handleLogout = () => {183 logout();184 navigate("/");185 };186 187 const handleDeleteProfile = async () => {188 try {189 await deleteProfile();190 navigate("/");191 } catch (err) {192 console.error(err);193 }194 };195 196 const handleThemeChange = (nextTheme) => {197 setTheme(nextTheme);198 setMessage("");199 };200 201 // --- change password/email modals ---202 const [showChangePassword, setShowChangePassword] = useState(false);203 const [showChangeEmail, setShowChangeEmail] = useState(false);204 205 const [cp_current, setCpCurrent] = useState("");206 const [cp_new, setCpNew] = useState("");207 const [cp_confirm, setCpConfirm] = useState("");208 const [cp_error, setCpError] = useState("");209 const [cp_loading, setCpLoading] = useState(false);210 211 const [ce_current, setCeCurrent] = useState("");212 const [ce_new, setCeNew] = useState("");213 const [ce_confirm, setCeConfirm] = useState("");214 const [ce_error, setCeError] = useState("");215 const [ce_loading, setCeLoading] = useState(false);216 217 218 219 const resetPasswordModal = () => {220 setCpCurrent(""); setCpNew(""); setCpConfirm(""); setCpError(""); setCpLoading(false);221 };222 223 const resetEmailModal = () => {224 setCeCurrent(""); setCeNew(""); setCeConfirm(""); setCeError(""); setCeLoading(false);225 };226 227 const submitChangePassword = async () => {228 setCpError("");229 if (!cp_current || !cp_new || !cp_confirm) { setCpError('Completa todos los campos'); return; }230 if (cp_new !== cp_confirm) { setCpError('La nueva contraseña no coincide'); return; }231 if (cp_new.length < 6) { setCpError('La contraseña debe tener al menos 6 caracteres'); return; }232 233 try {234 setCpLoading(true);235 await changePassword(cp_current, cp_new);236 resetPasswordModal();237 setShowChangePassword(false);238 setMessage('Contraseña cambiada correctamente');239 } catch (err) {240 setCpError(err?.response?.data?.error || err.message || 'Error al cambiar la contraseña');241 } finally {242 setCpLoading(false);243 }244 };245 246 const validateEmailLocal = (value) => {247 if (!value || !/[A-Za-z]/.test(value)) return 'El email debe contener al menos una letra.';248 if (!value.includes('@')) return 'Formato de email inválido.';249 return null;250 };251 252 const submitChangeEmail = async () => {253 setCeError("");254 const v = validateEmailLocal(ce_new);255 if (!ce_current || !ce_new || !ce_confirm) { setCeError('Completa todos los campos'); return; }256 if (ce_new !== ce_confirm) { setCeError('El nuevo email no coincide'); return; }257 if (v) { setCeError(v); return; }258 259 try {260 setCeLoading(true);261 await changeEmail(ce_current, ce_new);262 resetEmailModal();263 setShowChangeEmail(false);264 setMessage('Email cambiado correctamente');265 } catch (err) {266 setCeError(err?.response?.data?.error || err.message || 'Error al cambiar el email');267 } finally {268 setCeLoading(false);269 }270 };271 272 // =========================273 // FOTO DE PERFIL274 // =========================275 276 // Ahora, en lugar de abrir el popup primero, abrimos directamente277 // el explorador de archivos. El popup de recorte se muestra recién278 // cuando el usuario ya eligió una imagen (ver handleFileChosen).279 const openPfpPopup = () => {280 setSelectedImageUrl(null);281 setScale(1);282 setMinScale(1);283 setOffset({ x: 0, y: 0 });284 triggerFileSelect();285 };286 287 const closePfpPopup = () => {288 if (isSaving) return;289 290 if (selectedImageUrl) {291 URL.revokeObjectURL(selectedImageUrl);292 }293 294 setSelectedImageUrl(null);295 setShowPfpPopup(false);296 };297 298 const triggerFileSelect = () => {299 fileInputRef.current?.click();300 };301 302 const handleFileChosen = (e) => {303 const file = e.target.files[0];304 305 if (!file) return;306 307 // Solo permitir imágenes308 if (!file.type.startsWith("image/")) {309 return;310 }311 312 const url = URL.createObjectURL(file);313 314 // Liberar la URL anterior si existía315 if (selectedImageUrl) {316 URL.revokeObjectURL(selectedImageUrl);317 }318 319 setSelectedImageUrl(url);320 setScale(1);321 setMinScale(1);322 setOffset({ x: 0, y: 0 });323 324 // Una vez elegida la imagen, recién ahí mostramos el popup de recorte325 setShowPfpPopup(true);326 327 // Permite volver a seleccionar el mismo archivo328 e.target.value = "";329 };330 331 // =========================332 // CARGA DE IMAGEN333 // =========================334 335 useEffect(() => {336 if (!selectedImageUrl) {337 imgElRef.current = null;338 return;339 }340 341 const img = new Image();342 343 img.onload = () => {344 imgElRef.current = img;345 346 // Escala mínima para cubrir todo el cuadrado347 const coverScale = Math.max(348 CROP_SIZE / img.width,349 CROP_SIZE / img.height350 );351 352 setMinScale(coverScale);353 setScale(coverScale);354 355 // Centrar imagen356 const drawWidth = img.width * coverScale;357 const drawHeight = img.height * coverScale;358 359 setOffset({360 x: (CROP_SIZE - drawWidth) / 2,361 y: (CROP_SIZE - drawHeight) / 2,362 });363 };364 365 img.src = selectedImageUrl;366 }, [selectedImageUrl]);367 368 // =========================369 // LIMITAR MOVIMIENTO370 // =========================371 372 const clampOffset = useCallback((nextOffset, currentScale) => {373 const img = imgElRef.current;374 375 if (!img) {376 return nextOffset;377 }378 379 const drawWidth = img.width * currentScale;380 const drawHeight = img.height * currentScale;381 382 const minX = CROP_SIZE - drawWidth;383 const minY = CROP_SIZE - drawHeight;384 385 return {386 x: Math.min(0, Math.max(minX, nextOffset.x)),387 y: Math.min(0, Math.max(minY, nextOffset.y)),388 };389 }, []);390 391 // =========================392 // CANVAS PREVIEW393 // =========================394 395 useEffect(() => {396 const canvas = canvasRef.current;397 const img = imgElRef.current;398 399 if (!canvas || !img || !selectedImageUrl) {400 return;401 }402 403 const ctx = canvas.getContext("2d");404 405 ctx.clearRect(0, 0, CROP_SIZE, CROP_SIZE);406 407 ctx.drawImage(408 img,409 offset.x,410 offset.y,411 img.width * scale,412 img.height * scale413 );414 }, [scale, offset, selectedImageUrl]);415 416 // =========================417 // ZOOM418 // =========================419 420 const handleZoomChange = (e) => {421 const newScale = parseFloat(e.target.value);422 const center = CROP_SIZE / 2;423 424 // Punto de la imagen que actualmente está en el centro del círculo de recorte.425 const imgCenterX = (center - offset.x) / scale;426 const imgCenterY = (center - offset.y) / scale;427 428 // Recalculamos el offset para que ese mismo punto siga en el centro429 // después de aplicar el nuevo scale (zoom centrado, no desde la esquina).430 const nextOffset = {431 x: center - imgCenterX * newScale,432 y: center - imgCenterY * newScale,433 };434 435 setScale(newScale);436 setOffset(clampOffset(nextOffset, newScale));437 };438 439 // =========================440 // DRAG / PAN441 // =========================442 443 const handlePointerDown = (e) => {444 if (!imgElRef.current) return;445 446 isDraggingRef.current = true;447 448 const point = e.touches ? e.touches[0] : e;449 450 lastPosRef.current = {451 x: point.clientX,452 y: point.clientY,453 };454 };455 456 const handlePointerMove = (e) => {457 if (!isDraggingRef.current || !imgElRef.current) {458 return;459 }460 461 const point = e.touches ? e.touches[0] : e;462 463 const dx = point.clientX - lastPosRef.current.x;464 const dy = point.clientY - lastPosRef.current.y;465 466 lastPosRef.current = {467 x: point.clientX,468 y: point.clientY,469 };470 471 setOffset((prev) =>472 clampOffset(473 {474 x: prev.x + dx,475 y: prev.y + dy,476 },477 scale478 )479 );480 };481 482 const handlePointerUp = () => {483 isDraggingRef.current = false;484 };485 486 // =========================487 // GUARDAR FOTO RECORTADA488 // =========================489 490 const handleSaveCroppedPhoto = async () => {491 const img = imgElRef.current;492 493 if (!img) return;494 495 setIsSaving(true);496 497 try {498 const outputCanvas = document.createElement("canvas");499 500 outputCanvas.width = OUTPUT_SIZE;501 outputCanvas.height = OUTPUT_SIZE;502 503 const ctx = outputCanvas.getContext("2d");504 505 ctx.imageSmoothingEnabled = true;506 ctx.imageSmoothingQuality = "high";507 508 const ratio = OUTPUT_SIZE / CROP_SIZE;509 510 ctx.drawImage(511 img,512 offset.x * ratio,513 offset.y * ratio,514 img.width * scale * ratio,515 img.height * scale * ratio516 );517 518 // Convertir directamente a WebP519 const webpBlob = await new Promise((resolve, reject) => {520 outputCanvas.toBlob(521 (blob) => {522 if (!blob) {523 reject(524 new Error("Failed to create WebP image")525 );526 return;527 }528 529 resolve(blob);530 },531 "image/webp",532 0.9533 );534 });535 536 const formData = new FormData();537 538 formData.append(539 "image",540 webpBlob,541 "profile-picture.webp"542 );543 544 await uploadPfp(formData);545 546 if (selectedImageUrl) {547 URL.revokeObjectURL(selectedImageUrl);548 }549 550 setSelectedImageUrl(null);551 setShowPfpPopup(false);552 553 navigate("/");554 } catch (err) {555 console.error(err);556 } finally {557 setIsSaving(false);558 }559 };560 561 return (562 <>563 {/* Input de archivo global: debe existir siempre en el DOM para564 poder abrirse desde el botón "Cambiar foto" antes de que el565 popup de recorte se muestre. */}566 <input567 ref={fileInputRef}568 type="file"569 accept="image/*"570 onChange={handleFileChosen}571 style={{ display: "none" }}572 />573 574 <div className="profile-settings-layout">575 <div className="perfil-card">576 <div className="card-heading">577 <span className="card-heading-icon">578 <svg579 width="16"580 height="16"581 viewBox="0 0 24 24"582 fill="none"583 stroke="currentColor"584 strokeWidth="2"585 strokeLinecap="round"586 strokeLinejoin="round"587 >588 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />589 <circle cx="12" cy="7" r="4" />590 </svg>591 </span>592 593 <h2 className="card-title">Datos de tu perfil</h2>594 </div>595 596 <div className="form-grid">597 <div className="profile-picture-container">598 <div className="profile-picture-shell">599 <img600 className="change-profile-pic"601 src={602 user.profile_pic?.trim()603 ? user.profile_pic604 : perfilGenerico605 }606 alt="foto perfil"607 />608 </div>609 610 <ButtonPrimary611 type="button"612 className="profile-picture-action"613 onClick={openPfpPopup}614 >615 <Camera size={14} />616 <span>Cambiar foto</span>617 </ButtonPrimary>618 </div>619 620 <div className="form-left">621 <div className="form-field">622 <label className="form-label">NOMBRE</label>623 <div className="username-editor-row">624 <input625 ref={usernameInputRef}626 className="form-input"627 type="text"628 value={username}629 onChange={handleUsernameChange}630 onKeyDown={handleUsernameKeyDown}631 maxLength={50}632 readOnly={!isEditingUsername}633 style={634 usernameError635 ? {636 borderColor: "#e53e3e",637 }638 : {}639 }640 />641 642 {isEditingUsername ? (643 <div className="username-edit-actions">644 <ButtonPrimary645 type="button"646 className="icon-only-button"647 onClick={handleUsernameSave}648 aria-label="Guardar nombre"649 title="Guardar nombre"650 >651 <Save size={15} />652 </ButtonPrimary>653 <ButtonAlert654 type="button"655 className="icon-only-button"656 onClick={handleUsernameRevert}657 aria-label="Revertir nombre"658 title="Revertir nombre"659 >660 <Trash2 size={15} />661 </ButtonAlert>662 </div>663 ) : (664 <ButtonPrimary665 type="button"666 className="icon-only-button"667 onClick={() => setIsEditingUsername(true)}668 aria-label="Editar nombre"669 title="Editar nombre"670 >671 <Pencil size={15} />672 </ButtonPrimary>673 )}674 </div>675 {usernameError && <span className="field-error">{usernameError}</span>}676 </div>677 678 <div className="form-field">679 <label className="form-label">CORREO ELECTRÓNICO</label>680 <div className="email-editor-row">681 <div className="form-input form-readonly" aria-readonly="true">682 {user.email || "Sin correo asociado"}683 </div>684 <ButtonPrimary685 type="button"686 className="icon-only-button"687 onClick={() => setShowChangeEmail(true)}688 aria-label="Cambiar correo"689 title="Cambiar correo"690 >691 <Pencil size={15} />692 </ButtonPrimary>693 </div>694 {emailError && <span className="field-error">{emailError}</span>}695 </div>696 </div>697 </div>698 699 <div className="card-actions" style={{ display: "flex", gap: 10, alignItems: "center", justifyContent: "flex-end" }}>700 <div style={{ marginLeft: "auto" }}>701 <ButtonAlert className="btn-logout" onClick={handleLogout}>Logout</ButtonAlert>702 </div>703 </div>704 705 {message && (706 <p707 className="form-error"708 style={{709 margin: 0,710 color: "var(--color-danger)",711 fontSize: "12px",712 fontWeight: 600,713 }}714 >715 {message}716 </p>717 )}718 </div>719 720 <div className="settings-side-column">721 <div className="profile-aspect-card">722 <div className="profile-security-header">723 <div className="profile-security-title-wrap">724 725 <h3 className="profile-security-title">Opciones de aspecto</h3>726 </div>727 </div>728 729 <div className="theme-picker" aria-label="Selector de tema">730 <div className="theme-switch-row">731 <Sun732 size={18}733 className={`theme-icon ${theme !== "dark" ? "theme-icon--active" : ""}`}734 />735 <div736 role="switch"737 tabIndex={0}738 className={`theme-switch ${theme === "dark" ? "theme-switch--dark" : ""}`}739 onClick={() => handleThemeChange(theme === "dark" ? "light" : "dark")}740 onKeyDown={(e) => {741 if (e.key === "Enter" || e.key === " ") {742 handleThemeChange(theme === "dark" ? "light" : "dark");743 }744 }}745 aria-checked={theme === "dark"}746 >747 <div className="theme-switch__knob" />748 </div>749 <Moon750 size={18}751 className={`theme-icon ${theme === "dark" ? "theme-icon--active" : ""}`}752 />753 </div>754 </div>755 </div>756 757 <div className="profile-security-card">758 <div className="profile-security-header">759 <div className="profile-security-title-wrap">760 <AlertTriangle size={16} className="profile-security-icon" />761 <h3 className="profile-security-title">Zona de Peligro</h3>762 </div>763 </div>764 765 <p className="profile-security-text">766 Actualiza tu contraseña cuando lo necesites. Esta acción ayuda a mantener tus credenciales protegidas y tus accesos bajo control.767 </p>768 769 <div className="profile-security-actions">770 <ButtonPrimary771 className="profile-password-button"772 onClick={() => setShowChangePassword(true)}773 >774 Cambiar contraseña775 </ButtonPrimary>776 </div>777 </div>778 </div>779 </div>780 781 {/* =========================782 POPUP FOTO DE PERFIL (RECORTE)783 Solo se muestra una vez que el usuario ya eligió una imagen784 desde el explorador de archivos.785 ========================= */}786 787 {showPfpPopup && selectedImageUrl && (788 <div789 className="pfp-modal-overlay"790 onClick={closePfpPopup}791 >792 <div793 className="pfp-modal-content"794 onClick={(e) => e.stopPropagation()}795 >796 <h3 className="pfp-modal-title">797 Foto de perfil798 </h3>799 800 <div801 className="pfp-crop-container"802 onMouseDown={handlePointerDown}803 onMouseMove={handlePointerMove}804 onMouseUp={handlePointerUp}805 onMouseLeave={handlePointerUp}806 onTouchStart={handlePointerDown}807 onTouchMove={handlePointerMove}808 onTouchEnd={handlePointerUp}809 >810 <canvas811 ref={canvasRef}812 width={CROP_SIZE}813 height={CROP_SIZE}814 />815 816 <div className="pfp-crop-grid"></div>817 </div>818 819 {/* ZOOM */}820 821 <div className="pfp-zoom-controls">822 <span className="pfp-zoom-label">823 -824 </span>825 826 <input827 className="pfp-zoom-slider"828 type="range"829 min={minScale}830 max={minScale * 4}831 step={0.01}832 value={scale}833 onChange={handleZoomChange}834 />835 836 <span className="pfp-zoom-label">837 +838 </span>839 </div>840 841 <ButtonPrimary className="btn-change-photo" type="button" onClick={triggerFileSelect}>842 Elegir otra foto843 </ButtonPrimary>844 845 {/* ACCIONES */}846 847 <div className="pfp-modal-actions">848 <ButtonPrimary849 className="btn-save"850 type="button"851 disabled={852 !selectedImageUrl ||853 isSaving854 }855 onClick={handleSaveCroppedPhoto}856 >857 {isSaving ? "Guardando..." : "Guardar Cambios"}858 </ButtonPrimary>859 <ButtonAlert className="btn-logout" type="button" onClick={closePfpPopup} disabled={isSaving}>860 Cancelar861 </ButtonAlert>862 </div>863 </div>864 </div>865 )}866 867 {/* =========================868 MODAL CAMBIAR EMAIL869 ========================= */}870 871 {showChangeEmail && (872 <div className="pfp-modal-overlay" onClick={() => { setShowChangeEmail(false); resetEmailModal(); }}>873 <div className="pfp-modal-content" style={{ width: 620 }} onClick={(e) => e.stopPropagation()}>874 <h3 className="pfp-modal-title">Cambiar correo</h3>875 876 <div className="form-field">877 <label>Contraseña actual</label>878 <input className="form-input" type="password" value={ce_current} onChange={(e) => setCeCurrent(e.target.value)} />879 </div>880 881 <div className="form-field">882 <label>Nuevo correo</label>883 <input className="form-input" type="text" value={ce_new} onChange={(e) => setCeNew(e.target.value)} />884 </div>885 886 <div className="form-field">887 <label>Confirmar nuevo correo</label>888 <input className="form-input" type="text" value={ce_confirm} onChange={(e) => setCeConfirm(e.target.value)} />889 </div>890 891 {ce_error && <p className="field-error">{ce_error}</p>}892 893 <div className="pfp-modal-actions" style={{ display: 'flex', gap: 10 }}>894 {ce_loading ? (895 <ButtonPrimary className="btn-save" style={{ cursor: 'default', minWidth: 160 }}>896 <Loader size={16} className="loader-icon-animation" />897 </ButtonPrimary>898 ) : (899 <ButtonPrimary className="btn-save" onClick={submitChangeEmail} >Cambiar correo</ButtonPrimary>900 )}901 902 <ButtonAlert className="btn-logout" onClick={() => { setShowChangeEmail(false); resetEmailModal(); }}>Cancelar</ButtonAlert>903 </div>904 </div>905 </div>906 )}907 908 {/* =========================909 MODAL CAMBIAR CONTRASEÑA910 ========================= */}911 912 {showChangePassword && (913 <div className="pfp-modal-overlay" onClick={() => { setShowChangePassword(false); resetPasswordModal(); }}>914 <div className="pfp-modal-content" style={{ width: 620 }} onClick={(e) => e.stopPropagation()}>915 <h3 className="pfp-modal-title">Cambiar contraseña</h3>916 917 <div className="form-field">918 <label>Contraseña actual</label>919 <input className="form-input" type="password" value={cp_current} onChange={(e) => setCpCurrent(e.target.value)} />920 </div>921 922 <div className="form-field">923 <label>Nueva contraseña</label>924 <input className="form-input" type="password" value={cp_new} onChange={(e) => setCpNew(e.target.value)} />925 </div>926 927 <div className="form-field">928 <label>Confirmar nueva contraseña</label>929 <input className="form-input" type="password" value={cp_confirm} onChange={(e) => setCpConfirm(e.target.value)} />930 </div>931 932 {cp_error && <p className="field-error">{cp_error}</p>}933 934 <div className="pfp-modal-actions" style={{ display: 'flex', gap: 10 }}>935 {cp_loading ? (936 <ButtonPrimary className="btn-save" style={{ cursor: 'default', minWidth: 160 }}>937 <Loader size={16} className="loader-icon-animation" />938 </ButtonPrimary>939 ) : (940 <ButtonPrimary className="btn-save" onClick={submitChangePassword}>Cambiar contraseña</ButtonPrimary>941 )}942 943 <ButtonAlert className="btn-logout" onClick={() => { setShowChangePassword(false); resetPasswordModal(); }}>Cancelar</ButtonAlert>944 </div>945 </div>946 </div>947 )}948 949 {/* =========================950 MODAL BORRAR PERFIL951 ========================= */}952 953 {showDeleteModal && (954 <div955 className="pfp-modal-overlay"956 onClick={() =>957 setShowDeleteModal(false)958 }959 >960 <div961 className="delete-modal-content"962 onClick={(e) => e.stopPropagation()}963 >964 <h3 className="pfp-modal-title">965 Borrar Perfil966 </h3>967 968 <p className="delete-modal-text">969 ¿Estas totalmente seguro de que970 quieres borrar tu cuenta?971 972 <br />973 <br />974 975 <strong>976 Esta acción no se puede deshacer.977 </strong>978 979 Todos los datos de tu cuenta se980 eliminarán de forma permanente.981 </p>982 983 {/** 984 <div className="pfp-modal-actions" style={{ display: 'flex', gap: 10 }}>985 <ButtonAlert className="btn-delete-profile delete-confirm-btn" onClick={handleDeleteProfile}>BORRAR</ButtonAlert>986 987 <ButtonPrimary className="btn-logout" onClick={() => setShowDeleteModal(false)}>Cancelar</ButtonPrimary>988 </div>989 */}990 991 </div>992 </div>993 )}994 </>995 );996}