refactor: update authentication flow to use NextAuth, replace better-auth with axios for API calls, and clean up unused code

This commit is contained in:
2025-03-23 15:07:03 +05:00
parent 0fd269df31
commit 020d74c5e2
23 changed files with 1269 additions and 1271 deletions

View File

@ -1,14 +1,14 @@
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { auth } from "@/lib/auth";
import { auth } from "@/app/auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import Link from "next/link";
@ -17,180 +17,188 @@ import DeviceCard from "../device-card";
import Pagination from "../pagination";
export async function AdminDevicesTable({
searchParams,
parentalControl,
searchParams,
parentalControl,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
}>;
parentalControl?: boolean;
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
}>;
parentalControl?: boolean;
}) {
const session = await auth.api.getSession({
headers: await headers()
})
const isAdmin = session?.user.role === "ADMIN"
const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page;
const sortBy = (await searchParams)?.sortBy || "asc";
const totalDevices = await prisma.device.count({
where: {
OR: [
{
name: {
contains: query || "",
mode: "insensitive",
},
},
{
mac: {
contains: query || "",
mode: "insensitive",
},
},
],
},
});
const session = await auth.api.getSession({
headers: await headers(),
});
const isAdmin = session?.user.role === "ADMIN";
const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page;
const sortBy = (await searchParams)?.sortBy || "asc";
const totalDevices = await prisma.device.count({
where: {
OR: [
{
name: {
contains: query || "",
mode: "insensitive",
},
},
{
mac: {
contains: query || "",
mode: "insensitive",
},
},
],
},
});
const totalPages = Math.ceil(totalDevices / 10);
const limit = 10;
const offset = (Number(page) - 1) * limit || 0;
const totalPages = Math.ceil(totalDevices / 10);
const limit = 10;
const offset = (Number(page) - 1) * limit || 0;
const devices = await prisma.device.findMany({
where: {
OR: [
{
name: {
contains: query || "",
mode: "insensitive",
},
},
{
mac: {
contains: query || "",
mode: "insensitive",
},
},
],
},
include: {
User: true,
payments: true,
},
skip: offset,
take: limit,
orderBy: {
name: `${sortBy}` as "asc" | "desc",
},
});
const devices = await prisma.device.findMany({
where: {
OR: [
{
name: {
contains: query || "",
mode: "insensitive",
},
},
{
mac: {
contains: query || "",
mode: "insensitive",
},
},
],
},
include: {
User: true,
payments: true,
},
skip: offset,
take: limit,
orderBy: {
name: `${sortBy}` as "asc" | "desc",
},
});
return (
<div>
{devices.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No devices yet.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>User</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>isActive</TableHead>
<TableHead>blocked</TableHead>
<TableHead>blockedBy</TableHead>
<TableHead>expiryDate</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{devices.map((device) => (
<TableRow key={device.id}>
<TableCell>
<div className="flex flex-col items-start">
<Link
className="font-medium hover:underline"
href={`/devices/${device.id}`}
>
{device.name}
</Link>
{device.isActive && (
return (
<div>
{devices.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No devices yet.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>User</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>isActive</TableHead>
<TableHead>blocked</TableHead>
<TableHead>blockedBy</TableHead>
<TableHead>expiryDate</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{devices.map((device) => (
<TableRow key={device.id}>
<TableCell>
<div className="flex flex-col items-start">
<Link
className="font-medium hover:underline"
href={`/devices/${device.id}`}
>
{device.name}
</Link>
{device.isActive && (
<span className="text-muted-foreground">
Active until{" "}
{new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
)}
<span className="text-muted-foreground">
Active until{" "}
{new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
)}
{device.blocked && (
<div className="p-2 rounded border my-2">
<span>Comment: </span>
<p className="text-neutral-500">
blocked because he was watching youtube
</p>
</div>
)}
</div>
</TableCell>
<TableCell className="font-medium">
{device.User?.name}
</TableCell>
{device.blocked && (
<div className="p-2 rounded border my-2">
<span>Comment: </span>
<p className="text-neutral-500">
blocked because he was watching youtube
</p>
</div>
)}
</div>
</TableCell>
<TableCell className="font-medium">{device.User?.name}</TableCell>
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell>
{device.isActive ? "Active" : "Inactive"}
</TableCell>
<TableCell>
{device.blocked ? "Blocked" : "Not Blocked"}
</TableCell>
<TableCell>
{device.blocked ? device.blockedBy : ""}
</TableCell>
<TableCell>
{new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</TableCell>
<TableCell>
<BlockDeviceDialog admin={isAdmin} type={device.blocked ? "unblock" : "block"} device={device} />
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={7}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {devices.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{totalDevices} devices
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden my-4">
{devices.map((device) => (
<DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
))}
</div>
</>
)}
</div>
);
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell>
{device.isActive ? "Active" : "Inactive"}
</TableCell>
<TableCell>
{device.blocked ? "Blocked" : "Not Blocked"}
</TableCell>
<TableCell>
{device.blocked ? device.blockedBy : ""}
</TableCell>
<TableCell>
{new Date().toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</TableCell>
<TableCell>
<BlockDeviceDialog
admin={isAdmin}
type={device.blocked ? "unblock" : "block"}
device={device}
/>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={7}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {devices.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{totalDevices} devices
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden my-4">
{devices.map((device) => (
<DeviceCard
parentalControl={parentalControl}
key={device.id}
device={device}
/>
))}
</div>
</>
)}
</div>
);
}

View File

@ -10,7 +10,7 @@ import {
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { auth } from "@/lib/auth";
import { auth } from "@/app/auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import { AccountPopover } from "./account-popver";
@ -19,7 +19,7 @@ export async function ApplicationLayout({
children,
}: { children: React.ReactNode }) {
const session = await auth.api.getSession({
headers: await headers()
headers: await headers(),
});
const billFormula = await prisma.billFormula.findFirst();
const user = await prisma.user.findFirst({

View File

@ -23,6 +23,7 @@ export default function LoginForm() {
<PhoneInput
id="phone-number"
name="phoneNumber"
className="b0rder"
maxLength={8}
disabled={isPending}
placeholder="Enter phone number"
@ -32,11 +33,7 @@ export default function LoginForm() {
{state.status === "error" && (
<p className="text-red-500 text-sm">{state.message}</p>
)}
<Button
className=""
disabled={isPending}
type="submit"
>
<Button className="" disabled={isPending} type="submit">
{isPending ? <Loader2 className="animate-spin" /> : "Request OTP"}
</Button>
</div>

View File

@ -8,7 +8,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { auth } from "@/lib/auth";
import { auth } from "@/app/auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import ClickableRow from "./clickable-row";
@ -27,9 +27,9 @@ export async function DevicesTable({
parentalControl?: boolean;
}) {
const session = await auth.api.getSession({
headers: await headers()
})
const isAdmin = session?.user.role === "ADMIN"
headers: await headers(),
});
const isAdmin = session?.user.role === "ADMIN";
const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page;
const sortBy = (await searchParams)?.sortBy || "asc";
@ -53,12 +53,16 @@ export async function DevicesTable({
NOT: {
payments: {
some: {
paid: false
}
}
paid: false,
},
},
},
isActive: isAdmin ? undefined : parentalControl,
blocked: isAdmin ? undefined : parentalControl !== undefined ? undefined : false,
blocked: isAdmin
? undefined
: parentalControl !== undefined
? undefined
: false,
},
});
@ -86,8 +90,8 @@ export async function DevicesTable({
NOT: {
payments: {
some: {
paid: false
}
paid: false,
},
},
},
isActive: parentalControl,
@ -158,7 +162,12 @@ export async function DevicesTable({
// )}
// </TableCell>
// </TableRow>
<ClickableRow admin={isAdmin} key={device.id} device={device} parentalControl={parentalControl} />
<ClickableRow
admin={isAdmin}
key={device.id}
device={device}
parentalControl={parentalControl}
/>
))}
</TableBody>
<TableFooter>
@ -181,7 +190,11 @@ export async function DevicesTable({
</div>
<div className="sm:hidden my-4">
{devices.map((device) => (
<DeviceCard parentalControl={parentalControl} key={device.id} device={device} />
<DeviceCard
parentalControl={parentalControl}
key={device.id}
device={device}
/>
))}
</div>
</>

View File

@ -1,17 +1,17 @@
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import prisma from "@/lib/db";
import Link from "next/link";
import { auth } from "@/lib/auth";
import { auth } from "@/app/auth";
import { cn } from "@/lib/utils";
import type { Prisma } from "@prisma/client";
import { Calendar } from "lucide-react";
@ -22,219 +22,258 @@ import { Button } from "./ui/button";
import { Separator } from "./ui/separator";
type PaymentWithDevices = Prisma.PaymentGetPayload<{
include: {
devices: true;
};
}>
include: {
devices: true;
};
}>;
export async function PaymentsTable({
searchParams,
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
}>;
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
}>;
}) {
const session = await auth.api.getSession({
headers: await headers()
})
const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page;
const totalPayments = await prisma.payment.count({
where: {
userId: session?.session.userId,
OR: [
{
devices: {
every: {
name: {
contains: query || "",
mode: "insensitive",
},
},
},
},
],
},
});
const session = await auth.api.getSession({
headers: await headers(),
});
const query = (await searchParams)?.query || "";
const page = (await searchParams)?.page;
const totalPayments = await prisma.payment.count({
where: {
userId: session?.session.userId,
OR: [
{
devices: {
every: {
name: {
contains: query || "",
mode: "insensitive",
},
},
},
},
],
},
});
const totalPages = Math.ceil(totalPayments / 10);
const limit = 10;
const offset = (Number(page) - 1) * limit || 0;
const totalPages = Math.ceil(totalPayments / 10);
const limit = 10;
const offset = (Number(page) - 1) * limit || 0;
const payments = await prisma.payment.findMany({
where: {
userId: session?.session.userId,
OR: [
{
devices: {
every: {
name: {
contains: query || "",
mode: "insensitive",
},
},
},
},
],
},
include: {
devices: true
},
const payments = await prisma.payment.findMany({
where: {
userId: session?.session.userId,
OR: [
{
devices: {
every: {
name: {
contains: query || "",
mode: "insensitive",
},
},
},
},
],
},
include: {
devices: true,
},
skip: offset,
take: limit,
orderBy: {
createdAt: "desc",
},
});
skip: offset,
take: limit,
orderBy: {
createdAt: "desc",
},
});
return (
<div>
{payments.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No Payments yet.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
return (
<div>
{payments.length === 0 ? (
<div className="h-[calc(100svh-400px)] flex flex-col items-center justify-center my-4">
<h3>No Payments yet.</h3>
</div>
) : (
<>
<div className="hidden sm:block">
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Details</TableHead>
<TableHead>Duration</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
<div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground">
{new Date(payment.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{payments.map((payment) => (
<TableRow key={payment.id}>
<TableCell>
<div
className={cn(
"flex flex-col items-start border rounded p-2",
payment?.paid
? "bg-green-500/10 border-dashed border-green=500"
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
)}
>
<div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground">
{new Date(payment.createdAt).toLocaleDateString(
"en-US",
{
month: "short",
day: "2-digit",
year: "numeric",
},
)}
</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
<Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
{payment.paid ? "Paid" : "Unpaid"}
</Badge>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li key={device.id} className="text-sm text-muted-foreground">
{device.name}
</li>
))}
</ol>
</div>
</div>
</TableCell>
<TableCell className="font-medium" >
{payment.numberOfMonths} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {payments.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{totalPayments} payments
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden block">
{payments.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</div>
</>
)}
</div>
);
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/payments/${payment.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
<Badge
className={cn(
payment?.paid
? "text-green-500 bg-green-500/20"
: "text-yellow-500 bg-yellow-500/20",
)}
variant={payment.paid ? "outline" : "secondary"}
>
{payment.paid ? "Paid" : "Unpaid"}
</Badge>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li
key={device.id}
className="text-sm text-muted-foreground"
>
{device.name}
</li>
))}
</ol>
</div>
</div>
</TableCell>
<TableCell className="font-medium">
{payment.numberOfMonths} Months
</TableCell>
<TableCell>
<span className="font-semibold pr-2">
{payment.amount.toFixed(2)}
</span>
MVR
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
{query.length > 0 && (
<p className="text-sm text-muted-foreground">
Showing {payments.length} locations for &quot;{query}
&quot;
</p>
)}
</TableCell>
<TableCell className="text-muted-foreground">
{totalPayments} payments
</TableCell>
</TableRow>
</TableFooter>
</Table>
<Pagination totalPages={totalPages} currentPage={page} />
</div>
<div className="sm:hidden block">
{payments.map((payment) => (
<MobilePaymentDetails key={payment.id} payment={payment} />
))}
</div>
</>
)}
</div>
);
}
function MobilePaymentDetails({ payment }: { payment: PaymentWithDevices }) {
return (
<div className={cn("flex flex-col items-start border rounded p-2", payment?.paid ? "bg-green-500/10 border-dashed border-green=500" : "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50")}>
<div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground text-sm">
{new Date(payment.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
return (
<div
className={cn(
"flex flex-col items-start border rounded p-2",
payment?.paid
? "bg-green-500/10 border-dashed border-green=500"
: "bg-yellow-500/10 border-dashed border-yellow-500 dark:border-yellow-500/50",
)}
>
<div className="flex items-center gap-2">
<Calendar size={16} opacity={0.5} />
<span className="text-muted-foreground text-sm">
{new Date(payment.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
year: "numeric",
})}
</span>
</div>
<div className="flex items-center gap-2 mt-2">
<Link className="font-medium hover:underline" href={`/payments/${payment.id}`}>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
<Badge className={cn(payment?.paid ? "text-green-500 bg-green-500/20" : "text-yellow-500 bg-yellow-500/20")} variant={payment.paid ? "outline" : "secondary"}>
{payment.paid ? "Paid" : "Unpaid"}
</Badge>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li key={device.id} className="text-sm text-muted-foreground">
{device.name}
</li>
))}
</ol>
<div className="block sm:hidden">
<Separator className="my-2" />
<h3 className="text-sm font-medium">Duration</h3>
<span className="text-sm text-muted-foreground">
{payment.numberOfMonths} Months
</span>
<Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR
</span>
</div>
</div>
</div>
)
}
<div className="flex items-center gap-2 mt-2">
<Link
className="font-medium hover:underline"
href={`/payments/${payment.id}`}
>
<Button size={"sm"} variant="outline">
View Details
</Button>
</Link>
<Badge
className={cn(
payment?.paid
? "text-green-500 bg-green-500/20"
: "text-yellow-500 bg-yellow-500/20",
)}
variant={payment.paid ? "outline" : "secondary"}
>
{payment.paid ? "Paid" : "Unpaid"}
</Badge>
</div>
<div className="bg-white dark:bg-black p-2 rounded mt-2 w-full border">
<h3 className="text-sm font-medium">Devices</h3>
<ol className="list-disc list-inside text-sm">
{payment.devices.map((device) => (
<li key={device.id} className="text-sm text-muted-foreground">
{device.name}
</li>
))}
</ol>
<div className="block sm:hidden">
<Separator className="my-2" />
<h3 className="text-sm font-medium">Duration</h3>
<span className="text-sm text-muted-foreground">
{payment.numberOfMonths} Months
</span>
<Separator className="my-2" />
<h3 className="text-sm font-medium">Amount</h3>
<span className="text-sm text-muted-foreground">
{payment.amount.toFixed(2)} MVR
</span>
</div>
</div>
</div>
);
}

View File

@ -5,164 +5,160 @@ import flags from "react-phone-number-input/flags";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverContent,
PopoverTrigger,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
type PhoneInputProps = Omit<
React.ComponentProps<"input">,
"onChange" | "value" | "ref"
React.ComponentProps<"input">,
"onChange" | "value" | "ref"
> &
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
onChange?: (value: RPNInput.Value) => void;
};
Omit<RPNInput.Props<typeof RPNInput.default>, "onChange"> & {
onChange?: (value: RPNInput.Value) => void;
};
const PhoneInput: React.ForwardRefExoticComponent<PhoneInputProps> =
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
({ className, onChange, ...props }, ref) => {
return (
<RPNInput.default
ref={ref}
className={cn("flex", className)}
flagComponent={FlagComponent}
countrySelectComponent={CountrySelect}
inputComponent={InputComponent}
smartCaret={false}
/**
* Handles the onChange event.
*
* react-phone-number-input might trigger the onChange event as undefined
* when a valid phone number is not entered. To prevent this,
* the value is coerced to an empty string.
*
* @param {E164Number | undefined} value - The entered value
*/
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
{...props}
/>
);
},
);
React.forwardRef<React.ElementRef<typeof RPNInput.default>, PhoneInputProps>(
({ className, onChange, ...props }, ref) => {
return (
<RPNInput.default
ref={ref}
className={cn("flex", className)}
flagComponent={FlagComponent}
countrySelectComponent={CountrySelect}
inputComponent={InputComponent}
smartCaret={false}
/**
* Handles the onChange event.
*
* react-phone-number-input might trigger the onChange event as undefined
* when a valid phone number is not entered. To prevent this,
* the value is coerced to an empty string.
*
* @param {E164Number | undefined} value - The entered value
*/
onChange={(value) => onChange?.(value || ("" as RPNInput.Value))}
{...props}
/>
);
},
);
PhoneInput.displayName = "PhoneInput";
const InputComponent = React.forwardRef<
HTMLInputElement,
React.ComponentProps<"input">
HTMLInputElement,
React.ComponentProps<"input">
>(({ className, ...props }, ref) => (
<Input
className={cn("rounded-e-lg rounded-s-none", className)}
{...props}
ref={ref}
/>
<Input className={cn("mx-2", className)} {...props} ref={ref} />
));
InputComponent.displayName = "InputComponent";
type CountryEntry = { label: string; value: RPNInput.Country | undefined };
type CountrySelectProps = {
disabled?: boolean;
value: RPNInput.Country;
options: CountryEntry[];
onChange: (country: RPNInput.Country) => void;
disabled?: boolean;
value: RPNInput.Country;
options: CountryEntry[];
onChange: (country: RPNInput.Country) => void;
};
const CountrySelect = ({
disabled,
value: selectedCountry,
options: countryList,
onChange,
disabled,
value: selectedCountry,
options: countryList,
onChange,
}: CountrySelectProps) => {
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
className="flex gap-1 rounded-e-none rounded-s-lg border-r-0 px-3 focus:z-10"
disabled={true}
>
<FlagComponent
country={selectedCountry}
countryName={selectedCountry}
/>
<ChevronsUpDown
className={cn(
"-mr-2 size-4 opacity-50",
disabled ? "hidden" : "opacity-100",
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search country..." />
<CommandList>
<ScrollArea className="h-72">
<CommandEmpty>No country found.</CommandEmpty>
<CommandGroup>
{countryList.map(({ value, label }) =>
value ? (
<CountrySelectOption
key={value}
country={value}
countryName={label}
selectedCountry={selectedCountry}
onChange={onChange}
/>
) : null,
)}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
return (
<Popover>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
className="flex gap-1 px-3 focus:z-10"
disabled={true}
>
<FlagComponent
country={selectedCountry}
countryName={selectedCountry}
/>
<ChevronsUpDown
className={cn(
"-mr-2 size-4 opacity-50",
disabled ? "hidden" : "opacity-100",
)}
/>
</Button>
</PopoverTrigger>
<PopoverContent className="w-[300px] p-0">
<Command>
<CommandInput placeholder="Search country..." />
<CommandList>
<ScrollArea className="h-72">
<CommandEmpty>No country found.</CommandEmpty>
<CommandGroup>
{countryList.map(({ value, label }) =>
value ? (
<CountrySelectOption
key={value}
country={value}
countryName={label}
selectedCountry={selectedCountry}
onChange={onChange}
/>
) : null,
)}
</CommandGroup>
</ScrollArea>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
interface CountrySelectOptionProps extends RPNInput.FlagProps {
selectedCountry: RPNInput.Country;
onChange: (country: RPNInput.Country) => void;
selectedCountry: RPNInput.Country;
onChange: (country: RPNInput.Country) => void;
}
const CountrySelectOption = ({
country,
countryName,
selectedCountry,
onChange,
country,
countryName,
selectedCountry,
onChange,
}: CountrySelectOptionProps) => {
return (
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
<FlagComponent country={country} countryName={countryName} />
<span className="flex-1 text-sm">{countryName}</span>
<span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
<CheckIcon
className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
/>
</CommandItem>
);
return (
<CommandItem className="gap-2" onSelect={() => onChange(country)}>
<FlagComponent country={country} countryName={countryName} />
<span className="flex-1 text-sm">{countryName}</span>
<span className="text-sm text-foreground/50">{`+${RPNInput.getCountryCallingCode(country)}`}</span>
<CheckIcon
className={`ml-auto size-4 ${country === selectedCountry ? "opacity-100" : "opacity-0"}`}
/>
</CommandItem>
);
};
const FlagComponent = ({ country, countryName }: RPNInput.FlagProps) => {
const Flag = flags[country];
const Flag = flags[country];
return (
<span className="flex scale-125 h-4 w-6 overflow-hidden rounded-sm cursor-not-allowed">
{Flag && <Flag title={countryName} />}
</span>
);
return (
<span className="flex scale-125 h-4 w-6 overflow-hidden rounded-sm cursor-not-allowed">
{Flag && <Flag title={countryName} />}
</span>
);
};
export { PhoneInput };