From 99a35a7cad656380c58821b955ba458625a7fc41 Mon Sep 17 00:00:00 2001 From: Ryan Lau <47727459+ryanlau@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:09:44 -0500 Subject: [PATCH 1/3] mvp move form state to query params --- package-lock.json | 45 ++++++++++++-- package.json | 1 + src/components/SchemaSearch.tsx | 34 +++------- src/components/SearchBar.tsx | 106 +++++++++++++------------------- src/main.tsx | 5 +- src/routes/root.tsx | 77 +++++++++++++++-------- 6 files changed, 146 insertions(+), 122 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec0dc22..703fa65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@mantine/hooks": "^7.13.1", "@tabler/icons-react": "3.17.0", "dayjs": "^1.11.13", + "nuqs": "^2.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", @@ -1019,9 +1020,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", - "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.3.tgz", + "integrity": "sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3886,9 +3887,9 @@ "license": "MIT" }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -6401,6 +6402,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6480,6 +6487,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/nuqs": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/nuqs/-/nuqs-2.2.1.tgz", + "integrity": "sha512-P0swZtg6k9LRXzlT9gQAFtQg25Edan2Evt5vwyGvh00Z+rHH19gb2ZajU/E/SfhwE858aM/jb+wJziWbbwSlcg==", + "license": "MIT", + "dependencies": { + "mitt": "^3.0.1" + }, + "peerDependencies": { + "@remix-run/react": ">=2", + "next": ">=14.2.0", + "react": ">=18.2.0 || ^19.0.0-0", + "react-router-dom": ">=6" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "next": { + "optional": true + }, + "react-router-dom": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index 2655353..3bd01ee 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@mantine/hooks": "^7.13.1", "@tabler/icons-react": "3.17.0", "dayjs": "^1.11.13", + "nuqs": "^2.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", "react-router-dom": "^6.27.0", diff --git a/src/components/SchemaSearch.tsx b/src/components/SchemaSearch.tsx index 5a1c12e..6e7a663 100644 --- a/src/components/SchemaSearch.tsx +++ b/src/components/SchemaSearch.tsx @@ -1,44 +1,24 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import { MultiSelect } from "@mantine/core"; import "@/css/SchemaSearch.css"; +import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; interface SchemaSearch { schemas: string[]; - clear: boolean; } -const SchemaSearch: React.FC = ({ schemas, clear }) => { - const [value, setValue] = useState(""); - const [selectedSchemas, setSelectedSchemas] = useState([]); - - const filteredSchemas = schemas.filter((schema) => - schema.toLowerCase().includes(value.toLowerCase()), +const SchemaSearch: React.FC = ({ schemas }) => { + const [selectedSchemas, setSelectedSchemas] = useQueryState( + "schemas", + parseAsArrayOf(parseAsString).withDefault([]), ); - - - const clearSchema = () => { - setValue(""); - setSelectedSchemas([]) - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "Enter" && filteredSchemas.length > 0) { - setValue(filteredSchemas[0]); - setValue("") - } - }; - - useEffect(() => { - clearSchema(); - }, [clear]); return (
>; setSearch: React.Dispatch>; } -function SearchBarWithFilter({ - setSearchFilters, - setSearch, -}: SearchBarWithFilterProps) { - const [searchTerm, setSearchTerm] = useState(""); - const [selectedLocation, setSelectedLocation] = useState(""); - const [selectedEventType, setSelectedEventType] = useState(""); - const [beforeDate, setBeforeDate] = useState(""); - const [afterDate, setAfterDate] = useState(""); - const [clearSchemas, setClearSchemas] = useState(false); +function SearchBarWithFilter({ setSearch }: SearchBarWithFilterProps) { + const [searchTerm, setSearchTerm] = useQueryState( + "notes", + parseAsString.withDefault(""), + ); + const [selectedLocation, setSelectedLocation] = useQueryState( + "location", + parseAsString.withDefault(""), + ); + const [selectedEventType, setSelectedEventType] = useQueryState( + "event", + parseAsString.withDefault(""), + ); + const [beforeDate, setBeforeDate] = useQueryState( + "beforeDate", + parseAsString.withDefault(""), + ); + const [afterDate, setAfterDate] = useQueryState( + "afterDate", + parseAsString.withDefault(""), + ); + const [, setSelectedSchemas] = useQueryState( + "schemas", + parseAsArrayOf(parseAsString).withDefault([]), + ); const schemas = ["Schema1", "Schema2", "Schema3", "Schema4"]; - // Handle filter changes - function handleFilterChange( - e: React.ChangeEvent, - ) { - const { name } = e.target; - const { value } = e.target; - - - // Update local state based on input - switch (name) { - case "location": - setSelectedLocation(value); - break; - case "eventType": - setSelectedEventType(value); - break; - case "beforeDate": - setBeforeDate(value); - break; - case "afterDate": - setAfterDate(value); - break; - case "search_text": - setSearchTerm(value); - break; - default: - break; - } - } - // Clear all filters and search term const handleClear = () => { - setSearchTerm(""); - setSearchFilters({ - searchText: "", - location: "", - eventType: "", - beforeDate: "", - afterDate: "", - }); - setSelectedLocation(""); - setSelectedEventType(""); - setBeforeDate(""); - setAfterDate(""); - setClearSchemas((prev) => !prev); + setSearchTerm(null); + setSelectedLocation(null); + setSelectedEventType(null); + setBeforeDate(null); + setAfterDate(null); + setSelectedSchemas(null); }; const handleSearch = () => { @@ -86,9 +64,7 @@ function SearchBarWithFilter({ className="search-bar" placeholder="Search by file name or notes..." value={searchTerm} - onChange={(e) => { - handleFilterChange(e); - }} + onChange={(e) => setSearchTerm(e.target.value)} /> {/* Filter Options */} @@ -98,7 +74,7 @@ function SearchBarWithFilter({ setSelectedEventType(e.target.value)} className="filter-select" > @@ -133,7 +109,7 @@ function SearchBarWithFilter({ type="date" name="beforeDate" value={beforeDate} - onChange={handleFilterChange} + onChange={(e) => setBeforeDate(e.target.value)} className="date-picker" /> @@ -144,18 +120,20 @@ function SearchBarWithFilter({ type="date" name="afterDate" value={afterDate} - onChange={handleFilterChange} + onChange={(e) => setAfterDate(e.target.value)} + //value={afterDate.split("T")[0]} + //onChange={(e) => setAfterDate(e.target.value + "T23:59:59Z07:00")} className="date-picker" /> - +
{/* Clear Button */} diff --git a/src/main.tsx b/src/main.tsx index e512040..28af4bd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -6,6 +6,7 @@ import { Route, RouterProvider, } from "react-router-dom"; +import { NuqsAdapter } from "nuqs/adapters/react-router"; import Root from "./routes/root"; import Docs from "./routes/docs"; import Changelog from "./routes/changelog"; @@ -27,6 +28,8 @@ const router = createBrowserRouter( createRoot(document.getElementById("root")!).render( - + + + , ); diff --git a/src/routes/root.tsx b/src/routes/root.tsx index cbec9f5..bed010b 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -3,23 +3,47 @@ import SearchBar from "@/components/SearchBar"; import "@/css/Root.css"; import DataTable from "@/components/DataTable"; import PreviewCard from "@/components/PreviewCard"; +import { parseAsArrayOf, parseAsString, useQueryState } from "nuqs"; export default function Root() { const [filteredData, setFilteredData] = useState(); const [selectedRow, setSelectedRow] = useState(""); const [selectedData, setSelectedData] = useState(); - const [searchFilters, setSearchFilters] = useState({ - location: "", - eventType: "", - beforeDate: "", - afterDate: "", - searchText: "", - }); + + const [searchTerm] = useQueryState("notes", parseAsString.withDefault("")); + const [selectedLocation] = useQueryState( + "location", + parseAsString.withDefault(""), + ); + const [selectedEventType] = useQueryState( + "event", + parseAsString.withDefault(""), + ); + const [beforeDate] = useQueryState( + "beforeDate", + parseAsString.withDefault(""), + ); + const [afterDate] = useQueryState("afterDate", parseAsString.withDefault("")); + const [selectedSchemas] = useQueryState( + "schemas", + parseAsArrayOf(parseAsString).withDefault([]), + ); + + const searchFilters = { + location: selectedLocation, + date: selectedEventType, + eventType: selectedEventType, + beforeDate, + afterDate, + searchText: searchTerm, + selectedSchemas, + }; + const [search, setSearch] = useState(false); const formatDate = (dateStr: string) => { const [year, month, day] = dateStr.split("-"); if (month.length !== 2 || day.length !== 2 || year.length !== 4) { - throw new Error(`Invalid date format: ${dateStr}`); + throw new Error(`Invalid date format: ${dateStr}`); } return new Date(`${year}-${month}-${day}T00:00:00.000Z`).toISOString(); }; @@ -27,7 +51,7 @@ export default function Root() { const fetchData = async (filters: SearchFilter) => { const { location, date, eventType, searchText } = filters; let { afterDate, beforeDate } = filters; - + beforeDate = beforeDate ? formatDate(beforeDate) : undefined; afterDate = afterDate ? formatDate(afterDate) : undefined; const params = { @@ -44,7 +68,7 @@ export default function Root() { const res = await fetch( `${import.meta.env.VITE_API_URL}/api/v2/mcaps?${queryString}`, ); - + const data = await res.json(); return data.data; }; @@ -52,11 +76,13 @@ export default function Root() { const assignData = async () => { const data = await fetchData(searchFilters); console.log(data); - const sortedData = data.sort((a: MCAPFileInformation, b: MCAPFileInformation) => { - const dateA = new Date(a.date); - const dateB = new Date(b.date); - return dateB.getTime() - dateA.getTime(); - }); + const sortedData = data.sort( + (a: MCAPFileInformation, b: MCAPFileInformation) => { + const dateA = new Date(a.date); + const dateB = new Date(b.date); + return dateB.getTime() - dateA.getTime(); + }, + ); setFilteredData(sortedData); }; @@ -64,24 +90,27 @@ export default function Root() { assignData(); }, []); - // Two useEffects bc of the way we are handling the Search Button D: + // Two useEffects bc of the way we are handling the Search Button D: useEffect(() => { const getData = async () => { - if (search) { // Only fetch data when search is true + if (search) { + // Only fetch data when search is true const data = await fetchData(searchFilters); console.log(data); - const sortedData = data.sort((a: MCAPFileInformation, b: MCAPFileInformation) => { - const dateA = new Date(a.date); - const dateB = new Date(b.date); - return dateB.getTime() - dateA.getTime(); - }); + const sortedData = data.sort( + (a: MCAPFileInformation, b: MCAPFileInformation) => { + const dateA = new Date(a.date); + const dateB = new Date(b.date); + return dateB.getTime() - dateA.getTime(); + }, + ); setFilteredData(sortedData); setSearch(false); } }; getData(); - }, [search]); + }, []); return ( <> @@ -95,7 +124,7 @@ export default function Root() { />
- + From de8e21bfabf3985d79407691f31c5abd32419038 Mon Sep 17 00:00:00 2001 From: Ryan Lau <47727459+ryanlau@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:18:01 -0500 Subject: [PATCH 2/3] mvp move form state to query params --- src/routes/root.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/root.tsx b/src/routes/root.tsx index bed010b..54b4f7d 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -110,7 +110,7 @@ export default function Root() { } }; getData(); - }, []); + }, [search]); return ( <> From f42234b7880bdbf3eb5981950faa4b461f1507a7 Mon Sep 17 00:00:00 2001 From: Ryan Lau <47727459+ryanlau@users.noreply.github.com> Date: Sun, 24 Nov 2024 16:40:51 -0500 Subject: [PATCH 3/3] use 23:59:59 for time for end date --- src/routes/root.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/routes/root.tsx b/src/routes/root.tsx index 54b4f7d..a3510a1 100644 --- a/src/routes/root.tsx +++ b/src/routes/root.tsx @@ -40,20 +40,22 @@ export default function Root() { }; const [search, setSearch] = useState(false); - const formatDate = (dateStr: string) => { + const formatDate = (dateStr: string, time: string) => { const [year, month, day] = dateStr.split("-"); if (month.length !== 2 || day.length !== 2 || year.length !== 4) { throw new Error(`Invalid date format: ${dateStr}`); } - return new Date(`${year}-${month}-${day}T00:00:00.000Z`).toISOString(); + return new Date(`${year}-${month}-${day}T${time}Z`).toISOString(); }; const fetchData = async (filters: SearchFilter) => { const { location, date, eventType, searchText } = filters; let { afterDate, beforeDate } = filters; - beforeDate = beforeDate ? formatDate(beforeDate) : undefined; - afterDate = afterDate ? formatDate(afterDate) : undefined; + beforeDate = beforeDate + ? formatDate(beforeDate, "00:00:00.000") + : undefined; + afterDate = afterDate ? formatDate(afterDate, "23:59:59.999") : undefined; const params = { ...(location ? { location } : {}), ...(eventType ? { eventType } : {}),