File "ChangePasswordModal.tsx"

Full Path: /home/trinadezambia/public_html/student_panel/src/components/ui/pages/dashboard/ChangePasswordModal.tsx
File size: 9.91 KB
MIME-type: text/x-java
Charset: utf-8

'use client';

import { useState } from 'react';
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogDescription,
  DialogClose,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { BiX } from 'react-icons/bi';
import { FiEye, FiEyeOff } from 'react-icons/fi';
import { useStudentChangePassword } from '@/lib/api/student';
import { toastUtils } from '@/components/lib/toast';
import { useTranslate } from '@/components/hooks/useTranslate';

interface ChangePasswordModalProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}

export default function ChangePasswordModal({
  open,
  onOpenChange,
}: ChangePasswordModalProps) {
  const translate = useTranslate();
  // State for password visibility toggles
  const [showCurrentPassword, setShowCurrentPassword] = useState(false);
  const [showNewPassword, setShowNewPassword] = useState(false);
  const [showConfirmPassword, setShowConfirmPassword] = useState(false);

  // State for form values
  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');

  // Change password mutation hook
  const changePasswordMutation = useStudentChangePassword();

  // Handle form submission
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    // Validation: Check if all fields are filled
    if (!currentPassword || !newPassword || !confirmPassword) {
      toastUtils.error(translate('pleaseFillInAllFields'));
      return;
    }

    // Validation: Check if new passwords match
    if (newPassword !== confirmPassword) {
      toastUtils.error(translate('newPasswordsDoNotMatch'));
      return;
    }

    // Validation: Check if new password is different from current password
    if (currentPassword === newPassword) {
      toastUtils.error(translate('newPasswordMustBeDifferent'));
      return;
    }

    // Validation: Check password length (minimum 6 characters)
    if (newPassword.length < 6) {
      toastUtils.error(translate('passwordMustBeAtLeast6Characters'));
      return;
    }

    // Call the API to change password
    // API will return error code 109 (INVALID_PASSWORD) if current password is wrong
    // The mutation hook automatically handles all errors and shows toast messages
    changePasswordMutation.mutate(
      {
        current_password: currentPassword,
        new_password: newPassword,
        new_confirm_password: confirmPassword,
      },
      {
        onSuccess: () => {
          // Reset form fields on success
          setCurrentPassword('');
          setNewPassword('');
          setConfirmPassword('');
          // Close the modal
          onOpenChange(false);
        },
        // Error handling is done in the mutation hook
        // Errors like "Current password is incorrect" will be shown as toast
      }
    );
  };

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      {/* Modal is responsive: full width with margins on mobile, fixed width on larger screens */}
      <DialogContent
        className="max-w-[calc(100vw-2rem)] sm:max-w-[550px] p-0"
        showCloseButton={false}
      >
        {/* Header with close button - reduced padding on mobile */}
        <DialogHeader className="p-4 pb-3 sm:p-6 sm:pb-4 border-b border-gray-200">
          <div className="flex items-start justify-between text-left rtl:text-right">
            <div className="flex-1">
              {/* Responsive title: smaller on mobile, larger on desktop */}
              <DialogTitle className="text-lg sm:text-xl font-bold text-gray-900 mb-1.5 sm:mb-2">
                {translate('changeYourPassword')}
              </DialogTitle>
              {/* Responsive description: smaller text on mobile */}
              <DialogDescription className="text-xs sm:text-sm font-normal text-gray-600 leading-relaxed">
                {translate('toSecureYourAccountEnterCurrentPassword')}
              </DialogDescription>
            </div>
            {/* Close button: smaller on mobile, normal on desktop */}
            <DialogClose asChild>
              <button className="ml-2 sm:ml-4 rtl:ml-0 rtl:mr-2 sm:rtl:mr-4 p-1.5 sm:p-2 bg-(--light-primary-color) rounded-[4px] transition-colors border border-gray-200">
                <BiX className="w-4 h-4 sm:w-5 sm:h-5 text-gray-600" />
              </button>
            </DialogClose>
          </div>
        </DialogHeader>

        {/* Form content - reduced padding on mobile */}
        <form onSubmit={handleSubmit} className="px-4 pb-4 sm:px-6 sm:pb-6">
          {/* Reduced spacing between fields on mobile */}
          <div className="space-y-3.5 sm:space-y-5">
            {/* Current Password Field */}
            <div>
              {/* Responsive label: smaller on mobile */}
              <label className="block text-sm sm:text-base font-normal text-gray-900 mb-1.5 sm:mb-2">
                {translate('currentPassword')}
              </label>
              <div className="relative">
                {/* Responsive input height: shorter on mobile */}
                <Input
                  type={showCurrentPassword ? 'text' : 'password'}
                  placeholder={translate('enterCurrentPassword')}
                  value={currentPassword}
                  onChange={(e) => setCurrentPassword(e.target.value)}
                  className="h-11 sm:h-12 pr-10 sm:pr-12 rtl:pr-3 rtl:pl-10 sm:rtl:pl-12 bg-(--light-primary-color) rounded-[4px] shadow-none border border-gray-300 text-gray-900 placeholder:text-gray-400 text-sm sm:text-base"
                />
                {/* Responsive eye icon position and size */}
                <button
                  type="button"
                  onClick={() => setShowCurrentPassword(!showCurrentPassword)}
                  className="absolute right-2.5 rtl:left-2.5 rtl:right-auto sm:right-3 sm:rtl:left-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors"
                >
                  {showCurrentPassword ? (
                    <FiEyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
                  ) : (
                    <FiEye className="w-4 h-4 sm:w-5 sm:h-5" />
                  )}
                </button>
              </div>
            </div>

            {/* New Password Field */}
            <div>
              {/* Responsive label: smaller on mobile */}
              <label className="block text-sm sm:text-base font-normal text-gray-900 mb-1.5 sm:mb-2">
                {translate('newPassword')}
              </label>
              <div className="relative">
                {/* Responsive input height: shorter on mobile */}
                <Input
                  type={showNewPassword ? 'text' : 'password'}
                  placeholder={translate('enterNewPassword')}
                  value={newPassword}
                  onChange={(e) => setNewPassword(e.target.value)}
                  className="h-11 sm:h-12 pr-10 sm:pr-12 rtl:pr-3 rtl:pl-10 sm:rtl:pl-12 bg-(--light-primary-color) rounded-[4px] shadow-none border border-gray-300 text-gray-900 placeholder:text-gray-400 text-sm sm:text-base"
                />
                {/* Responsive eye icon position and size */}
                <button
                  type="button"
                  onClick={() => setShowNewPassword(!showNewPassword)}
                  className="absolute right-2.5 rtl:left-2.5 rtl:right-auto sm:right-3 sm:rtl:left-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors"
                >
                  {showNewPassword ? (
                    <FiEyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
                  ) : (
                    <FiEye className="w-4 h-4 sm:w-5 sm:h-5" />
                  )}
                </button>
              </div>
            </div>

            {/* Confirm New Password Field */}
            <div>
              {/* Responsive label: smaller on mobile */}
              <label className="block text-sm sm:text-base font-normal text-gray-900 mb-1.5 sm:mb-2">
                {translate('confirmNewPassword')}
              </label>
              <div className="relative">
                {/* Responsive input height: shorter on mobile */}
                <Input
                  type={showConfirmPassword ? 'text' : 'password'}
                  placeholder={translate('reEnterNewPassword')}
                  value={confirmPassword}
                  onChange={(e) => setConfirmPassword(e.target.value)}
                  className="h-11 sm:h-12 pr-10 sm:pr-12 rtl:pr-3 rtl:pl-10 sm:rtl:pl-12 bg-(--light-primary-color) rounded-[4px] shadow-none border border-gray-300 text-gray-900 placeholder:text-gray-400 text-sm sm:text-base"
                />
                {/* Responsive eye icon position and size */}
                <button
                  type="button"
                  onClick={() => setShowConfirmPassword(!showConfirmPassword)}
                  className="absolute right-2.5 rtl:left-2.5 rtl:right-auto sm:right-3 sm:rtl:left-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 transition-colors"
                >
                  {showConfirmPassword ? (
                    <FiEyeOff className="w-4 h-4 sm:w-5 sm:h-5" />
                  ) : (
                    <FiEye className="w-4 h-4 sm:w-5 sm:h-5" />
                  )}
                </button>
              </div>
            </div>

            {/* Submit Button - responsive height and text size */}
            <button
              type="submit"
              disabled={changePasswordMutation.isPending}
              className="w-full h-11 sm:h-12 bg-(--primary-color) text-white text-base sm:text-xl font-normal rounded-[4px] mt-1.5 sm:mt-2 disabled:opacity-50 disabled:cursor-not-allowed transition-opacity"
            >
              {changePasswordMutation.isPending
                ? translate('changingPassword')
                : translate('changePassword')}
            </button>
          </div>
        </form>
      </DialogContent>
    </Dialog>
  );
}