Refactor authentication and dashboard components

- Updated login and signup pages to include session checks and redirection based on user authentication status.
- Introduced QueryProvider for managing server state in the application.
- Enhanced user experience by integrating session management in the devices and payments dashboard.
- Added new user management features with role-based access control in the sidebar.
- Created new components for user devices and payments, improving the overall structure and maintainability of the dashboard.
- Implemented a table component for better data presentation in user-related views.
This commit is contained in:
i701 2024-11-27 14:18:17 +05:00
parent 8e6f802218
commit 0322bee567
16 changed files with 713 additions and 372 deletions

View File

@ -1,19 +1,29 @@
import LoginForm from "@/components/auth/login-form";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import Image from "next/image";
import { redirect } from "next/navigation";
import React from "react";
export default function LoginPage() {
return <div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<div className="mt-4 flex flex-col items-center justify-center">
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">Pay for your devices and track your bills.</p>
export default async function LoginPage() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (session) {
return redirect("/devices");
}
return (
<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<div className="mt-4 flex flex-col items-center justify-center">
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">
Pay for your devices and track your bills.
</p>
</div>
<LoginForm />
</div>
<LoginForm />
</div>
</div>;
);
}

View File

@ -1,25 +1,51 @@
import SignUpForm from "@/components/auth/signup-form";
import { auth } from "@/lib/auth";
import prisma from "@/lib/db";
import { headers } from "next/headers";
import Image from "next/image";
import { redirect } from "next/navigation";
import React from "react";
export default async function LoginPage() {
const atolls = await prisma.atoll.findMany({
include: {
islands: true
}
})
return <div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image priority alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<div className="mt-4 flex flex-col items-center justify-center">
export default async function SignupPage({
searchParams,
}: {
searchParams: Promise<{ phone_number: string }>;
}) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (session) {
return redirect("/devices");
}
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">Pay for your devices and track your bills.</p>
</div>
<SignUpForm atolls={atolls} />
</div>
</div>;
const phone_number = (await searchParams).phone_number;
if (!phone_number) {
return redirect("/login");
}
const atolls = await prisma.atoll.findMany({
include: {
islands: true,
},
});
return (
<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image
priority
alt="Sar Link Logo"
src="/logo.png"
width={100}
height={100}
/>
<div className="mt-4 flex flex-col items-center justify-center">
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">
Pay for your devices and track your bills.
</p>
</div>
<SignUpForm atolls={atolls} />
</div>
</div>
);
}

View File

@ -1,24 +1,34 @@
import VerifyOTPForm from "@/components/auth/verify-otp-form";
import Image from "next/image";
import { redirect } from "next/navigation";
import React from "react";
export default async function VerifyOTP({
searchParams,
searchParams,
}: {
searchParams: Promise<{ phone_number: string }>
searchParams: Promise<{ phone_number: string }>;
}) {
const phone_number = (await searchParams).phone_number
return <div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<div className="mt-4 flex flex-col items-center justify-center">
const phone_number = (await searchParams).phone_number;
if (!phone_number) {
return redirect("/login");
}
console.log(
"phone number from server page params (verify otp page)",
phone_number,
);
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">Pay for your devices and track your bills.</p>
</div>
<VerifyOTPForm phone_number={phone_number} />
</div>
</div>;
return (
<div className="bg-gray-100 dark:bg-black w-full h-screen flex items-center justify-center font-sans">
<div className="flex flex-col items-center justify-center w-full h-full ">
<Image alt="Sar Link Logo" src="/logo.png" width={100} height={100} />
<div className="mt-4 flex flex-col items-center justify-center">
<h4 className="font-bold text-xl text-gray-600">SAR Link Portal</h4>
<p className="text-gray-500">
Pay for your devices and track your bills.
</p>
</div>
<VerifyOTPForm phone_number={phone_number} />
</div>
</div>
);
}

View File

@ -1,5 +1,15 @@
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export default async function Devices() {
return <div>
<h2>Devices</h2>
</div>;
const session = await auth.api.getSession({
headers: await headers(),
});
return (
<div>
<h2>Server session</h2>
<pre>{JSON.stringify(session?.user, null, 2)}</pre>
</div>
);
}

View File

@ -1,17 +1,14 @@
'use client'
import { PhoneInput } from '@/components/ui/phone-input'
import React from 'react'
"use client";
import { authClient } from "@/lib/auth-client";
import React from "react";
export default function MyPayments() {
return (
<div>
<PhoneInput
id="phone-number"
name="phoneNumber"
placeholder="Enter phone number"
defaultCountry="MV"
/>
</div>
const session = authClient.useSession();
)
return (
<div>
<h3>Client session</h3>
<pre>{JSON.stringify(session.data, null, 2)}</pre>
</div>
);
}

View File

@ -0,0 +1,5 @@
import React from "react";
export default function UserDevices() {
return <div>UserDevices</div>;
}

View File

@ -0,0 +1,11 @@
import { AdminAuthGuard } from "@/lib/auth-guard";
import React from "react";
export default async function UserPayments() {
await AdminAuthGuard();
return (
<div>
<h3>User Payments</h3>
</div>
);
}

View File

@ -0,0 +1,56 @@
import Filter from "@/components/filter";
import Search from "@/components/search";
import { UsersTable } from "@/components/user-table";
import { AdminAuthGuard } from "@/lib/auth-guard";
import { CheckCheck, Hourglass, Minus } from "lucide-react";
import React, { Suspense } from "react";
export default async function AdminUsers({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
await AdminAuthGuard();
return (
<div>
<h3 className="border-b-2 text-2xl font-bold title-bg py-4 px-2 mb-4">
Users
</h3>
<div
id="user-filters"
className=" border-b-2 pb-4 gap-4 flex items-center justify-start"
>
<Search />
<Filter
options={[
{
value: "all",
label: "ALL",
icon: <Minus size={14} />,
},
{
value: "unverified",
label: "Unverfieid",
icon: <CheckCheck size={14} />,
},
{
value: "verified",
label: "Verified",
icon: <Hourglass size={14} />,
},
]}
defaultOption="all"
queryParamKey="status"
/>
</div>
<Suspense fallback={"loading...."}>
<UsersTable searchParams={searchParams} />
</Suspense>
</div>
);
}

View File

@ -1,10 +1,11 @@
import type { Metadata } from "next";
import "./globals.css";
import { ThemeProvider } from "@/components/theme-provider";
import { Barlow } from "next/font/google";
import NextTopLoader from 'nextjs-toploader';
import { Toaster } from 'sonner'
import type { Metadata } from "next";
import { Barlow } from "next/font/google";
import NextTopLoader from "nextjs-toploader";
import { Toaster } from "sonner";
import "./globals.css";
import QueryProvider from "@/components/query-provider";
const barlow = Barlow({
subsets: ["latin"],
weight: ["100", "300", "400", "500", "600", "700", "800", "900"],
@ -32,7 +33,7 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
{children}
<QueryProvider>{children}</QueryProvider>
</ThemeProvider>
</body>
</html>

View File

@ -18,24 +18,24 @@ import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { AccountPopover } from "./account-popver";
export async function ApplicationLayout({ children }: { children: React.ReactNode }) {
export async function ApplicationLayout({
children,
}: { children: React.ReactNode }) {
const session = await auth.api.getSession({
headers: await headers(), // you need to pass the headers object.
});
return (
<SidebarProvider>
<AppSidebar />
<AppSidebar role={session?.user?.role || "USER"} />
<SidebarInset>
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4">
<div className="flex items-center gap-2">
<header className="flex justify-between sticky top-0 bg-background h-16 shrink-0 items-center gap-2 border-b px-4 z-10">
<div className="flex items-center gap-2 ">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
MENU
</BreadcrumbLink>
<BreadcrumbLink href="#">MENU</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
@ -50,10 +50,7 @@ export async function ApplicationLayout({ children }: { children: React.ReactNod
<AccountPopover user={session?.user} />
</div>
</header>
<div className="px-8 py-6">
{children}
</div>
<div className="px-8 py-6">{children}</div>
</SidebarInset>
</SidebarProvider>
);

View File

@ -1,6 +1,5 @@
"use client";
import { Button } from "@/components/ui/button";
import { signin } from "@/actions/auth-actions";
@ -15,13 +14,17 @@ export default function LoginForm() {
});
return (
<form className="bg-white overflow-clip dark:bg-transparent dark:border-2 w-full max-w-xs mx-auto rounded-lg shadow mt-4" action={formAction}>
<div className="py-6 px-10">
<form
className="bg-white overflow-clip dark:bg-transparent dark:border-2 w-full max-w-xs mx-auto rounded-lg shadow mt-4"
action={formAction}
>
<div className="py-4 px-4">
<div className="grid gap-4">
<PhoneInput
id="phone-number"
name="phoneNumber"
maxLength={8}
disabled={isPending}
placeholder="Enter phone number"
defaultCountry="MV"
/>
@ -29,7 +32,11 @@ export default function LoginForm() {
{state.status === "error" && (
<p className="text-red-500 text-sm">{state.message}</p>
)}
<Button className="dark:bg-gray-800 w-full dark:text-white" disabled={isPending} type="submit">
<Button
className="dark:bg-gray-800 w-full dark:text-white"
disabled={isPending}
type="submit"
>
{isPending ? <Loader className="animate-spin" /> : "Request OTP"}
</Button>
</div>

View File

@ -8,234 +8,244 @@ import { cn } from "@/lib/utils";
import { Loader } from "lucide-react";
import { useActionState } from "react";
import { useSearchParams } from "next/navigation";
import { Atoll, Island, Prisma } from "@prisma/client";
import * as React from "react"
import type { Island, Prisma } from "@prisma/client";
import * as React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
type AtollWithIslands = Prisma.AtollGetPayload<{
include: {
islands: true;
}
}>
include: {
islands: true;
};
}>;
export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
const [atoll, setAtoll] = React.useState<AtollWithIslands>()
const [island, setIsland] = React.useState<string>()
const [atoll, setAtoll] = React.useState<AtollWithIslands>();
const [islands, setIslands] = React.useState<Island[]>();
React.useEffect(() => {
setIslands(atoll?.islands);
}, [atoll]);
const [actionState, action, isPending] = useActionState(signup, {
message: "",
});
const params = useSearchParams();
const phoneNumberFromUrl = params.get("phone_number");
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
const [actionState, action, isPending] = useActionState(signup, {
message: "",
});
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-4 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>
return (
<form
action={action}
className="max-w-xs mt-4 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>
<div>
<label htmlFor="house_name" className="text-sm">
House Name
</label>
<Input
className={cn(
"text-base",
actionState.errors?.fieldErrors?.house_name &&
"border-2 border-red-500",
)}
disabled={isPending}
name="house_name"
defaultValue={
(actionState.payload?.get("house_name") || "") as string
}
type="text"
placeholder="House Name"
/>
{actionState.errors?.fieldErrors?.house_name && (
<span className="text-sm inline-block text-red-500">
{actionState.errors?.fieldErrors?.house_name}
</span>
)}
</div>
<Input
className={cn(
'text-base',
actionState.errors?.fieldErrors.name && 'border-2 border-red-500',
)}
name="name"
type="text"
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}
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>
{/* <label htmlFor="island_name" className="text-sm">Island Name</label>
<Input
name="island_name"
type="text"
defaultValue={"F.Dharaboondhoo"}
className={cn(
'text-base cursor-not-allowed',
actionState.errors?.fieldErrors?.island_name && 'border-2 border-red-500',
)}
readOnly
/>
{actionState?.errors?.fieldErrors.island_name && (
<span className="text-sm inline-block text-red-500">
{actionState?.errors?.fieldErrors.island_name}
</span>
)} */}
<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="atoll" className="text-sm">Atoll</label>
<Select onValueChange={(v) => {
setAtoll(atolls.find((atoll) => atoll.id === v))
setIsland("")
}} 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
name="island_id">
<SelectTrigger className="w-full">
<SelectValue placeholder="Select island" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Islands</SelectLabel>
{atoll?.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="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>
)}
<Button disabled={isPending} className="mt-4 w-full" type="submit">
{isPending ? <Loader className="animate-spin" /> : "Submit"}
</Button>
</div>
<div>
<label htmlFor="house_name" className="text-sm">House Name</label>
<Input
className={cn(
'text-base',
actionState.errors?.fieldErrors?.house_name && 'border-2 border-red-500',
)}
name="house_name"
defaultValue={(actionState.payload?.get("house_name") || "") as string}
type="text"
placeholder="House Name"
/>
{actionState.errors?.fieldErrors?.house_name && (
<span className="text-sm inline-block text-red-500">
{actionState.errors?.fieldErrors?.house_name}
</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"
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="phone_number" className="text-sm">Phone Number</label>
<Input
id="phone-number"
name="phone_number"
maxLength={8}
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>
)}
<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>
);
<div className="mb-4 text-center text-sm">
Already have an account?{" "}
<Link href="login" className="underline">
login
</Link>
</div>
</form>
);
}

View File

@ -4,84 +4,86 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { authClient } from "@/lib/auth-client";
import { zodResolver } from '@hookform/resolvers/zod';
import { zodResolver } from "@hookform/resolvers/zod";
import { Loader } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { type SubmitHandler, useForm } from "react-hook-form";
import { toast } from 'sonner'
import { toast } from "sonner";
import { z } from "zod";
const OTPSchema = z.object({
pin: z.string().min(6, {
message: "Your one-time password must be 6 characters.",
}),
pin: z.string().min(6, {
message: "Your one-time password must be 6 characters.",
}),
});
export default function VerifyOTPForm({ phone_number }: { phone_number: string }) {
const [isPending, startTransition] = useTransition();
const router = useRouter();
console.log("verification in OTP form", phone_number)
const {
register,
handleSubmit,
formState: { errors },
} = useForm<z.infer<typeof OTPSchema>>({
defaultValues: {
pin: "",
},
resolver: zodResolver(OTPSchema),
});
export default function VerifyOTPForm({
phone_number,
}: { phone_number: string }) {
const [isPending, startTransition] = useTransition();
const router = useRouter();
console.log("verification in OTP form", phone_number);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<z.infer<typeof OTPSchema>>({
defaultValues: {
pin: "",
},
resolver: zodResolver(OTPSchema),
});
const onSubmit: SubmitHandler<z.infer<typeof OTPSchema>> = (data) => {
const onSubmit: SubmitHandler<z.infer<typeof OTPSchema>> = (data) => {
startTransition(async () => {
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: phone_number,
code: data.pin,
});
console.log({ isVerified });
if (!isVerified.error) {
router.push("/devices");
} else {
toast.error(isVerified.error.message);
}
});
};
startTransition(async () => {
const isVerified = await authClient.phoneNumber.verify({
phoneNumber: phone_number,
code: data.pin,
});
console.log({ isVerified });
if (!isVerified.error) {
router.push("/devices");
} else {
toast.error(isVerified.error.message);
}
});
}
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="bg-white dark:bg-gray-900 w-full max-w-xs rounded-lg shadow my-4"
>
<div className="grid pb-4 pt-2 gap-4 px-10">
<div className="">
<Label htmlFor="otp-number" className="text-gray-500">
Enter the OTP
</Label>
<Input
id="otp-number"
{...register("pin")}
type="text"
/>
{errors.pin && (
<p className="text-red-500 text-sm">{errors.pin.message}</p>
)}
</div>
<Button
className="dark:bg-gray-800 w-full dark:text-white"
disabled={isPending}
type="submit"
>
{isPending ? <Loader className="animate-spin" /> : "Login"}
</Button>
</div>
<div className="mb-4 text-center text-sm">
Go back to{" "}
<Link href="login" className="underline">
login
</Link>
</div>
</form>
);
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="bg-white dark:bg-gray-900 w-full max-w-xs rounded-lg shadow my-4"
>
<div className="grid pb-4 pt-4 gap-4 px-4">
<div className="">
<Label htmlFor="otp-number" className="text-gray-500">
Enter the OTP
</Label>
<Input
disabled={isPending}
id="otp-number"
{...register("pin")}
type="text"
/>
{errors.pin && (
<p className="text-red-500 text-sm">{errors.pin.message}</p>
)}
</div>
<Button
className="dark:bg-gray-800 w-full dark:text-white"
disabled={isPending}
type="submit"
>
{isPending ? <Loader className="animate-spin" /> : "Login"}
</Button>
</div>
<div className="mb-4 text-center text-sm">
Go back to{" "}
<Link href="login" className="underline">
login
</Link>
</div>
</form>
);
}

View File

@ -0,0 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function QueryProvider({
children,
}: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}

View File

@ -24,6 +24,7 @@ const data = {
{
title: "MENU",
url: "#",
requiredRoles: ["ADMIN", "USER"],
items: [
{
title: "Devices",
@ -35,13 +36,34 @@ const data = {
},
],
},
{
title: "ADMIN CONTROL",
url: "#",
requiredRoles: ["ADMIN"],
items: [
{
title: "Users",
url: "/users",
},
{
title: "User Devices",
url: "/user-devices",
},
{
title: "User Payments",
url: "/user-payments",
},
],
},
],
};
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
export function AppSidebar({
role,
...props
}: React.ComponentProps<typeof Sidebar> & { role: string }) {
return (
<Sidebar {...props}>
<Sidebar {...props} className="z-50">
<SidebarHeader>
<h4 className="bg-gray-200 p-2 rounded shadow text-center uppercase dark:bg-gray-800">
Sar Link Portal
@ -49,7 +71,8 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarHeader>
<SidebarContent className="gap-0">
{/* We create a collapsible SidebarGroup for each parent. */}
{data.navMain.map((item) => (
{/* {data.navMain.map((item) => (
<Collapsible
key={item.title}
title={item.title}
@ -83,7 +106,51 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
))}
))} */}
{data.navMain
.filter(
(item) =>
!item.requiredRoles || item.requiredRoles.includes(role || ""),
)
.map((item) => {
if (item.requiredRoles?.includes(role)) {
return (
<Collapsible
key={item.title}
title={item.title}
defaultOpen
className="group/collapsible"
>
<SidebarGroup>
<SidebarGroupLabel
asChild
className="group/label text-sm text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
>
<CollapsibleTrigger>
{item.title}{" "}
<ChevronRight className="ml-auto transition-transform group-data-[state=open]/collapsible:rotate-90" />
</CollapsibleTrigger>
</SidebarGroupLabel>
<CollapsibleContent>
<SidebarGroupContent>
<SidebarMenu>
{item.items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton className="py-6" asChild>
<Link className="text-md" href={item.url}>
{item.title}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
</CollapsibleContent>
</SidebarGroup>
</Collapsible>
);
}
})}
</SidebarContent>
<SidebarRail />
</Sidebar>

120
components/ui/table.tsx Normal file
View File

@ -0,0 +1,120 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
))
Table.displayName = "Table"
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
))
TableHeader.displayName = "TableHeader"
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody
ref={ref}
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
))
TableBody.displayName = "TableBody"
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn(
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
))
TableFooter.displayName = "TableFooter"
const TableRow = React.forwardRef<
HTMLTableRowElement,
React.HTMLAttributes<HTMLTableRowElement>
>(({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className
)}
{...props}
/>
))
TableRow.displayName = "TableRow"
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableHead.displayName = "TableHead"
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
))
TableCell.displayName = "TableCell"
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption
ref={ref}
className={cn("mt-4 text-sm text-muted-foreground", className)}
{...props}
/>
))
TableCaption.displayName = "TableCaption"
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}