diff --git a/amplify/data/resource.ts b/amplify/data/resource.ts index 62834945..f19ea9c7 100644 --- a/amplify/data/resource.ts +++ b/amplify/data/resource.ts @@ -97,7 +97,7 @@ const schema = a teamRooms: a.hasMany("TeamRoom", "teamId"), }) .authorization((allow) => [ - allow.group("Admin").to(["read", "update", "create"]), + allow.group("Admin").to(["read", "update", "create", "delete"]), allow.authenticated().to(["read"]), ]), Score: a diff --git a/package-lock.json b/package-lock.json index 94e3da61..80df1892 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,12 @@ "@emotion/styled": "^11.13.0", "@react-email/components": "^0.0.19", "@react-email/render": "^0.0.15", + "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-async-storage-persister": "^5.36.0", "@tanstack/react-query": "^5.32.1", "@tanstack/react-query-devtools": "^5.32.0", "@tanstack/react-query-persist-client": "^5.36.0", + "@tanstack/react-table": "^8.20.5", "@typescript-eslint/parser": "^7.11.0", "@yudiel/react-qr-scanner": "^2.0.4", "aws-amplify": "^6.0.18", @@ -20327,6 +20329,22 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/match-sorter-utils": { + "version": "8.19.4", + "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", + "integrity": "sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==", + "license": "MIT", + "dependencies": { + "remove-accents": "0.5.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@tanstack/query-async-storage-persister": { "version": "5.51.5", "resolved": "https://registry.npmjs.org/@tanstack/query-async-storage-persister/-/query-async-storage-persister-5.51.5.tgz", @@ -20416,6 +20434,39 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.5.tgz", + "integrity": "sha512-WEHopKw3znbUZ61s9i0+i9g8drmDo6asTWbrQh8Us63DAk/M0FkmIqERew6P71HI75ksZ2Pxyuf4vvKh9rAkiA==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.5", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.5.tgz", + "integrity": "sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@trivago/prettier-plugin-sort-imports": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-4.3.0.tgz", @@ -28751,6 +28802,12 @@ "invariant": "^2.2.4" } }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" + }, "node_modules/repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", diff --git a/package.json b/package.json index 0e9b844b..e9ab8ec9 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,12 @@ "@emotion/styled": "^11.13.0", "@react-email/components": "^0.0.19", "@react-email/render": "^0.0.15", + "@tanstack/match-sorter-utils": "^8.19.4", "@tanstack/query-async-storage-persister": "^5.36.0", "@tanstack/react-query": "^5.32.1", "@tanstack/react-query-devtools": "^5.32.0", "@tanstack/react-query-persist-client": "^5.36.0", + "@tanstack/react-table": "^8.20.5", "@typescript-eslint/parser": "^7.11.0", "@yudiel/react-qr-scanner": "^2.0.4", "aws-amplify": "^6.0.18", @@ -82,5 +84,6 @@ "hooks": { "pre-commit": "lint-staged" } - } + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" } diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx index ea58c89e..42b04f3a 100644 --- a/src/app/admin/layout.tsx +++ b/src/app/admin/layout.tsx @@ -18,11 +18,11 @@ export const metadata: Metadata = { function AdminLayout({ children }: { children: React.ReactNode }) { return ( -
+
-
+
-
{children}
+ {children}
); diff --git a/src/app/admin/teams/TeamsTablePage.tsx b/src/app/admin/teams/TeamsTablePage.tsx deleted file mode 100644 index 6e663fec..00000000 --- a/src/app/admin/teams/TeamsTablePage.tsx +++ /dev/null @@ -1,170 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -import { client } from "@/app/QueryProvider"; -import LoadingRing from "@/components/LoadingRing"; -import DataTableSection from "@/components/admin/TeamsTable/DataTableSection"; -import FilterSection from "@/components/admin/TeamsTable/FilterSection"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useQuery } from "@tanstack/react-query"; - -const LOADING_SCREEN_STYLES = - "flex h-screen w-full items-center justify-center bg-awesome-purple"; - -const tableHeaders = [ - { columnHeader: "Team Name", className: "w-2/5" }, - { columnHeader: "Check-in Status", className: "w-1/4" }, - { columnHeader: "Approved Status", className: "w-1/4" }, -]; - -const filters = [{ label: "Approved" }, { label: "Checked-in" }]; - -const TeamsTablePage = () => { - const [teamData, setTeamData] = useState< - Array<{ - teamName: string; - checkinStatus: string; - members: string[]; - membersStatus: string[]; - approveStatus: string; - teamId: string; - }> - >([]); - - const [tableData, setTableData] = useState([]); - const [filteredData, setFilteredData] = useState([]); - const [selectedFilters, setSelectedFilters] = useState([]); - - const { data, isFetching } = useQuery({ - initialData: [], - initialDataUpdatedAt: 0, - queryKey: ["Teams"], - queryFn: async () => { - const response = await client.models.Team.list({ - selectionSet: ["members.*", "id", "name", "approved"], - limit: 1000, - }); - - if (response.errors) { - throw new Error(response.errors[0].message); - } - - return response.data; - }, - }); - - useEffect(() => { - if (data) { - const formattedData = data.map((team) => ({ - teamName: team.name ?? "", - checkinStatus: team.members.every((member) => member.checkedIn) - ? "Checked In" - : "Not Checked In", - members: team.members.map((member) => - member.lastName - ? `${member.firstName} ${member.lastName}` - : member.firstName, - ), - membersStatus: team.members.map((member) => - member.checkedIn ? "Checked In" : "Not Checked In", - ), - approveStatus: team.approved ? "Approved" : "Not Approved", - teamId: team.id ?? "", - })); - - const removeNullData = formattedData.map((item) => ({ - ...item, - members: item.members.filter((member) => member !== null) as string[], - })); - - setTeamData(removeNullData); - - const displayedData = removeNullData.map((cellData) => [ - cellData.teamName, - cellData.checkinStatus, - cellData.approveStatus, - ]); - - setTableData(displayedData); - setFilteredData(displayedData); - } - }, [data]); - - useEffect(() => { - const applyFilters = () => { - let newFilteredData = tableData; - - if (selectedFilters.includes("Approved")) { - newFilteredData = newFilteredData.filter( - (row) => row[2] === "Approved", - ); - } - - if (selectedFilters.includes("Checked-in")) { - newFilteredData = newFilteredData.filter( - (row) => row[1] === "Checked In", - ); - } - - // Sort the filtered data alphabetically based on team name (first column) - newFilteredData.sort((a, b) => a[0].localeCompare(b[0])); - - setFilteredData(newFilteredData); - }; - - applyFilters(); - }, [selectedFilters, tableData]); - - const handleFilterChange = (filters: string[]) => { - setSelectedFilters(filters); - }; - - const queryClient = useQueryClient(); - const tableDataMutation = useMutation({ - mutationFn: async (updatedData: any) => { - console.log("Updating data:", updatedData); - try { - const response = await client.models.Team.update(updatedData); - return response.data; - } catch (error) { - console.error("Error updating table data:", error); - throw error; - } - }, - onSuccess: (data) => { - queryClient.invalidateQueries({ queryKey: ["Teams"] }); - console.log("Table data updated successfully:", data); - }, - onError: (error) => { - console.error("Error updating table data:", error); - }, - }); - - return ( -
- {isFetching ? ( -
- -
- ) : ( - <> - - - - )} -
- ); -}; - -export default TeamsTablePage; diff --git a/src/app/admin/teams/components/DeleteButton.tsx b/src/app/admin/teams/components/DeleteButton.tsx new file mode 100644 index 00000000..a0836438 --- /dev/null +++ b/src/app/admin/teams/components/DeleteButton.tsx @@ -0,0 +1,61 @@ +import { useState } from "react"; + +import type { Row, TableMeta } from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; +import Modal from "./Modal"; + +export default function DeleteButton({ + row, + meta, +}: { + row: Row; + meta?: TableMeta; +}) { + const [showPopup, setShowPopup] = useState(false); + + return ( + <> + + {showPopup && ( + setShowPopup(false)}> +

+ {row.original.teamName} + {"'s"} Team - #{row.original.teamID} +

+
+

+ Are you sure you want to delete this record? +

+

+ This record will be deleted{" "} + + permanently + + . You cannot undo this action. +

+
+ + +
+
+
+ )} + + ); +} diff --git a/src/app/admin/teams/components/Modal.tsx b/src/app/admin/teams/components/Modal.tsx new file mode 100644 index 00000000..577b9dec --- /dev/null +++ b/src/app/admin/teams/components/Modal.tsx @@ -0,0 +1,35 @@ +"use client"; + +import Image from "next/image"; +import { createPortal } from "react-dom"; + +import exit_icon from "@/svgs/admin/exit_icon.svg"; + +export default function Modal({ + onClose, + children, +}: { + onClose?: () => void; + children?: React.ReactNode; +}) { + return createPortal( + { + if (onClose) onClose(); + }} + onKeyDown={(e) => e.key === "Escape" && onClose && onClose()} + className="fixed flex size-full items-center justify-center bg-light-grey/40" + > + +
e.stopPropagation()} + className="flex min-w-96 flex-col gap-2 rounded-md bg-white p-6 outline outline-4 outline-awesomer-purple" + > + {children} +
+
, + document.body, + ); +} diff --git a/src/app/admin/teams/components/SaveEditButton.tsx b/src/app/admin/teams/components/SaveEditButton.tsx new file mode 100644 index 00000000..48e35c82 --- /dev/null +++ b/src/app/admin/teams/components/SaveEditButton.tsx @@ -0,0 +1,24 @@ +import type { Row, TableMeta } from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; + +export default function SaveEditButton({ + row, + meta, +}: { + row: Row; + meta?: TableMeta; +}) { + return ( + + ); +} diff --git a/src/app/admin/teams/components/SearchTeam.tsx b/src/app/admin/teams/components/SearchTeam.tsx new file mode 100644 index 00000000..1f8d0877 --- /dev/null +++ b/src/app/admin/teams/components/SearchTeam.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Image from "next/image"; +import { memo, useEffect, useState } from "react"; + +import search_icon from "@/svgs/admin/search_icon.svg"; + +const SearchTeam = ({ + tableDataLength, + handleSearchChange, +}: { + tableDataLength: number; + handleSearchChange: (e: string) => void; +}) => { + const [value, setValue] = useState(""); + useEffect(() => { + handleSearchChange(value); + }, [value]); + return ( +
+

+ Search Results ({tableDataLength} + {" record"} + {tableDataLength !== 1 && "s"} found) +

+ setValue(e.target.value)} + /> + Magnifying glass icon +
+ ); +}; + +export default memo(SearchTeam); diff --git a/src/app/admin/teams/components/TableFooter.tsx b/src/app/admin/teams/components/TableFooter.tsx new file mode 100644 index 00000000..5d7d4f10 --- /dev/null +++ b/src/app/admin/teams/components/TableFooter.tsx @@ -0,0 +1,79 @@ +import type { Table } from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; + +export default function TableFooter({ table }: { table: Table }) { + return ( +
+
+ Showing {table.getRowModel().rows.length.toLocaleString()} of{" "} + {table.getRowCount().toLocaleString()} Rows +
+
+ +
Page
+ + {table.getState().pagination.pageIndex + 1} of{" "} + {table.getPageCount().toLocaleString()} + +
+ + | Go to page: + { + const page = e.target.value ? Number(e.target.value) - 1 : 0; + table.setPageIndex(page); + }} + className="w-16 rounded border p-1" + /> + + +
+
+ + + + +
+
+ ); +} diff --git a/src/app/admin/teams/components/TeamMembers.tsx b/src/app/admin/teams/components/TeamMembers.tsx new file mode 100644 index 00000000..6adf59bc --- /dev/null +++ b/src/app/admin/teams/components/TeamMembers.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useCallback, useMemo, useState } from "react"; +import { toast } from "react-toastify"; + +import type { Schema } from "@/amplify/data/resource"; +import { client } from "@/app/QueryProvider"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { PaginationState } from "@tanstack/react-table"; +import { + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; +import { tableSettings } from "../tanstackTableSetup"; +import SearchTeam from "./SearchTeam"; +import TableFooter from "./TableFooter"; +import TeamTableBody from "./TeamTableBody"; +import TeamsTableHead from "./TeamsTableHead"; + +const { columns, fuzzyFilter } = tableSettings; +export default function TeamMembers({ team }: { team: Team[] }) { + const [pagination, setPagination] = useState({ + pageSize: 10, + pageIndex: 0, + }); + const [data, setData] = useState(team); + const queryClient = useQueryClient(); + const deleteTeam = useMutation({ + mutationFn: async ({ + rowIndex, + teamID, + }: { + rowIndex: number; + teamID: Schema["Team"]["deleteType"]; + }) => { + const prev = data; + setData((old) => old.filter((_, index) => index !== rowIndex)); + try { + const response = await client.models.Team.delete(teamID); + if (response.errors) { + throw new Error(response.errors[0].message); + } + } catch (error) { + setData(prev); + throw error; + } + return teamID; + }, + onError: (error) => { + toast.error("Error updating teams: " + error.message); + }, + onSuccess: (teamID) => { + queryClient.invalidateQueries({ queryKey: ["Teams"] }); + toast.success(`Team ${teamID.id} deleted succesfully`); + }, + }); + const updateTeam = useMutation({ + mutationFn: async (updatedData: Schema["Team"]["updateType"]) => { + try { + const response = await client.models.Team.update(updatedData); + if (response.errors) { + throw new Error(response.errors[0].message); + } + } catch (error) { + throw error; + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["Teams"] }); + toast.success("Table data updated succesfully"); + }, + onError: () => { + toast.error("Error updating teams"); + }, + }); + const [globalFilter, setGlobalFilter] = useState(""); + const table = useReactTable({ + data: data, + onGlobalFilterChange: setGlobalFilter, + columns, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + filterFns: { + fuzzy: fuzzyFilter, + }, + globalFilterFn: "fuzzy", + state: { + pagination, + globalFilter, + }, + autoResetPageIndex: false, + meta: { + updateData: (rowIndex, columnId, value) => { + setData((old) => + old.map((row, index) => { + if (index !== rowIndex) return row; + return { + ...old[rowIndex]!, + [columnId]: value, + }; + }), + ); + }, + deleteTeam: (team, rowIndex) => { + const teamID = { + id: team.teamID, + }; + deleteTeam.mutate({ teamID, rowIndex }); + }, + saveData: (team) => { + const updatedData = { + id: team.teamID, + name: team.teamName, + approved: team.approvedStatus, + }; + updateTeam.mutate(updatedData); + }, + }, + }); + return ( +
+
+ setGlobalFilter(value), + [], + )} + /> + + table.getHeaderGroups(), [table])} + /> + + + + + +
+
+
+ +
+ ); +} diff --git a/src/app/admin/teams/components/TeamTableBody.tsx b/src/app/admin/teams/components/TeamTableBody.tsx new file mode 100644 index 00000000..9b7c9a96 --- /dev/null +++ b/src/app/admin/teams/components/TeamTableBody.tsx @@ -0,0 +1,20 @@ +import type { Table } from "@tanstack/react-table"; +import { flexRender } from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; + +export default function TeamTableBody({ table }: { table: Table }) { + return ( + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + ); +} diff --git a/src/app/admin/teams/components/TeamsTable.tsx b/src/app/admin/teams/components/TeamsTable.tsx new file mode 100644 index 00000000..fb0af3a6 --- /dev/null +++ b/src/app/admin/teams/components/TeamsTable.tsx @@ -0,0 +1,34 @@ +import client from "@/components/_Amplify/AmplifyBackendClient"; + +import type { Team } from "../tanstackTableSetup"; +import TeamMembers from "./TeamMembers"; + +export default async function TeamsTable() { + const { data: team } = await client.models.Team.list({ + selectionSet: [ + "name", + "approved", + "id", + "members.id", + "members.firstName", + "members.lastName", + "members.checkedIn", + ], + }); + if (!team || !Array.isArray(team)) + return
No teams were found
; + else { + const teamData: Team[] = team.map((t) => ({ + teamName: t.name, + approvedStatus: t.approved, + teamID: t.id, + members: t.members.map((m) => ({ + id: m.id, + firstName: m.firstName, + lastName: m.lastName, + checkedIn: m.checkedIn, + })), + })); + return ; + } +} diff --git a/src/app/admin/teams/components/TeamsTableHead.tsx b/src/app/admin/teams/components/TeamsTableHead.tsx new file mode 100644 index 00000000..89c8a84e --- /dev/null +++ b/src/app/admin/teams/components/TeamsTableHead.tsx @@ -0,0 +1,52 @@ +import { memo } from "react"; + +import type { HeaderGroup } from "@tanstack/react-table"; +import { flexRender } from "@tanstack/react-table"; + +import type { Team } from "../tanstackTableSetup"; + +const TeamsTableHead = ({ table }: { table: HeaderGroup[] }) => { + return ( + + {table.map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} + { + { + asc: " 🔼", + desc: " 🔽", + }[header.column.getIsSorted() as string] + } +
+ )} + + ))} + + ))} + + ); +}; +export default memo(TeamsTableHead); diff --git a/src/app/admin/teams/components/ViewButton.tsx b/src/app/admin/teams/components/ViewButton.tsx new file mode 100644 index 00000000..31b575a7 --- /dev/null +++ b/src/app/admin/teams/components/ViewButton.tsx @@ -0,0 +1,46 @@ +import { useState } from "react"; + +import type { Team } from "../tanstackTableSetup"; +import Modal from "./Modal"; + +export default function ViewButton({ team }: { team: Team }) { + const [showPopup, setShowPopup] = useState(false); + return ( + <> + + {showPopup && ( + setShowPopup(!showPopup)}> +

+ {team.teamName} + {"'s"} Team +

+ + + + + + + + + {team.members.map((member) => ( + + + + + ))} + +
MembersStatus
+ {`${member.firstName} ${member.lastName}`} + + {member.checkedIn ? "Checked In" : "Not Checked In"} +
+
+ )} + + ); +} diff --git a/src/app/admin/teams/page.tsx b/src/app/admin/teams/page.tsx index eedf3507..ee3cf76a 100644 --- a/src/app/admin/teams/page.tsx +++ b/src/app/admin/teams/page.tsx @@ -1,38 +1,16 @@ -import TeamsTablePage from "./TeamsTablePage"; +import { SuspenseWrapper } from "@/components/SuspenseWrapper"; -export default function Teams() { - /*== STEP 2 =============================================================== -Go to your frontend source code. From your client-side code, generate a -Data client to make CRUDL requests to your table. (THIS SNIPPET WILL ONLY -WORK IN THE FRONTEND CODE FILE.) - -Using JavaScript or Next.js React Server Components, Middleware, Server -Actions or Pages Router? Review how to generate Data clients for those use -cases: https://docs.amplify.aws/gen2/build-a-backend/data/connect-to-API/ -=========================================================================*/ - - //const client = generateClient(); // use this Data client for CRUDL requests +import TeamsTable from "./components/TeamsTable"; - /*== STEP 3 =============================================================== -Fetch records from the database and use them in your frontend component. -(THIS SNIPPET WILL ONLY WORK IN THE FRONTEND CODE FILE.) -=========================================================================*/ - - /* For example, in a React component, you can use this snippet in your - function's RETURN statement */ - - // client.models.Team?.list() - // .then((teams) => { - // console.log(teams); - // return; - // }) - // .catch((error) => { - // console.log(error); - // }); +export const revalidate = 0; +export const dynamic = "force-dynamic"; +export default function Teams() { return ( -
- +
+ + +
); } diff --git a/src/app/admin/teams/tanstackTableSetup.tsx b/src/app/admin/teams/tanstackTableSetup.tsx new file mode 100644 index 00000000..5cddfa31 --- /dev/null +++ b/src/app/admin/teams/tanstackTableSetup.tsx @@ -0,0 +1,179 @@ +import { useEffect, useState } from "react"; + +import type { RankingInfo } from "@tanstack/match-sorter-utils"; +import { rankItem } from "@tanstack/match-sorter-utils"; +import type { FilterFn, RowData } from "@tanstack/react-table"; +import { createColumnHelper } from "@tanstack/react-table"; + +import DeleteButton from "./components/DeleteButton"; +import SaveEditButton from "./components/SaveEditButton"; +import ViewButton from "./components/ViewButton"; + +export type Team = { + teamName: string; + approvedStatus: boolean | null; + teamID: string; + members: { + id: string; + firstName: string | null; + lastName: string | null; + checkedIn: boolean | null; + }[]; +}; +declare module "@tanstack/react-table" { + interface TableMeta { + updateData: ( + rowIndex: number, + columnId: keyof TData, + value: TData[keyof TData], + ) => void; + saveData: (team: TData) => void; + deleteTeam: (team: TData, rowIndex: number) => void; + } + interface FilterFns { + fuzzy: FilterFn; + } + interface FilterMeta { + itemRank: RankingInfo; + } +} + +const columnHelper = createColumnHelper(); +const columns = [ + columnHelper.accessor("teamID", { + cell: (info) => info.getValue(), + header: "Team ID", + sortingFn: "basic", + }), + columnHelper.accessor("teamName", { + cell: ({ + getValue, + row: { getIsSelected, index }, + table: { + options: { meta }, + }, + }) => { + const initialValue = getValue() as string; + const [value, setValue] = useState(initialValue); + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + if (!getIsSelected()) { + return getValue(); + } + const onBlur = () => { + meta?.updateData(index, "teamName", value); + }; + return ( + setValue(e.target.value)} + onBlur={onBlur} + /> + ); + }, + header: (ctx) => { + const canSort = ctx.column.getCanSort(); + return ( +
+
Team Name
+
+ ); + }, + filterFn: "includesString", + sortingFn: "alphanumeric", + }), + columnHelper.accessor("members", { + cell: (info) => + info.getValue().every((member) => member.checkedIn) + ? "Checked In" + : "Not Checked In", + header: "Check-in Status", + sortingFn: "basic", + }), + columnHelper.accessor("approvedStatus", { + cell: ({ + getValue, + row, + row: { getIsSelected, index }, + table: { + options: { meta }, + }, + }) => { + const initialValue = getValue(); + const [value, setValue] = useState(initialValue); + useEffect(() => { + setValue(initialValue); + }, [initialValue]); + if (!getIsSelected()) { + return getValue() ? "Approved" : "Not Approved"; + } + const ApproveStatus = { + Approved: true, + "Not Approved": false, + } as const; + const onBlur = () => { + meta?.updateData(index, "approvedStatus", value); + }; + return ( + + ); + }, + + header: "Approved Status", + sortingFn: "basic", + }), + columnHelper.display({ + id: "_ACTIONS", + cell: ({ + row, + table: { + options: { meta }, + }, + }) => { + const team = row.original; + return ( +
+ + + +
+ ); + }, + }), +]; +// Define a custom fuzzy filter function that will apply ranking info to rows (using match-sorter utils) +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + const itemRank = rankItem(row.getValue(columnId), value); + addMeta({ + itemRank, + }); + + // Return if the item should be filtered in/out + return itemRank.passed; +}; + +export const tableSettings = { + columns, + fuzzyFilter, +}; diff --git a/src/components/SuspenseWrapper.tsx b/src/components/SuspenseWrapper.tsx index 3eb17ae0..e88ca7bd 100644 --- a/src/components/SuspenseWrapper.tsx +++ b/src/components/SuspenseWrapper.tsx @@ -13,7 +13,7 @@ export function SuspenseWrapper({ - +
} > diff --git a/src/components/admin/TeamsTable/DataTableSection.tsx b/src/components/admin/TeamsTable/DataTableSection.tsx deleted file mode 100644 index c2ae9c18..00000000 --- a/src/components/admin/TeamsTable/DataTableSection.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import Image from "next/image"; -import { useEffect, useState } from "react"; - -import Popup from "@/components/admin/TeamsTable/PopupTile"; - -const search_icon = "/svgs/admin/search_icon.svg"; -const entries_per_page = 10; - -const DATA_TABLE_SECTION_STYLES = - "bg-light-grey border border-awesomer-purple m-4 p-4 rounded-md text-lg text-black w-full max-w-[1500px]"; -const SEARCH_RESULTS_SECTION_STYLES = - "bg-white rounded-t-md flex justify-between items-center px-4 py-2 relative"; -const SEARCH_RESULTS_TEXT_STYLES = "py-6 text-2xl font-semibold"; -const SEARCH_BAR_STYLES = - "border rounded-md border-black p-4 font-light h-3/5 w-2/5 max-w-[500px]"; -const SEARCH_ICON_STYLES = "absolute right-8 top-1/2 -translate-y-1/2"; -const DATA_TABLE_CONTENT_STYLES = - "w-full border-separate border-spacing-x-0.5 text-left"; -const DATA_TABLE_HEADER_CELL_STYLES = "p-3 font-normal text-xl"; -const DATA_TABLE_CELL_STYLES = "p-3 py-4 text-md font-light"; -const EDIT_BUTTON_STYLES = "mr-6 text-awesomer-purple"; -const EDIT_MODE_TEXT_INPUT_STYLES = - "w-full rounded-md border border-awesomer-purple bg-white p-2 focus:outline-none focus:ring-1 focus:ring-awesomer-purple"; -const CHANGE_PAGE_BUTTON_STYLING = - "rounded-md border border-awesomer-purple bg-white px-6 hover:bg-awesomer-purple hover:text-white"; -const CHANGE_PAGE_BUTTON_TEXT_STYLING = "rounded-md bg-white p-2 px-8"; - -interface DataTableProps { - tableData: Array>; - tableHeaders: Array<{ columnHeader: string; className: string }>; - showViewButton?: boolean; - teamData?: Array; - tableDataMutation: any; -} - -const DataTableSection = (props: DataTableProps) => { - const { - tableData, - tableHeaders, - showViewButton = false, - teamData = [], - tableDataMutation, - } = props; - - const [editModes, setEditModes] = useState( - Array(tableData.length).fill(false), - ); - const [editedValues, setEditedValues] = useState([]); - const [showViewPopup, setShowViewPopup] = useState(false); - const [showDeletePopup, setShowDeletePopup] = useState(false); - const [recordToDeleteId, setRecordToDeleteId] = useState(""); - const [selectedMembersData, setSelectedMembersData] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedTeamName, setSelectedTeamName] = useState(""); - const [selectedMemberStatus, setSelectedMemberStatus] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [currentPageData, setCurrentPageData] = useState>>( - [], - ); - - useEffect(() => { - if (tableData.length > 0) { - const formattedEditedValues = tableData.map((rowData) => [...rowData]); - setEditedValues(formattedEditedValues); - } - }, [tableData]); - - useEffect(() => { - const filteredData = tableData.filter((rowData) => - rowData.some((cellData) => - cellData.toLowerCase().includes(searchQuery.toLowerCase()), - ), - ); - - const totalPages = Math.ceil(filteredData.length / entries_per_page); - const startIndex = (currentPage - 1) * entries_per_page; - const endIndex = Math.min( - startIndex + entries_per_page, - filteredData.length, - ); - const currentPageData = filteredData.slice(startIndex, endIndex); - - setTotalPages(totalPages); - setCurrentPageData(currentPageData); - }, [tableData, searchQuery, currentPage]); - - const handlePreviousPage = () => { - setCurrentPage((prevPage) => Math.max(prevPage - 1, 1)); - }; - - const handleNextPage = () => { - setCurrentPage((prevPage) => Math.min(prevPage + 1, totalPages)); - }; - - const handleViewButtonClick = (rowData: Array) => { - const teamName = rowData[0]; - const team = teamData.find((team) => team.teamName === teamName); - - if (team) { - const { members, membersStatus } = team; - setSelectedMembersData(members); - setSelectedTeamName(teamName); - setSelectedMemberStatus(membersStatus); - setShowViewPopup(true); - } - }; - - const handleSaveButtonClick = (index: number) => { - const actualIndex = (currentPage - 1) * entries_per_page + index; - const editedTeamName = editedValues[actualIndex][0]; - const approvalStatus = editedValues[actualIndex][2] === "Approved"; - const teamId = teamData[actualIndex].teamId; - - tableDataMutation.mutate({ - id: teamId, - name: editedTeamName, - approved: approvalStatus, - }); - - toggleEditMode(index); - }; - - const toggleEditMode = (index: number) => { - const actualIndex = (currentPage - 1) * entries_per_page + index; - const newEditModes = [...editModes]; - newEditModes[actualIndex] = !newEditModes[actualIndex]; - setEditModes(newEditModes); - }; - - const handleInputChange = ( - value: string, - rowIndex: number, - cellIndex: number, - ) => { - const actualIndex = (currentPage - 1) * entries_per_page + rowIndex; - const newEditedValues = [...editedValues]; - newEditedValues[actualIndex][cellIndex] = value; - setEditedValues(newEditedValues); - }; - - const handleSearchChange = (e: React.ChangeEvent) => { - setSearchQuery(e.target.value); - setCurrentPage(1); - }; - - const handleDeleteButton = async (recordId: string) => { - setShowDeletePopup(true); - setRecordToDeleteId(recordId); - }; - - return ( -
-
- {/* search results bar */} -
-

- Search Results ( - {searchQuery === "" ? tableData.length : currentPageData.length}{" "} - {searchQuery === "" - ? tableData.length === 1 - ? "record" - : "records" - : "records"}{" "} - found) -

- - Magnifying glass icon -
-
- - - - {tableHeaders.map((header, index) => ( - - ))} - - - - - {currentPageData.map((rowData, rowIndex) => ( - - {rowData.map((cellData, cellIndex) => ( - - ))} - - - ))} - - {Array(tableHeaders.length + 1) - .fill(null) - .map((_, index) => ( - - ))} - - -
- {header.columnHeader} -
- {/* ONLY FOR TEAMS DATA */} - {editModes[ - (currentPage - 1) * entries_per_page + rowIndex - ] ? ( - cellIndex === 0 ? ( - - handleInputChange( - e.target.value, - rowIndex, - cellIndex, - ) - } - /> - ) : cellIndex === 2 ? ( - - ) : ( - cellData - ) - ) : ( - cellData - )} - - {editModes[ - (currentPage - 1) * entries_per_page + rowIndex - ] ? ( - <> - - - - ) : ( - <> - - {/* ONLY FOR TEAMS DATA */} - {showViewButton && ( - - )} - - - )} -
-
- {/* popup component for teams table ONLY */} - {showViewPopup && ( - setShowViewPopup(false)} - /> - )} - {/* popup component to confirm deletion of record */} - {showDeletePopup && ( - setShowDeletePopup(false)} - /> - )} -
- {/* replace dynamically */} -

- Showing {currentPage} of {totalPages} of {tableData.length} entries -

-
-

Previous

- - -

Next

-
-
-
-
- ); -}; - -export default DataTableSection; diff --git a/src/components/admin/TeamsTable/FilterSection.tsx b/src/components/admin/TeamsTable/FilterSection.tsx deleted file mode 100644 index 5648d3a9..00000000 --- a/src/components/admin/TeamsTable/FilterSection.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { useState } from "react"; - -const FILTER_TILE_STYLES = - "bg-light-grey m-4 p-4 rounded-md border-awesomer-purple border text-lg text-black w-full max-w-[1500px]"; -const FILTER_HEADER_BAR_STYLES = "bg-white rounded-md"; -const FILTER_HEADER_BAR_TEXT_STYLES = "m-2 p-4 text-2xl font-semibold"; -const FILTER_CHECKBOX_COLUMN_STYLES = "max-w-[1200px] p-8"; -const FILTER_CHECKBOX_STYLES = "h-5 w-5 m-2"; - -interface Filter { - label: string; -} - -interface FilterSectionProps { - topLabel: string; - filterLabels: Filter[]; - onFilterChange: (filters: string[]) => void; -} - -const FilterSection = ({ - filterLabels, - onFilterChange, -}: FilterSectionProps) => { - const [selectedFilters, setSelectedFilters] = useState([]); - - const handleCheckboxChange = (label: string) => { - setSelectedFilters((prevFilters) => { - const newFilters = prevFilters.includes(label) - ? prevFilters.filter((filter) => filter !== label) - : [...prevFilters, label]; - onFilterChange(newFilters); - return newFilters; - }); - }; - - return ( -
-
-
-

Filters

-
-
-
-
- {filterLabels.map((filter, index) => ( -
-
- handleCheckboxChange(filter.label)} - /> - -
-
- ))} -
-
-
-
-
- ); -}; - -export default FilterSection; diff --git a/src/components/admin/TeamsTable/PopupTile.tsx b/src/components/admin/TeamsTable/PopupTile.tsx deleted file mode 100644 index 6441f92d..00000000 --- a/src/components/admin/TeamsTable/PopupTile.tsx +++ /dev/null @@ -1,149 +0,0 @@ -"use client"; - -import Image from "next/image"; - -import { client } from "@/app/QueryProvider"; -import { useMutation } from "@tanstack/react-query"; - -const exit_icon = "/svgs/admin/exit_icon.svg"; - -const VIEW_TEAM_POPUP_SECTION_STYLES = - "fixed left-0 top-0 flex size-full items-center justify-center bg-black/60"; -const VIEW_TEAM_POPUP_TILE_STYLES = - "w-4/5 max-w-[1200px] rounded-md bg-white p-6"; - -const VIEW_TEAM_POPUP_HEADER_STYLES = "mb-4 flex justify-between"; - -const VIEW_TEAM_POPUP_TABLE_SECTION_STYLES = - "rounded-md borderborder-awesomer-purple bg-light-grey p-2"; -const VIEW_TEAM_POPUP_TABLE_CONTENT_STYLES = - "w-full border-separate border-spacing-2 text-left"; -const VIEW_TEAM_POPUP_TABLE_HEADER_STYLES = "bg-awesome-purple text-white"; -const VIEW_TEAM_POPUP_TABLE_CELL_STYLES = "rounded-md p-2"; - -const DELETE_RECORD_POPUP_TILE_STYLES = "w-2/5 bg-white p-8 rounded-md "; - -interface PopupProps { - selectedMembersData: string[]; - selectedMemberStatus: string | string[]; - popupType: string; - teamName: string; - recordToDelete: string; - onClose: () => void; -} - -const Popup = ({ - selectedMembersData, - selectedMemberStatus, - popupType, - onClose, - recordToDelete, - teamName, -}: PopupProps) => { - const deleteRecord = useMutation({ - mutationFn: async (recordId: string) => { - try { - await client.models.Team.delete({ id: recordId }); - } catch (error) { - console.error("Error deleting record", error); - throw error; - } - }, - onError: (error) => { - console.log("Error deleting record:", error); - }, - onSuccess: () => { - onClose(); - }, - }); - - const handleDelete = async () => { - try { - await deleteRecord.mutateAsync(recordToDelete); - } catch (error) { - // onError callback will handle the error - } - }; - - return ( -
- {popupType === "view" ? ( -
-
-

{teamName}'s Team

- -
-
- - - - - - - - - {selectedMembersData.map((member, index) => ( - - - - - ))} - -
MembersStatus
- {member} - - {selectedMemberStatus[index]} -
-
-
- ) : ( -
-
-

- Are you sure you want to delete this record? -

- -
-

- This record will be deleted{" "} - - permanently - - . You cannot undo this action. -

-
- - -
-
- )} -
- ); -}; - -export default Popup; diff --git a/src/components/contexts/ToastProvider.tsx b/src/components/contexts/ToastProvider.tsx index 614f81ce..f0450c24 100644 --- a/src/components/contexts/ToastProvider.tsx +++ b/src/components/contexts/ToastProvider.tsx @@ -1,6 +1,6 @@ "use client"; -import { ToastContainer } from "react-toastify"; +import { Bounce, ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import "../../app/globals.css"; @@ -13,7 +13,7 @@ export default function ToastProvider({ children }: ToastProviderProps) { return ( <> {children} - + ); }