mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-02-22 09:22:01 +00:00
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:
parent
490150f9b7
commit
b91f34b6b1
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />
|
||||
|
@ -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");
|
||||
}
|
@ -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({
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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"
|
||||
>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
>
|
||||
|
133
components/devices-table.tsx
Normal file
133
components/devices-table.tsx
Normal 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 "{query}
|
||||
"
|
||||
</p>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{totalDevices} devices
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableFooter>
|
||||
</Table>
|
||||
<Pagination totalPages={totalPages} currentPage={page} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -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>
|
||||
|
@ -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 : ""}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user