Refactor dashboard components and update global styles

- Updated the title and description in layout.tsx to reflect the new application name.
- Replaced the background color in globals.css with a background image for the title section.
- Enhanced the Devices and UserDevices pages by adding search and filter components for improved user interaction.
- Introduced a new DevicesTable component for displaying device data with pagination.
- Updated the Users page to improve layout and added a filter for user status.
- Made various UI adjustments across components for better consistency and usability.
This commit is contained in:
i701 2024-11-30 23:38:32 +05:00
parent 490150f9b7
commit b91f34b6b1
14 changed files with 330 additions and 68 deletions

View File

@ -1,15 +1,37 @@
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export default async function Devices() {
const session = await auth.api.getSession({
headers: await headers(),
});
import { DevicesTable } from "@/components/devices-table";
import Search from "@/components/search";
import { Button } from "@/components/ui/button";
import React, { Suspense } from "react";
export default async function Devices({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
return (
<div>
<h2>Server session</h2>
<pre>{JSON.stringify(session?.user, null, 2)}</pre>
<div className="flex justify-between items-center border-b-2 text-gray-500 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3>
My Devices
</h3>
<Button>Add new device</Button>
</div>
<div
id="user-filters"
className=" border-b-2 pb-4 gap-4 flex items-center justify-start"
>
<Search />
</div>
<Suspense fallback={"loading...."}>
<DevicesTable searchParams={searchParams} />
</Suspense>
</div>
);
}

View File

@ -1,5 +1,54 @@
import React from "react";
import Filter from "@/components/filter";
import Search from "@/components/search";
import { UsersTable } from "@/components/user-table";
import { CheckCheck, Hourglass, Minus } from "lucide-react";
import React, { Suspense } from "react";
export default async function UserDevcies({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
status: string;
}>;
}) {
export default function UserDevices() {
return <div>UserDevices</div>;
return (
<div>
<h3 className="border-b-2 text-2xl font-bold title-bg py-4 px-2 mb-4">
My Devices
</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

@ -18,35 +18,38 @@ export default async function AdminUsers({
return (
<div>
<h3 className="border-b-2 text-2xl font-bold title-bg py-4 px-2 mb-4">
<h3 className="border-b-2 text-gray-500 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 className="flex flex-col sm:flex-row flex-wrap items-start justify-start gap-2">
<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>
</div>
<Suspense fallback={"loading...."}>
<UsersTable searchParams={searchParams} />

View File

@ -89,6 +89,5 @@ body {
.title-bg {
background-color: #fefefe;
background-image: url("data:image/svg+xml,%3Csvg width='6' height='6' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23c1d3c8' fill-opacity='0.21' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
}

View File

@ -13,8 +13,8 @@ const barlow = Barlow({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "SAR Link Portal",
description: "Sarlink Portal",
};
export default function RootLayout({

View File

@ -50,7 +50,7 @@ export async function ApplicationLayout({
<AccountPopover user={session?.user} />
</div>
</header>
<div className="px-8 py-6">{children}</div>
<div className="p-4">{children}</div>
</SidebarInset>
</SidebarProvider>
);

View File

@ -33,7 +33,7 @@ export default function LoginForm() {
<p className="text-red-500 text-sm">{state.message}</p>
)}
<Button
className="dark:bg-gray-800 w-full dark:text-white"
className=""
disabled={isPending}
type="submit"
>

View File

@ -5,10 +5,10 @@ import Link from "next/link";
import { signup } from "@/actions/auth-actions";
import { cn } from "@/lib/utils";
import { Loader } from "lucide-react";
import { useActionState } from "react";
import { useSearchParams } from "next/navigation";
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 {
@ -20,6 +20,7 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Checkbox } from "../ui/checkbox";
type AtollWithIslands = Prisma.AtollGetPayload<{
include: {
@ -31,13 +32,16 @@ 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 [actionState, action, isPending] = useActionState(signup, {
message: "",
});
const params = useSearchParams();
const phoneNumberFromUrl = params.get("phone_number");
const NUMBER_WITHOUT_DASH = phoneNumberFromUrl?.split("-").join("");
@ -83,7 +87,7 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
className={cn(
"text-base",
actionState.errors?.fieldErrors?.id_card &&
"border-2 border-red-500",
"border-2 border-red-500",
)}
placeholder="ID Card"
/>
@ -160,26 +164,26 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
</div>
<div>
<label htmlFor="house_name" className="text-sm">
House Name
<label htmlFor="address" className="text-sm">
Address
</label>
<Input
className={cn(
"text-base",
actionState.errors?.fieldErrors?.house_name &&
"border-2 border-red-500",
actionState.errors?.fieldErrors?.address &&
"border-2 border-red-500",
)}
disabled={isPending}
name="house_name"
name="address"
defaultValue={
(actionState.payload?.get("house_name") || "") as string
(actionState.payload?.get("address") || "") as string
}
type="text"
placeholder="House Name"
placeholder="Address"
/>
{actionState.errors?.fieldErrors?.house_name && (
{actionState.errors?.fieldErrors?.address && (
<span className="text-sm inline-block text-red-500">
{actionState.errors?.fieldErrors?.house_name}
{actionState.errors?.fieldErrors?.address}
</span>
)}
</div>
@ -217,8 +221,8 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
disabled={isPending}
className={cn(
!phoneNumberFromUrl &&
actionState.errors?.fieldErrors?.phone_number &&
"border-2 border-red-500 rounded-md",
actionState.errors?.fieldErrors?.phone_number &&
"border-2 border-red-500 rounded-md",
)}
defaultValue={NUMBER_WITHOUT_DASH ?? ""}
readOnly={Boolean(phoneNumberFromUrl)}
@ -235,6 +239,51 @@ export default function SignUpForm({ atolls }: { atolls: AtollWithIslands[] }) {
{actionState.message}
</span>
)}
<div className="flex flex-col gap-2 items-start justify-start py-2">
<div className="flex gap-2 items-center">
<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">
<Checkbox
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>

View File

@ -53,7 +53,7 @@ export default function VerifyOTPForm({
return (
<form
onSubmit={handleSubmit(onSubmit)}
className="bg-white dark:bg-gray-900 w-full max-w-xs rounded-lg shadow my-4"
className="w-full max-w-xs rounded-lg shadow my-4"
>
<div className="grid pb-4 pt-4 gap-4 px-4">
<div className="">
@ -71,7 +71,7 @@ export default function VerifyOTPForm({
)}
</div>
<Button
className="dark:bg-gray-800 w-full dark:text-white"
className="w-full"
disabled={isPending}
type="submit"
>

View File

@ -0,0 +1,133 @@
import {
Table,
TableBody,
TableCaption,
TableCell,
TableFooter,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import prisma from "@/lib/db";
import Pagination from "./pagination";
export async function DevicesTable({
searchParams,
}: {
searchParams: Promise<{
query: string;
page: number;
sortBy: string;
}>;
}) {
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 devices = await prisma.device.findMany({
where: {
OR: [
{
name: {
contains: query || "",
mode: "insensitive",
},
},
{
mac: {
contains: query || "",
mode: "insensitive",
},
},
],
},
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>
) : (
<>
<Table className="overflow-scroll">
<TableCaption>Table of all devices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Device Name</TableHead>
<TableHead>MAC Address</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody className="overflow-scroll">
{devices.map((device) => (
<TableRow
key={device.id}
>
<TableCell className="font-medium">{device.name}</TableCell>
<TableCell className="font-medium">{device.mac}</TableCell>
<TableCell>
Hi
{/* <UserVerifyDialog user={user} /> */}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell colSpan={2}>
{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>
);
}

View File

@ -52,7 +52,7 @@ export default function Filter({
value={selectedOption}
onValueChange={(val) => handleFilterChange(val)}
>
<SelectTrigger className="w-auto bg-white">
<SelectTrigger className="w-auto">
<SelectValue>
{options.find((option) => option.value === selectedOption)?.label}
</SelectValue>

View File

@ -1,11 +1,10 @@
"use client";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { Loader } from "lucide-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useRef, useTransition } from "react";
import { Button } from "./ui/button";
import { Loader } from "lucide-react";
export default function Search({ disabled }: { disabled?: boolean }) {
const inputRef = useRef<HTMLInputElement>(null);
@ -32,12 +31,12 @@ export default function Search({ disabled }: { disabled?: boolean }) {
}
return (
<div className="flex gap-2 items-center justify-end">
<div className="flex w-full gap-2 items-center justify-between">
<Input
ref={inputRef}
placeholder="Search..."
className={cn("bg-white")}
type="text"
className="w-full"
name="search"
id="search"
defaultValue={searchQuery ? searchQuery : ""}

View File

@ -30,10 +30,18 @@ const data = {
title: "Devices",
url: "/devices",
},
{
title: "Parental Controls",
url: "/parental-controls",
},
{
title: "Payments",
url: "/payments",
},
{
title: "Agreements",
url: "/agreements",
},
],
},
{
@ -65,7 +73,7 @@ export function AppSidebar({
return (
<Sidebar {...props} className="z-50">
<SidebarHeader>
<h4 className="bg-gray-200 p-2 rounded shadow text-center uppercase dark:bg-gray-800">
<h4 className="p-2 rounded shadow text-center uppercase ">
Sar Link Portal
</h4>
</SidebarHeader>

View File

@ -9,8 +9,8 @@ import {
TableRow,
} from "@/components/ui/table";
import prisma from "@/lib/db";
import { Badge } from "./ui/badge";
import Pagination from "./pagination";
import { Badge } from "./ui/badge";
import { UserVerifyDialog } from "./user/user-verify-dialog";
export async function UsersTable({
@ -139,7 +139,7 @@ export async function UsersTable({
<TableBody className="overflow-scroll">
{users.map((user) => (
<TableRow
className={`${user.verified && "title-bg"}`}
className={`${user.verified && "title-bg dark:bg-black"}`}
key={user.id}
>
<TableCell className="font-medium">{user.name}</TableCell>