signup update

This commit is contained in:
ghost2023
2026-06-06 11:12:19 +03:00
parent e067da29df
commit 4994e19d57
3 changed files with 138 additions and 26 deletions

View File

@@ -17,7 +17,6 @@ import {
PositionType,
Position,
Project,
UnitSetting,
GlobalUnitConfiguration,
Unit,
EmployeeSignature,
@@ -64,7 +63,6 @@ const iamEntities = [
PositionType,
Position,
Project,
UnitSetting,
GlobalUnitConfiguration,
Unit,
EmployeeSignature,
@@ -98,10 +96,8 @@ const iamMigrationsGlob = join(
);
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
export default registerAs(
"database",
(): TypeOrmModuleOptions => {
return {
export default registerAs("database", (): TypeOrmModuleOptions => {
return {
type: "postgres",
host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5433", 10),
@@ -124,5 +120,4 @@ export default registerAs(
synchronize: false,
logging: process.env.NODE_ENV === "development",
};
},
);
});

View File

@@ -3,12 +3,21 @@ import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ArrowRight, UserPlus, Loader2 } from "lucide-react";
import {
ArrowRight,
Eye,
EyeOff,
UserPlus,
Loader2,
Check,
X,
} from "lucide-react";
import { userType } from "@/enums/userType";
import useAuth from "@/hooks/useAuth";
import type { SignupPayload } from "@/types/auth";
import AuthLayout from "@/components/auth/AuthLayout";
import PhoneInput from "@/components/auth/PhoneInput";
import { cn } from "@/lib/utils";
import {
Button,
Input,
@@ -18,23 +27,47 @@ import {
FieldGroup,
} from "@edr/ui-common";
const userSchema = z.object({
email: z.string().email("Invalid email address"),
countryCode: z.string().min(1, "Country code is required"),
phone: z
.string()
.min(9, "Phone number is too short")
.max(9, "Phone number is too long"),
userType: z.string(),
firstName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
lastName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
});
const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 },
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
{ label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
{ label: "One number", test: (v: string) => /\d/.test(v) },
{
label: "One special character",
test: (v: string) => /[^A-Za-z0-9]/.test(v),
},
] as const;
const userSchema = z
.object({
email: z.string().email("Invalid email address"),
countryCode: z.string().min(1, "Country code is required"),
phone: z
.string()
.min(9, "Phone number is too short")
.max(9, "Phone number is too long"),
userType: z.string(),
firstName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
lastName: z.object({
en: z.string().min(2, "Name is required"),
am: z.string().nullable(),
}),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must include an uppercase letter")
.regex(/[a-z]/, "Password must include a lowercase letter")
.regex(/\d/, "Password must include a number")
.regex(/[^A-Za-z0-9]/, "Password must include a special character"),
confirmPassword: z.string().min(1, "Please confirm your password"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type FormData = z.infer<typeof userSchema>;
@@ -43,10 +76,13 @@ export default function SignupPage() {
const { signup } = useAuth();
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(userSchema),
@@ -57,6 +93,8 @@ export default function SignupPage() {
userType: userType.individual,
firstName: { en: "", am: "" },
lastName: { en: "", am: "" },
password: "",
confirmPassword: "",
},
});
@@ -73,6 +111,8 @@ export default function SignupPage() {
phoneNumber: `${data.countryCode}${normalizedPhone}`,
userType: data.userType,
name: { en: `${data.firstName.en} ${data.lastName.en}`, am: "" },
password: data.password,
confirmPassword: data.confirmPassword,
};
const result = await signup(payload);
if (result.success) {
@@ -171,6 +211,81 @@ export default function SignupPage() {
countryCodeError={errors.countryCode}
phoneError={errors.phone}
/>
<Field data-invalid={Boolean(errors.password)}>
<FieldLabel>Password</FieldLabel>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
placeholder="Create a strong password"
disabled={loading}
aria-invalid={Boolean(errors.password)}
className="pr-10"
{...register("password")}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
tabIndex={-1}
>
{showPassword ? (
<EyeOff className="size-4" />
) : (
<Eye className="size-4" />
)}
</button>
</div>
<FieldError errors={[errors.password]} />
<div className="mt-2 space-y-1">
{passwordRequirements.map((req) => {
const met = req.test(watch("password") ?? "");
return (
<div
key={req.label}
className={cn(
"flex items-center gap-1.5 text-xs transition-colors",
met ? "text-emerald-600" : "text-muted-foreground",
)}
>
{met ? (
<Check className="size-3" />
) : (
<X className="size-3" />
)}
{req.label}
</div>
);
})}
</div>
</Field>
<Field data-invalid={Boolean(errors.confirmPassword)}>
<FieldLabel>Confirm Password</FieldLabel>
<div className="relative">
<Input
type={showConfirmPassword ? "text" : "password"}
placeholder="Re-enter your password"
disabled={loading}
aria-invalid={Boolean(errors.confirmPassword)}
className="pr-10"
{...register("confirmPassword")}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
tabIndex={-1}
>
{showConfirmPassword ? (
<EyeOff className="size-4" />
) : (
<Eye className="size-4" />
)}
</button>
</div>
<FieldError errors={[errors.confirmPassword]} />
</Field>
</FieldGroup>
<Button type="submit" disabled={loading} size="lg" className="w-full">

View File

@@ -21,6 +21,8 @@ export interface SignupPayload {
phoneNumber: string;
userType: string;
name: { en: string; am: string };
password: string;
confirmPassword: string;
}
export interface SignupResponse {