From 833bd6c6b3a0413c9083b72e103aae6b8f545d67 Mon Sep 17 00:00:00 2001 From: Kevin Wu Date: Sat, 30 Dec 2023 18:28:18 -0800 Subject: [PATCH] feat: use react datepicker (#38) * feat: swap input for datepicker * test: update for new types --- __tests__/search-filters.test.ts | 21 ++--- components/search/FilterComponents.tsx | 43 +++++----- components/search/Filters.tsx | 4 +- components/search/Search.tsx | 14 +--- components/search/filter-utils.ts | 32 ++++---- package-lock.json | 104 ++++++++++++++++++++++++- package.json | 2 + 7 files changed, 159 insertions(+), 61 deletions(-) diff --git a/__tests__/search-filters.test.ts b/__tests__/search-filters.test.ts index 58934fa..ee78c82 100644 --- a/__tests__/search-filters.test.ts +++ b/__tests__/search-filters.test.ts @@ -80,8 +80,8 @@ const defaultFilterValues: FilterValues = { format: [true, true], enrollment: [false], available: [false], - start: "2023-12-20", - end: "", + start: new Date(2023, 11, 20), + end: undefined, institution: "Any Institution", min: 0, max: 20, @@ -192,33 +192,28 @@ describe("Search Filters", () => { }); describe("Filter Utils' Time Utilities", () => { - test("startsAfter none", async () => { - const result = startsAfter("", data.courses[0]); - expect(result).toBe(true); - }); - test("startsAfter defined returns true", async () => { - const result = startsAfter("2023-12-25", data.courses[0]); + const result = startsAfter(new Date("2023-12-25"), data.courses[0]); expect(result).toBe(true); }); test("startsAfter defined returns false", async () => { - const result = startsAfter("2024-12-25", data.courses[0]); + const result = startsAfter(new Date("2024-12-25"), data.courses[0]); expect(result).toBe(false); }); - test("endsBefore none", async () => { - const result = endsBefore("", data.courses[0]); + test("endsBefore undefined", async () => { + const result = endsBefore(undefined, data.courses[0]); expect(result).toBe(true); }); test("endsBefore defined returns true", async () => { - const result = endsBefore("2024-06-14", data.courses[0]); + const result = endsBefore(new Date("2024-06-14"), data.courses[0]); expect(result).toBe(true); }); test("endsBefore defined returns false", async () => { - const result = endsBefore("2024-05-14", data.courses[0]); + const result = endsBefore(new Date("2024-05-14"), data.courses[0]); expect(result).toBe(false); }); diff --git a/components/search/FilterComponents.tsx b/components/search/FilterComponents.tsx index d09126c..2694bb5 100644 --- a/components/search/FilterComponents.tsx +++ b/components/search/FilterComponents.tsx @@ -3,6 +3,9 @@ import React, { ChangeEvent, Dispatch, SetStateAction, useState } from "react"; import { FaCheck, FaChevronDown } from "react-icons/fa"; import { CollegeObject } from "./Search"; +import DatePicker from "react-datepicker"; + +import "react-datepicker/dist/react-datepicker.css"; interface FilterCheckboxProps { title: string; @@ -58,26 +61,28 @@ export const CustomFilterCheckbox = (props: FilterCheckboxProps) => { }; interface CalendarFilterProps { - onStartChange: Dispatch>; - onEndChange: Dispatch>; - defaultStart: string | undefined; - defaultEnd: string | undefined; + onStartChange: Dispatch>; + onEndChange: Dispatch>; + defaultStart: Date; + defaultEnd: Date | undefined; } export const CalendarFilter = (props: CalendarFilterProps) => { const { onStartChange, onEndChange, defaultStart, defaultEnd } = props; - const [start, setStart] = useState(defaultStart); + const [start, setStart] = useState( + defaultStart ? new Date(defaultStart) : new Date(), + ); const [end, setEnd] = useState(defaultEnd); - const handleStartChange = (e: ChangeEvent) => { - onStartChange(e.target.value); - setStart(e.target.value); + const handleStartChange = (date: Date) => { + onStartChange(date); + setStart(date); }; - const handleEndChange = (e: ChangeEvent) => { - onEndChange(e.target.value); - setEnd(e.target.value); + const handleEndChange = (date: Date) => { + onEndChange(date); + setEnd(date); }; return ( @@ -89,11 +94,11 @@ export const CalendarFilter = (props: CalendarFilterProps) => { Starts After
-
@@ -102,11 +107,11 @@ export const CalendarFilter = (props: CalendarFilterProps) => { Ends Before
-
diff --git a/components/search/Filters.tsx b/components/search/Filters.tsx index 8aaa4e0..8fc3113 100644 --- a/components/search/Filters.tsx +++ b/components/search/Filters.tsx @@ -13,8 +13,8 @@ interface SearchFilterProps { setFormat: Dispatch>; setEnrollment: Dispatch>; setAvailable: Dispatch>; - setStart: Dispatch>; - setEnd: Dispatch>; + setStart: Dispatch>; + setEnd: Dispatch>; setInstitution: Dispatch>; setMin: Dispatch>; setMax: Dispatch>; diff --git a/components/search/Search.tsx b/components/search/Search.tsx index 2e519eb..bfe7b59 100644 --- a/components/search/Search.tsx +++ b/components/search/Search.tsx @@ -43,8 +43,8 @@ export type FilterValues = { format: boolean[]; enrollment: boolean[]; available: boolean[]; - start: string; - end: string; + start: Date; + end: Date | undefined; institution: string; min: number; max: number; @@ -70,14 +70,8 @@ const Search = () => { const [format, setFormat] = useState([true, true]); const [enrollment, setEnrollment] = useState([true]); const [available, setAvailable] = useState([true]); - const [start, setStart] = useState(() => { - const today = new Date(); - const year = today.getFullYear(); - const month = (today.getMonth() + 1).toString().padStart(2, "0"); - const day = today.getDate().toString().padStart(2, "0"); - return `${year}-${month}-${day}`; - }); - const [end, setEnd] = useState(""); + const [start, setStart] = useState(new Date()); + const [end, setEnd] = useState(); const [institution, setInstitution] = useState("Any Institution"); const [min, setMin] = useState(0); const [max, setMax] = useState(20); diff --git a/components/search/filter-utils.ts b/components/search/filter-utils.ts index c67dbbc..76333d0 100644 --- a/components/search/filter-utils.ts +++ b/components/search/filter-utils.ts @@ -1,23 +1,27 @@ import { CollegeObject, FilterValues } from "./Search"; -export const startsAfter = (start: string, result: CollegeObject) => { - if (start == undefined) return true; +export const startsAfter = (start: Date, result: CollegeObject) => { + const month = result.startMonth.toString().padStart(2, "0"); + const day = result.startDay.toString().padStart(2, "0"); + const courseDate = new Date(`2024-${month}-${day}`); + courseDate.setHours(0, 0, 0, 0); + courseDate.setDate(courseDate.getDate() + 1); - return ( - `2024-${result.startMonth.toString().padStart(2, "0")}-${result.startDay - .toString() - .padStart(2, "0")}` >= start - ); + return courseDate > start; }; -export const endsBefore = (end: string, result: CollegeObject) => { - if (end == "") return true; +export const endsBefore = (end: Date | undefined, result: CollegeObject) => { + if (end == undefined) { + return true; + } + + const month = result.endMonth.toString().padStart(2, "0"); + const day = result.endDay.toString().padStart(2, "0"); + const courseDate = new Date(`2024-${month}-${day}`); + courseDate.setHours(0, 0, 0, 0); + courseDate.setDate(courseDate.getDate() + 1); - return ( - `2024-${result.endMonth.toString().padStart(2, "0")}-${result.endDay - .toString() - .padStart(2, "0")}` <= end - ); + return courseDate < end; }; export function filterData( diff --git a/package-lock.json b/package-lock.json index a136b8d..a220ab1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "lucide-react": "^0.303.0", "next": "^14.0.2", "react": "^18.2.0", + "react-datepicker": "^4.25.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-lazy-load": "^4.0.1", @@ -28,6 +29,7 @@ "@types/jest": "^29.5.11", "@types/node": "^20", "@types/react": "^18", + "@types/react-datepicker": "^4.19.4", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", "eslint": "^8", @@ -1555,6 +1557,15 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -2397,6 +2408,18 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-datepicker": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.4.tgz", + "integrity": "sha512-HRD0LHTxBVe61LRJgTdPscbapLQl7+jI/7bxnPGpvzdJ/iXN9q7ucYv8HKULeIAN84O5LzFhwTMOkO4QnIUJaQ==", + "dev": true, + "dependencies": { + "@popperjs/core": "^2.9.2", + "@types/react": "*", + "date-fns": "^2.0.1", + "react-popper": "^2.2.5" + } + }, "node_modules/@types/react-dom": { "version": "18.2.15", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", @@ -3299,6 +3322,11 @@ "url": "https://joebell.co.uk" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -3499,6 +3527,21 @@ "node": ">=12" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7608,7 +7651,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -7682,6 +7724,23 @@ "node": ">=0.10.0" } }, + "node_modules/react-datepicker": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.25.0.tgz", + "integrity": "sha512-zB7CSi44SJ0sqo8hUQ3BF1saE/knn7u25qEMTO1CQGofY1VAKahO8k9drZtp0cfW1DMfoYLR3uSY1/uMvbEzbg==", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -7694,6 +7753,11 @@ "react": "^18.2.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, "node_modules/react-icons": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", @@ -7705,8 +7769,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-lazy-load": { "version": "4.0.1", @@ -7717,6 +7780,33 @@ "react-dom": "^17.0.0 || ^18.0.0" } }, + "node_modules/react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-remove-scroll": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", @@ -8975,6 +9065,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index b1b0b1a..7af4d4a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "lucide-react": "^0.303.0", "next": "^14.0.2", "react": "^18.2.0", + "react-datepicker": "^4.25.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-lazy-load": "^4.0.1", @@ -31,6 +32,7 @@ "@types/jest": "^29.5.11", "@types/node": "^20", "@types/react": "^18", + "@types/react-datepicker": "^4.19.4", "@types/react-dom": "^18", "autoprefixer": "^10.0.1", "eslint": "^8",