mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-22 22:22:01 +00:00
Some checks failed
Build and Push Docker Images / Build and Push Docker Images (push) Failing after 1m24s
- Updated `auth-actions.ts` to improve user verification notification formatting and date handling. - Modified `layout.tsx` to support dark mode styling for better user experience. - Refactored `signup/page.tsx` to enhance layout and responsiveness. - Introduced a new API route in `route.ts` for sending user verification notifications. - Improved user feedback in `user-payments-table.tsx` by updating the no payment message. - Made minor adjustments in `application-layout.tsx` for consistent padding. - Enhanced `signup-form.tsx` to display error messages for invalid user validation. These changes improve the user verification process, enhance UI consistency, and provide better feedback to users.
337 lines
9.2 KiB
TypeScript
337 lines
9.2 KiB
TypeScript
"use client";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import Link from "next/link";
|
|
|
|
import { signup } from "@/actions/auth-actions";
|
|
import { cn } from "@/lib/utils";
|
|
import type { Island, Prisma } from "@prisma/client";
|
|
import { Loader2 } from "lucide-react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import * as React from "react";
|
|
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectGroup,
|
|
SelectItem,
|
|
SelectLabel,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
|
|
type AtollWithIslands = Prisma.AtollGetPayload<{
|
|
include: {
|
|
islands: true;
|
|
};
|
|
}>;
|
|
|
|
export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
|
|
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
|
|
const [islands, setIslands] = React.useState<Island[]>();
|
|
|
|
const [actionState, action, isPending] = React.useActionState(signup, {
|
|
message: "",
|
|
});
|
|
|
|
|
|
React.useEffect(() => {
|
|
setIslands(atoll?.islands);
|
|
}, [atoll]);
|
|
|
|
|
|
const params = useSearchParams();
|
|
const phoneNumberFromUrl = params.get("phone_number");
|
|
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
|
|
|
|
|
|
if (actionState.db_error === "invalidPersonValidation") {
|
|
return (
|
|
<>
|
|
<div className="h-24 w-72 text-center text-green-500 p-4 flex my-4 flex-col items-center justify-center border dark:title-bg bg-white rounded-lg">{actionState.message}</div>
|
|
<div className="mb-4 text-center text-sm">
|
|
Go to {" "}
|
|
<Link href="login" className="underline">
|
|
login
|
|
</Link>
|
|
</div>
|
|
</>
|
|
)
|
|
}
|
|
return (
|
|
<form
|
|
action={action}
|
|
className="max-w-xs mt-2 w-full bg-white dark:bg-transparent dark:border-2 shadow rounded-lg mx-auto"
|
|
>
|
|
<div className="py-2 px-4 my-2 space-y-2">
|
|
<div>
|
|
<label htmlFor="name" className="text-sm">
|
|
Name
|
|
</label>
|
|
|
|
<Input
|
|
className={cn(
|
|
"text-base",
|
|
actionState?.errors?.fieldErrors.name && "border-2 border-red-500",
|
|
)}
|
|
name="name"
|
|
type="text"
|
|
disabled={isPending}
|
|
defaultValue={(actionState?.payload?.get("name") || "") as string}
|
|
placeholder="Full Name"
|
|
/>
|
|
{actionState?.errors?.fieldErrors.name && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors.name}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="id_card" className="text-sm">
|
|
ID Card
|
|
</label>
|
|
<Input
|
|
name="id_card"
|
|
type="text"
|
|
maxLength={7}
|
|
disabled={isPending}
|
|
defaultValue={(actionState?.payload?.get("id_card") || "") as string}
|
|
className={cn(
|
|
"text-base",
|
|
actionState?.errors?.fieldErrors?.id_card &&
|
|
"border-2 border-red-500",
|
|
)}
|
|
placeholder="ID Card"
|
|
/>
|
|
{actionState?.errors?.fieldErrors?.id_card?.[0] && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors.fieldErrors.id_card[0]}
|
|
</span>
|
|
)}
|
|
{actionState?.db_error === "id_card" && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.message}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<div>
|
|
<label htmlFor="atoll" className="text-sm">
|
|
Atoll
|
|
</label>
|
|
<Select
|
|
disabled={isPending}
|
|
onValueChange={(v) => {
|
|
setAtoll(atolls.find((atoll) => atoll.id === v));
|
|
setIslands([]);
|
|
}}
|
|
name="atoll_id"
|
|
value={atoll?.id}
|
|
>
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select atoll" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel>Atolls</SelectLabel>
|
|
{atolls.map((atoll) => (
|
|
<SelectItem key={atoll.id} value={atoll.id}>
|
|
{atoll.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
{actionState?.errors?.fieldErrors?.atoll_id && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.atoll_id}
|
|
</span>
|
|
)}
|
|
</Select>
|
|
</div>
|
|
<div>
|
|
<label htmlFor="island" className="text-sm">
|
|
Island
|
|
</label>
|
|
<Select disabled={isPending} name="island_id">
|
|
<SelectTrigger className="w-full">
|
|
<SelectValue placeholder="Select island" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectGroup>
|
|
<SelectLabel>Islands</SelectLabel>
|
|
{islands?.map((island) => (
|
|
<SelectItem key={island.id} value={island.id}>
|
|
{island.name}
|
|
</SelectItem>
|
|
))}
|
|
</SelectGroup>
|
|
</SelectContent>
|
|
{actionState?.errors?.fieldErrors?.island_id && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.island_id}
|
|
</span>
|
|
)}
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="address" className="text-sm">
|
|
Address
|
|
</label>
|
|
<Input
|
|
className={cn(
|
|
"text-base",
|
|
actionState?.errors?.fieldErrors?.address &&
|
|
"border-2 border-red-500",
|
|
)}
|
|
disabled={isPending}
|
|
name="address"
|
|
defaultValue={
|
|
(actionState?.payload?.get("address") || "") as string
|
|
}
|
|
type="text"
|
|
placeholder="Address"
|
|
/>
|
|
{actionState?.errors?.fieldErrors?.address && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.address}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label htmlFor="dob" className="text-sm">
|
|
Date of Birth
|
|
</label>
|
|
<Input
|
|
className={cn(
|
|
"text-base",
|
|
actionState?.errors?.fieldErrors?.dob && "border-2 border-red-500",
|
|
)}
|
|
name="dob"
|
|
disabled={isPending}
|
|
defaultValue={(actionState?.payload?.get("dob") || "") as string}
|
|
type="date"
|
|
placeholder="Date of birth"
|
|
/>
|
|
{actionState?.errors?.fieldErrors?.dob && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.dob}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="accNo" className="text-sm">
|
|
Account Number
|
|
</label>
|
|
|
|
<Input
|
|
className={cn(
|
|
"text-base",
|
|
actionState?.errors?.fieldErrors.accNo && "border-2 border-red-500",
|
|
)}
|
|
name="accNo"
|
|
type="number"
|
|
disabled={isPending}
|
|
defaultValue={(actionState?.payload?.get("accNo") || "") as string}
|
|
placeholder="Account no"
|
|
/>
|
|
{actionState?.errors?.fieldErrors.accNo && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors.accNo}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="phone_number" className="text-sm">
|
|
Phone Number
|
|
</label>
|
|
<Input
|
|
id="phone-number"
|
|
name="phone_number"
|
|
maxLength={8}
|
|
disabled={isPending}
|
|
className={cn(
|
|
!phoneNumberFromUrl &&
|
|
actionState?.errors?.fieldErrors?.phone_number &&
|
|
"border-2 border-red-500 rounded-md",
|
|
)}
|
|
defaultValue={NUMBER_WITHOUT_DASH ?? ""}
|
|
readOnly={Boolean(phoneNumberFromUrl)}
|
|
placeholder={phoneNumberFromUrl ?? "Phone number"}
|
|
/>
|
|
</div>
|
|
{actionState?.errors?.fieldErrors?.phone_number?.[0] && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors.fieldErrors.phone_number[0]}
|
|
</span>
|
|
)}
|
|
{actionState?.db_error === "phone_number" && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.message}
|
|
</span>
|
|
)}
|
|
<div className="flex flex-col gap-2 items-start justify-start py-2">
|
|
<div className="flex gap-2 items-center">
|
|
<input
|
|
type="checkbox"
|
|
defaultChecked={(actionState?.payload?.get("terms") || "") as string === 'on'}
|
|
name="terms" id="terms" />
|
|
<label
|
|
htmlFor="terms"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
<span>
|
|
i accept
|
|
</span>
|
|
<Link className="ml-1 underline" href="">
|
|
terms and conditions
|
|
</Link>
|
|
</label>
|
|
</div>
|
|
{actionState?.errors?.fieldErrors?.terms && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.terms}
|
|
</span>
|
|
)}
|
|
<div className="flex gap-2 items-center">
|
|
|
|
<input
|
|
type="checkbox"
|
|
defaultChecked={(actionState?.payload?.get("policy") || "") as string === 'on'}
|
|
name="policy" id="terms" />
|
|
<label
|
|
htmlFor="terms"
|
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
>
|
|
<span>
|
|
i undertand
|
|
</span>
|
|
<Link className="ml-1 underline" href="">
|
|
the privacy policy
|
|
</Link>
|
|
</label>
|
|
</div>
|
|
{actionState?.errors?.fieldErrors?.policy && (
|
|
<span className="text-sm inline-block text-red-500">
|
|
{actionState?.errors?.fieldErrors?.policy}
|
|
</span>
|
|
)}
|
|
|
|
</div>
|
|
<Button disabled={isPending} className="mt-4 w-full" type="submit">
|
|
{isPending ? <Loader2 className="animate-spin" /> : "Submit"}
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="mb-4 text-center text-sm">
|
|
Already have an account?{" "}
|
|
<Link href="login" className="underline">
|
|
login
|
|
</Link>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|