diff --git a/apps/web/package.json b/apps/web/package.json index 0881f048..4aff4af5 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-switch": "^1.0.3", "@react-email/components": "^0.0.21", "@t3-oss/env-nextjs": "^0.10.1", + "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/react-query": "^5.51.11", "@tanstack/react-table": "^8.19.3", "@trpc/client": "11.0.0-rc.466", diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx index 5ea82481..a800519d 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -3,41 +3,16 @@ import { DataTable } from "@/components/admin/users/UserDataTable"; import { columns } from "@/components/admin/users/UserColumns"; import { Button } from "@/components/shadcn/ui/button"; import { FolderInput } from "lucide-react"; -import { DefaultPagination } from "@/components/admin/users/DefaultPagination"; -import SearchBar from "@/components/admin/users/SearchBar"; +import { getAllUsers } from "db/functions"; import { userCommonData } from "db/schema"; -export default async function Page({ - searchParams, -}: { - searchParams: { [key: string]: string | undefined }; -}) { - // COME BACK AND CHANGE - const maxPerPage = 30; - - let page = +(searchParams["page"] ?? "1"); - let user = searchParams["user"] ?? ""; - const checkedBoxes = searchParams["checkedBoxes"] ?? ""; - - console.log(checkedBoxes); - - const start = maxPerPage * (page - 1); - const end = maxPerPage + start; - - // Might want to work with cache in prod to see if this will be plausible to do - const userData = await db.query.userCommonData.findMany({ - with: { hackerData: true }, - where: and( - or( - ilike(userCommonData.firstName, `%${user}%`), - ilike(userCommonData.lastName, `%${user}%`), - ), - ), - }); +// This begs a question where we might want to have an option later on to sort by the role as we might want different things +export default async function Page() { + const userData = await getAllUsers(); return ( -
-
+
+

@@ -48,7 +23,6 @@ export default async function Page({

-
- {/* TODO: Would very much like to not have "as any" here in the future */} -
+
{userData && userData.length > 0 ? ( <> - + ) : (

No Results :(

)} - {/* */}
-
); } diff --git a/apps/web/src/components/admin/users/DefaultPagination.tsx b/apps/web/src/components/admin/users/DefaultPagination.tsx deleted file mode 100644 index a0d5771c..00000000 --- a/apps/web/src/components/admin/users/DefaultPagination.tsx +++ /dev/null @@ -1,73 +0,0 @@ -"use client"; - -import { - Pagination, - PaginationContent, - PaginationEllipsis, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/shadcn/ui/pagination"; -import { useEffect, useRef, useState } from "react"; -import { usePathname, useSearchParams } from "next/navigation"; -import { createPath } from "@/lib/utils/shared/pageParams"; - -export function DefaultPagination({ maxPages }: { maxPages: number }) { - // FIXME: Come back and change after done testing - - const path = usePathname(); - const params = useSearchParams(); - - const page = params.get("page") ?? "1"; - - const [currPage, setCurrPage] = useState(+page); - const pageRef = useRef(1); - - function incPage() { - pageRef.current = Math.min(maxPages, pageRef.current + 1); - setCurrPage(Math.min(maxPages, currPage + 1)); - } - - function decPage() { - pageRef.current = Math.max(1, pageRef.current - 1); - setCurrPage(Math.max(1, currPage - 1)); - } - - function createPaginationPath(reqPage: string) { - const url = `${path}?${reqPage}&user=${ - params.get("user") ?? "" - }&checkedBoxes=${params.get("checkedBoxes") ?? ""}`; - console.log("Pagination", url); - return url; - } - - return ( - - - - { - decPage(); - }} - /> - - {currPage} - - { - incPage(); - }} - /> - - - - ); -} diff --git a/apps/web/src/components/admin/users/SearchBar.tsx b/apps/web/src/components/admin/users/SearchBar.tsx deleted file mode 100644 index 2af198de..00000000 --- a/apps/web/src/components/admin/users/SearchBar.tsx +++ /dev/null @@ -1,45 +0,0 @@ -"use client"; - -import { Input } from "@/components/shadcn/ui/input"; -import { useRouter, useSearchParams, usePathname } from "next/navigation"; -import { useRef, useState } from "react"; -import { useDebouncedCallback } from "use-debounce"; - -import { X } from "lucide-react"; -export default function SearchBar() { - const searchParams = useSearchParams(); - const { replace } = useRouter(); - const pathname = usePathname(); - - // We use a debouncing strategy to prevent the search from querying every single keystroke and instead will run a time after the user completes typing - const handleSearch = useDebouncedCallback((term) => { - // @ts-ignore Works perfectly fine and is apprporiate accoriding to the docs. Might be a version issue? - const params = new URLSearchParams(searchParams); - if (term) { - params.set("user", term); - } else { - params.delete("user"); - } - replace(`${pathname}?${params.toString()}`); - }, 100); - - return ( -
- {/* Needs to clear text */} - handleSearch(e.target.value)} - /> - -
- ); -} diff --git a/apps/web/src/components/admin/users/UserColumns.tsx b/apps/web/src/components/admin/users/UserColumns.tsx index a65e1ec8..63ecc115 100644 --- a/apps/web/src/components/admin/users/UserColumns.tsx +++ b/apps/web/src/components/admin/users/UserColumns.tsx @@ -6,46 +6,139 @@ import { createSelectSchema } from "drizzle-zod"; import { userCommonData } from "db/schema"; import Link from "next/link"; import { Button } from "@/components/shadcn/ui/button"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../../shadcn/ui/dropdown-menu"; +import { Input } from "@/components/shadcn/ui/input"; +import { MoreHorizontal, ArrowUpDown, User } from "lucide-react"; +import type { Column, Row } from "@tanstack/react-table"; +import { dataTableFuzzyFilter } from "@/lib/utils/client/shared"; +import { Badge } from "@/components/shadcn/ui/badge"; const userValidator = createSelectSchema(userCommonData); +// default fuzzy search and add filters by each column if possible export type userValidatorType = Pick< z.infer, | "clerkID" | "signupTime" | "firstName" | "lastName" - | "hackerTag" | "email" | "role" + | "isRSVPed" + | "hackerTag" + | "checkinTimestamp" >; +type UserColumnType = Column; + export const columns: ColumnDef[] = [ { - accessorKey: "firstName", - header: "Name", - cell: ({ row }) => `${row.original.firstName} ${row.original.lastName}`, + accessorFn: (row) => `${row.firstName} ${row.lastName}`, + id: "name", + header: ({ column }) => ( + + ), + cell: (info) => info.getValue(), + // filterFn: (row, _columnId, filterValue) => { + // return row.original.firstName.toLocaleLowerCase().includes(filterValue.toLocaleLowerCase()) || row.original.lastName.toLocaleLowerCase().includes(filterValue.toLocaleLowerCase()); + // }, + filterFn: dataTableFuzzyFilter, }, { accessorKey: "email", - header: "Email", + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (info) => info.getValue(), }, { accessorKey: "hackerTag", - header: "Hacker Tag", + header: ({ column }) => ( + + ), cell: ({ row }) => `@${row.original.hackerTag}`, + filterFn: dataTableFuzzyFilter, }, { - accessorKey: "clerkID", - header: "Account ID", + accessorKey: "role", + header: ({ column }) => ( + + ), + filterFn: "includesString", }, { - accessorKey: "role", - header: "Role", + accessorKey: "isRSVPed", + header: ({ column }) => ( +
+ RSVP Status + +
+ ), + // row.original.isRSVPed ? + cell: ({ row }) => ( + +
+ + RSVP: {row.original.isRSVPed ? "Yes" : "No"} + + + ), }, + { + accessorKey: "checkinTimestamp", + header: ({ column }) => ( +
+ Checkin Time + +
+ ), + cell: ({ row }) => ( + + {row.original.checkinTimestamp + ? new Date( + row.original.checkinTimestamp, + ).toLocaleDateString() + + " " + + new Date( + row.original.checkinTimestamp, + ).toLocaleTimeString("en-US", { + hour: "2-digit", + minute: "2-digit", + }) + : "Not Checked In"} + + ), + }, + // { + // accessorKey: "role", + // header: ({ column }) => ( + // + // ), + // filterFn: "includesString", + // }, { accessorKey: "signupTime", - header: "Signup Date", + header: ({ column }) => ( +
+ Signup Time + +
+ ), cell: ({ row }) => ( {new Date(row.original.signupTime).toLocaleDateString() + " "} @@ -57,12 +150,85 @@ export const columns: ColumnDef[] = [ ), }, { - accessorKey: "clerkID2", - header: "View", - cell: ({ row }) => ( - - - - ), + id: "actions", + enableHiding: false, + cell: ({ row }) => { + return ; + }, }, ]; + +function UserDropDownActions({ row }: { row: Row }) { + const user = row.original; + return ( + + + + + + + View User + + navigator.clipboard.writeText(user.clerkID)} + className="cursor-pointer" + > + Copy Clerk ID + + + + Email User + + + + + ); +} + +function UserTableHeader({ + name, + column, + hasFilter, +}: { + name: string; + column: UserColumnType; + hasFilter: boolean; +}) { + return ( +
+
+ {name} + {hasFilter && } +
+ column.setFilterValue(e.target.value)} + placeholder="search..." + /> +
+ ); +} + +function SortColumnButton({ + name, + column, +}: { + name: string; + column: UserColumnType; +}) { + return ( + + ); +} diff --git a/apps/web/src/components/admin/users/UserDataTable.tsx b/apps/web/src/components/admin/users/UserDataTable.tsx index 5f4191b9..a9a0aa55 100644 --- a/apps/web/src/components/admin/users/UserDataTable.tsx +++ b/apps/web/src/components/admin/users/UserDataTable.tsx @@ -5,8 +5,12 @@ import { flexRender, getCoreRowModel, useReactTable, + SortingState, + getSortedRowModel, + getPaginationRowModel, + ColumnFiltersState, + getFilteredRowModel, } from "@tanstack/react-table"; - import { Table, TableBody, @@ -15,10 +19,10 @@ import { TableHeader, TableRow, } from "@/components/shadcn/ui/table"; - -import { useCallback } from "react"; -import { useRouter } from "next/navigation"; - +import { Input } from "@/components/shadcn/ui/input"; +import { Button } from "@/components/shadcn/ui/button"; +import { useEffect, useState } from "react"; +import { dataTableFuzzyFilter } from "@/lib/utils/client/shared"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; @@ -28,68 +32,124 @@ export function DataTable({ columns, data, }: DataTableProps) { - const formatTrProps = (state = {}) => { - console.log("qua"); - return { onClick: () => console.log("state", state) }; - }; + const [sorting, setSorting] = useState([]); + const [columnFilters, setColumnFilters] = useState([]); + const [globalFilter, setGlobalFilter] = useState(""); const table = useReactTable({ data, columns, + filterFns: { + fuzzy: dataTableFuzzyFilter, + }, + state: { + sorting, + columnFilters, + globalFilter, + }, + onColumnFiltersChange: setColumnFilters, + globalFilterFn: dataTableFuzzyFilter, getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), }); + useEffect(() => { + console.log("column filters", columnFilters); + }, [columnFilters]); + return ( -
- - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( +
+
+
+ { + // we want to set our global filter + setGlobalFilter(event.target.value); + }} + className="max-w-sm" + /> +
+
+ + {table.getHeaderGroups().map((headerGroup) => ( - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ))} + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef + .header, + header.getContext(), + )} + + ); + })} - )) - ) : ( - - - No results. - - - )} - -
+ ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ))} + + )) + ) : ( + + + No results. + + + )} + + +
+
+ + +
); } diff --git a/apps/web/src/components/dash/shared/ProfileButton.tsx b/apps/web/src/components/dash/shared/ProfileButton.tsx index 5f2b8d13..f13cfcdd 100644 --- a/apps/web/src/components/dash/shared/ProfileButton.tsx +++ b/apps/web/src/components/dash/shared/ProfileButton.tsx @@ -17,7 +17,7 @@ import { auth, SignOutButton } from "@clerk/nextjs"; import Link from "next/link"; import { DropdownSwitcher } from "@/components/shared/ThemeSwitcher"; import { getUser } from "db/functions"; -import { clientLogOut } from "@/lib/utils/client/shared"; +import { clientLogOut } from "@/lib/utils/server/user"; export default async function ProfileButton() { const clerkUser = auth(); diff --git a/apps/web/src/components/shared/ProfileButton.tsx b/apps/web/src/components/shared/ProfileButton.tsx index 555924e7..f833ad99 100644 --- a/apps/web/src/components/shared/ProfileButton.tsx +++ b/apps/web/src/components/shared/ProfileButton.tsx @@ -19,7 +19,7 @@ import { DropdownSwitcher } from "@/components/shared/ThemeSwitcher"; import DefaultDropdownTrigger from "../dash/shared/DefaultDropDownTrigger"; import MobileNavBarLinks from "./MobileNavBarLinks"; import { getUser } from "db/functions"; -import { clientLogOut } from "@/lib/utils/client/shared"; +import { clientLogOut } from "@/lib/utils/server/user"; export default async function ProfileButton() { const clerkUser = await auth(); diff --git a/apps/web/src/lib/utils/client/shared.ts b/apps/web/src/lib/utils/client/shared.ts index db3df016..8030090f 100644 --- a/apps/web/src/lib/utils/client/shared.ts +++ b/apps/web/src/lib/utils/client/shared.ts @@ -1,8 +1,26 @@ +import { rankItem } from "@tanstack/match-sorter-utils"; +import { FilterFn } from "@tanstack/react-table"; import { redirect } from "next/navigation"; export function getClientTimeZone(vercelIPTimeZone: string | null) { return vercelIPTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone; } -export async function clientLogOut() { - "use server"; - redirect("/"); -} + +export const dataTableFuzzyFilter: FilterFn = ( + row, + columnId, + value, + addMeta, +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value); + + // Store the itemRank info + addMeta({ itemRank }); + // Return if the item should be filtered in/out + if (columnId === "name") { + console.log( + `row:, ${row.getValue(columnId)} value: ${value} itemRank: ${itemRank.passed}`, + ); + } + return itemRank.passed; +}; diff --git a/apps/web/src/lib/utils/server/user.ts b/apps/web/src/lib/utils/server/user.ts new file mode 100644 index 00000000..4789dea3 --- /dev/null +++ b/apps/web/src/lib/utils/server/user.ts @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; +export async function clientLogOut() { + "use server"; + redirect("/"); +} diff --git a/packages/db/functions/user.ts b/packages/db/functions/user.ts index de507ac4..6d6a8783 100644 --- a/packages/db/functions/user.ts +++ b/packages/db/functions/user.ts @@ -4,11 +4,19 @@ import { HackerData, User } from "../types"; // const _getAllUsers = db.query.userCommonData.findMany().prepare("getAllUsers"); -export function getAllUsers(): Promise { +export function getAllUsers() { // return _getAllUsers.execute(); return db.query.userCommonData.findMany(); } +export async function getAllUsersWithHackerData() { + return db.query.userCommonData.findMany({ + with: { + hackerData: true, + }, + }); +} + // ID // const _getUser = db.query.userCommonData diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c9fcd38d..b740d8f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -130,6 +130,9 @@ importers: '@t3-oss/env-nextjs': specifier: ^0.10.1 version: 0.10.1(typescript@5.5.3)(zod@3.23.8) + '@tanstack/match-sorter-utils': + specifier: ^8.19.4 + version: 8.19.4 '@tanstack/react-query': specifier: ^5.51.11 version: 5.51.11(react@18.3.1) @@ -6426,6 +6429,13 @@ packages: zod: 3.23.8 dev: false + /@tanstack/match-sorter-utils@8.19.4: + resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==} + engines: {node: '>=12'} + dependencies: + remove-accents: 0.5.0 + dev: false + /@tanstack/query-core@5.51.9: resolution: {integrity: sha512-HsAwaY5J19MD18ykZDS3aVVh+bAt0i7m6uQlFC2b77DLV9djo+xEN7MWQAQQTR8IM+7r/zbozTQ7P0xr0bHuew==} dev: false @@ -11280,6 +11290,10 @@ packages: rc: 1.2.8 dev: true + /remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + dev: false + /require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'}