LoginView.tsx 9.28 KB
import React from "react";
import { toast } from "sonner";

import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { PasswordInput } from "../ui/password-input";
import { login } from "../../services/accountService";
import { confirmPasswordReset, sendPasswordResetCode } from "../../services/passwordResetService";
import { useAuth } from "./AuthProvider";

const brandLogo = new URL("../../assets/773f0c39e1986271e9144596caac519f934a6ae6.png", import.meta.url).href;

const CODE_COOLDOWN_SEC = 60;
const MIN_PASSWORD_LEN = 6;

type Mode = "signin" | "forgot";

export function LoginView() {
  const auth = useAuth();
  const [mode, setMode] = React.useState<Mode>("signin");

  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");
  const [submitting, setSubmitting] = React.useState(false);

  const [fpEmail, setFpEmail] = React.useState("");
  const [fpCode, setFpCode] = React.useState("");
  const [fpNew, setFpNew] = React.useState("");
  const [fpConfirm, setFpConfirm] = React.useState("");
  const [sendingCode, setSendingCode] = React.useState(false);
  const [resetting, setResetting] = React.useState(false);
  const [cooldown, setCooldown] = React.useState(0);

  React.useEffect(() => {
    if (cooldown <= 0) return;
    const id = window.setTimeout(() => setCooldown((c) => Math.max(0, c - 1)), 1000);
    return () => window.clearTimeout(id);
  }, [cooldown]);

  const canSubmit = Boolean(email.trim() && password.trim() && !submitting);

  const submit = async () => {
    if (!canSubmit) return;
    const emailText = email.trim();
    setSubmitting(true);
    try {
      await login({ userName: emailText, password: password.trim() });
      await auth.refresh();
      toast.success("Signed in");
    } catch (e: unknown) {
      toast.error("Sign-in failed", {
        description: e instanceof Error ? e.message : "Please check your email/password and try again.",
      });
    } finally {
      setSubmitting(false);
    }
  };

  const goForgot = () => {
    setFpEmail(email.trim());
    setFpCode("");
    setFpNew("");
    setFpConfirm("");
    setCooldown(0);
    setMode("forgot");
  };

  const goSignIn = () => {
    setMode("signin");
    setFpCode("");
    setFpNew("");
    setFpConfirm("");
    setCooldown(0);
  };

  const sendCode = async () => {
    const em = fpEmail.trim();
    if (!em) {
      toast.error("Email required", { description: "Enter the email address for your account." });
      return;
    }
    if (cooldown > 0 || sendingCode) return;
    setSendingCode(true);
    try {
      await sendPasswordResetCode(em);
      setCooldown(CODE_COOLDOWN_SEC);
      toast.success("Verification code sent", {
        description: "Check your inbox for the code.",
      });
    } catch (e: unknown) {
      toast.error("Could not send code", {
        description: e instanceof Error ? e.message : "Please try again.",
      });
    } finally {
      setSendingCode(false);
    }
  };

  const submitReset = async () => {
    const em = fpEmail.trim();
    const code = fpCode.trim();
    const np = fpNew;
    const cf = fpConfirm;
    if (!em || !code) {
      toast.error("Missing fields", { description: "Email and verification code are required." });
      return;
    }
    if (np.length < MIN_PASSWORD_LEN) {
      toast.error("Password too short", {
        description: `Use at least ${MIN_PASSWORD_LEN} characters.`,
      });
      return;
    }
    if (np !== cf) {
      toast.error("Passwords do not match", { description: "Re-enter the new password in both fields." });
      return;
    }
    setResetting(true);
    try {
      await confirmPasswordReset({ email: em, code, newPassword: np });
      toast.success("Password updated", { description: "You can sign in with your new password." });
      setEmail(em);
      setPassword("");
      goSignIn();
    } catch (e: unknown) {
      toast.error("Reset failed", {
        description: e instanceof Error ? e.message : "Please try again.",
      });
    } finally {
      setResetting(false);
    }
  };

  const title = mode === "signin" ? "Platform Sign In" : "Reset password";

  return (
    <div className="w-screen h-screen grid items-center justify-center bg-[#f6f7fb] p-4">
      <div
        className="bg-white border border-gray-200 rounded-2xl shadow-sm p-8"
        style={{ width: "25vw", maxWidth: "100%" }}
      >
        <div className="text-center">
          <img
            src={brandLogo}
            alt="MedVantage"
            className="mx-auto block h-16 w-auto max-w-full object-contain object-center"
            decoding="async"
          />
          <div className="mt-4 text-xl font-semibold text-gray-900">{title}</div>
        </div>

        {mode === "signin" ? (
          <div className="mt-6 space-y-4">
            <div className="space-y-2">
              <Label>Email</Label>
              <Input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                placeholder="Enter your email"
                autoComplete="username"
                onKeyDown={(e) => {
                  if (e.key === "Enter") submit();
                }}
              />
            </div>
            <div className="space-y-2">
              <Label>Password</Label>
              <PasswordInput
                value={password}
                onChange={(e) => setPassword(e.target.value)}
                placeholder="Enter your password"
                autoComplete="current-password"
                onKeyDown={(e) => {
                  if (e.key === "Enter") submit();
                }}
              />
            </div>

            <div className="flex justify-end">
              <button
                type="button"
                className="text-sm font-medium text-blue-600 hover:text-blue-800 hover:underline"
                onClick={goForgot}
              >
                Forgot password?
              </button>
            </div>

            <Button className="w-full bg-blue-600 text-white hover:bg-blue-700" disabled={!canSubmit} onClick={submit}>
              {submitting ? "Signing in..." : "Sign In"}
            </Button>
          </div>
        ) : (
          <div className="mt-6 space-y-4">
            <div className="space-y-2">
              <Label>Email</Label>
              <Input
                type="email"
                value={fpEmail}
                onChange={(e) => setFpEmail(e.target.value)}
                placeholder="Enter your email"
                autoComplete="email"
              />
            </div>

            <div className="space-y-2">
              <Label>Verification code</Label>
              <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
                <Input
                  className="min-w-0 sm:flex-1"
                  type="text"
                  inputMode="numeric"
                  autoComplete="one-time-code"
                  value={fpCode}
                  onChange={(e) => setFpCode(e.target.value)}
                  placeholder="Enter the code from email"
                  onKeyDown={(e) => {
                    if (e.key === "Enter") submitReset();
                  }}
                />
                <Button
                  type="button"
                  variant="outline"
                  className="h-9 w-full shrink-0 border-gray-300 sm:w-auto"
                  disabled={sendingCode || cooldown > 0 || !fpEmail.trim()}
                  onClick={() => void sendCode()}
                >
                  {sendingCode ? "Sending…" : cooldown > 0 ? `Resend (${cooldown}s)` : "Send code"}
                </Button>
              </div>
            </div>

            <div className="space-y-2">
              <Label>New password</Label>
              <PasswordInput
                value={fpNew}
                onChange={(e) => setFpNew(e.target.value)}
                placeholder={`At least ${MIN_PASSWORD_LEN} characters`}
                autoComplete="new-password"
              />
            </div>
            <div className="space-y-2">
              <Label>Confirm new password</Label>
              <PasswordInput
                value={fpConfirm}
                onChange={(e) => setFpConfirm(e.target.value)}
                placeholder="Re-enter new password"
                autoComplete="new-password"
                onKeyDown={(e) => {
                  if (e.key === "Enter") submitReset();
                }}
              />
            </div>

            <Button
              className="w-full bg-blue-600 text-white hover:bg-blue-700"
              disabled={
                resetting ||
                !fpEmail.trim() ||
                !fpCode.trim() ||
                !fpNew ||
                !fpConfirm ||
                fpNew.length < MIN_PASSWORD_LEN
              }
              onClick={() => void submitReset()}
            >
              {resetting ? "Updating…" : "Update password"}
            </Button>

            <div className="text-center">
              <button
                type="button"
                className="text-sm font-medium text-gray-600 hover:text-gray-900 hover:underline"
                onClick={goSignIn}
              >
                Back to sign in
              </button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}