added profile page
This commit is contained in:
@@ -1,192 +1,567 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { CustomLoader, CustomModal, CustomInput, CustomButton } from "../../components/custom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { authApi } from "../Authentication/AuthApi";
|
||||
import { Key, Pencil } from "lucide-react";
|
||||
import type { PasswordForm, FormatDateFunction, FormatNameFunction, RenderStatusBadgeFunction } from "./ProfileTypes";
|
||||
import { useTheme } from "../../context/ThemeContext";
|
||||
import { paletteApi } from "../theme/PaletteApi";
|
||||
import type { ColorPalette } from "../theme/ThemeTypes";
|
||||
import CustomButton from "../../components/custom/CustomButton";
|
||||
import { Loader } from "../../components/custom/CustomLoader";
|
||||
import { Check, User } from "lucide-react";
|
||||
|
||||
type ProfileForm = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phoneNumber: string;
|
||||
};
|
||||
|
||||
const formatDate: FormatDateFunction = (value: string) => {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatName: FormatNameFunction = (firstName: string, lastName?: string | null) =>
|
||||
[firstName, lastName].filter(Boolean).join(" ");
|
||||
|
||||
const renderStatusBadge: RenderStatusBadgeFunction = (status?: string) => (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-semibold ${status === "active"
|
||||
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
||||
: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{status ? status.charAt(0).toUpperCase() + status.slice(1) : "Unknown"}
|
||||
</span>
|
||||
);
|
||||
|
||||
const ProfilePage: React.FC = () => {
|
||||
const { user, refreshUser } = useAuth();
|
||||
const { t } = useTranslation(['profile', 'common']);
|
||||
const { user, isLoading, refreshUser } = useAuth();
|
||||
|
||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false);
|
||||
const [passwordForm, setPasswordForm] = useState<PasswordForm>({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const [passwordError, setPasswordError] = useState("");
|
||||
const [passwordSuccess, setPasswordSuccess] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
||||
const [profileForm, setProfileForm] = useState<ProfileForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
phoneNumber: "",
|
||||
});
|
||||
const [profileError, setProfileError] = useState("");
|
||||
const [profileSuccess, setProfileSuccess] = useState("");
|
||||
const [isProfileSubmitting, setIsProfileSubmitting] = useState(false);
|
||||
|
||||
// Theme Logic
|
||||
const { currentPalette, setTheme } = useTheme();
|
||||
|
||||
const [palettes, setPalettes] = useState<ColorPalette[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Form states
|
||||
const [firstName, setFirstName] = useState(user?.first_name || "");
|
||||
const [lastName, setLastName] = useState(user?.last_name || "");
|
||||
const [phone, setPhone] = useState(user?.phone_number || "");
|
||||
const [isThemesLoading, setIsThemesLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPalettes = async () => {
|
||||
setIsLoading(true);
|
||||
setIsThemesLoading(true);
|
||||
try {
|
||||
const data = await paletteApi.getAllPalettes();
|
||||
setPalettes(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch palettes", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsThemesLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchPalettes();
|
||||
}, []);
|
||||
|
||||
// Update form when user data changes
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setFirstName(user.first_name || "");
|
||||
setLastName(user.last_name || "");
|
||||
setPhone(user.phone_number || "");
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleThemeSelect = (palette: ColorPalette) => {
|
||||
setTheme(palette);
|
||||
toast.success("Theme updated successfully");
|
||||
};
|
||||
|
||||
const handleProfileUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!user) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await authApi.updateProfile(user.id, {
|
||||
first_name: firstName,
|
||||
last_name: lastName,
|
||||
phone_number: phone,
|
||||
});
|
||||
await refreshUser();
|
||||
toast.success("Profile updated successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to update profile", error);
|
||||
toast.error("Failed to update profile");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading && palettes.length === 0) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Loader />
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8 animate-fade-in">
|
||||
<div className="rounded-lg border border-yellow-200 bg-yellow-50 p-6 text-sm text-yellow-800">
|
||||
{t('messages.loadError')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fullName = formatName(user.first_name, user.last_name);
|
||||
const initials =
|
||||
user.first_name && user.last_name
|
||||
? `${user.first_name[0]}${user.last_name[0]}`
|
||||
: user.first_name
|
||||
? user.first_name[0]
|
||||
: "U";
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setPasswordForm((prev) => ({ ...prev, [name]: value }));
|
||||
setPasswordError("");
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setPasswordError("");
|
||||
setPasswordSuccess("");
|
||||
|
||||
// Client-side validation
|
||||
if (!passwordForm.currentPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {
|
||||
setPasswordError(t('messages.requiredFields'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.newPassword.length < 8) {
|
||||
setPasswordError(t('messages.passwordLength'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
|
||||
setPasswordError(t('messages.passwordMismatch'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const response = await authApi.resetPassword(
|
||||
passwordForm.currentPassword,
|
||||
passwordForm.newPassword
|
||||
);
|
||||
setPasswordSuccess(response.message || t('messages.passwordSuccess'));
|
||||
setPasswordForm({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// Close modal after 1.5 seconds
|
||||
setTimeout(() => {
|
||||
setIsPasswordModalOpen(false);
|
||||
setPasswordSuccess("");
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to update password";
|
||||
setPasswordError(errorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordModalClose = () => {
|
||||
setIsPasswordModalOpen(false);
|
||||
setPasswordForm({
|
||||
currentPassword: "",
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
setPasswordError("");
|
||||
setPasswordSuccess("");
|
||||
};
|
||||
|
||||
// Edit Profile handlers
|
||||
const handleOpenEditModal = () => {
|
||||
if (user) {
|
||||
setProfileForm({
|
||||
firstName: user.first_name || "",
|
||||
lastName: user.last_name || "",
|
||||
phoneNumber: user.phone_number || "",
|
||||
});
|
||||
}
|
||||
setIsEditModalOpen(true);
|
||||
};
|
||||
|
||||
const handleProfileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setProfileForm((prev) => ({ ...prev, [name]: value }));
|
||||
setProfileError("");
|
||||
};
|
||||
|
||||
const handleProfileSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setProfileError("");
|
||||
setProfileSuccess("");
|
||||
|
||||
if (!user) return;
|
||||
|
||||
if (!profileForm.firstName.trim()) {
|
||||
setProfileError(t('messages.firstNameRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProfileSubmitting(true);
|
||||
|
||||
try {
|
||||
await authApi.updateProfile(user.id, {
|
||||
first_name: profileForm.firstName.trim(),
|
||||
last_name: profileForm.lastName.trim() || undefined,
|
||||
phone_number: profileForm.phoneNumber.trim() || undefined,
|
||||
});
|
||||
setProfileSuccess(t('messages.profileSuccess'));
|
||||
|
||||
// Refresh user data
|
||||
if (refreshUser) {
|
||||
await refreshUser();
|
||||
}
|
||||
|
||||
// Close modal after 1.5 seconds
|
||||
setTimeout(() => {
|
||||
setIsEditModalOpen(false);
|
||||
setProfileSuccess("");
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to update profile";
|
||||
setProfileError(errorMessage);
|
||||
} finally {
|
||||
setIsProfileSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditModalClose = () => {
|
||||
setIsEditModalOpen(false);
|
||||
setProfileError("");
|
||||
setProfileSuccess("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-[var(--text-primary)]">Profile Settings</h1>
|
||||
<p className="text-[var(--text-secondary)] mt-2">Manage your account settings and preferences.</p>
|
||||
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{t('subTitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Information Section */}
|
||||
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-xl p-6 shadow-sm">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="p-3 bg-blue-100 rounded-lg text-blue-600">
|
||||
<User size={24} />
|
||||
{/* Profile Card */}
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] shadow-sm">
|
||||
{/* User Header Section */}
|
||||
<div className="border-b border-(--card-border) bg-(--table-row-hover) px-6 py-8">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
{/* User Info */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-linear-to-br from-blue-500 to-indigo-600 text-2xl font-bold text-white shadow-lg">
|
||||
{initials}
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">Personal Information</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Update your personal details.</p>
|
||||
<h2 className="text-2xl font-bold text-(--text-primary)">{fullName}</h2>
|
||||
<p className="text-sm text-(--text-secondary)">{user.email}</p>
|
||||
<div className="mt-2">{renderStatusBadge(user.status)}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleProfileUpdate} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-primary)]">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-[var(--card-border)] bg-[var(--background)] text-[var(--text-primary)] focus:ring-2 focus:ring-[var(--primary)] outline-none"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-primary)]">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-[var(--card-border)] bg-[var(--background)] text-[var(--text-primary)] focus:ring-2 focus:ring-[var(--primary)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-primary)]">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={user?.email || ""}
|
||||
disabled
|
||||
className="w-full px-3 py-2 rounded-lg border border-[var(--card-border)] bg-[var(--background-secondary)] text-[var(--text-secondary)] cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium text-[var(--text-primary)]">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded-lg border border-[var(--card-border)] bg-[var(--background)] text-[var(--text-primary)] focus:ring-2 focus:ring-[var(--primary)] outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end pt-4">
|
||||
<CustomButton type="submit" loading={isSaving} variant="primary">
|
||||
Save Changes
|
||||
<div className="md:self-center flex flex-col gap-2">
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={() => setIsPasswordModalOpen(true)}
|
||||
>
|
||||
<Key size={16} className="mr-2" />
|
||||
{t('buttons.changePassword')}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={handleOpenEditModal}
|
||||
>
|
||||
<Pencil size={16} className="mr-2" />
|
||||
{t('buttons.editProfile')}
|
||||
</CustomButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Theme Selection Section */}
|
||||
<div className="bg-[var(--card-bg)] border border-[var(--card-border)] rounded-xl p-6 shadow-sm">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">Theme Preferences</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">Choose a color theme for your interface.</p>
|
||||
{/* User Details Section */}
|
||||
<div className="p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sections.accountInfo')}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* First Name */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.firstName')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.first_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Last Name */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.lastName')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.last_name || "--"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Email */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.email')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.email}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Phone Number */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.phone')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.phone_number || "--"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tenant */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.tenant')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.tenant_name || "--"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Role */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.role')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{user.role?.role_name || "--"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Account Created */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.accountCreated')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{formatDate(user.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Last Updated */}
|
||||
<div>
|
||||
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">
|
||||
{t('fields.lastUpdated')}
|
||||
</p>
|
||||
<p className="mt-1 text-sm font-medium text-[var(--text-primary)]">
|
||||
{formatDate(user.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Appearance / Theme Section */}
|
||||
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] shadow-sm p-6">
|
||||
<h3 className="mb-4 text-lg font-semibold text-[var(--text-primary)]">
|
||||
{t('sections.appearance')}
|
||||
</h3>
|
||||
<p className="text-sm text-[var(--text-secondary)] mb-6">
|
||||
{t('sections.appearanceDesc')}
|
||||
</p>
|
||||
|
||||
{isThemesLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<CustomLoader />
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{palettes.map((palette) => (
|
||||
<button
|
||||
key={palette.id}
|
||||
onClick={() => handleThemeSelect(palette)}
|
||||
onClick={() => setTheme(palette)}
|
||||
className={`
|
||||
relative flex items-center gap-3 p-3 rounded-xl border text-left transition-all duration-200
|
||||
relative flex items-center p-4 border rounded-xl text-left transition-all hover:shadow-md
|
||||
${currentPalette?.id === palette.id
|
||||
? "border-[var(--primary)] bg-[var(--primary)]/5 ring-1 ring-[var(--primary)]"
|
||||
: "border-[var(--card-border)] hover:border-[var(--text-secondary)]"
|
||||
? "border-blue-500 bg-blue-50 ring-1 ring-blue-500"
|
||||
: "border-[var(--card-border)] bg-[var(--card-bg)] hover:border-[var(--text-secondary)]"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="flex gap-1">
|
||||
<div className="w-6 h-6 rounded-full shadow-sm border border-[var(--card-border)]" style={{ backgroundColor: palette.colors.primary }} />
|
||||
<div className="w-6 h-6 rounded-full shadow-sm border border-[var(--card-border)]" style={{ backgroundColor: palette.colors.sidebar_bg }} />
|
||||
<div className="w-6 h-6 rounded-full shadow-sm border border-[var(--card-border)]" style={{ backgroundColor: palette.colors.header_bg }} />
|
||||
<div className="flex-1">
|
||||
<h4 className={`font-medium ${currentPalette?.id === palette.id ? "text-blue-700" : "text-[var(--text-primary)]"}`}>
|
||||
{palette.name}
|
||||
</h4>
|
||||
{palette.description && (
|
||||
<p className={`text-xs mt-1 ${currentPalette?.id === palette.id ? "text-blue-600" : "text-[var(--text-secondary)]"}`}>
|
||||
{palette.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span className="font-medium text-[var(--text-primary)]">{palette.name}</span>
|
||||
{/* Color Preview Circles */}
|
||||
<div className="flex -space-x-2 overflow-hidden ml-4">
|
||||
<div className="inline-block h-6 w-6 rounded-full ring-2 ring-white border border-(--card-border)" style={{ backgroundColor: palette.colors.sidebar_bg }} />
|
||||
<div className="inline-block h-6 w-6 rounded-full ring-2 ring-white border border-(--card-border)" style={{ backgroundColor: palette.colors.primary }} />
|
||||
<div className="inline-block h-6 w-6 rounded-full ring-2 ring-white border border-(--card-border)" style={{ backgroundColor: palette.colors.background }} />
|
||||
</div>
|
||||
|
||||
{currentPalette?.id === palette.id && (
|
||||
<div className="absolute top-3 right-3 text-[var(--primary)]">
|
||||
<Check size={18} />
|
||||
<div className="absolute top-0 right-0 -mt-2 -mr-2 bg-blue-500 text-white rounded-full p-1 shadow-sm">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Change Password Modal */}
|
||||
<CustomModal
|
||||
isOpen={isPasswordModalOpen}
|
||||
onClose={handlePasswordModalClose}
|
||||
title={t('modals.changePasswordTitle')}
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={handlePasswordModalClose}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('common:actions.cancel')}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
form="change-password-form"
|
||||
variant="primary"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{t('buttons.updatePassword')}
|
||||
</CustomButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="change-password-form" onSubmit={handlePasswordSubmit} className="space-y-4">
|
||||
<CustomInput
|
||||
label={t('modals.currentPassword')}
|
||||
name="currentPassword"
|
||||
type="password"
|
||||
value={passwordForm.currentPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
placeholder={t('modals.currentPasswordPlaceholder')}
|
||||
/>
|
||||
<CustomInput
|
||||
label={t('modals.newPassword')}
|
||||
name="newPassword"
|
||||
type="password"
|
||||
value={passwordForm.newPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
placeholder={t('modals.newPasswordPlaceholder')}
|
||||
/>
|
||||
<CustomInput
|
||||
label={t('modals.confirmPassword')}
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={passwordForm.confirmPassword}
|
||||
onChange={handlePasswordChange}
|
||||
required
|
||||
placeholder={t('modals.confirmPasswordPlaceholder')}
|
||||
/>
|
||||
|
||||
{passwordError && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-600">
|
||||
{passwordError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{passwordSuccess && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-2 text-sm text-green-600">
|
||||
{passwordSuccess}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</CustomModal>
|
||||
|
||||
{/* Edit Profile Modal */}
|
||||
<CustomModal
|
||||
isOpen={isEditModalOpen}
|
||||
onClose={handleEditModalClose}
|
||||
title={t('modals.editProfileTitle')}
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
<CustomButton
|
||||
variant="outlined"
|
||||
onClick={handleEditModalClose}
|
||||
disabled={isProfileSubmitting}
|
||||
>
|
||||
{t('common:actions.cancel')}
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
type="submit"
|
||||
form="edit-profile-form"
|
||||
variant="primary"
|
||||
loading={isProfileSubmitting}
|
||||
>
|
||||
{t('buttons.saveChanges')}
|
||||
</CustomButton>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<form id="edit-profile-form" onSubmit={handleProfileSubmit} className="space-y-4">
|
||||
<CustomInput
|
||||
label={t('fields.firstName')}
|
||||
name="firstName"
|
||||
value={profileForm.firstName}
|
||||
onChange={handleProfileChange}
|
||||
required
|
||||
placeholder={t('modals.firstNamePlaceholder')}
|
||||
/>
|
||||
<CustomInput
|
||||
label={t('fields.lastName')}
|
||||
name="lastName"
|
||||
value={profileForm.lastName}
|
||||
onChange={handleProfileChange}
|
||||
placeholder={t('modals.lastNamePlaceholder')}
|
||||
/>
|
||||
<CustomInput
|
||||
label={t('fields.phone')}
|
||||
name="phoneNumber"
|
||||
type="tel"
|
||||
value={profileForm.phoneNumber}
|
||||
onChange={handleProfileChange}
|
||||
placeholder={t('modals.phonePlaceholder')}
|
||||
/>
|
||||
|
||||
{profileError && (
|
||||
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-600">
|
||||
{profileError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{profileSuccess && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 px-4 py-2 text-sm text-green-600">
|
||||
{profileSuccess}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</CustomModal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { JSX } from "react/jsx-dev-runtime";
|
||||
|
||||
export interface PasswordForm {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
export type FormatDateFunction = (value: string) => string;
|
||||
|
||||
export type FormatNameFunction = (firstName: string, lastName?: string | null) => string;
|
||||
|
||||
export type RenderStatusBadgeFunction = (status?: string) => JSX.Element;
|
||||
Reference in New Issue
Block a user