main page improvements
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 8m2s
All checks were successful
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Successful in 8m2s
This commit is contained in:
@@ -16,6 +16,7 @@ import { AddressForm } from './AddressForm';
|
||||
import { MapView } from '../../../components/MapView';
|
||||
import { useDebounce } from '../../../hooks/useDebounce';
|
||||
import type { AuthStatus } from '../../../hooks/useAuth';
|
||||
import { AuthView } from './AuthView';
|
||||
|
||||
interface ProfileManagerProps {
|
||||
isOpen: boolean;
|
||||
@@ -28,11 +29,6 @@ interface ProfileManagerProps {
|
||||
onLoginSuccess: (user: User, token: string, rememberMe: boolean) => void; // Add login handler
|
||||
}
|
||||
|
||||
interface AuthResponse {
|
||||
user: User;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose, user, authStatus, profile, onProfileUpdate, onSignOut, onLoginSuccess }) => { // This line had a type error due to syntax issues below.
|
||||
const [activeTab, setActiveTab] = useState('profile');
|
||||
|
||||
@@ -40,6 +36,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
const [fullName, setFullName] = useState(profile?.full_name || '');
|
||||
const [avatarUrl, setAvatarUrl] = useState(profile?.avatar_url || '');
|
||||
const [address, setAddress] = useState<Partial<Address>>({});
|
||||
const [initialAddress, setInitialAddress] = useState<Partial<Address>>({}); // Store initial address for comparison
|
||||
const { execute: updateProfile, loading: profileLoading } = useApi<Profile, [Partial<Profile>]>(apiClient.updateUserProfile);
|
||||
const { execute: updateAddress, loading: addressLoading } = useApi<Address, [Partial<Address>]>(apiClient.updateUserAddress);
|
||||
const { execute: geocode, loading: isGeocoding } = useApi<{ lat: number; lng: number }, [string]>(apiClient.geocodeAddress);
|
||||
@@ -61,18 +58,6 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
|
||||
const [passwordForDelete, setPasswordForDelete] = useState('');
|
||||
|
||||
// Login/Register State
|
||||
const [isRegistering, setIsRegistering] = useState(false);
|
||||
const [authEmail, setAuthEmail] = useState('');
|
||||
const [authPassword, setAuthPassword] = useState('');
|
||||
const [authFullName, setAuthFullName] = useState(''); // State for full name
|
||||
const [authAvatarUrl, setAuthAvatarUrl] = useState(''); // State for avatar URL
|
||||
const [isForgotPassword, setIsForgotPassword] = useState(false);
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const { execute: executeLogin, loading: loginLoading } = useApi<AuthResponse, [string, string, boolean]>(apiClient.loginUser);
|
||||
const { execute: executeRegister, loading: registerLoading } = useApi<AuthResponse, [string, string, string, string]>(apiClient.registerUser);
|
||||
const { execute: executePasswordReset, loading: passwordResetLoading } = useApi<{ message: string }, [string]>(apiClient.requestPasswordReset);
|
||||
|
||||
// New hook to fetch address details
|
||||
const { execute: fetchAddress } = useApi<Address, [number]>(apiClient.getUserAddress);
|
||||
|
||||
@@ -80,6 +65,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
const fetchedAddress = await fetchAddress(addressId);
|
||||
if (fetchedAddress) {
|
||||
setAddress(fetchedAddress);
|
||||
setInitialAddress(fetchedAddress); // Set initial address on fetch
|
||||
}
|
||||
}, [fetchAddress]);
|
||||
|
||||
@@ -95,19 +81,14 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
} else {
|
||||
// Reset address form if user has no address
|
||||
setAddress({});
|
||||
setInitialAddress({});
|
||||
}
|
||||
setActiveTab('profile');
|
||||
setIsConfirmingDelete(false);
|
||||
setPasswordForDelete('');
|
||||
setAuthEmail('');
|
||||
setAuthPassword('');
|
||||
setAuthFullName('');
|
||||
setAuthAvatarUrl('');
|
||||
setIsRegistering(false);
|
||||
setIsForgotPassword(false);
|
||||
setRememberMe(false); // Reset on open
|
||||
} else {
|
||||
setAddress({});
|
||||
setInitialAddress({});
|
||||
}
|
||||
}, [isOpen, profile, handleAddressFetch]); // Depend on isOpen and profile
|
||||
|
||||
@@ -118,33 +99,56 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use Promise.allSettled to ensure both API calls complete, regardless of individual success or failure.
|
||||
// This prevents a race condition where a successful call could trigger a state update before a failed call's error is handled.
|
||||
const profileUpdatePromise = updateProfile({
|
||||
full_name: fullName,
|
||||
avatar_url: avatarUrl,
|
||||
});
|
||||
const addressUpdatePromise = updateAddress(address);
|
||||
// Determine if profile or address data has changed
|
||||
const profileDataChanged = fullName !== profile?.full_name || avatarUrl !== profile?.avatar_url;
|
||||
const addressDataChanged = JSON.stringify(address) !== JSON.stringify(initialAddress);
|
||||
|
||||
const [profileResult, addressResult] = await Promise.allSettled([
|
||||
profileUpdatePromise,
|
||||
addressUpdatePromise
|
||||
]);
|
||||
|
||||
// Only proceed if both operations were successful. The useApi hook handles individual error notifications.
|
||||
if (profileResult.status === 'fulfilled' && addressResult.status === 'fulfilled') {
|
||||
if (profileResult.value) { // The value can be null if useApi fails internally but doesn't throw
|
||||
onProfileUpdate(profileResult.value);
|
||||
}
|
||||
notifySuccess('Profile and address updated successfully!');
|
||||
if (!profileDataChanged && !addressDataChanged) {
|
||||
notifySuccess("No changes to save.");
|
||||
onClose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [];
|
||||
if (profileDataChanged) {
|
||||
promises.push(updateProfile({ full_name: fullName, avatar_url: avatarUrl }));
|
||||
}
|
||||
if (addressDataChanged) {
|
||||
promises.push(updateAddress(address));
|
||||
}
|
||||
|
||||
try {
|
||||
const results = await Promise.allSettled(promises);
|
||||
let allSucceeded = true;
|
||||
let updatedProfileData: Profile | null = null;
|
||||
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'rejected') {
|
||||
allSucceeded = false;
|
||||
// Error is already handled by useApi hook, but we log it for good measure.
|
||||
logger.error('A profile save operation failed:', { error: result.reason });
|
||||
} else if (result.status === 'fulfilled') {
|
||||
// If this was the profile update promise, capture its result.
|
||||
// We assume the profile promise is always first if it exists.
|
||||
if (profileDataChanged && index === 0 && result.value) {
|
||||
updatedProfileData = result.value as Profile;
|
||||
onProfileUpdate(updatedProfileData);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (allSucceeded) {
|
||||
notifySuccess('Profile updated successfully!');
|
||||
onClose();
|
||||
} else {
|
||||
// If some succeeded, we still might want to give feedback.
|
||||
if (updatedProfileData) {
|
||||
notifySuccess('Profile details updated, but address failed to save.');
|
||||
}
|
||||
// The modal remains open for the user to correct the error.
|
||||
}
|
||||
} catch (error) {
|
||||
// Although the useApi hook is designed to handle errors, we log here
|
||||
// as a safeguard to catch any unexpected issues during profile save.
|
||||
logger.error('An unexpected error occurred in handleProfileSave:', { error });
|
||||
logger.error('An unexpected error occurred in handleProfileSave:', { error });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -273,36 +277,6 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
}
|
||||
};
|
||||
|
||||
const handleAuthSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const authResult = isRegistering
|
||||
? await executeRegister(authEmail, authPassword, authFullName, authAvatarUrl)
|
||||
: await executeLogin(authEmail, authPassword, rememberMe);
|
||||
|
||||
if (authResult) {
|
||||
onLoginSuccess(authResult.user, authResult.token, rememberMe);
|
||||
onClose(); // Close modal on success
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordResetRequest = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const result = await executePasswordReset(authEmail);
|
||||
if (result) {
|
||||
const { message } = result;
|
||||
notifySuccess(message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthSignIn = (provider: 'google' | 'github') => {
|
||||
// Redirect to the backend OAuth initiation route
|
||||
// The backend will then handle the redirect to the OAuth provider
|
||||
// and eventually redirect back to the frontend with a token.
|
||||
window.location.href = '/api/auth/' + provider;
|
||||
// The loading state is now managed by the hooks, but for a full-page redirect,
|
||||
// we can manually set a loading state if desired, though it's often not necessary.
|
||||
};
|
||||
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
@@ -339,103 +313,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
</button>
|
||||
|
||||
{authStatus === 'SIGNED_OUT' ? (
|
||||
isForgotPassword ? (
|
||||
<div className="p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-1">Reset Password</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">Enter your email to receive a password reset link.</p> <form data-testid="reset-password-form" onSubmit={handlePasswordResetRequest} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="resetEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email Address</label>
|
||||
<input id="resetEmail" type="email" value={authEmail} onChange={e => setAuthEmail(e.target.value)} required className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm" disabled={passwordResetLoading} />
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<button type="submit" disabled={passwordResetLoading} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center">
|
||||
{passwordResetLoading ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Send Reset Link'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center mt-4">
|
||||
<button onClick={() => { setIsForgotPassword(false); }} className="text-sm font-medium text-brand-primary hover:underline">
|
||||
Back to Sign In
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-8">
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-1">{isRegistering ? 'Create an Account' : 'Sign In'}</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">{isRegistering ? 'to get started.' : 'to access your account.'}</p>
|
||||
{/* When registering, show optional fields for full name and avatar URL */}
|
||||
{isRegistering && (
|
||||
<div className="space-y-4 mb-4">
|
||||
<div>
|
||||
<label htmlFor="authFullName" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Full Name</label>
|
||||
<input id="authFullName" type="text" value={authFullName} onChange={e => setAuthFullName(e.target.value)} className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm" placeholder="Optional" />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="authAvatarUrl" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Avatar URL</label>
|
||||
<input id="authAvatarUrl" type="url" value={authAvatarUrl} onChange={e => setAuthAvatarUrl(e.target.value)} className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm" placeholder="Optional, e.g., http://example.com/avatar.png" />
|
||||
</div>
|
||||
</div>
|
||||
)} <form data-testid="auth-form" onSubmit={handleAuthSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="authEmail" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email Address</label>
|
||||
<input id="authEmail" type="email" value={authEmail} onChange={e => setAuthEmail(e.target.value)} required className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-brand-primary focus:border-brand-primary sm:text-sm" disabled={loginLoading || registerLoading} />
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="authPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
||||
<PasswordInput
|
||||
id="authPassword"
|
||||
value={authPassword}
|
||||
onChange={e => setAuthPassword(e.target.value)}
|
||||
required
|
||||
className="mt-1"
|
||||
disabled={loginLoading || registerLoading}
|
||||
showStrength={isRegistering}
|
||||
/>
|
||||
</div>
|
||||
{!isRegistering && (
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center">
|
||||
<input id="remember-me" name="remember-me" type="checkbox" checked={rememberMe} onChange={(e) => setRememberMe(e.target.checked)} className="h-4 w-4 text-brand-primary border-gray-300 rounded focus:ring-brand-secondary" />
|
||||
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-900 dark:text-gray-300">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" onClick={() => setIsForgotPassword(true)} className="font-medium text-brand-primary hover:underline">
|
||||
Forgot password?
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2">
|
||||
<button type="submit" disabled={loginLoading || registerLoading} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center">
|
||||
{(loginLoading || registerLoading) ? <div className="w-5 h-5"><LoadingSpinner /></div> : (isRegistering ? 'Register' : 'Sign In')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="text-center mt-4">
|
||||
<button onClick={() => { setIsRegistering(!isRegistering); }} className="text-sm font-medium text-brand-primary hover:underline">
|
||||
{isRegistering ? 'Already have an account? Sign In' : "Don't have an account? Register"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative my-6">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="w-full border-t border-gray-300 dark:border-gray-600" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="bg-white dark:bg-gray-800 px-2 text-sm text-gray-500 dark:text-gray-400">Or continue with</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<button type="button" onClick={() => handleOAuthSignIn('google')} disabled={loginLoading || registerLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{(loginLoading || registerLoading) ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GoogleIcon className="w-5 h-5 mr-3" />}
|
||||
Sign In with Google
|
||||
</button>
|
||||
<button type="button" onClick={() => handleOAuthSignIn('github')} disabled={loginLoading || registerLoading} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
{(loginLoading || registerLoading) ? <div className="w-5 h-5 mr-3"><LoadingSpinner /></div> : <GithubIcon className="w-5 h-5 mr-3" />}
|
||||
Sign In with GitHub
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
<AuthView onLoginSuccess={onLoginSuccess} onClose={onClose} />
|
||||
) : (
|
||||
// Wrap the authenticated view in a React Fragment to return a single element
|
||||
<>
|
||||
@@ -443,18 +321,18 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
<h2 className="text-2xl font-bold text-gray-800 dark:text-white mb-1">My Account</h2>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6">Manage your profile, preferences, and security.</p>
|
||||
|
||||
<div className="border-b border-gray-200 dark:border-gray-700 mb-6">
|
||||
<nav className="-mb-px flex space-x-6" aria-label="Tabs">
|
||||
<button onClick={() => setActiveTab('profile')} className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm ${activeTab === 'profile' ? 'border-brand-primary text-brand-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
<div className="mb-6">
|
||||
<nav className="flex space-x-2" aria-label="Tabs">
|
||||
<button onClick={() => setActiveTab('profile')} className={`whitespace-nowrap py-2 px-4 rounded-md font-medium text-sm transition-colors ${activeTab === 'profile' ? 'bg-brand-primary/10 text-brand-primary dark:bg-brand-primary/20' : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:text-gray-700 dark:hover:text-gray-300'}`}>
|
||||
Profile
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('security')} className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm ${activeTab === 'security' ? 'border-brand-primary text-brand-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
<button onClick={() => setActiveTab('security')} className={`whitespace-nowrap py-2 px-4 rounded-md font-medium text-sm transition-colors ${activeTab === 'security' ? 'bg-brand-primary/10 text-brand-primary dark:bg-brand-primary/20' : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:text-gray-700 dark:hover:text-gray-300'}`}>
|
||||
Security
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('data')} className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm ${activeTab === 'data' ? 'border-brand-primary text-brand-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
<button onClick={() => setActiveTab('data')} className={`whitespace-nowrap py-2 px-4 rounded-md font-medium text-sm transition-colors ${activeTab === 'data' ? 'bg-brand-primary/10 text-brand-primary dark:bg-brand-primary/20' : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:text-gray-700 dark:hover:text-gray-300'}`}>
|
||||
Data & Privacy
|
||||
</button>
|
||||
<button onClick={() => setActiveTab('preferences')} className={`whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm ${activeTab === 'preferences' ? 'border-brand-primary text-brand-primary' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'}`}>
|
||||
<button onClick={() => setActiveTab('preferences')} className={`whitespace-nowrap py-2 px-4 rounded-md font-medium text-sm transition-colors ${activeTab === 'preferences' ? 'bg-brand-primary/10 text-brand-primary dark:bg-brand-primary/20' : 'text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700/50 hover:text-gray-700 dark:hover:text-gray-300'}`}>
|
||||
Preferences
|
||||
</button>
|
||||
</nav>
|
||||
@@ -479,7 +357,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2">
|
||||
<button type="submit" disabled={profileLoading || addressLoading} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center">
|
||||
<button type="submit" disabled={profileLoading || addressLoading} className="w-full bg-brand-primary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center shadow-sm transition-colors">
|
||||
{(profileLoading || addressLoading) ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Save Profile'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -512,7 +390,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<button type="submit" disabled={passwordLoading} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center">
|
||||
<button type="submit" disabled={passwordLoading} className="w-full bg-brand-primary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center shadow-sm transition-colors">
|
||||
{passwordLoading ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Update Password'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -539,7 +417,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
<div>
|
||||
<h3 className="text-md font-semibold text-gray-800 dark:text-white">Export Your Data</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Download a JSON file of your profile, watched items, and shopping lists.</p>
|
||||
<button onClick={handleExportData} disabled={exportLoading} className="mt-3 w-full sm:w-auto bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 text-gray-800 dark:text-white font-bold py-2 px-4 rounded-lg flex items-center justify-center">
|
||||
<button onClick={handleExportData} disabled={exportLoading} className="mt-3 w-full sm:w-auto bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-800 dark:text-white font-semibold py-2 px-4 rounded-lg flex items-center justify-center border border-gray-300 dark:border-gray-600 shadow-sm transition-colors">
|
||||
{exportLoading ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Export My Data'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -551,7 +429,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
<p className="text-sm text-red-700 dark:text-red-400 mt-1">This action is permanent and cannot be undone. All your data will be erased.</p>
|
||||
|
||||
{!isConfirmingDelete ? (
|
||||
<button type="button" onClick={() => setIsConfirmingDelete(true)} className="mt-3 w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg">
|
||||
<button type="button" onClick={() => setIsConfirmingDelete(true)} className="mt-3 w-full sm:w-auto bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg shadow-sm transition-colors">
|
||||
Delete My Account
|
||||
</button>
|
||||
) : (
|
||||
@@ -565,10 +443,10 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
placeholder="Enter your password"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row sm:space-x-2 space-y-2 sm:space-y-0">
|
||||
<button type="button" onClick={() => setIsConfirmingDelete(false)} className="flex-1 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded-lg">
|
||||
<button type="button" onClick={() => setIsConfirmingDelete(false)} className="flex-1 bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500 text-gray-800 dark:text-white font-bold py-2 px-4 rounded-lg shadow-sm transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" disabled={deleteLoading || !passwordForDelete} className="flex-1 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white font-bold py-2 px-4 rounded-lg flex justify-center">
|
||||
<button type="submit" disabled={deleteLoading || !passwordForDelete} className="flex-1 bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white font-bold py-2 px-4 rounded-lg flex justify-center shadow-sm transition-colors">
|
||||
{deleteLoading ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Delete Account Permanently'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user