This commit is contained in:
Nathnael
2026-07-04 09:51:06 +00:00
parent 1d8fcaa4f1
commit e910e6c5bd
2 changed files with 223 additions and 214 deletions

View File

@@ -34,8 +34,6 @@ import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
import { api } from "@/services/api"; import { api } from "@/services/api";
import { extractApiError } from "@/utils/result"; import { extractApiError } from "@/utils/result";
const EDR_LOGO = "/assets/edr-logo.png";
const passwordRequirements = [ const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 }, { label: "At least 8 characters", test: (v: string) => v.length >= 8 },
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) }, { label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
@@ -244,259 +242,270 @@ export default function SignupPage() {
return ( return (
<AuthShell <AuthShell
tagline="Smart Freight Operations" tagline= "Smart Freight Operations"
taglineBody="Join EDR Freight to manage shipments, track consignments, and streamline logistics workflows across Ethiopia and Djibouti." taglineBody = "Join EDR Freight to manage shipments, track consignments, and streamline logistics workflows across Ethiopia and Djibouti."
> >
<div className="flex w-full flex-col"> <div className="flex w-full flex-col" >
{stage === "form" ? ( { stage === "form" ? (
<form <form
onSubmit={handleSubmit(requestOtp)} onSubmit= { handleSubmit(requestOtp) }
className="flex w-full flex-col" className = "flex w-full flex-col"
> >
<div className="mb-4 space-y-1.5 text-center sm:mb-5"> <div className="mb-4 space-y-1.5 text-center sm:mb-5" >
<h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl"> <h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl" >
Create account Create account
</h1> </h1>
<p className="text-sm leading-relaxed text-gray-500"> < p className = "text-sm leading-relaxed text-gray-500" >
Register to access EDR Freight services. Register to access EDR Freight services.
</p> </p>
</div> </div>
<Stack gap="sm"> < Stack gap = "sm" >
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md"> <SimpleGrid cols={ { base: 1, sm: 2 } } spacing = "md" >
<TextInput <TextInput
label="First name" label="First name"
placeholder="John" placeholder = "John"
required required
disabled={sending} disabled = { sending }
error={errors.firstName?.en?.message} error = { errors.firstName?.en?.message }
{...register("firstName.en")} {...register("firstName.en") }
/> />
<TextInput < TextInput
label="Last name" label = "Last name"
placeholder="Doe" placeholder = "Doe"
required required
disabled={sending} disabled = { sending }
error={errors.lastName?.en?.message} error = { errors.lastName?.en?.message }
{...register("lastName.en")} {...register("lastName.en") }
/> />
</SimpleGrid> </SimpleGrid>
<TextInput < TextInput
label="Email" label = "Email"
type="email" type = "email"
placeholder="john@example.com" placeholder = "john@example.com"
required required
disabled={sending} disabled = { sending }
error={errors.email?.message} error = { errors.email?.message }
{...register("email")} {...register("email") }
/> />
<ControlledPhoneField < ControlledPhoneField
control={control} control = { control }
name="phone" name = "phone"
label="Phone" label = "Phone"
required required
disabled={sending} disabled = { sending }
/> />
<div className="space-y-1.5"> <div className="space-y-1.5" >
<Text size="sm" fw={500} c="edr-text"> <Text size="sm" fw = { 500} c = "edr-text" >
Send verification code via Send verification code via
</Text> </Text>
<SegmentedControl < SegmentedControl
fullWidth fullWidth
disabled={sending} disabled = { sending }
value={channel} value = { channel }
onChange={(v) => setChannel(v as OtpChannel)} onChange = {(v) => setChannel(v as OtpChannel)
data={[ }
{ data = {
value: "phone", [
label: ( {
<span className="flex items-center justify-center gap-1.5"> value: "phone",
<Smartphone size={14} /> Phone label: (
</span> <span className= "flex items-center justify-center gap-1.5" >
<Smartphone size={ 14} /> Phone
</span>
), ),
}, },
{ {
value: "email", value: "email",
label: ( label: (
<span className="flex items-center justify-center gap-1.5"> <span className= "flex items-center justify-center gap-1.5" >
<Mail size={14} /> Email <Mail size={ 14 } /> Email
</span> </span>
), ),
}, },
]} ]}
/> />
</div> </div>
<div> < div >
<PasswordInput <PasswordInput
label="Password" label="Password"
placeholder="Create a strong password" placeholder = "Create a strong password"
required required
disabled={sending} disabled = { sending }
error={errors.password?.message} error = { errors.password?.message }
{...register("password")} {...register("password") }
/> />
{passwordValue.length > 0 ? ( {
<div className="mt-2 space-y-1"> passwordValue.length > 0 ? (
{passwordRequirements.map((req) => { <div className= "mt-2 space-y-1" >
const met = req.test(passwordValue); {
return ( passwordRequirements.map((req) => {
<div const met = req.test(passwordValue);
key={req.label} return (
className="flex items-center gap-2" <div
> key= { req.label }
<span className = "flex items-center gap-2"
className={`flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${met >
? "bg-primary text-primary-foreground" <span
: "bg-gray-200 text-gray-500" className={
}`} `flex h-4 w-4 shrink-0 items-center justify-center rounded-full ${met
? "bg-primary text-primary-foreground"
: "bg-gray-200 text-gray-500"
}`
}
> >
{met ? ( {
<Check className="h-2.5 w-2.5" /> met?(
) : ( <Check className = "h-2.5 w-2.5" />
<X className="h-2.5 w-2.5" /> ): (
)} <X className = "h-2.5 w-2.5" />
</span> )
<span }
className={`text-xs ${met ? "text-primary" : "text-gray-500"}`} </span>
< span
className = {`text-xs ${met ? "text-primary" : "text-gray-500"}`
}
> >
{req.label} { req.label }
</span> </span>
</div> </div>
); );
})} })}
</div> </div>
) : null} ) : null}
</div> </div>
<PasswordInput < PasswordInput
label="Confirm password" label = "Confirm password"
placeholder="Re-enter your password" placeholder = "Re-enter your password"
required required
disabled={sending} disabled = { sending }
error={errors.confirmPassword?.message} error = { errors.confirmPassword?.message }
{...register("confirmPassword")} {...register("confirmPassword") }
/> />
{error ? ( {
<Alert error ? (
color="red" <Alert
variant="light" color= "red"
icon={<AlertCircle size={18} />} variant = "light"
icon = {< AlertCircle size = { 18} />}
> >
{error} { error }
</Alert> </Alert>
) : null} ) : null}
<Button <Button
type="submit" type="submit"
color="edr-green" color = "edr-green"
fullWidth fullWidth
loading={sending} loading = { sending }
rightSection={!sending ? <ArrowRight size={16} /> : undefined} rightSection = {!sending ? <ArrowRight size={ 16 } /> : undefined}
> >
Continue Continue
</Button> </Button>
<p className="text-center text-sm text-gray-500"> < p className = "text-center text-sm text-gray-500" >
Already have an account?{" "} Already have an account ? { " "}
<button < button
type="button" type = "button"
onClick={() => navigate("/login")} onClick = {() => navigate("/login")}
className="font-semibold text-primary hover:underline" className = "font-semibold text-primary hover:underline"
> >
Sign In Sign In
</button> </button>
</p> </p>
</Stack> </Stack>
</form> </form>
) : ( ) : (
<Stack gap="md"> <Stack gap= "md" >
<div className="mb-1 flex justify-center"> <div className="mb-1 flex justify-center" >
<span className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary"> <span className="flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 text-primary" >
<ShieldCheck size={22} /> <ShieldCheck size={ 22 } />
</span> </span>
</div> </div>
<div className="space-y-1.5 text-center"> < div className = "space-y-1.5 text-center" >
<h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl"> <h1 className="text-xl font-bold tracking-tight text-gray-900 sm:text-2xl" >
Verify your {otpChannel === "email" ? "email" : "phone"} Verify your { otpChannel === "email" ? "email" : "phone" }
</h1> </h1>
<p className="text-sm leading-relaxed text-gray-500"> < p className = "text-sm leading-relaxed text-gray-500" >
We sent a 6-digit code to{" "} We sent a 6 - digit code to{ " " }
<span className="font-medium text-gray-700"> <span className="font-medium text-gray-700" >
{otpChannel === "email" { otpChannel === "email"
? maskEmail(pendingData?.email ?? "") ? maskEmail(pendingData?.email ?? "")
: maskPhone(pendingData?.phone ?? "")} : maskPhone(pendingData?.phone ?? "")}
</span> </span>
. Enter it to finish creating your account. .Enter it to finish creating your account.
</p> </p>
</div> </div>
{otpError ? ( {
<Alert otpError ? (
color="red" <Alert
variant="light" color= "red"
icon={<AlertCircle size={18} />} variant = "light"
icon = {< AlertCircle size = { 18} />}
> >
{otpError} { otpError }
</Alert> </Alert>
) : null} ) : null}
<Stack gap={6} align="center"> <Stack gap={ 6 } align = "center" >
<Text size="sm" fw={500} c="edr-text"> <Text size="sm" fw = { 500} c = "edr-text" >
Verification code Verification code
</Text> </Text>
<PinInput < PinInput
length={6} length = { 6}
type="number" type = "number"
oneTimeCode oneTimeCode
value={otpCode} value = { otpCode }
placeholder="0" placeholder = "0"
disabled={verifying} disabled = { verifying }
styles={{ input: { textAlign: "center" } }} styles = {{ input: { textAlign: "center" } }}
onChange={setOtpCode} onChange = { setOtpCode }
/> />
</Stack> </Stack>
<Button < Button
color="edr-green" color = "edr-green"
fullWidth fullWidth
loading={verifying} loading = { verifying }
disabled={verifying || otpCode.trim().length !== 6} disabled = { verifying || otpCode.trim().length !== 6}
onClick={confirmOtp} onClick = { confirmOtp }
> >
Verify &amp; create account Verify & amp; create account
</Button> </Button>
<div className="flex items-center justify-between"> < div className = "flex items-center justify-between" >
<Button <Button
variant="subtle" variant="subtle"
color="gray" color = "gray"
leftSection={<ArrowLeft size={14} />} leftSection = {< ArrowLeft size = { 14} />}
disabled={sending || verifying} disabled = { sending || verifying}
onClick={() => { onClick = {() => {
setStage("form"); setStage("form");
setOtpError(null); setOtpError(null);
}} }}
> >
Back Back
</Button> </Button>
<Button < Button
variant="subtle" variant = "subtle"
color="edr-green" color = "edr-green"
leftSection={<RotateCw size={14} />} leftSection = {< RotateCw size = { 14} />}
disabled={resendIn > 0 || sending || verifying} disabled = { resendIn > 0 || sending || verifying}
onClick={resendOtp} onClick = { resendOtp }
> >
{resendIn > 0 ? `Resend in ${resendIn}s` : "Resend code"} { resendIn > 0 ? `Resend in ${resendIn}s` : "Resend code"}
</Button> </Button>
</div> </div>
</Stack> </Stack>
)} )}
</div> </div>
</AuthShell> </AuthShell>
); );
} }

View File

@@ -54,7 +54,7 @@ export const authService = {
}, },
checkAvailability: async (params: CheckAvailabilityPayload) => { checkAvailability: async (params: CheckAvailabilityPayload) => {
const res = await client.get<ApiResponse<CheckAvailabilityResponse>>( const res = await client.get<CheckAvailabilityResponse>(
URL_CONSTANTS.USERS.CHECK_AVAILABILITY, URL_CONSTANTS.USERS.CHECK_AVAILABILITY,
{ params }, { params },
); );