mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-23 09:42:19 +00:00
- Updated `package.json` to include a new script for launching Prisma Studio. - Modified `signup` function in `auth-actions.ts` to include account number in user data. - Refactored `createPayment` function in `payment.ts` to improve error handling and return structured responses. - Updated UI components in the dashboard to improve layout and responsiveness, including changes to `UserDevices` and `UserPayments` pages. - Introduced new `AdminDevicesTable` and `UsersPaymentsTable` components for better admin functionalities. - Enhanced `DeviceCartDrawer` to provide user feedback during payment processing. - Added account number input to the signup form and updated validation schema accordingly. - Updated Prisma schema to include a new `ninja_user_id` field for user management. These changes improve the overall functionality, maintainability, and user experience of the application, particularly in user management and payment processing.
324 lines
8.8 KiB
TypeScript
324 lines
8.8 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 { Loader } from "lucide-react";
|
|
import { useSearchParams } from "next/navigation";
|
|
import { useActionState } from "react";
|
|
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] = 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("");
|
|
|
|
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 ? <Loader 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>
|
|
);
|
|
}
|