mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 22:25:42 +00:00
refactor login page to simplify identifier normalization and remove unused login methods
This commit is contained in:
@@ -1,11 +1,7 @@
|
|||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
import { parsePhoneNumberFromString } from "libphonenumber-js";
|
|
||||||
import {
|
import {
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Mail,
|
|
||||||
Smartphone,
|
|
||||||
UserRound,
|
|
||||||
ArrowUpRight,
|
ArrowUpRight,
|
||||||
Globe,
|
Globe,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -14,65 +10,15 @@ import { useNavigate } from "react-router-dom";
|
|||||||
|
|
||||||
import { useAuth } from "@/auth/useAuth";
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
|
||||||
type LoginMode = "email" | "phone" | "username";
|
/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */
|
||||||
|
const normaliseIdentifier = (raw: string): string => {
|
||||||
const loginModes: Array<{
|
const v = raw.trim();
|
||||||
value: LoginMode;
|
const digits = v.replace(/\D/g, "");
|
||||||
label: string;
|
if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) {
|
||||||
icon: typeof Mail;
|
const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, "");
|
||||||
placeholder: string;
|
return `+251${local}`;
|
||||||
}> = [
|
|
||||||
{
|
|
||||||
value: "email",
|
|
||||||
label: "Email",
|
|
||||||
icon: Mail,
|
|
||||||
placeholder: "name@company.com",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "phone",
|
|
||||||
label: "Phone",
|
|
||||||
icon: Smartphone,
|
|
||||||
placeholder: "09XXXXXXXX",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "username",
|
|
||||||
label: "Username",
|
|
||||||
icon: UserRound,
|
|
||||||
placeholder: "username",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
||||||
const usernamePattern = /^[a-zA-Z0-9._-]{3,32}$/;
|
|
||||||
|
|
||||||
const normalizeIdentifier = (mode: LoginMode, value: string) => {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
|
|
||||||
if (mode === "email") {
|
|
||||||
if (!emailPattern.test(trimmed.toLowerCase())) {
|
|
||||||
throw new Error("Enter a valid email address.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed.toLowerCase();
|
|
||||||
}
|
}
|
||||||
|
return v.toLowerCase();
|
||||||
if (mode === "phone") {
|
|
||||||
const parsed = parsePhoneNumberFromString(trimmed, "ET");
|
|
||||||
|
|
||||||
if (!parsed?.isValid()) {
|
|
||||||
throw new Error("Enter a valid Ethiopian phone number.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsed.number;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!usernamePattern.test(trimmed)) {
|
|
||||||
throw new Error(
|
|
||||||
"Username must be 3-32 characters and use letters, numbers, ., _, or -.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const LOGIN_IMAGE = "/assets/login.png";
|
const LOGIN_IMAGE = "/assets/login.png";
|
||||||
@@ -214,7 +160,6 @@ const FormFooter = () => (
|
|||||||
const LoginPage = () => {
|
const LoginPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { login, verifyMfa } = useAuth();
|
const { login, verifyMfa } = useAuth();
|
||||||
const [mode, setMode] = useState<LoginMode>("email");
|
|
||||||
const [identifier, setIdentifier] = useState("");
|
const [identifier, setIdentifier] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [otp, setOtp] = useState("");
|
const [otp, setOtp] = useState("");
|
||||||
@@ -224,15 +169,13 @@ const LoginPage = () => {
|
|||||||
const [normalizedIdentifier, setNormalizedIdentifier] = useState("");
|
const [normalizedIdentifier, setNormalizedIdentifier] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const currentMode = loginModes.find((item) => item.value === mode)!;
|
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const normalized = normalizeIdentifier(mode, identifier);
|
const normalized = normaliseIdentifier(identifier);
|
||||||
setNormalizedIdentifier(normalized);
|
setNormalizedIdentifier(normalized);
|
||||||
|
|
||||||
const result = await login({ email: normalized, password });
|
const result = await login({ email: normalized, password });
|
||||||
@@ -284,32 +227,14 @@ const LoginPage = () => {
|
|||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-sm font-medium text-gray-800">
|
<label className="text-sm font-medium text-gray-800">
|
||||||
Sign in method
|
Email or Phone <span className="text-red-500">*</span>
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={mode}
|
|
||||||
onChange={(event) => setMode(event.target.value as LoginMode)}
|
|
||||||
className={`${fieldClass} appearance-none pr-10`}
|
|
||||||
>
|
|
||||||
{loginModes.map((item) => (
|
|
||||||
<option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-sm font-medium text-gray-800">
|
|
||||||
{currentMode.label} <span className="text-red-500">*</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
type="text"
|
||||||
value={identifier}
|
value={identifier}
|
||||||
onChange={(event) => setIdentifier(event.target.value)}
|
onChange={(event) => setIdentifier(event.target.value)}
|
||||||
placeholder={currentMode.placeholder}
|
placeholder="name@company.com or 09XXXXXXXX"
|
||||||
|
autoComplete="username"
|
||||||
className={fieldClass}
|
className={fieldClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export default function ETradeInfo({
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group align="flex-start" grow>
|
<Group align="flex-start" grow>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="TIN Number (10 digits)"
|
label={<>TIN Number (10 digits) <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="1234567890"
|
placeholder="0012345678"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
error={error}
|
error={error}
|
||||||
{...register}
|
{...register}
|
||||||
|
|||||||
@@ -450,14 +450,15 @@ export default function CompanyProfileForm({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// The business owner/manager pulled from eTrade — powers "Use owner as
|
// The business owner/manager pulled from eTrade — powers "Use owner as
|
||||||
// manager" on the General Manager step. Null until a TIN lookup succeeds.
|
// manager" on the General Manager step.
|
||||||
const [etradeOwner, setEtradeOwner] = useState<{
|
const [etradeOwner, setEtradeOwner] = useState<{
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
|
email?: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
// Mirror the two "copy from previous person" checkboxes so they can be
|
// Mirror the three "copy from previous person" checkboxes.
|
||||||
// re-toggled (re-checking re-pulls the latest values).
|
const [ownerIsGm, setOwnerIsGm] = useState(false);
|
||||||
const [gmIsContact, setGmIsContact] = useState(false);
|
const [gmIsContact, setGmIsContact] = useState(false);
|
||||||
const [contactIsPoa, setContactIsPoa] = useState(false);
|
const [contactIsPoa, setContactIsPoa] = useState(false);
|
||||||
|
|
||||||
@@ -507,15 +508,20 @@ export default function CompanyProfileForm({
|
|||||||
phone: toEthiopianE164(
|
phone: toEthiopianE164(
|
||||||
data.managerPhone || data.regularPhone || data.mobilePhone,
|
data.managerPhone || data.regularPhone || data.mobilePhone,
|
||||||
),
|
),
|
||||||
|
email: data.managerEmail || undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Fill the General Manager from the eTrade business owner. */
|
/** Fill the General Manager from the eTrade business owner. */
|
||||||
const useOwnerAsManager = () => {
|
const toggleOwnerAsGm = (checked: boolean) => {
|
||||||
if (!etradeOwner) return;
|
setOwnerIsGm(checked);
|
||||||
|
if (!checked || !etradeOwner) return;
|
||||||
const { first, last } = splitName(etradeOwner.name);
|
const { first, last } = splitName(etradeOwner.name);
|
||||||
setValue("generalManagerFirstName", first, { shouldValidate: true });
|
setValue("generalManagerFirstName", first, { shouldValidate: true });
|
||||||
setValue("generalManagerLastName", last, { shouldValidate: true });
|
setValue("generalManagerLastName", last, { shouldValidate: true });
|
||||||
|
setValue("generalManagerEmail", etradeOwner.email ?? "", {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
setValue("generalManagerPhone", etradeOwner.phone ?? "", {
|
setValue("generalManagerPhone", etradeOwner.phone ?? "", {
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
});
|
});
|
||||||
@@ -702,13 +708,13 @@ export default function CompanyProfileForm({
|
|||||||
|
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="First Name"
|
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Global"
|
placeholder="Global"
|
||||||
error={errors.companyFirstName?.message}
|
error={errors.companyFirstName?.message}
|
||||||
{...register("companyFirstName")}
|
{...register("companyFirstName")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Last Name"
|
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Logistics Ltd"
|
placeholder="Logistics Ltd"
|
||||||
error={errors.companyLastName?.message}
|
error={errors.companyLastName?.message}
|
||||||
{...register("companyLastName")}
|
{...register("companyLastName")}
|
||||||
@@ -716,7 +722,7 @@ export default function CompanyProfileForm({
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Company Email"
|
label={<>Company Email <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="ops@company.com"
|
placeholder="ops@company.com"
|
||||||
error={errors.companyEmail?.message}
|
error={errors.companyEmail?.message}
|
||||||
@@ -730,21 +736,21 @@ export default function CompanyProfileForm({
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Location"
|
label={<>Location <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Addis Ababa, Ethiopia"
|
placeholder="Addis Ababa, Ethiopia"
|
||||||
error={errors.companyLocation?.message}
|
error={errors.companyLocation?.message}
|
||||||
{...register("companyLocation")}
|
{...register("companyLocation")}
|
||||||
/>
|
/>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="VAT Number"
|
label={<>VAT Number <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="VAT-12345"
|
placeholder="VAT-12345"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
error={errors.vatNumber?.message}
|
error={errors.vatNumber?.message}
|
||||||
{...register("vatNumber")}
|
{...register("vatNumber")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="FAN Number (16 digits)"
|
label={<>FAN Number (16 digits) <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="1234567890123456"
|
placeholder="1234567890123456"
|
||||||
maxLength={16}
|
maxLength={16}
|
||||||
error={errors.fanNumber?.message}
|
error={errors.fanNumber?.message}
|
||||||
@@ -757,16 +763,21 @@ export default function CompanyProfileForm({
|
|||||||
<Text fw={600} size="sm" c="edr-text">
|
<Text fw={600} size="sm" c="edr-text">
|
||||||
Registration Details
|
Registration Details
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text size="xs" c="edr-muted">
|
||||||
|
Auto-filled from eTrade — these fields cannot be edited.
|
||||||
|
</Text>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="License Number"
|
label="License Number"
|
||||||
placeholder="01/23/01/19786/2006"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.licenceNumber?.message}
|
error={errors.licenceNumber?.message}
|
||||||
{...register("licenceNumber")}
|
{...register("licenceNumber")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Status"
|
label="Status"
|
||||||
placeholder="Not renewed for 2 years"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.statusDescription?.message}
|
error={errors.statusDescription?.message}
|
||||||
{...register("statusDescription")}
|
{...register("statusDescription")}
|
||||||
/>
|
/>
|
||||||
@@ -774,13 +785,15 @@ export default function CompanyProfileForm({
|
|||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Date Registered"
|
label="Date Registered"
|
||||||
placeholder="12/17/2013"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.dateRegistered?.message}
|
error={errors.dateRegistered?.message}
|
||||||
{...register("dateRegistered")}
|
{...register("dateRegistered")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Renewal Date"
|
label="Renewal Date"
|
||||||
placeholder="3/17/2016"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.renewalDate?.message}
|
error={errors.renewalDate?.message}
|
||||||
{...register("renewalDate")}
|
{...register("renewalDate")}
|
||||||
/>
|
/>
|
||||||
@@ -788,13 +801,15 @@ export default function CompanyProfileForm({
|
|||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Renewed From"
|
label="Renewed From"
|
||||||
placeholder="3/17/2016"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.renewedFrom?.message}
|
error={errors.renewedFrom?.message}
|
||||||
{...register("renewedFrom")}
|
{...register("renewedFrom")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Renewed To"
|
label="Renewed To"
|
||||||
placeholder="7/7/2016"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.renewedTo?.message}
|
error={errors.renewedTo?.message}
|
||||||
{...register("renewedTo")}
|
{...register("renewedTo")}
|
||||||
/>
|
/>
|
||||||
@@ -806,13 +821,15 @@ export default function CompanyProfileForm({
|
|||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Region"
|
label="Region"
|
||||||
placeholder="Tigray"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.region?.message}
|
error={errors.region?.message}
|
||||||
{...register("region")}
|
{...register("region")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Zone"
|
label="Zone"
|
||||||
placeholder="EASTERN TIGRAY"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.zone?.message}
|
error={errors.zone?.message}
|
||||||
{...register("zone")}
|
{...register("zone")}
|
||||||
/>
|
/>
|
||||||
@@ -820,13 +837,15 @@ export default function CompanyProfileForm({
|
|||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Woreda"
|
label="Woreda"
|
||||||
placeholder="EROB"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.woreda?.message}
|
error={errors.woreda?.message}
|
||||||
{...register("woreda")}
|
{...register("woreda")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Kebele"
|
label="Kebele"
|
||||||
placeholder="ARAS"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.kebele?.message}
|
error={errors.kebele?.message}
|
||||||
{...register("kebele")}
|
{...register("kebele")}
|
||||||
/>
|
/>
|
||||||
@@ -834,7 +853,8 @@ export default function CompanyProfileForm({
|
|||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="House No"
|
label="House No"
|
||||||
placeholder="House Number"
|
readOnly
|
||||||
|
styles={{ input: { backgroundColor: "var(--mantine-color-gray-0)", cursor: "default" } }}
|
||||||
error={errors.houseNo?.message}
|
error={errors.houseNo?.message}
|
||||||
{...register("houseNo")}
|
{...register("houseNo")}
|
||||||
/>
|
/>
|
||||||
@@ -850,31 +870,25 @@ export default function CompanyProfileForm({
|
|||||||
|
|
||||||
{step === "personnel" && (
|
{step === "personnel" && (
|
||||||
<>
|
<>
|
||||||
<Group justify="space-between" align="center">
|
<Text fw={600} size="sm" c="edr-text">
|
||||||
<Text fw={600} size="sm" c="edr-text">
|
General Manager
|
||||||
General Manager
|
</Text>
|
||||||
</Text>
|
<Checkbox
|
||||||
{etradeOwner && (
|
color="edr-green"
|
||||||
<Button
|
label="Use eTrade business owner as General Manager"
|
||||||
variant="light"
|
checked={ownerIsGm}
|
||||||
color="edr-green"
|
disabled={!etradeOwner}
|
||||||
size="xs"
|
onChange={(e) => toggleOwnerAsGm(e.currentTarget.checked)}
|
||||||
leftSection={<UserCheck size={14} />}
|
/>
|
||||||
onClick={useOwnerAsManager}
|
|
||||||
>
|
|
||||||
Use owner as manager
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="First Name"
|
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Abebe"
|
placeholder="Abebe"
|
||||||
error={errors.generalManagerFirstName?.message}
|
error={errors.generalManagerFirstName?.message}
|
||||||
{...register("generalManagerFirstName")}
|
{...register("generalManagerFirstName")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Last Name"
|
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Bikila"
|
placeholder="Bikila"
|
||||||
error={errors.generalManagerLastName?.message}
|
error={errors.generalManagerLastName?.message}
|
||||||
{...register("generalManagerLastName")}
|
{...register("generalManagerLastName")}
|
||||||
@@ -882,7 +896,7 @@ export default function CompanyProfileForm({
|
|||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label={<>Email <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="gm@company.com"
|
placeholder="gm@company.com"
|
||||||
error={errors.generalManagerEmail?.message}
|
error={errors.generalManagerEmail?.message}
|
||||||
@@ -911,13 +925,13 @@ export default function CompanyProfileForm({
|
|||||||
/>
|
/>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="First Name"
|
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Jane"
|
placeholder="Jane"
|
||||||
error={errors.contactPersonFirstName?.message}
|
error={errors.contactPersonFirstName?.message}
|
||||||
{...register("contactPersonFirstName")}
|
{...register("contactPersonFirstName")}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Last Name"
|
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
||||||
placeholder="Smith"
|
placeholder="Smith"
|
||||||
error={errors.contactPersonLastName?.message}
|
error={errors.contactPersonLastName?.message}
|
||||||
{...register("contactPersonLastName")}
|
{...register("contactPersonLastName")}
|
||||||
|
|||||||
@@ -1,48 +1,39 @@
|
|||||||
import { type FormEvent, useState } from "react";
|
import { type FormEvent, useState } from "react";
|
||||||
import { ChevronDown, Eye, EyeOff, Mail, Smartphone } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import RPNInput from "react-phone-number-input";
|
|
||||||
import "react-phone-number-input/style.css";
|
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
import AuthShell, { fieldClass, primaryButtonClass } from "@/components/auth/AuthShell";
|
||||||
import "@/components/phone-field.css";
|
|
||||||
|
|
||||||
const EDR_LOGO = "/assets/edr-logo.png";
|
const EDR_LOGO = "/assets/edr-logo.png";
|
||||||
|
|
||||||
type LoginMethod = "email" | "phone";
|
/** Normalise Ethiopian local phone (09…/07…) to E.164; pass email through unchanged. */
|
||||||
|
function normaliseIdentifier(raw: string): string {
|
||||||
const loginMethods: Array<{
|
const v = raw.trim();
|
||||||
value: LoginMethod;
|
const digits = v.replace(/\D/g, "");
|
||||||
label: string;
|
if (digits.length >= 9 && (v.startsWith("0") || v.startsWith("+251"))) {
|
||||||
icon: typeof Mail;
|
const local = digits.startsWith("251") ? digits.slice(3) : digits.replace(/^0/, "");
|
||||||
placeholder: string;
|
return `+251${local}`;
|
||||||
}> = [
|
}
|
||||||
{ value: "email", label: "Email", icon: Mail, placeholder: "name@company.com" },
|
return v.toLowerCase();
|
||||||
{ value: "phone", label: "Phone", icon: Smartphone, placeholder: "09XXXXXXXX" },
|
}
|
||||||
];
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
const [method, setMethod] = useState<LoginMethod>("email");
|
|
||||||
const [identifier, setIdentifier] = useState("");
|
const [identifier, setIdentifier] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const currentMethod = loginMethods.find((item) => item.value === method)!;
|
|
||||||
|
|
||||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setError(null);
|
setError(null);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// In phone mode the identifier is already a canonical E.164 string
|
const result = await login({ email: normaliseIdentifier(identifier), password });
|
||||||
// (e.g. +251912345678) from the phone field; email mode passes through.
|
|
||||||
const result = await login({ email: identifier, password });
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const from = (location.state as { from?: { pathname: string } } | null)?.from
|
const from = (location.state as { from?: { pathname: string } } | null)?.from
|
||||||
?.pathname;
|
?.pathname;
|
||||||
@@ -74,55 +65,19 @@ export default function LoginPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-4">
|
<div className="flex w-full flex-col gap-4">
|
||||||
<div className="space-y-1.5">
|
|
||||||
<label className="text-sm font-medium text-gray-800">Sign in method</label>
|
|
||||||
<div className="relative">
|
|
||||||
<select
|
|
||||||
value={method}
|
|
||||||
onChange={(event) => {
|
|
||||||
setMethod(event.target.value as LoginMethod);
|
|
||||||
setIdentifier("");
|
|
||||||
}}
|
|
||||||
disabled={loading}
|
|
||||||
className={`${fieldClass} appearance-none pr-10`}
|
|
||||||
>
|
|
||||||
{loginMethods.map((item) => (
|
|
||||||
<option key={item.value} value={item.value}>
|
|
||||||
{item.label}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-sm font-medium text-gray-800">
|
<label className="text-sm font-medium text-gray-800">
|
||||||
{currentMethod.label} <span className="text-red-500">*</span>
|
Email or Phone <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
{method === "phone" ? (
|
<input
|
||||||
<div className="edr-phone-wrapper">
|
type="text"
|
||||||
<RPNInput
|
value={identifier}
|
||||||
international
|
onChange={(event) => setIdentifier(event.target.value)}
|
||||||
defaultCountry="ET"
|
placeholder="name@company.com or 09XXXXXXXX"
|
||||||
countryCallingCodeEditable={false}
|
disabled={loading}
|
||||||
addInternationalOption
|
autoComplete="username"
|
||||||
placeholder="912 345 678"
|
className={fieldClass}
|
||||||
disabled={loading}
|
/>
|
||||||
value={identifier || undefined}
|
|
||||||
onChange={(v) => setIdentifier(v ?? "")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
value={identifier}
|
|
||||||
onChange={(event) => setIdentifier(event.target.value)}
|
|
||||||
placeholder={currentMethod.placeholder}
|
|
||||||
disabled={loading}
|
|
||||||
className={fieldClass}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
|
|||||||
Reference in New Issue
Block a user