From 474f7c03c0d66b4359c09fb8aada76352d8d1afc Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 15:34:42 +0100 Subject: [PATCH 01/15] wip: enable filtering date columns --- app/events/EventTable.tsx | 4 +- app/students/StudentTable.tsx | 3 +- components/DateTimePicker.tsx | 3 + components/TanstackTable.tsx | 119 ++++++++++++++++++++++------------ 4 files changed, 84 insertions(+), 45 deletions(-) diff --git a/app/events/EventTable.tsx b/app/events/EventTable.tsx index ac3d1298..56ded15a 100644 --- a/app/events/EventTable.tsx +++ b/app/events/EventTable.tsx @@ -2,7 +2,7 @@ import { DeleteEvent } from "@/components/DeleteEvent" import Link from "@/components/Link" -import TanstackTable from "@/components/TanstackTable" +import TanstackTable, { dateFilterFn } from "@/components/TanstackTable" import { EditEvent } from "@/components/UpsertEvent" import styles from "./eventTable.module.scss" @@ -73,7 +73,7 @@ const EventTable = ({ header: "Start Date", sortingFn: "datetime", id: "dateStart", - enableColumnFilter: false, + filterFn: dateFilterFn, }, shortDescription: { cell: info => info.getValue(), diff --git a/app/students/StudentTable.tsx b/app/students/StudentTable.tsx index cd5c1bc1..3df034ab 100644 --- a/app/students/StudentTable.tsx +++ b/app/students/StudentTable.tsx @@ -1,7 +1,7 @@ "use client" import Link from "@/components/Link" -import TanstackTable from "@/components/TanstackTable" +import TanstackTable, { dateFilterFn } from "@/components/TanstackTable" import UserAvatar from "@/components/UserAvatar" import { StudentProfile, User } from "@prisma/client" @@ -46,6 +46,7 @@ const StudentTable = ({ sortingFn: "datetime", id: "graduationDate", enableColumnFilter: true, + filterFn: dateFilterFn, }, course: { cell: info => info.getValue(), diff --git a/components/DateTimePicker.tsx b/components/DateTimePicker.tsx index 1c71b4bc..1d1b8829 100644 --- a/components/DateTimePicker.tsx +++ b/components/DateTimePicker.tsx @@ -13,11 +13,13 @@ const DateTimePicker = ({ placeholder, required = false, defaultDate, + onChange, }: { name: string placeholder: string required?: boolean defaultDate?: Date | null + onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void }) => { return ( <TextField.Root @@ -27,6 +29,7 @@ const DateTimePicker = ({ placeholder={placeholder} required={required} defaultValue={!!defaultDate ? format(toZonedTime(defaultDate, TIMEZONE), "yyyy-MM-dd'T'HH:mm") : ""} + onChange={onChange} /> ) } diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 7dd84f31..69aff0f7 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -1,6 +1,7 @@ "use client" import Chip from "./Chip" +import DateTimePicker from "./DateTimePicker" import { Dropdown } from "./Dropdown" import styles from "./tanstack-table.module.scss" @@ -19,6 +20,7 @@ import { Box, Button, Flex, Grid, IconButton, Table, Text } from "@radix-ui/them import { ColumnDef, ColumnFiltersState, + Row, SortDirection, SortingState, VisibilityState, @@ -29,6 +31,7 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table" +import { isAfter } from "date-fns" import React, { useEffect, useMemo, useState } from "react" import { Pagination } from "react-headless-pagination" import { useMediaQuery } from "react-responsive" @@ -43,6 +46,9 @@ interface TanstackTableProps<T> { invisibleColumns?: VisibilityState } +export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: any[]): boolean => + isAfter(row.getValue(id), filterValue[0]) && (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { case false: @@ -95,6 +101,10 @@ export default function TanstackTable<T>({ setIsClient(true) }, []) + const dateFilterColumns = useMemo(() => { + return columns.filter(column => column.sortingFn === "datetime").map(column => column.id) + }, [columns]) + const filterableColumns = useMemo( () => table @@ -107,8 +117,9 @@ export default function TanstackTable<T>({ const [searchQuery, setSearchQuery] = useState("") const [currentFilteredColumn, setCurrentFilteredColumn] = useState(filterableColumns[0]?.id ?? "") - const [prevFilters, setPrevFilters] = useState<ColumnFiltersState>([]) + const [dateStart, setDateStart] = useState("") + const [dateEnd, setDateEnd] = useState("") if (!isClient) { return ( @@ -134,52 +145,76 @@ export default function TanstackTable<T>({ setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) } + const updateChips = (value: any) => { + const newFilters = [ + ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value }, // Add the new filter for this column + ] + + // Live-update filter chips which are currently displayed (i.e. in prevFilters) + if (prevFilters.find(f => f.id === currentFilteredColumn)) { + if (!value) { + // Delete the chip if the search query is now empty + setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) + } else { + // Update the chip with the new search query + setPrevFilters(newFilters) + } + } + + // Update the actual filters that are applied to the table + setColumnFilters(newFilters) + } + return ( <Flex gap="4" direction="column" width="100%"> {enableSearch && ( <Flex direction="column" gap="3"> - <Flex gap="3" className={styles.searchBarContainer}> - <TextField.Root - placeholder={`Search by ${table.getColumn(currentFilteredColumn)?.columnDef.header?.toString() ?? "..."}`} - className={styles.searchBar} - onKeyDown={e => { - if (e.key === "Enter") { - addFilter() - } - }} - onChange={e => { - setSearchQuery(e.target.value) - - const newFilters = [ - ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value: e.target.value }, // Add the new filter for this column - ] - - // Live-update filter chips which are currently displayed (i.e. in prevFilters) - if (prevFilters.find(f => f.id === currentFilteredColumn)) { - if (e.target.value === "") { - // Delete the chip if the search query is now empty - setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) - } else { - // Update the chip with the new search query - setPrevFilters(newFilters) + <Flex justify="center" gap="3" className={styles.searchBarContainer}> + {!dateFilterColumns.includes(currentFilteredColumn) ? ( + <TextField.Root + placeholder={`Search by ${table.getColumn(currentFilteredColumn)?.columnDef.header?.toString() ?? "..."}`} + className={styles.searchBar} + onKeyDown={e => { + if (e.key === "Enter") { + addFilter() } - } - - // Update the actual filters that are applied to the table - setColumnFilters(newFilters) - }} - value={searchQuery} - > - <TextField.Slot> - <MagnifyingGlassIcon height="16" width="16" /> - </TextField.Slot> - <TextField.Slot> - <Button variant="ghost" onClick={() => setSearchQuery("")}> - Reset - </Button> - </TextField.Slot> - </TextField.Root> + }} + onChange={e => { + setSearchQuery(e.target.value) + updateChips(e.target.value) + }} + value={searchQuery} + > + <TextField.Slot> + <MagnifyingGlassIcon height="16" width="16" /> + </TextField.Slot> + <TextField.Slot> + <Button variant="ghost" onClick={() => setSearchQuery("")}> + Reset + </Button> + </TextField.Slot> + </TextField.Root> + ) : ( + <Flex gap="3"> + <DateTimePicker + name="dateStart" + placeholder="Enter start date here" + onChange={e => { + setDateStart(e.target.value) + updateChips([e.target.value, dateEnd]) + }} + /> + <DateTimePicker + name="dateEnd" + placeholder="Enter end date here" + onChange={e => { + setDateEnd(e.target.value) + updateChips([dateStart, e.target.value]) + }} + /> + </Flex> + )} <Dropdown items={filterableColumns.map(col => ({ item: col.columnDef.header!.toString(), From c06a3b935c879ce0b4f240da507b5bcf8ce00b68 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 15:38:22 +0100 Subject: [PATCH 02/15] style: change filter chips to show column header instead of id --- components/TanstackTable.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 69aff0f7..0fd0e471 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -231,7 +231,12 @@ export default function TanstackTable<T>({ </Flex> <Flex gap="2" justify="center"> {prevFilters.map(({ id, value }, index) => ( - <Chip key={index} label={`${id} includes ${value}`} deletable onDelete={() => deleteFilter(index)} /> + <Chip + key={index} + label={`${columns.find(def => def.id === id)?.header} includes "${value}"`} + deletable + onDelete={() => deleteFilter(index)} + /> ))} </Flex> </Flex> From 70c2c85c075e1d25e37e4edc2260abff2ef4e4b8 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 15:44:15 +0100 Subject: [PATCH 03/15] feat: disable add filter button if search queries not filled in properly --- components/TanstackTable.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 0fd0e471..98852cde 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -47,7 +47,8 @@ interface TanstackTableProps<T> { } export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: any[]): boolean => - isAfter(row.getValue(id), filterValue[0]) && (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + (!filterValue[0] || isAfter(row.getValue(id), filterValue[0])) && + (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { @@ -227,7 +228,12 @@ export default function TanstackTable<T>({ title: "Filter by column", }} /> - <Button onClick={addFilter}>Add filter</Button> + <Button + disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart || dateEnd) : !searchQuery} + onClick={addFilter} + > + Add filter + </Button> </Flex> <Flex gap="2" justify="center"> {prevFilters.map(({ id, value }, index) => ( From 1befb4fc746bcee52500f8b85033942e8882fba0 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 16:31:12 +0100 Subject: [PATCH 04/15] feat: allow date ranges as filter Co-authored-by: Alexander Biraben-Renard <ab1223@ic.ac.uk> Co-authored-by: IlliaDerevianko <IlliaDerevianko@users.noreply.github.com> Co-authored-by: nick-bolas <nick-bolas@users.noreply.github.com> --- components/DateTimePicker.tsx | 8 ++++++-- components/TanstackTable.tsx | 31 ++++++++++++++++++++++--------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/components/DateTimePicker.tsx b/components/DateTimePicker.tsx index 1d1b8829..6c6a3a33 100644 --- a/components/DateTimePicker.tsx +++ b/components/DateTimePicker.tsx @@ -14,21 +14,25 @@ const DateTimePicker = ({ required = false, defaultDate, onChange, + showTime = true, }: { name: string placeholder: string required?: boolean defaultDate?: Date | null onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void + showTime?: boolean }) => { return ( <TextField.Root className={styles.inputBox} - type="datetime-local" + type={showTime ? "datetime-local" : "date"} name={name} placeholder={placeholder} required={required} - defaultValue={!!defaultDate ? format(toZonedTime(defaultDate, TIMEZONE), "yyyy-MM-dd'T'HH:mm") : ""} + defaultValue={ + !!defaultDate ? format(toZonedTime(defaultDate, TIMEZONE), showTime ? "yyyy-MM-dd'T'HH:mm" : "yyyy-MM-dd") : "" + } onChange={onChange} /> ) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 98852cde..f0642966 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -47,8 +47,8 @@ interface TanstackTableProps<T> { } export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: any[]): boolean => - (!filterValue[0] || isAfter(row.getValue(id), filterValue[0])) && - (!filterValue[1] || isAfter(filterValue[1], row.getValue(id))) + (!filterValue[0] || isAfter(row.getValue(id), filterValue[0] + "T00:00")) && + (!filterValue[1] || isAfter(filterValue[1] + "T23:59", row.getValue(id))) const getSortingIcon = (isSorted: false | SortDirection): React.ReactNode => { switch (isSorted) { @@ -134,11 +134,18 @@ export default function TanstackTable<T>({ const FooterWrapper = isLowWidth ? Flex : Grid const addFilter = () => { - setPrevFilters([ - ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value: searchQuery }, - ]) - setSearchQuery("") + if (dateFilterColumns.includes(currentFilteredColumn)) { + setPrevFilters([ + ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value: [dateStart, dateEnd] }, + ]) + } else { + setPrevFilters([ + ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value: searchQuery }, + ]) + setSearchQuery("") + } } const deleteFilter = (index: number) => { @@ -199,6 +206,7 @@ export default function TanstackTable<T>({ ) : ( <Flex gap="3"> <DateTimePicker + showTime={false} name="dateStart" placeholder="Enter start date here" onChange={e => { @@ -207,6 +215,7 @@ export default function TanstackTable<T>({ }} /> <DateTimePicker + showTime={false} name="dateEnd" placeholder="Enter end date here" onChange={e => { @@ -229,7 +238,7 @@ export default function TanstackTable<T>({ }} /> <Button - disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart || dateEnd) : !searchQuery} + disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart && dateEnd) : !searchQuery} onClick={addFilter} > Add filter @@ -239,7 +248,11 @@ export default function TanstackTable<T>({ {prevFilters.map(({ id, value }, index) => ( <Chip key={index} - label={`${columns.find(def => def.id === id)?.header} includes "${value}"`} + label={ + dateFilterColumns.includes(currentFilteredColumn) + ? `${columns.find(def => def.id === id)?.header} between "${(value as any[])[0]}" and "${(value as any[])[1]}"` + : `${columns.find(def => def.id === id)?.header} includes "${value}"` + } deletable onDelete={() => deleteFilter(index)} /> From 91b2e708ed0fbad3a6f460cd2d5b6c4cf4225ce1 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 16:43:46 +0100 Subject: [PATCH 05/15] feat: allow filtering of date type columns --- app/companies/CompanyTable.tsx | 6 +++--- app/companies/[slug]/page.tsx | 2 +- app/events/EventTable.tsx | 2 +- app/opportunities/OpportunityTable.tsx | 15 +++++++++++++-- app/opportunities/page.tsx | 2 +- app/students/StudentTable.tsx | 1 - 6 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/companies/CompanyTable.tsx b/app/companies/CompanyTable.tsx index e93aa534..c3d0423c 100644 --- a/app/companies/CompanyTable.tsx +++ b/app/companies/CompanyTable.tsx @@ -68,13 +68,13 @@ const CompanyTable = ({ sortingFn: "text", }, size: { - cell: info => info.getValue() || "N/A", + cell: info => info.getValue(), header: "Size", id: "size", sortingFn: "alphanumeric", }, hq: { - cell: info => info.getValue() || "N/A", + cell: info => info.getValue(), header: "HQ", id: "hq", sortingFn: "text", @@ -97,7 +97,7 @@ const CompanyTable = ({ columnName, columnDefsMap[columnName] ?? { // default column definition, so that all company values are also accessible - cell: info => info.getValue() || "N/A", + cell: info => info.getValue(), header: columnName, sortingFn: "alphanumeric", id: columnName, diff --git a/app/companies/[slug]/page.tsx b/app/companies/[slug]/page.tsx index 2cdf2671..9d880fb9 100644 --- a/app/companies/[slug]/page.tsx +++ b/app/companies/[slug]/page.tsx @@ -197,7 +197,7 @@ const CompanyPage = async ({ params }: { params: { slug: string } }) => { <Box p="8"> <OpportunityTable opportunities={companyProfile.opportunities} - columns={["position", "location", "type", "createdAt"]} + columns={["position", "location", "type", "createdAt", "deadline"]} displayColumns={ !!session && (session.user.role === Role.ADMIN || (await checkCompany(companyProfile.id)(session))) ? ["adminButtons"] diff --git a/app/events/EventTable.tsx b/app/events/EventTable.tsx index 56ded15a..ac92891f 100644 --- a/app/events/EventTable.tsx +++ b/app/events/EventTable.tsx @@ -112,7 +112,7 @@ const EventTable = ({ columnName, columnDefsMap[columnName] ?? { // default column definition, so that all company values are also accessible - cell: info => info.getValue() || "N/A", + cell: info => info.getValue(), header: columnName, sortingFn: "alphanumeric", id: columnName, diff --git a/app/opportunities/OpportunityTable.tsx b/app/opportunities/OpportunityTable.tsx index 2ee5a821..aba90b26 100644 --- a/app/opportunities/OpportunityTable.tsx +++ b/app/opportunities/OpportunityTable.tsx @@ -2,7 +2,7 @@ import { DeleteOpportunity } from "@/components/DeleteOpportunity" import Link from "@/components/Link" -import TanstackTable from "@/components/TanstackTable" +import TanstackTable, { dateFilterFn } from "@/components/TanstackTable" import { EditOpportunity } from "@/components/UpsertOpportunity" import styles from "./opportunityTable.module.scss" @@ -87,7 +87,18 @@ const OpportunityTable = ({ header: "Posted", sortingFn: "datetime", id: "posted", - enableColumnFilter: false, + filterFn: dateFilterFn, + }, + deadline: { + cell: info => ( + <time suppressHydrationWarning={true}> + {formatDistanceToNowStrict(info.getValue(), { addSuffix: true })}{" "} + </time> + ), + header: "Application Deadline", + sortingFn: "datetime", + id: "deadline", + filterFn: dateFilterFn, }, } diff --git a/app/opportunities/page.tsx b/app/opportunities/page.tsx index 58696560..c1d899ae 100644 --- a/app/opportunities/page.tsx +++ b/app/opportunities/page.tsx @@ -19,7 +19,7 @@ const OpportunitiesPage = async () => { <Heading size="8">Opportunities</Heading> <OpportunityTable opportunities={opportunities} - columns={["company.name", "position", "location", "type", "createdAt"]} + columns={["company.name", "position", "location", "type", "createdAt", "deadline"]} /> </Flex> </RestrictedArea> diff --git a/app/students/StudentTable.tsx b/app/students/StudentTable.tsx index 3df034ab..9681a3cd 100644 --- a/app/students/StudentTable.tsx +++ b/app/students/StudentTable.tsx @@ -45,7 +45,6 @@ const StudentTable = ({ header: "Graduating", sortingFn: "datetime", id: "graduationDate", - enableColumnFilter: true, filterFn: dateFilterFn, }, course: { From ed827501657af6177cc2e913d503bfb21f29f653 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 17:10:10 +0100 Subject: [PATCH 06/15] feat: allow date ranges with only start or end Co-authored-by: Alexander Biraben-Renard <ab1223@ic.ac.uk> Co-authored-by: IlliaDerevianko <IlliaDerevianko@users.noreply.github.com> Co-authored-by: nick-bolas <nick-bolas@users.noreply.github.com> --- components/DateTimePicker.tsx | 3 +++ components/TanstackTable.tsx | 24 +++++++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/components/DateTimePicker.tsx b/components/DateTimePicker.tsx index 6c6a3a33..c3e3de11 100644 --- a/components/DateTimePicker.tsx +++ b/components/DateTimePicker.tsx @@ -15,6 +15,7 @@ const DateTimePicker = ({ defaultDate, onChange, showTime = true, + value, }: { name: string placeholder: string @@ -22,6 +23,7 @@ const DateTimePicker = ({ defaultDate?: Date | null onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void showTime?: boolean + value?: string }) => { return ( <TextField.Root @@ -34,6 +36,7 @@ const DateTimePicker = ({ !!defaultDate ? format(toZonedTime(defaultDate, TIMEZONE), showTime ? "yyyy-MM-dd'T'HH:mm" : "yyyy-MM-dd") : "" } onChange={onChange} + value={value} /> ) } diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index f0642966..877350b4 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -31,7 +31,7 @@ import { getSortedRowModel, useReactTable, } from "@tanstack/react-table" -import { isAfter } from "date-fns" +import { format, isAfter } from "date-fns" import React, { useEffect, useMemo, useState } from "react" import { Pagination } from "react-headless-pagination" import { useMediaQuery } from "react-responsive" @@ -46,6 +46,20 @@ interface TanstackTableProps<T> { invisibleColumns?: VisibilityState } +const formatDateRange = (dates: [string, string]): string => { + const [start, end] = dates + if (start && end) { + return `between "${format(start, "dd-MM-yyyy")}" and "${format(end, "dd-MM-yyyy")}"` + } + if (start) { + return `on or after "${format(start, "dd-MM-yyyy")}"` + } + if (end) { + return `on or before "${format(end, "dd-MM-yyyy")}"` + } + return "" +} + export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: any[]): boolean => (!filterValue[0] || isAfter(row.getValue(id), filterValue[0] + "T00:00")) && (!filterValue[1] || isAfter(filterValue[1] + "T23:59", row.getValue(id))) @@ -139,6 +153,8 @@ export default function TanstackTable<T>({ ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column { id: currentFilteredColumn, value: [dateStart, dateEnd] }, ]) + setDateStart("") + setDateEnd("") } else { setPrevFilters([ ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column @@ -213,6 +229,7 @@ export default function TanstackTable<T>({ setDateStart(e.target.value) updateChips([e.target.value, dateEnd]) }} + value={dateStart} /> <DateTimePicker showTime={false} @@ -222,6 +239,7 @@ export default function TanstackTable<T>({ setDateEnd(e.target.value) updateChips([dateStart, e.target.value]) }} + value={dateEnd} /> </Flex> )} @@ -238,7 +256,7 @@ export default function TanstackTable<T>({ }} /> <Button - disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart && dateEnd) : !searchQuery} + disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart || dateEnd) : !searchQuery} onClick={addFilter} > Add filter @@ -250,7 +268,7 @@ export default function TanstackTable<T>({ key={index} label={ dateFilterColumns.includes(currentFilteredColumn) - ? `${columns.find(def => def.id === id)?.header} between "${(value as any[])[0]}" and "${(value as any[])[1]}"` + ? `${columns.find(def => def.id === id)?.header} ${formatDateRange(value as [string, string])}` : `${columns.find(def => def.id === id)?.header} includes "${value}"` } deletable From 5e8f5e0295de7ed9d872114b791d692dbc880263 Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Thu, 22 Aug 2024 17:17:15 +0100 Subject: [PATCH 07/15] feat: delete date chip if neither start nor end is set --- components/TanstackTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 877350b4..7fa3c224 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -177,7 +177,7 @@ export default function TanstackTable<T>({ // Live-update filter chips which are currently displayed (i.e. in prevFilters) if (prevFilters.find(f => f.id === currentFilteredColumn)) { - if (!value) { + if (!value || (Array.isArray(value) && value.every(v => !v))) { // Delete the chip if the search query is now empty setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) } else { From a2db8ce15323e20f210aabc33abde89e4777897c Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Fri, 23 Aug 2024 10:22:17 +0100 Subject: [PATCH 08/15] feat: add labels to datepickers in tanstack table --- components/TanstackTable.tsx | 50 +++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 7fa3c224..eb6da6ce 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -221,26 +221,36 @@ export default function TanstackTable<T>({ </TextField.Root> ) : ( <Flex gap="3"> - <DateTimePicker - showTime={false} - name="dateStart" - placeholder="Enter start date here" - onChange={e => { - setDateStart(e.target.value) - updateChips([e.target.value, dateEnd]) - }} - value={dateStart} - /> - <DateTimePicker - showTime={false} - name="dateEnd" - placeholder="Enter end date here" - onChange={e => { - setDateEnd(e.target.value) - updateChips([dateStart, e.target.value]) - }} - value={dateEnd} - /> + <Flex asChild gap="2" align="center"> + <label> + <Text>Start:</Text> + <DateTimePicker + showTime={false} + name="dateStart" + placeholder="Enter start date here" + onChange={e => { + setDateStart(e.target.value) + updateChips([e.target.value, dateEnd]) + }} + value={dateStart} + /> + </label> + </Flex> + <Flex asChild gap="2" align="center"> + <label> + <Text>End:</Text> + <DateTimePicker + showTime={false} + name="dateEnd" + placeholder="Enter end date here" + onChange={e => { + setDateEnd(e.target.value) + updateChips([dateStart, e.target.value]) + }} + value={dateEnd} + /> + </label> + </Flex> </Flex> )} <Dropdown From 8247ed63c42d7254d3f8aae510378a732e6f51f2 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi <kishansambhi@hotmail.co.uk> Date: Fri, 23 Aug 2024 11:15:18 +0100 Subject: [PATCH 09/15] fix: lookup by id when determine if date filter --- components/TanstackTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index eb6da6ce..61aef7e9 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -277,7 +277,7 @@ export default function TanstackTable<T>({ <Chip key={index} label={ - dateFilterColumns.includes(currentFilteredColumn) + dateFilterColumns.includes(id) ? `${columns.find(def => def.id === id)?.header} ${formatDateRange(value as [string, string])}` : `${columns.find(def => def.id === id)?.header} includes "${value}"` } From 9d8a6e7204844619b014ef74442adff559db6d19 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi <kishansambhi@hotmail.co.uk> Date: Fri, 23 Aug 2024 11:24:20 +0100 Subject: [PATCH 10/15] fix: clear filters if not saved --- components/TanstackTable.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 61aef7e9..05b99e84 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -259,7 +259,17 @@ export default function TanstackTable<T>({ value: col.columnDef.id!, }))} defaultValue={filterableColumns[0].id ?? ""} - onValueChange={setCurrentFilteredColumn} + onValueChange={newFilterCol => { + // If not in prevFilters, remove from current filters + if (!prevFilters.find(f => f.id === currentFilteredColumn)) { + setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) + } + + setCurrentFilteredColumn(newFilterCol) + setSearchQuery("") + setDateStart("") + setDateEnd("") + }} triggerProps={{ "aria-label": "Filter by column", title: "Filter by column", From 09417ac161b9cbc12842cc68a44a05008bffdbda Mon Sep 17 00:00:00 2001 From: Kishan Sambhi <kishansambhi@hotmail.co.uk> Date: Fri, 23 Aug 2024 11:27:03 +0100 Subject: [PATCH 11/15] docs: better documentation for internal filter funcs --- components/TanstackTable.tsx | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 05b99e84..157cd3f8 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -147,7 +147,8 @@ export default function TanstackTable<T>({ const FooterWrapper = isLowWidth ? Flex : Grid - const addFilter = () => { + /** Creates a chip to display a filter that the user has created as they were typing into the search box & they have since pressed Add Filter */ + const addChip = () => { if (dateFilterColumns.includes(currentFilteredColumn)) { setPrevFilters([ ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column @@ -164,12 +165,18 @@ export default function TanstackTable<T>({ } } - const deleteFilter = (index: number) => { + /** Unset a filter and delete its chip that is displayed to the user */ + const deleteChip = (index: number) => { setPrevFilters(prevFilters.filter((_, i) => i !== index)) setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) } - const updateChips = (value: any) => { + /** + * Update the chips, and then set the filters on the table. + * + * If a filter already exists & is shown as a chip, update the chip instead of setting a new filter. + */ + const updateFilters = (value: any) => { const newFilters = [ ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column { id: currentFilteredColumn, value }, // Add the new filter for this column @@ -201,12 +208,12 @@ export default function TanstackTable<T>({ className={styles.searchBar} onKeyDown={e => { if (e.key === "Enter") { - addFilter() + addChip() } }} onChange={e => { setSearchQuery(e.target.value) - updateChips(e.target.value) + updateFilters(e.target.value) }} value={searchQuery} > @@ -230,7 +237,7 @@ export default function TanstackTable<T>({ placeholder="Enter start date here" onChange={e => { setDateStart(e.target.value) - updateChips([e.target.value, dateEnd]) + updateFilters([e.target.value, dateEnd]) }} value={dateStart} /> @@ -245,7 +252,7 @@ export default function TanstackTable<T>({ placeholder="Enter end date here" onChange={e => { setDateEnd(e.target.value) - updateChips([dateStart, e.target.value]) + updateFilters([dateStart, e.target.value]) }} value={dateEnd} /> @@ -277,7 +284,7 @@ export default function TanstackTable<T>({ /> <Button disabled={dateFilterColumns.includes(currentFilteredColumn) ? !(dateStart || dateEnd) : !searchQuery} - onClick={addFilter} + onClick={addChip} > Add filter </Button> @@ -292,7 +299,7 @@ export default function TanstackTable<T>({ : `${columns.find(def => def.id === id)?.header} includes "${value}"` } deletable - onDelete={() => deleteFilter(index)} + onDelete={() => deleteChip(index)} /> ))} </Flex> From 151c6b5e9e25b9eeaee8759d64a62c7228954f31 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi <kishansambhi@hotmail.co.uk> Date: Fri, 23 Aug 2024 11:32:13 +0100 Subject: [PATCH 12/15] fix: make reset button clear filter as well --- components/TanstackTable.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index 157cd3f8..bb6878ce 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -197,6 +197,15 @@ export default function TanstackTable<T>({ setColumnFilters(newFilters) } + /** Clears the current filter if it is not set as a chip */ + const clearCurrentFilter = () => { + if (!prevFilters.find(f => f.id === currentFilteredColumn)) { + setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) + } + + setSearchQuery("") + } + return ( <Flex gap="4" direction="column" width="100%"> {enableSearch && ( @@ -221,7 +230,7 @@ export default function TanstackTable<T>({ <MagnifyingGlassIcon height="16" width="16" /> </TextField.Slot> <TextField.Slot> - <Button variant="ghost" onClick={() => setSearchQuery("")}> + <Button variant="ghost" onClick={clearCurrentFilter}> Reset </Button> </TextField.Slot> @@ -267,13 +276,9 @@ export default function TanstackTable<T>({ }))} defaultValue={filterableColumns[0].id ?? ""} onValueChange={newFilterCol => { - // If not in prevFilters, remove from current filters - if (!prevFilters.find(f => f.id === currentFilteredColumn)) { - setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) - } + clearCurrentFilter() setCurrentFilteredColumn(newFilterCol) - setSearchQuery("") setDateStart("") setDateEnd("") }} From 333bbf44a890c168c01bfc1ccc3f91c24f1e9265 Mon Sep 17 00:00:00 2001 From: Kishan Sambhi <kishansambhi@hotmail.co.uk> Date: Fri, 23 Aug 2024 11:35:51 +0100 Subject: [PATCH 13/15] refactor: useCallback, useCallbacks everywhere --- components/TanstackTable.tsx | 74 +++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index bb6878ce..bf495cee 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -32,7 +32,7 @@ import { useReactTable, } from "@tanstack/react-table" import { format, isAfter } from "date-fns" -import React, { useEffect, useMemo, useState } from "react" +import React, { useCallback, useEffect, useMemo, useState } from "react" import { Pagination } from "react-headless-pagination" import { useMediaQuery } from "react-responsive" @@ -136,19 +136,10 @@ export default function TanstackTable<T>({ const [dateStart, setDateStart] = useState("") const [dateEnd, setDateEnd] = useState("") - if (!isClient) { - return ( - <Flex align="center" justify="center" p="4" gap="3"> - <Text>Loading...</Text> - <Spinner size="3" /> - </Flex> - ) - } - const FooterWrapper = isLowWidth ? Flex : Grid /** Creates a chip to display a filter that the user has created as they were typing into the search box & they have since pressed Add Filter */ - const addChip = () => { + const addChip = useCallback(() => { if (dateFilterColumns.includes(currentFilteredColumn)) { setPrevFilters([ ...prevFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column @@ -163,47 +154,62 @@ export default function TanstackTable<T>({ ]) setSearchQuery("") } - } + }, [currentFilteredColumn, dateEnd, dateFilterColumns, dateStart, prevFilters, searchQuery]) /** Unset a filter and delete its chip that is displayed to the user */ - const deleteChip = (index: number) => { - setPrevFilters(prevFilters.filter((_, i) => i !== index)) - setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) - } + const deleteChip = useCallback( + (index: number) => { + setPrevFilters(prevFilters.filter((_, i) => i !== index)) + setColumnFilters([...columnFilters.filter((_, i) => i !== index)]) + }, + [columnFilters, prevFilters], + ) /** * Update the chips, and then set the filters on the table. * * If a filter already exists & is shown as a chip, update the chip instead of setting a new filter. */ - const updateFilters = (value: any) => { - const newFilters = [ - ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column - { id: currentFilteredColumn, value }, // Add the new filter for this column - ] + const updateFilters = useCallback( + (value: any) => { + const newFilters = [ + ...columnFilters.filter(f => f.id !== currentFilteredColumn), // Remove the previous filter for the same column + { id: currentFilteredColumn, value }, // Add the new filter for this column + ] - // Live-update filter chips which are currently displayed (i.e. in prevFilters) - if (prevFilters.find(f => f.id === currentFilteredColumn)) { - if (!value || (Array.isArray(value) && value.every(v => !v))) { - // Delete the chip if the search query is now empty - setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) - } else { - // Update the chip with the new search query - setPrevFilters(newFilters) + // Live-update filter chips which are currently displayed (i.e. in prevFilters) + if (prevFilters.find(f => f.id === currentFilteredColumn)) { + if (!value || (Array.isArray(value) && value.every(v => !v))) { + // Delete the chip if the search query is now empty + setPrevFilters(prevFilters.filter(f => f.id !== currentFilteredColumn)) + } else { + // Update the chip with the new search query + setPrevFilters(newFilters) + } } - } - // Update the actual filters that are applied to the table - setColumnFilters(newFilters) - } + // Update the actual filters that are applied to the table + setColumnFilters(newFilters) + }, + [columnFilters, currentFilteredColumn, prevFilters], + ) /** Clears the current filter if it is not set as a chip */ - const clearCurrentFilter = () => { + const clearCurrentFilter = useCallback(() => { if (!prevFilters.find(f => f.id === currentFilteredColumn)) { setColumnFilters(columnFilters.filter(f => f.id !== currentFilteredColumn)) } setSearchQuery("") + }, [columnFilters, currentFilteredColumn, prevFilters]) + + if (!isClient) { + return ( + <Flex align="center" justify="center" p="4" gap="3"> + <Text>Loading...</Text> + <Spinner size="3" /> + </Flex> + ) } return ( From 48337b486b321204a233b09f58462b0526c4ec0d Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Fri, 23 Aug 2024 14:51:10 +0100 Subject: [PATCH 14/15] chore: change deadline date format --- app/opportunities/OpportunityTable.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/opportunities/OpportunityTable.tsx b/app/opportunities/OpportunityTable.tsx index aba90b26..15f419e5 100644 --- a/app/opportunities/OpportunityTable.tsx +++ b/app/opportunities/OpportunityTable.tsx @@ -11,7 +11,7 @@ import { getCompanyLink } from "../companies/getCompanyLink" import type { CompanyProfile, Opportunity } from "@prisma/client" import { Flex } from "@radix-ui/themes" import { ColumnDef, DisplayColumnDef, createColumnHelper } from "@tanstack/react-table" -import { formatDistanceToNowStrict } from "date-fns" +import { format, formatDistanceToNowStrict } from "date-fns" import Image from "next/image" import { useMemo } from "react" @@ -90,11 +90,12 @@ const OpportunityTable = ({ filterFn: dateFilterFn, }, deadline: { - cell: info => ( - <time suppressHydrationWarning={true}> - {formatDistanceToNowStrict(info.getValue(), { addSuffix: true })}{" "} - </time> - ), + cell: info => <time suppressHydrationWarning={true}>{format(info.getValue(), "EEEE do MMMM yyyy")}</time>, + // cell: info => ( + // <time suppressHydrationWarning={true}> + // {formatDistanceToNowStrict(info.getValue(), { addSuffix: true })}{" "} + // </time> + // ), header: "Application Deadline", sortingFn: "datetime", id: "deadline", From 3de01734c59bcd8fd9e0479f0c8a5ed2bb93784a Mon Sep 17 00:00:00 2001 From: Matthew Alex <matalex412@gmail.com> Date: Fri, 23 Aug 2024 14:58:49 +0100 Subject: [PATCH 15/15] docs: improve documentation for dateFilterFn --- app/opportunities/OpportunityTable.tsx | 5 ----- components/TanstackTable.tsx | 10 +++++++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/opportunities/OpportunityTable.tsx b/app/opportunities/OpportunityTable.tsx index 15f419e5..29408b1b 100644 --- a/app/opportunities/OpportunityTable.tsx +++ b/app/opportunities/OpportunityTable.tsx @@ -91,11 +91,6 @@ const OpportunityTable = ({ }, deadline: { cell: info => <time suppressHydrationWarning={true}>{format(info.getValue(), "EEEE do MMMM yyyy")}</time>, - // cell: info => ( - // <time suppressHydrationWarning={true}> - // {formatDistanceToNowStrict(info.getValue(), { addSuffix: true })}{" "} - // </time> - // ), header: "Application Deadline", sortingFn: "datetime", id: "deadline", diff --git a/components/TanstackTable.tsx b/components/TanstackTable.tsx index bf495cee..afb74260 100644 --- a/components/TanstackTable.tsx +++ b/components/TanstackTable.tsx @@ -60,7 +60,15 @@ const formatDateRange = (dates: [string, string]): string => { return "" } -export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: any[]): boolean => +/** + * Filter function for columns with type date. Keeps rows which are after the start date (if set) and before the end date (if set). + * @template T The type of the row + * @param row The row to filter + * @param id The column id to filter + * @param filterValue The filter value to be applied of the form [start, end] + * @returns Whether the row should be displayed + */ +export const dateFilterFn = <T,>(row: Row<T>, id: string, filterValue: [string, string]): boolean => (!filterValue[0] || isAfter(row.getValue(id), filterValue[0] + "T00:00")) && (!filterValue[1] || isAfter(filterValue[1] + "T23:59", row.getValue(id)))