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)))