|
@@ -0,0 +1,371 @@
|
|
|
|
|
+import { useState, useEffect } from "react";
|
|
|
|
|
+import { Button } from "@/components/ui/button";
|
|
|
|
|
+import { Input } from "@/components/ui/input";
|
|
|
|
|
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
|
|
|
+import { Mail, Lock, Loader2, AlertCircle, CheckCircle, User, Building } from "lucide-react";
|
|
|
|
|
+import { supabase } from "@/lib/supabase";
|
|
|
|
|
+import { useTranslation } from "react-i18next";
|
|
|
|
|
+
|
|
|
|
|
+const Settings = () => {
|
|
|
|
|
+ const { t } = useTranslation();
|
|
|
|
|
+ const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
+ const [user, setUser] = useState<any>(null);
|
|
|
|
|
+ const [profile, setProfile] = useState<any>(null);
|
|
|
|
|
+
|
|
|
|
|
+ // Form states
|
|
|
|
|
+ const [fullName, setFullName] = useState("");
|
|
|
|
|
+ const [companyName, setCompanyName] = useState("");
|
|
|
|
|
+ const [currentEmail, setCurrentEmail] = useState("");
|
|
|
|
|
+ const [newEmail, setNewEmail] = useState("");
|
|
|
|
|
+ const [currentPassword, setCurrentPassword] = useState("");
|
|
|
|
|
+ const [newPassword, setNewPassword] = useState("");
|
|
|
|
|
+ const [confirmPassword, setConfirmPassword] = useState("");
|
|
|
|
|
+
|
|
|
|
|
+ // Messages
|
|
|
|
|
+ const [successMessage, setSuccessMessage] = useState("");
|
|
|
|
|
+ const [errorMessage, setErrorMessage] = useState("");
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ loadUserData();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const loadUserData = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data: { user } } = await supabase.auth.getUser();
|
|
|
|
|
+ if (user) {
|
|
|
|
|
+ setUser(user);
|
|
|
|
|
+ setCurrentEmail(user.email || "");
|
|
|
|
|
+ setFullName(user.user_metadata?.full_name || "");
|
|
|
|
|
+ setCompanyName(user.user_metadata?.company_name || "");
|
|
|
|
|
+
|
|
|
|
|
+ // Load profile data
|
|
|
|
|
+ const { data: profileData } = await supabase
|
|
|
|
|
+ .from('profiles')
|
|
|
|
|
+ .select('*')
|
|
|
|
|
+ .eq('id', user.id)
|
|
|
|
|
+ .single();
|
|
|
|
|
+
|
|
|
|
|
+ if (profileData) {
|
|
|
|
|
+ setProfile(profileData);
|
|
|
|
|
+ setFullName(profileData.full_name || user.user_metadata?.full_name || "");
|
|
|
|
|
+ setCompanyName(profileData.company_name || user.user_metadata?.company_name || "");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Error loading user data:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleUpdateProfile = async (e: React.FormEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ setIsLoading(true);
|
|
|
|
|
+ setErrorMessage("");
|
|
|
|
|
+ setSuccessMessage("");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { data: { user } } = await supabase.auth.getUser();
|
|
|
|
|
+ if (!user) {
|
|
|
|
|
+ throw new Error("Not authenticated");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Update user metadata
|
|
|
|
|
+ const { error: metadataError } = await supabase.auth.updateUser({
|
|
|
|
|
+ data: {
|
|
|
|
|
+ full_name: fullName,
|
|
|
|
|
+ company_name: companyName,
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (metadataError) throw metadataError;
|
|
|
|
|
+
|
|
|
|
|
+ // Update profile table
|
|
|
|
|
+ const { error: profileError } = await supabase
|
|
|
|
|
+ .from('profiles')
|
|
|
|
|
+ .upsert({
|
|
|
|
|
+ id: user.id,
|
|
|
|
|
+ full_name: fullName,
|
|
|
|
|
+ company_name: companyName,
|
|
|
|
|
+ email: user.email,
|
|
|
|
|
+ updated_at: new Date().toISOString()
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (profileError) throw profileError;
|
|
|
|
|
+
|
|
|
|
|
+ setSuccessMessage(t('settings.profileUpdated') || "Profile updated successfully!");
|
|
|
|
|
+ await loadUserData();
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error("Error updating profile:", error);
|
|
|
|
|
+ setErrorMessage(error.message || "Failed to update profile");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleUpdateEmail = async (e: React.FormEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ setIsLoading(true);
|
|
|
|
|
+ setErrorMessage("");
|
|
|
|
|
+ setSuccessMessage("");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!newEmail || !newEmail.includes("@")) {
|
|
|
|
|
+ throw new Error("Please enter a valid email address");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { error } = await supabase.auth.updateUser({
|
|
|
|
|
+ email: newEmail
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (error) throw error;
|
|
|
|
|
+
|
|
|
|
|
+ setSuccessMessage(
|
|
|
|
|
+ t('settings.emailUpdateSent') ||
|
|
|
|
|
+ "Confirmation email sent! Please check both your old and new email addresses to confirm the change."
|
|
|
|
|
+ );
|
|
|
|
|
+ setNewEmail("");
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error("Error updating email:", error);
|
|
|
|
|
+ setErrorMessage(error.message || "Failed to update email");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleUpdatePassword = async (e: React.FormEvent) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ setIsLoading(true);
|
|
|
|
|
+ setErrorMessage("");
|
|
|
|
|
+ setSuccessMessage("");
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Validate password requirements
|
|
|
|
|
+ if (!newPassword || newPassword.length < 8) {
|
|
|
|
|
+ throw new Error("Password must be at least 8 characters long");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (newPassword !== confirmPassword) {
|
|
|
|
|
+ throw new Error("Passwords do not match");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { error } = await supabase.auth.updateUser({
|
|
|
|
|
+ password: newPassword
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (error) throw error;
|
|
|
|
|
+
|
|
|
|
|
+ setSuccessMessage(t('settings.passwordUpdated') || "Password updated successfully!");
|
|
|
|
|
+ setCurrentPassword("");
|
|
|
|
|
+ setNewPassword("");
|
|
|
|
|
+ setConfirmPassword("");
|
|
|
|
|
+ } catch (error: any) {
|
|
|
|
|
+ console.error("Error updating password:", error);
|
|
|
|
|
+ setErrorMessage(error.message || "Failed to update password");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setIsLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="min-h-screen bg-slate-900 text-white p-6">
|
|
|
|
|
+ <div className="max-w-4xl mx-auto space-y-6">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="text-3xl font-bold mb-2">{t('settings.title') || 'Account Settings'}</h1>
|
|
|
|
|
+ <p className="text-slate-400">{t('settings.subtitle') || 'Manage your account details and preferences'}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Success/Error Messages */}
|
|
|
|
|
+ {successMessage && (
|
|
|
|
|
+ <div className="bg-green-900/20 border border-green-500/50 rounded-md p-3 flex items-center gap-2">
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-green-400 flex-shrink-0" />
|
|
|
|
|
+ <span className="text-green-400 text-sm">{successMessage}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {errorMessage && (
|
|
|
|
|
+ <div className="bg-red-900/20 border border-red-500/50 rounded-md p-3 flex items-center gap-2">
|
|
|
|
|
+ <AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
|
|
|
|
+ <span className="text-red-400 text-sm">{errorMessage}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Profile Information */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white">{t('settings.profileInfo') || 'Profile Information'}</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <form onSubmit={handleUpdateProfile} className="space-y-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.fullName') || 'Full Name'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <User className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ placeholder="John Doe"
|
|
|
|
|
+ value={fullName}
|
|
|
|
|
+ onChange={(e) => setFullName(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.companyName') || 'Company Name'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <Building className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ placeholder="Acme Inc."
|
|
|
|
|
+ value={companyName}
|
|
|
|
|
+ onChange={(e) => setCompanyName(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ disabled={isLoading}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ {t('settings.updating') || 'Updating...'}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ t('settings.updateProfile') || 'Update Profile'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Email Change */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white">{t('settings.changeEmail') || 'Change Email'}</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <form onSubmit={handleUpdateEmail} className="space-y-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.currentEmail') || 'Current Email'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ value={currentEmail}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-slate-500 placeholder-slate-400"
|
|
|
|
|
+ disabled
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.newEmail') || 'New Email'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="email"
|
|
|
|
|
+ placeholder="newemail@example.com"
|
|
|
|
|
+ value={newEmail}
|
|
|
|
|
+ onChange={(e) => setNewEmail(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
|
|
|
|
|
+ required
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ disabled={isLoading || !newEmail}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ {t('settings.updating') || 'Updating...'}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ t('settings.updateEmail') || 'Update Email'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Password Change */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white">{t('settings.changePassword') || 'Change Password'}</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <form onSubmit={handleUpdatePassword} className="space-y-4">
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.newPassword') || 'New Password'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ placeholder="Enter new password"
|
|
|
|
|
+ value={newPassword}
|
|
|
|
|
+ onChange={(e) => setNewPassword(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
|
|
|
|
|
+ required
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-slate-400">
|
|
|
|
|
+ {t('settings.passwordRequirement') || 'Password must be at least 8 characters'}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <label className="text-sm font-medium text-slate-300">
|
|
|
|
|
+ {t('settings.confirmPassword') || 'Confirm Password'}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <div className="relative">
|
|
|
|
|
+ <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ placeholder="Confirm new password"
|
|
|
|
|
+ value={confirmPassword}
|
|
|
|
|
+ onChange={(e) => setConfirmPassword(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
|
|
|
|
|
+ required
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ disabled={isLoading || !newPassword || !confirmPassword}
|
|
|
|
|
+ >
|
|
|
|
|
+ {isLoading ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
+ {t('settings.updating') || 'Updating...'}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ t('settings.updatePassword') || 'Update Password'
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export default Settings;
|