import React, { useState, useRef } from 'react'; import ReactDOM from 'react-dom'; import { X, User as UserIcon, Lock, Trash2, AlertTriangle } from 'lucide-react'; const overlay = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000, }; const modal = { background: 'var(--bg-primary)', borderRadius: 16, padding: 0, width: 560, maxWidth: '95vw', maxHeight: '85vh', overflow: 'hidden', boxShadow: 'var(--shadow-xl)', display: 'flex', flexDirection: 'column', }; const header = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '20px 24px', borderBottom: '1px solid var(--border-primary)', }; const tabRow = { display: 'flex', gap: 4, padding: '12px 16px 0', borderBottom: '1px solid var(--border-primary)', }; const tabBtn = (active) => ({ display: 'flex', alignItems: 'center', gap: 8, padding: '10px 14px', background: 'transparent', border: 'none', borderBottom: active ? '2px solid var(--accent-primary)' : '2px solid transparent', color: active ? 'var(--accent-primary)' : 'var(--text-secondary)', cursor: 'pointer', fontSize: 13.5, fontWeight: 500, marginBottom: -1, }); const body = { padding: 24, overflowY: 'auto', flex: 1 }; const label = { display: 'block', fontSize: 13, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 6 }; const input = { width: '100%', padding: '10px 12px', borderRadius: 8, border: '1px solid var(--border-primary)', background: 'var(--bg-secondary)', color: 'var(--text-primary)', fontSize: 14, boxSizing: 'border-box', }; const primaryBtn = { padding: '10px 16px', background: 'var(--accent-primary)', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 500, }; const dangerBtn = { padding: '10px 16px', background: '#dc2626', color: '#fff', border: 'none', borderRadius: 8, cursor: 'pointer', fontSize: 14, fontWeight: 500, }; const SettingsModal = ({ user, authToken, onUserUpdate, onSignOut, onClose }) => { const [activeTab, setActiveTab] = useState('profile'); // Track where the mouse went DOWN so we don't close the modal when a user // drags to select text inside an input and the mouseup happens outside the modal. // (React's onClick fires on the common ancestor of down+up, which can be the // overlay itself — causing accidental close on text selection.) const mouseDownOnOverlay = useRef(false); const handleOverlayMouseDown = (e) => { mouseDownOnOverlay.current = e.target === e.currentTarget; }; const handleOverlayMouseUp = (e) => { if (mouseDownOnOverlay.current && e.target === e.currentTarget) onClose(); mouseDownOnOverlay.current = false; }; const [firstName, setFirstName] = useState(user?.firstName || ''); const [lastName, setLastName] = useState(user?.lastName || ''); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [deleteConfirmPassword, setDeleteConfirmPassword] = useState(''); const [deleteConfirmText, setDeleteConfirmText] = useState(''); const [message, setMessage] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const apiUrl = process.env.REACT_APP_API_URL; const extractError = (data, fallback) => { if (!data) return fallback; if (typeof data.detail === 'string') return data.detail; if (Array.isArray(data.detail) && data.detail[0]?.msg) return data.detail[0].msg; return fallback; }; const handleProfileSubmit = async (e) => { e.preventDefault(); setMessage(null); if (!firstName.trim() && !lastName.trim()) { setMessage({ type: 'error', text: 'Enter a first or last name.' }); return; } setIsSubmitting(true); try { const response = await fetch(`${apiUrl}/auth/me`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ first_name: firstName.trim(), last_name: lastName.trim(), }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not update profile.') }); return; } onUserUpdate?.(data); setFirstName(data.firstName || ''); setLastName(data.lastName || ''); setMessage({ type: 'success', text: 'Profile updated.' }); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const handlePasswordSubmit = async (e) => { e.preventDefault(); setMessage(null); if (newPassword !== confirmPassword) { setMessage({ type: 'error', text: 'New passwords do not match.' }); return; } if (newPassword.length < 8) { setMessage({ type: 'error', text: 'New password must be at least 8 characters.' }); return; } setIsSubmitting(true); try { const response = await fetch(`${apiUrl}/auth/me/password`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not change password.') }); return; } setCurrentPassword(''); setNewPassword(''); setConfirmPassword(''); setMessage({ type: 'success', text: 'Password changed.' }); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const handleDeleteAccount = async (e) => { e.preventDefault(); setMessage(null); if (deleteConfirmText !== 'DELETE') { setMessage({ type: 'error', text: 'Type DELETE to confirm.' }); return; } if (!deleteConfirmPassword) { setMessage({ type: 'error', text: 'Password required to delete account.' }); return; } setIsSubmitting(true); try { const response = await fetch(`${apiUrl}/auth/me`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${authToken}`, }, body: JSON.stringify({ password: deleteConfirmPassword }), }); const data = await response.json().catch(() => null); if (!response.ok) { setMessage({ type: 'error', text: extractError(data, 'Could not delete account.') }); return; } onClose?.(); onSignOut?.(); } catch (err) { setMessage({ type: 'error', text: 'Network error. Please try again.' }); } finally { setIsSubmitting(false); } }; const messageStyle = (type) => ({ padding: '10px 12px', borderRadius: 8, marginBottom: 16, fontSize: 13, background: type === 'error' ? 'rgba(220,38,38,0.1)' : type === 'success' ? 'rgba(22,163,74,0.1)' : 'var(--bg-secondary)', color: type === 'error' ? '#dc2626' : type === 'success' ? '#16a34a' : 'var(--text-secondary)', border: `1px solid ${ type === 'error' ? 'rgba(220,38,38,0.3)' : type === 'success' ? 'rgba(22,163,74,0.3)' : 'var(--border-primary)' }`, }); return ReactDOM.createPortal(