From 43732d525bf4ff0c204106c6b0f6e878f3533e19 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:28:44 -0500 Subject: [PATCH 1/5] update table version and style --- package-lock.json | 102 +++++++++++++++++---------------- package.json | 4 +- src/components/ClassTable.scss | 41 ------------- src/components/ClassTable.tsx | 39 ++++++++----- 4 files changed, 79 insertions(+), 107 deletions(-) delete mode 100644 src/components/ClassTable.scss diff --git a/package-lock.json b/package-lock.json index 5ea04fb..f5e60ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "name": "hydrant", "version": "0.1.0", "dependencies": { - "@ag-grid-community/client-side-row-model": "^28.1.0", - "@ag-grid-community/core": "^28.1.0", - "@ag-grid-community/react": "^28.1.0", "@chakra-ui/react": "^3.2.3", "@emotion/react": "^11.13.5", "@fontsource-variable/inter": "^5.1.0", @@ -19,6 +16,7 @@ "@fullcalendar/react": "^6.1.15", "@fullcalendar/timegrid": "^6.1.15", "@react-oauth/google": "^0.2.6", + "ag-grid-react": "^33.0.2", "html-entities": "^2.3.3", "ical-generator": "^6.0.0", "msgpack-lite": "^0.1.26", @@ -56,32 +54,6 @@ "node": ">=18.0" } }, - "node_modules/@ag-grid-community/client-side-row-model": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/client-side-row-model/-/client-side-row-model-28.2.1.tgz", - "integrity": "sha512-JLLNVNAtSbVjg/h0+JqNxbRGzHR9xl3YHO4kVt8Bu0UO1XQLfGLT8wkzqQTcEOiq0pEfFTee7/BGT3oHOQfUQg==", - "dependencies": { - "@ag-grid-community/core": "~28.2.1" - } - }, - "node_modules/@ag-grid-community/core": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-28.2.1.tgz", - "integrity": "sha512-qGhqZhY8KbPlr3cJBVkNjGKu+cOyFc8IGvPcvlm0pjNC9cdxO/ct9Sa1Dg+k3ACCEroMEt1apBvnoOgurpvewQ==" - }, - "node_modules/@ag-grid-community/react": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/react/-/react-28.2.1.tgz", - "integrity": "sha512-VWtiBMhvDKp//lIa2Ovzv+/gcQj6wnZDhngyPW5e97Sl+sIs6/UngHSTbbmhIb+wWUCIKmSRL1+R05PNnKjFTA==", - "dependencies": { - "prop-types": "^15.8.1" - }, - "peerDependencies": { - "@ag-grid-community/core": "~28.2.1", - "react": "^16.3.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -3180,6 +3152,35 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ag-charts-types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-11.0.0.tgz", + "integrity": "sha512-8KUCZtKaUNjxqvo5E71sP7nBpRHMSv81anK93UCMMWYZC3iBSEF1pxfYS2/GEM9oo5VP65mevDJsq10dp0vooQ==", + "license": "MIT" + }, + "node_modules/ag-grid-community": { + "version": "33.0.2", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-33.0.2.tgz", + "integrity": "sha512-ymD2ADPVfsAMNoKRB9BvKYPWo+MSJ2L9gOwiwyBS1TykNwf0bL7kfQParZXcY+h+b8JpfeVbx4oGDLZBEe3Gag==", + "license": "MIT", + "dependencies": { + "ag-charts-types": "11.0.0" + } + }, + "node_modules/ag-grid-react": { + "version": "33.0.2", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-33.0.2.tgz", + "integrity": "sha512-qgwnNoBZ18H18kCHnwHoGvH1U9nl2CHbSmlfCtg3TC1NUr17IjoiXMSqA0gVdsAo3LyLPN4fYatwQsVIpiWmvw==", + "license": "MIT", + "dependencies": { + "ag-grid-community": "33.0.2", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -8310,27 +8311,6 @@ } }, "dependencies": { - "@ag-grid-community/client-side-row-model": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/client-side-row-model/-/client-side-row-model-28.2.1.tgz", - "integrity": "sha512-JLLNVNAtSbVjg/h0+JqNxbRGzHR9xl3YHO4kVt8Bu0UO1XQLfGLT8wkzqQTcEOiq0pEfFTee7/BGT3oHOQfUQg==", - "requires": { - "@ag-grid-community/core": "~28.2.1" - } - }, - "@ag-grid-community/core": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/core/-/core-28.2.1.tgz", - "integrity": "sha512-qGhqZhY8KbPlr3cJBVkNjGKu+cOyFc8IGvPcvlm0pjNC9cdxO/ct9Sa1Dg+k3ACCEroMEt1apBvnoOgurpvewQ==" - }, - "@ag-grid-community/react": { - "version": "28.2.1", - "resolved": "https://registry.npmjs.org/@ag-grid-community/react/-/react-28.2.1.tgz", - "integrity": "sha512-VWtiBMhvDKp//lIa2Ovzv+/gcQj6wnZDhngyPW5e97Sl+sIs6/UngHSTbbmhIb+wWUCIKmSRL1+R05PNnKjFTA==", - "requires": { - "prop-types": "^15.8.1" - } - }, "@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -10571,6 +10551,28 @@ "dev": true, "requires": {} }, + "ag-charts-types": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ag-charts-types/-/ag-charts-types-11.0.0.tgz", + "integrity": "sha512-8KUCZtKaUNjxqvo5E71sP7nBpRHMSv81anK93UCMMWYZC3iBSEF1pxfYS2/GEM9oo5VP65mevDJsq10dp0vooQ==" + }, + "ag-grid-community": { + "version": "33.0.2", + "resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-33.0.2.tgz", + "integrity": "sha512-ymD2ADPVfsAMNoKRB9BvKYPWo+MSJ2L9gOwiwyBS1TykNwf0bL7kfQParZXcY+h+b8JpfeVbx4oGDLZBEe3Gag==", + "requires": { + "ag-charts-types": "11.0.0" + } + }, + "ag-grid-react": { + "version": "33.0.2", + "resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-33.0.2.tgz", + "integrity": "sha512-qgwnNoBZ18H18kCHnwHoGvH1U9nl2CHbSmlfCtg3TC1NUr17IjoiXMSqA0gVdsAo3LyLPN4fYatwQsVIpiWmvw==", + "requires": { + "ag-grid-community": "33.0.2", + "prop-types": "^15.8.1" + } + }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", diff --git a/package.json b/package.json index b2d3db9..3458ee4 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,6 @@ "private": true, "type": "module", "dependencies": { - "@ag-grid-community/client-side-row-model": "^28.1.0", - "@ag-grid-community/core": "^28.1.0", - "@ag-grid-community/react": "^28.1.0", "@chakra-ui/react": "^3.2.3", "@emotion/react": "^11.13.5", "@fontsource-variable/inter": "^5.1.0", @@ -16,6 +13,7 @@ "@fullcalendar/react": "^6.1.15", "@fullcalendar/timegrid": "^6.1.15", "@react-oauth/google": "^0.2.6", + "ag-grid-react": "^33.0.2", "html-entities": "^2.3.3", "ical-generator": "^6.0.0", "msgpack-lite": "^0.1.26", diff --git a/src/components/ClassTable.scss b/src/components/ClassTable.scss deleted file mode 100644 index c072d5a..0000000 --- a/src/components/ClassTable.scss +++ /dev/null @@ -1,41 +0,0 @@ -.ag-theme-hydrant { - height: 320px; - width: 100%; - --ag-icon-font-family: agGridAlpine; -} - -.ag-cell-wrapper { - cursor: pointer; - padding: 8px 5px; -} - -.ag-row { - border-bottom-width: 1px; -} - -.ag-row:hover { - background-color: var(--chakra-colors-color-palette-subtle); - - .ag-cell:first-child { - text-decoration: underline; - } -} - -.ag-cell:first-child { - font-weight: 500; -} - -.ag-header-cell-text { - font-size: 0.8rem; - letter-spacing: 0.05rem; - font-weight: bold; - text-transform: uppercase; -} - -.ag-header-cell-label { - padding: 8px 5px; -} -.ag-header-viewport { - background-color: var(--chakra-colors-bg-subtle); - border-bottom-width: 1px; -} diff --git a/src/components/ClassTable.tsx b/src/components/ClassTable.tsx index 831bbc1..4ddae4c 100644 --- a/src/components/ClassTable.tsx +++ b/src/components/ClassTable.tsx @@ -1,6 +1,9 @@ -import { AgGridReact } from "@ag-grid-community/react"; -import AgGrid, { ModuleRegistry } from "@ag-grid-community/core"; -import { ClientSideRowModelModule } from "@ag-grid-community/client-side-row-model"; +import { AgGridReact } from "ag-grid-react"; +import AgGrid, { + ModuleRegistry, + AllCommunityModule, + themeQuartz, +} from "ag-grid-community"; import { Box, Group, Flex, Image, Input } from "@chakra-ui/react"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -14,11 +17,19 @@ import { Class, DARK_IMAGES, Flags, getFlagImg } from "../lib/class"; import { classNumberMatch, classSort, simplifyString } from "../lib/utils"; import { State } from "../lib/state"; -import "@ag-grid-community/core/dist/styles/ag-grid.css"; -import "@ag-grid-community/core/dist/styles/agGridAlpineFont.css"; -import "./ClassTable.scss"; +const hydrantTheme = themeQuartz.withParams({ + accentColor: "var(--chakra-colors-fg)", + backgroundColor: "var(--chakra-colors-bg)", + borderColor: "var(--chakra-colors-border)", + browserColorScheme: "inherit", + fontFamily: "inherit", + foregroundColor: "var(--chakra-colors-fg)", + headerBackgroundColor: "var(--chakra-colors-bg-subtle)", + rowHoverColor: "var(--chakra-colors-color-palette-subtle)", + wrapperBorderRadius: "var(--chakra-radii-l2)", +}); -ModuleRegistry.registerModules([ClientSideRowModelModule]); +ModuleRegistry.registerModules([AllCommunityModule]); /** A single row in the class table. */ type ClassTableRow = { @@ -287,7 +298,7 @@ export function ClassTable(props: { const sortingOrder: Array<"asc" | "desc"> = ["asc", "desc"]; const sortProps = { sortable: true, unSortIcon: true, sortingOrder }; const numberSortProps = { - maxWidth: 90, + maxWidth: 100, // sort by number, N/A is infinity, tiebreak with class number comparator: ( valueA: string, @@ -306,15 +317,16 @@ export function ClassTable(props: { return [ { field: "number", + resizable: false, headerName: "Class", comparator: classSort, initialSort, maxWidth: 100, ...sortProps, }, - { field: "rating", ...numberSortProps }, - { field: "hours", ...numberSortProps }, - { field: "name", flex: 1 }, + { field: "rating", resizable: false, ...numberSortProps }, + { field: "hours", resizable: false, ...numberSortProps }, + { field: "name", resizable: false, sortable: false, flex: 1 }, ]; }, []); @@ -363,8 +375,9 @@ export function ClassTable(props: { state={state} updateFilter={() => gridRef.current?.api?.onFilterChanged()} /> - + state.setViewedActivity(e.data.class)} onRowDoubleClicked={(e) => state.toggleActivity(e.data.class)} - onGridReady={() => gridRef.current?.columnApi?.autoSizeAllColumns()} + onGridReady={() => gridRef.current?.api?.autoSizeAllColumns()} // these have to be set here, not in css: headerHeight={40} rowHeight={40} From 8c14e3a5769b8b8c40254e380d974761969d99f0 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Sun, 15 Dec 2024 21:47:55 -0500 Subject: [PATCH 2/5] type fixes --- src/components/ClassTable.tsx | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/components/ClassTable.tsx b/src/components/ClassTable.tsx index 4ddae4c..9dea6db 100644 --- a/src/components/ClassTable.tsx +++ b/src/components/ClassTable.tsx @@ -1,8 +1,10 @@ import { AgGridReact } from "ag-grid-react"; -import AgGrid, { +import { ModuleRegistry, AllCommunityModule, themeQuartz, + type IRowNode, + type ColDef, } from "ag-grid-community"; import { Box, Group, Flex, Image, Input } from "@chakra-ui/react"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -41,7 +43,7 @@ type ClassTableRow = { inCharge: string; }; -type ClassFilter = (cls: Class) => boolean; +type ClassFilter = (cls?: Class) => boolean; /** Type of filter on class list; null if no filter. */ type SetClassFilter = React.Dispatch>; @@ -97,7 +99,7 @@ function ClassInput(props: { row.inCharge.includes(simplifyInput), ); const index = new Set(searchResults.current.map((cls) => cls.numbers[0])); - setInputFilter(() => (cls: Class) => index.has(cls.number)); + setInputFilter(() => (cls?: Class) => index.has(cls?.number ?? "")); } else { setInputFilter(null); } @@ -212,7 +214,8 @@ function ClassFlags(props: { // careful! we have to wrap it with a () => because otherwise React will // think it's an updater function instead of the actual function. - setFlagsFilter(() => (cls: Class) => { + setFlagsFilter(() => (cls?: Class) => { + if (!cls) return false; let result = true; newFlags.forEach((value, flag) => { if (value && flag === "fits" && !state.fitsSchedule(cls)) { @@ -293,7 +296,7 @@ export function ClassTable(props: { const gridRef = useRef(null); // Setup table columns - const columnDefs = useMemo(() => { + const columnDefs: ColDef[] = useMemo(() => { const initialSort = "asc" as const; const sortingOrder: Array<"asc" | "desc"> = ["asc", "desc"]; const sortProps = { sortable: true, unSortIcon: true, sortingOrder }; @@ -303,9 +306,10 @@ export function ClassTable(props: { comparator: ( valueA: string, valueB: string, - nodeA: AgGrid.RowNode, - nodeB: AgGrid.RowNode, + nodeA: IRowNode, + nodeB: IRowNode, ) => { + if (!nodeA.data || !nodeB.data) return 0; const numberA = valueA === "N/A" ? Infinity : Number(valueA); const numberB = valueB === "N/A" ? Infinity : Number(valueB); return numberA !== numberB @@ -351,9 +355,9 @@ export function ClassTable(props: { const [flagsFilter, setFlagsFilter] = useState(null); const doesExternalFilterPass = useMemo(() => { - return (node: AgGrid.RowNode) => { - if (inputFilter && !inputFilter(node.data.class)) return false; - if (flagsFilter && !flagsFilter(node.data.class)) return false; + return (node: IRowNode) => { + if (inputFilter && !inputFilter(node.data?.class)) return false; + if (flagsFilter && !flagsFilter(node.data?.class)) return false; return true; }; }, [inputFilter, flagsFilter]); @@ -376,7 +380,7 @@ export function ClassTable(props: { updateFilter={() => gridRef.current?.api?.onFilterChanged()} /> - theme={hydrantTheme} ref={gridRef} columnDefs={columnDefs} @@ -385,8 +389,8 @@ export function ClassTable(props: { enableCellTextSelection={true} isExternalFilterPresent={() => true} doesExternalFilterPass={doesExternalFilterPass} - onRowClicked={(e) => state.setViewedActivity(e.data.class)} - onRowDoubleClicked={(e) => state.toggleActivity(e.data.class)} + onRowClicked={(e) => state.setViewedActivity(e.data?.class)} + onRowDoubleClicked={(e) => state.toggleActivity(e.data?.class)} onGridReady={() => gridRef.current?.api?.autoSizeAllColumns()} // these have to be set here, not in css: headerHeight={40} From 4f2813b951a00c6810ab622e4756067195438499 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:02:08 -0500 Subject: [PATCH 3/5] add subtle background based on rating --- src/components/ClassTable.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/components/ClassTable.tsx b/src/components/ClassTable.tsx index 9dea6db..5506341 100644 --- a/src/components/ClassTable.tsx +++ b/src/components/ClassTable.tsx @@ -5,6 +5,7 @@ import { themeQuartz, type IRowNode, type ColDef, + CellClassParams, } from "ag-grid-community"; import { Box, Group, Flex, Image, Input } from "@chakra-ui/react"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -33,6 +34,14 @@ const hydrantTheme = themeQuartz.withParams({ ModuleRegistry.registerModules([AllCommunityModule]); +const getRatingColor = (rating?: number | string) => { + if (rating === undefined || rating === "N/A") return undefined; + const ratingNumber = Number(rating); + if (ratingNumber >= 6) return "success"; + if (ratingNumber >= 5) return "warning"; + return "error"; +}; + /** A single row in the class table. */ type ClassTableRow = { number: string; @@ -328,7 +337,18 @@ export function ClassTable(props: { maxWidth: 100, ...sortProps, }, - { field: "rating", resizable: false, ...numberSortProps }, + { + field: "rating", + resizable: false, + cellStyle: (params: CellClassParams) => { + const rating = getRatingColor(params.value); + if (!rating) return { backgroundColor: "" }; + return { + backgroundColor: `var(--chakra-colors-bg-${rating})`, + }; + }, + ...numberSortProps, + }, { field: "hours", resizable: false, ...numberSortProps }, { field: "name", resizable: false, sortable: false, flex: 1 }, ]; From 0cc73fd0eb573a38773e7137a53d794b6fd306a6 Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:19:24 -0500 Subject: [PATCH 4/5] add class title on hover --- src/components/Calendar.tsx | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 78ce320..9908494 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -7,8 +7,10 @@ import interactionPlugin from "@fullcalendar/interaction"; import { Activity, NonClass, Timeslot } from "../lib/activity"; import { Slot } from "../lib/dates"; import { State } from "../lib/state"; +import { Class } from "../lib/class"; import "./Calendar.scss"; +import { Tooltip } from "./ui/tooltip"; /** * Calendar showing all the activities, including the buttons on top that @@ -30,15 +32,33 @@ export function Calendar(props: { cursor="pointer" height="100%" > - - {event.title} - + {event.extendedProps.activity instanceof Class ? ( + + + {event.title} + + + ) : ( + + {event.title} + + )} {event.extendedProps.room} ); From 2cd236072db03234db37284094084fa0ef0be76b Mon Sep 17 00:00:00 2001 From: Diego Temkin <65834932+dtemkin1@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:26:26 -0500 Subject: [PATCH 5/5] undo duplicate code --- src/components/Calendar.tsx | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 9908494..f154df8 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -24,6 +24,18 @@ export function Calendar(props: { const { selectedActivities, viewedActivity, state } = props; const renderEvent = ({ event }: EventContentArg) => { + const TitleText = () => ( + + {event.title} + + ); + return ( - - {event.title} - - + children={TitleText()} + /> ) : ( - - {event.title} - + )} {event.extendedProps.room}