Files
frontend/components/organisms/forms/LoginForm.tsx
T
2026-05-14 15:57:15 +00:00

135 lines
4.3 KiB
TypeScript

"use client";
import Button from "@/components/atoms/buttons/Button";
import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
const PHONE_REGEX = /^(0[35789])[0-9]{8}$/;
export default function LoginForm() {
const router = useRouter();
const [phone, setPhone] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({ phone: "", general: "" });
const validate = (): boolean => {
if (!phone.trim()) {
setErrors({ phone: "Please enter your phone number", general: "" });
return false;
}
if (!PHONE_REGEX.test(phone)) {
setErrors({
phone: "Invalid phone number (e.g. 0987654321)",
general: "",
});
return false;
}
return true;
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!validate()) return;
setIsLoading(true);
setErrors({ phone: "", general: "" });
try {
const res = await fetch("/api/sms_otp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone }),
});
const role = (await res.text().catch(() => "")).trim().toLowerCase();
if (
res.ok &&
(role === "customer" || role === "manager" || role === "staff")
) {
sessionStorage.setItem("login_phone", phone);
sessionStorage.setItem("login_role", role);
router.push(role === "customer" ? "/login/otp" : "/login/password");
} else if (res.status === 404) {
setErrors({ phone: "", general: "Phone number not registered" });
} else {
setErrors({
phone: "",
general: "An error occurred, please try again",
});
}
} catch {
setErrors({ phone: "", general: "Unable to connect, please try again" });
} finally {
setIsLoading(false);
}
};
return (
<div>
{errors.general && <ErrorMessageLogin message={errors.general} />}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label
htmlFor="phone"
className="mb-2 block text-sm font-medium text-(--color-text-secondary)"
>
Phone number
</label>
<div className="relative">
<i className="fa-solid fa-phone absolute top-1/2 left-4 hidden -translate-y-1/2 text-(--color-text-muted) lg:block"></i>
<input
id="phone"
type="tel"
value={phone}
onChange={(e) => {
setPhone(e.target.value);
setErrors({ phone: "", general: "" });
}}
placeholder="0987654321"
disabled={isLoading}
className={`text-foreground focus:ring-opacity-20 w-full rounded-xl border bg-white px-10 py-3 transition-all duration-150 outline-none placeholder:text-(--color-text-muted) focus:border-(--color-primary) focus:ring-2 focus:ring-(--color-primary) disabled:opacity-60 lg:pl-11 ${errors.phone ? "border-red-400" : "border-(--color-border)"}`}
/>
</div>
{errors.phone && (
<ErrorMessageLogin message={errors.phone} type="secondary" />
)}
<p className="mt-2 text-xs text-(--color-text-muted)">
Enter a Vietnamese phone number (10 digits, starting with 0)
</p>
</div>
<div className="space-y-3 pt-2">
<Button
variant="primaryNoBorder"
type="submit"
style="login"
size="lg"
disabled={isLoading}
>
{isLoading ? (
<>
<i className="fa-solid fa-spinner fa-spin mr-2"></i>
Processing...
</>
) : (
"Continue"
)}
</Button>
<Link
href="/register"
className="flex w-full items-center justify-center rounded-xl border-2 border-(--color-primary) bg-white py-3 font-semibold text-(--color-primary) no-underline transition-all duration-150 hover:bg-(--color-primary) hover:text-white active:scale-98"
>
Create account
</Link>
</div>
</form>
</div>
);
}