Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 56s
661 lines
29 KiB
TypeScript
661 lines
29 KiB
TypeScript
// src/pages/admin/components/ProfileManager.tsx
|
|
import React, { useState, useEffect } from 'react';
|
|
import type { Profile, Address, UserProfile } from '../../../types';
|
|
import { useApi } from '../../../hooks/useApi';
|
|
import * as apiClient from '../../../services/apiClient';
|
|
import { notifySuccess, notifyError } from '../../../services/notificationService';
|
|
import { logger } from '../../../services/logger.client';
|
|
import { LoadingSpinner } from '../../../components/LoadingSpinner';
|
|
import { XMarkIcon } from '../../../components/icons/XMarkIcon';
|
|
import { GoogleIcon } from '../../../components/icons/GoogleIcon';
|
|
import { GithubIcon } from '../../../components/icons/GithubIcon';
|
|
import { ConfirmationModal } from '../../../components/ConfirmationModal'; // This path is correct
|
|
import { PasswordInput } from '../../../components/PasswordInput';
|
|
import { MapView } from '../../../components/MapView';
|
|
import type { AuthStatus } from '../../../hooks/useAuth';
|
|
import { AuthView } from './AuthView';
|
|
import { AddressForm } from './AddressForm';
|
|
import { useProfileAddress } from '../../../hooks/useProfileAddress';
|
|
|
|
export interface ProfileManagerProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
authStatus: AuthStatus;
|
|
userProfile: UserProfile | null; // Can be null for login/register
|
|
onProfileUpdate: (updatedProfile: Profile) => void;
|
|
onSignOut: () => void;
|
|
onLoginSuccess: (user: UserProfile, token: string, rememberMe: boolean) => void; // Add login handler
|
|
}
|
|
|
|
// --- API Hook Wrappers ---
|
|
// These wrappers adapt the apiClient functions (which expect an ApiOptions object)
|
|
// to the signature expected by the useApi hook (which passes a raw AbortSignal).
|
|
// They are defined outside the component to ensure they have a stable identity
|
|
// across re-renders, preventing infinite loops in useEffect hooks.
|
|
const updateAddressWrapper = (data: Partial<Address>, signal?: AbortSignal) =>
|
|
apiClient.updateUserAddress(data, { signal });
|
|
const updatePasswordWrapper = (password: string, signal?: AbortSignal) =>
|
|
apiClient.updateUserPassword(password, { signal });
|
|
const exportDataWrapper = (signal?: AbortSignal) => apiClient.exportUserData({ signal });
|
|
const deleteAccountWrapper = (password: string, signal?: AbortSignal) =>
|
|
apiClient.deleteUserAccount(password, { signal });
|
|
const updatePreferencesWrapper = (prefs: Partial<Profile['preferences']>, signal?: AbortSignal) =>
|
|
apiClient.updateUserPreferences(prefs, { signal });
|
|
const updateProfileWrapper = (data: Partial<Profile>, signal?: AbortSignal) =>
|
|
apiClient.updateUserProfile(data, { signal });
|
|
|
|
export const ProfileManager: React.FC<ProfileManagerProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
authStatus,
|
|
userProfile,
|
|
onProfileUpdate,
|
|
onSignOut,
|
|
onLoginSuccess,
|
|
}) => {
|
|
const [activeTab, setActiveTab] = useState('profile');
|
|
|
|
// Profile state
|
|
const [fullName, setFullName] = useState(userProfile?.full_name || '');
|
|
const [avatarUrl, setAvatarUrl] = useState(userProfile?.avatar_url || '');
|
|
|
|
// Address logic is now encapsulated in this custom hook.
|
|
const { address, initialAddress, isGeocoding, handleAddressChange, handleManualGeocode } =
|
|
useProfileAddress(userProfile, isOpen);
|
|
|
|
const { execute: updateProfile, loading: profileLoading } = useApi<Profile, [Partial<Profile>]>(
|
|
updateProfileWrapper,
|
|
);
|
|
const { execute: updateAddress, loading: addressLoading } = useApi<Address, [Partial<Address>]>(
|
|
updateAddressWrapper,
|
|
);
|
|
|
|
// Password state
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const { execute: updatePassword, loading: passwordLoading } = useApi<unknown, [string]>(
|
|
updatePasswordWrapper,
|
|
);
|
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
|
|
// Data & Privacy state
|
|
const { execute: exportData, loading: exportLoading } = useApi<unknown, []>(exportDataWrapper);
|
|
const { execute: deleteAccount, loading: deleteLoading } = useApi<unknown, [string]>(
|
|
deleteAccountWrapper,
|
|
);
|
|
|
|
// Preferences state
|
|
const { execute: updatePreferences } = useApi<Profile, [Partial<Profile['preferences']>]>(
|
|
updatePreferencesWrapper,
|
|
);
|
|
|
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false);
|
|
const [passwordForDelete, setPasswordForDelete] = useState('');
|
|
|
|
useEffect(() => {
|
|
// Only reset state when the modal is opened.
|
|
// Do not reset on profile changes, which can happen during sign-out.
|
|
logger.debug('[useEffect] Running effect due to change in isOpen or userProfile.', {
|
|
isOpen,
|
|
profileExists: !!userProfile,
|
|
});
|
|
if (isOpen && userProfile) {
|
|
// Ensure userProfile exists before setting state
|
|
logger.debug('[useEffect] Modal is open with a valid profile. Resetting component state.');
|
|
setFullName(userProfile.full_name || '');
|
|
setAvatarUrl(userProfile.avatar_url || '');
|
|
setActiveTab('profile');
|
|
setIsConfirmingDelete(false);
|
|
setPasswordForDelete('');
|
|
}
|
|
}, [isOpen, userProfile]); // Depend on isOpen and userProfile
|
|
|
|
const handleProfileSave = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
logger.debug('[handleProfileSave] Save process started.');
|
|
|
|
if (!userProfile) {
|
|
notifyError('Cannot save profile, no user is logged in.');
|
|
logger.warn('[handleProfileSave] Aborted: No user is logged in.');
|
|
return;
|
|
}
|
|
|
|
// Determine if profile or address data has changed
|
|
const profileDataChanged =
|
|
fullName !== userProfile?.full_name || avatarUrl !== userProfile?.avatar_url;
|
|
const addressDataChanged = JSON.stringify(address) !== JSON.stringify(initialAddress);
|
|
|
|
// --- Start Debug Logging ---
|
|
logger.debug('[handleProfileSave] Checking for data changes.', {
|
|
profileDataChanged,
|
|
addressDataChanged,
|
|
currentFullName: fullName,
|
|
initialFullName: userProfile?.full_name,
|
|
currentAvatarUrl: avatarUrl,
|
|
initialAvatarUrl: userProfile?.avatar_url,
|
|
currentAddress: JSON.stringify(address),
|
|
initialAddress: JSON.stringify(initialAddress),
|
|
});
|
|
// --- End Debug Logging ---
|
|
|
|
if (!profileDataChanged && !addressDataChanged) {
|
|
notifySuccess('No changes to save.');
|
|
logger.debug('[handleProfileSave] No changes detected. Closing modal.');
|
|
onClose();
|
|
return;
|
|
}
|
|
|
|
// Create an array of promises for the API calls that need to be made.
|
|
// Because useApi() catches errors and returns null, we can safely use Promise.all.
|
|
const promisesToRun = [];
|
|
if (profileDataChanged) {
|
|
logger.debug('[handleProfileSave] Queuing profile update promise.');
|
|
promisesToRun.push(updateProfile({ full_name: fullName, avatar_url: avatarUrl }));
|
|
}
|
|
if (addressDataChanged) {
|
|
logger.debug('[handleProfileSave] Queuing address update promise.');
|
|
promisesToRun.push(updateAddress(address));
|
|
}
|
|
|
|
try {
|
|
logger.debug(`[handleProfileSave] Awaiting ${promisesToRun.length} promises...`);
|
|
// Use Promise.allSettled to handle partial successes gracefully.
|
|
const results = await Promise.allSettled(promisesToRun);
|
|
logger.debug('[handleProfileSave] Promise.allSettled finished.', { results });
|
|
|
|
let anyFailures = false;
|
|
let successfulProfileUpdate: Profile | null = null;
|
|
|
|
// Determine which promises succeeded or failed.
|
|
results.forEach((result, index) => {
|
|
const isProfilePromise = profileDataChanged && index === 0;
|
|
if (result.status === 'rejected' || (result.status === 'fulfilled' && !result.value)) {
|
|
anyFailures = true;
|
|
} else if (result.status === 'fulfilled' && isProfilePromise) {
|
|
successfulProfileUpdate = result.value as Profile;
|
|
}
|
|
});
|
|
|
|
// If the profile update itself was successful, notify the parent component.
|
|
if (successfulProfileUpdate) {
|
|
onProfileUpdate(successfulProfileUpdate);
|
|
}
|
|
|
|
// Only show the global success message and close the modal if everything worked.
|
|
if (!anyFailures) {
|
|
notifySuccess('Profile updated successfully!');
|
|
onClose();
|
|
} else {
|
|
logger.warn(
|
|
'[handleProfileSave] One or more operations failed. The useApi hook should have shown an error. The modal will remain open.',
|
|
);
|
|
}
|
|
} catch (error) {
|
|
// This catch block is a safeguard. In normal operation, the useApi hook
|
|
// should prevent any promises from rejecting.
|
|
logger.error(
|
|
{ err: error },
|
|
"[CRITICAL] An unexpected error was caught directly in handleProfileSave's catch block.",
|
|
);
|
|
notifyError(
|
|
`An unexpected critical error occurred: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
|
|
// This log confirms the function has completed its execution.
|
|
logger.debug('[handleProfileSave] Save process finished.');
|
|
};
|
|
|
|
const handleOAuthLink = async (provider: 'google' | 'github') => {
|
|
// This will redirect the user to the OAuth provider to link the account.
|
|
// TODO: This is a placeholder. Implement OAuth account linking via the Passport.js backend.
|
|
if (!userProfile) {
|
|
return; // Should not be possible to see this button if not logged in
|
|
}
|
|
|
|
const errorMessage = `Account linking with ${provider} is not yet implemented.`; // This was a duplicate, fixed.
|
|
logger.warn(errorMessage, { userId: userProfile.user.user_id });
|
|
notifyError(errorMessage);
|
|
};
|
|
|
|
const handlePasswordUpdate = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (password !== confirmPassword) {
|
|
notifyError('Passwords do not match.');
|
|
return;
|
|
}
|
|
if (password.length < 6) {
|
|
notifyError('Password must be at least 6 characters long.');
|
|
return;
|
|
}
|
|
|
|
const result = await updatePassword(password);
|
|
if (result) {
|
|
notifySuccess('Password updated successfully!');
|
|
setPassword('');
|
|
setConfirmPassword('');
|
|
}
|
|
};
|
|
|
|
const handleExportData = async () => {
|
|
const userData = await exportData();
|
|
if (userData) {
|
|
const jsonString = `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(userData, null, 2))}`;
|
|
const link = document.createElement('a');
|
|
link.href = jsonString;
|
|
link.download = `flyer-crawler-data-export-${new Date().toISOString().split('T')[0]}.json`;
|
|
link.click();
|
|
}
|
|
};
|
|
|
|
const handleDeleteAccount = async () => {
|
|
setIsDeleteModalOpen(false); // Close the confirmation modal
|
|
const result = await deleteAccount(passwordForDelete);
|
|
|
|
if (result) {
|
|
// useApi returns null on failure, so this check is sufficient.
|
|
notifySuccess('Account deleted successfully. You will be logged out shortly.');
|
|
setTimeout(() => {
|
|
onClose();
|
|
onSignOut();
|
|
}, 3000);
|
|
}
|
|
};
|
|
|
|
const handleToggleDarkMode = async (newMode: boolean) => {
|
|
const updatedProfile = await updatePreferences({ darkMode: newMode });
|
|
if (updatedProfile) {
|
|
onProfileUpdate(updatedProfile);
|
|
}
|
|
};
|
|
|
|
const handleToggleUnitSystem = async (newSystem: 'metric' | 'imperial') => {
|
|
const updatedProfile = await updatePreferences({ unitSystem: newSystem });
|
|
if (updatedProfile) {
|
|
onProfileUpdate(updatedProfile);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black bg-opacity-60 z-50 flex justify-center items-center p-4"
|
|
onClick={onClose}
|
|
aria-modal="true"
|
|
role="dialog"
|
|
>
|
|
<ConfirmationModal
|
|
isOpen={isDeleteModalOpen}
|
|
onClose={() => setIsDeleteModalOpen(false)}
|
|
onConfirm={handleDeleteAccount} // Pass the handler directly
|
|
title="Confirm Account Deletion"
|
|
message={
|
|
<>
|
|
Are you absolutely sure you want to delete your account?
|
|
<strong className="block mt-2">This action is permanent and cannot be undone.</strong>
|
|
</>
|
|
}
|
|
confirmButtonText="Yes, Delete My Account"
|
|
/>
|
|
<div
|
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-lg relative"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
|
|
aria-label="Close profile manager"
|
|
>
|
|
<XMarkIcon className="w-6 h-6" />
|
|
</button>
|
|
|
|
{authStatus === 'SIGNED_OUT' ? (
|
|
<AuthView onLoginSuccess={onLoginSuccess} onClose={onClose} />
|
|
) : (
|
|
// Wrap the authenticated view in a React Fragment to return a single element
|
|
<>
|
|
<div className="p-8">
|
|
<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="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-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-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-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>
|
|
</div>
|
|
|
|
{activeTab === 'profile' && (
|
|
<form aria-label="Profile Form" onSubmit={handleProfileSave} className="space-y-4">
|
|
<div>
|
|
<label
|
|
htmlFor="fullName"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
Full Name
|
|
</label>
|
|
<input
|
|
id="fullName"
|
|
type="text"
|
|
value={fullName}
|
|
onChange={(e) => setFullName(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"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="avatarUrl"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
Avatar URL
|
|
</label>
|
|
<input
|
|
id="avatarUrl"
|
|
type="url"
|
|
value={avatarUrl}
|
|
onChange={(e) => setAvatarUrl(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"
|
|
/>
|
|
</div>
|
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<AddressForm
|
|
address={address}
|
|
onAddressChange={handleAddressChange}
|
|
onGeocode={handleManualGeocode}
|
|
isGeocoding={isGeocoding}
|
|
/>
|
|
</div>
|
|
{address.latitude && address.longitude && (
|
|
<div className="pt-4" data-testid="map-view-container">
|
|
<MapView latitude={address.latitude} longitude={address.longitude} />
|
|
</div>
|
|
)}
|
|
<div className="pt-2">
|
|
<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>
|
|
</form>
|
|
)}
|
|
|
|
{activeTab === 'security' && (
|
|
<form
|
|
data-testid="update-password-form"
|
|
onSubmit={handlePasswordUpdate}
|
|
className="space-y-4"
|
|
>
|
|
<div>
|
|
<label
|
|
htmlFor="newPassword"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
New Password
|
|
</label>
|
|
<PasswordInput
|
|
id="newPassword"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
required
|
|
className="mt-1"
|
|
showStrength
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="confirmPassword"
|
|
className="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
>
|
|
Confirm New Password
|
|
</label>
|
|
<PasswordInput
|
|
id="confirmPassword"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
required
|
|
className="mt-1"
|
|
/>
|
|
</div>
|
|
<div className="pt-2">
|
|
<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>
|
|
|
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<h3 className="text-md font-semibold text-gray-800 dark:text-white">
|
|
Link Social Accounts
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1 mb-3">
|
|
Connect other accounts for easier sign-in.
|
|
</p>
|
|
<div className="space-y-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOAuthLink('google')}
|
|
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"
|
|
>
|
|
<GoogleIcon className="w-5 h-5 mr-3" />
|
|
Link Google Account
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleOAuthLink('github')}
|
|
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"
|
|
>
|
|
<GithubIcon className="w-5 h-5 mr-3" />
|
|
Link GitHub Account
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{activeTab === 'data' && (
|
|
<div className="space-y-6">
|
|
<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-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>
|
|
|
|
<div className="border-t border-gray-200 dark:border-gray-700"></div>
|
|
|
|
<div className="p-4 border border-red-500/50 dark:border-red-400/50 bg-red-50 dark:bg-red-900/20 rounded-lg">
|
|
<h3 className="text-md font-semibold text-red-800 dark:text-red-300">
|
|
Danger Zone
|
|
</h3>
|
|
<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 shadow-sm transition-colors"
|
|
>
|
|
Delete My Account
|
|
</button>
|
|
) : (
|
|
<form
|
|
data-testid="delete-account-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
setIsDeleteModalOpen(true);
|
|
}}
|
|
className="mt-4 space-y-3 bg-white dark:bg-gray-800 p-4 rounded-md border border-red-500/50"
|
|
>
|
|
<p className="text-sm font-medium text-gray-800 dark:text-white">
|
|
To confirm, please enter your current password.
|
|
</p>
|
|
<PasswordInput
|
|
id="delete-password"
|
|
value={passwordForDelete}
|
|
onChange={(e) => setPasswordForDelete(e.target.value)}
|
|
required
|
|
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 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 shadow-sm transition-colors"
|
|
>
|
|
{deleteLoading ? (
|
|
<div className="w-5 h-5">
|
|
<LoadingSpinner />
|
|
</div>
|
|
) : (
|
|
'Delete Account Permanently'
|
|
)}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{activeTab === 'preferences' && (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="text-md font-semibold text-gray-800 dark:text-white">Theme</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
Choose your preferred visual theme.
|
|
</p>
|
|
<div className="mt-3 flex items-center">
|
|
<label htmlFor="darkModeToggle" className="flex items-center cursor-pointer">
|
|
<div className="relative">
|
|
<input
|
|
type="checkbox"
|
|
id="darkModeToggle"
|
|
className="sr-only"
|
|
checked={userProfile?.preferences?.darkMode ?? false}
|
|
onChange={(e) => handleToggleDarkMode(e.target.checked)}
|
|
/>
|
|
<div className="block bg-gray-300 dark:bg-gray-600 w-14 h-8 rounded-full"></div>
|
|
<div className="dot absolute left-1 top-1 bg-white w-6 h-6 rounded-full transition"></div>
|
|
</div>
|
|
<div className="ml-3 text-gray-700 dark:text-gray-300 font-medium">
|
|
Dark Mode
|
|
</div>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<h3 className="text-md font-semibold text-gray-800 dark:text-white">
|
|
Unit System
|
|
</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
|
Select your preferred system of measurement.
|
|
</p>
|
|
<div className="mt-3 flex space-x-4">
|
|
<label className="inline-flex items-center">
|
|
<input
|
|
type="radio"
|
|
className="form-radio text-brand-primary"
|
|
name="unitSystem"
|
|
value="imperial"
|
|
checked={userProfile?.preferences?.unitSystem === 'imperial'}
|
|
onChange={() => handleToggleUnitSystem('imperial')}
|
|
/>
|
|
<span className="ml-2 text-gray-700 dark:text-gray-300">
|
|
Imperial (e.g., lbs, oz)
|
|
</span>
|
|
</label>
|
|
<label className="inline-flex items-center">
|
|
<input
|
|
type="radio"
|
|
className="form-radio text-brand-primary"
|
|
name="unitSystem"
|
|
value="metric"
|
|
checked={userProfile?.preferences?.unitSystem === 'metric'}
|
|
onChange={() => handleToggleUnitSystem('metric')}
|
|
/>
|
|
<span className="ml-2 text-gray-700 dark:text-gray-300">
|
|
Metric (e.g., kg, g)
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|