diff --git a/components/filter.tsx b/components/filter.tsx new file mode 100644 index 0000000..f78fd42 --- /dev/null +++ b/components/filter.tsx @@ -0,0 +1,73 @@ +"use client"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import { useState, useTransition } from "react"; + +interface FilterOption { + value: string; + label: string; + icon: React.ReactNode; +} + +interface FilterProps { + options: FilterOption[]; + defaultOption: string; + queryParamKey: string; +} + +export default function Filter({ + options, + defaultOption, + queryParamKey, +}: FilterProps) { + const [selectedOption, setSelectedOption] = useState(defaultOption); + const searchParams = useSearchParams(); + const { replace } = useRouter(); + const pathname = usePathname(); + const [isPending, startTransition] = useTransition(); + + function handleFilterChange(value: string) { + setSelectedOption(value); + const params = new URLSearchParams(searchParams.toString()); + + params.set(queryParamKey, value); + params.set("page", "1"); + + startTransition(() => { + replace(`${pathname}?${params.toString()}`); + }); + } + + return ( +
+ +
+ ); +} diff --git a/components/pagination.tsx b/components/pagination.tsx new file mode 100644 index 0000000..bb04780 --- /dev/null +++ b/components/pagination.tsx @@ -0,0 +1,109 @@ +"use client"; +import { Button } from "@/components/ui/button"; +import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import React, { useEffect, useState } from "react"; + +type PaginationProps = { + totalPages: number; + currentPage: number; + maxVisible?: number; +}; + +export default function Pagination({ + totalPages, + currentPage, + maxVisible = 4, +}: PaginationProps) { + const searchParams = useSearchParams(); + const activePage = searchParams.get("page") ?? 1; + const router = useRouter(); + + const [queryParams, setQueryParams] = useState<{ [key: string]: string }>({}); + + useEffect(() => { + const params = Object.fromEntries( + Array.from(searchParams.entries()).filter(([key]) => key !== "page"), + ); + setQueryParams(params); + }, [searchParams]); + + useEffect(() => { + if (!searchParams.has("page")) { + router.replace(`?page=1${IncludeQueries()}`); + } + }); + + function IncludeQueries() { + return Object.entries(queryParams) + .map(([key, value]) => `&${key}=${value}`) + .join(""); + } + + const generatePageNumbers = (): (number | string)[] => { + const halfVisible = Math.floor(maxVisible / 2); + let startPage = Math.max(currentPage - halfVisible, 1); + const endPage = Math.min(startPage + maxVisible - 1, totalPages); + + if (endPage - startPage + 1 < maxVisible) { + startPage = Math.max(endPage - maxVisible + 1, 1); + } + + const pageNumbers: (number | string)[] = []; + + if (startPage > 1) { + pageNumbers.push(1); + if (startPage > 2) pageNumbers.push("..."); + } + + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push(i); + } + + if (endPage < totalPages) { + if (endPage < totalPages - 1) pageNumbers.push("..."); + pageNumbers.push(totalPages); + } + + return pageNumbers; + }; + + const pageNumbers = generatePageNumbers(); + + return ( +
+ {currentPage > 1 && ( + + + + )} + + {pageNumbers.map((page) => ( + + {typeof page === "number" ? ( + + + + ) : ( + ... + )} + + ))} + + {currentPage < totalPages && ( + + + + )} +
+ ); +} diff --git a/components/search.tsx b/components/search.tsx new file mode 100644 index 0000000..0c798e0 --- /dev/null +++ b/components/search.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +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(null); + const { replace } = useRouter(); + + const pathname = usePathname(); + const [isPending, startTransition] = useTransition(); + const searchParams = useSearchParams(); + const searchQuery = searchParams.get("query"); + + function handleSearch(term: string) { + const params = new URLSearchParams(searchParams.toString()); + + if (term) { + params.set("query", term); + params.set("page", "1"); + } else { + params.delete("query"); + } + + startTransition(() => { + replace(`${pathname}?${params.toString()}`); + }); + } + + return ( +
+ handleSearch(e.target.value)} + /> + +
+ ); +} diff --git a/components/user-table.tsx b/components/user-table.tsx new file mode 100644 index 0000000..d8149f3 --- /dev/null +++ b/components/user-table.tsx @@ -0,0 +1,204 @@ +import { + Table, + TableBody, + TableCaption, + TableCell, + TableFooter, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import prisma from "@/lib/db"; +import { Badge } from "./ui/badge"; +import Pagination from "./pagination"; +import { UserVerifyDialog } from "./user/user-verify-dialog"; + +export async function UsersTable({ + searchParams, +}: { + searchParams: Promise<{ + query: string; + page: number; + sortBy: string; + status: string; + }>; +}) { + const query = (await searchParams)?.query || ""; + const page = (await searchParams)?.page; + const sortBy = (await searchParams)?.sortBy || "asc"; + const verified = (await searchParams)?.status || "all"; + const totalUsers = await prisma.user.count({ + where: { + OR: [ + { + name: { + contains: query || "", + mode: "insensitive", + }, + }, + { + phoneNumber: { + contains: query || "", + mode: "insensitive", + }, + }, + { + house_name: { + contains: query || "", + mode: "insensitive", + }, + }, + { + id_card: { + contains: query || "", + mode: "insensitive", + }, + }, + ], + verified: verified === "all" ? undefined : verified === "verified", + }, + }); + + const totalPages = Math.ceil(totalUsers / 10); + const limit = 10; + const offset = (Number(page) - 1) * limit || 0; + + const users = await prisma.user.findMany({ + where: { + OR: [ + { + name: { + contains: query || "", + mode: "insensitive", + }, + }, + { + phoneNumber: { + contains: query || "", + mode: "insensitive", + }, + }, + { + house_name: { + contains: query || "", + mode: "insensitive", + }, + }, + { + id_card: { + contains: query || "", + mode: "insensitive", + }, + }, + ], + verified: verified === "all" ? undefined : verified === "verified", + }, + include: { + island: true, + atoll: true, + }, + skip: offset, + take: limit, + orderBy: { + name: `${sortBy}` as "asc" | "desc", + }, + }); + + // const users = await prisma.user.findMany({ + // where: { + // role: "USER", + // }, + // include: { + // atoll: true, + // island: true, + // }, + // }); + return ( +
+ {users.length === 0 ? ( +
+

No Users yet.

+
+ ) : ( + <> + + Table of all users. + + + Name + ID Card + Atoll + Island + House Name + Status + Dob + Phone Number + Action + + + + {users.map((user) => ( + + {user.name} + {user.id_card} + {user.atoll?.name} + {user.island?.name} + {user.house_name} + + + {user.verified ? ( + + Verified + + ) : ( + + Unverified + + )} + + + {new Date(user.dob ?? "").toLocaleDateString("en-US", { + month: "short", + day: "2-digit", + year: "numeric", + })} + + + {user.phoneNumber} + + + + + ))} + + + + + {query.length > 0 && ( +

+ Showing {users.length} locations for "{query} + " +

+ )} +
+ + {totalUsers} users + +
+
+
+ + + )} +
+ ); +}