Package updates and DataTable migration
Some checks failed
Deploy to production / build (push) Failing after 1m0s

This commit is contained in:
2026-09-28 14:05:07 -05:00
parent 6c3c8266b9
commit cc33b6fe08
9 changed files with 1222 additions and 841 deletions

View File

@@ -118,4 +118,11 @@ sudo pm2 start server.cjs
- Mobile improvements - Mobile improvements
- New Carosel on weather description - New Carosel on weather description
- Updates to weather modal - Updates to weather modal
- Reivew Mantine v9 for changes - Add mantine-led for monitoring status (https://gfazioli.github.io/mantine-led)
- Update Theme playlists
- Include mantine-reflection (https://gfazioli.github.io/mantine-reflection#example-complex-components)
- Include mantine-parallax (https://gfazioli.github.io/mantine-parallax#example-presentation)
- Include mantine-depth-select (https://gfazioli.github.io/mantine-depth-select/)
- Explore https://gfazioli.github.io/mantine-scene/
- Explore https://gfazioli.github.io/mantine-window/
- Upgrade spinners/loaders (https://gfazioli.github.io/mantine-spinner#example-random)

1520
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,39 +10,40 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@gfazioli/mantine-border-animate": "^2.0.9", "@gfazioli/mantine-border-animate": "^3.0.2",
"@gfazioli/mantine-text-animate": "^4.0.8", "@gfazioli/mantine-text-animate": "^4.0.10",
"@hello-pangea/dnd": "^18.0.1", "@hello-pangea/dnd": "^18.0.1",
"@mantine/core": "^9.5.0", "@mantine/core": "^9.6.2",
"@mantine/dates": "^9.5.0", "@mantine/dates": "^9.6.2",
"@mantine/form": "^9.5.0", "@mantine/form": "^9.6.2",
"@mantine/hooks": "^9.5.0", "@mantine/hooks": "^9.6.2",
"@mantine/notifications": "^9.5.0", "@mantine/notifications": "^9.6.2",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.103.2",
"@tanstack/react-query-devtools": "^5.101.4", "@tanstack/react-query-devtools": "^5.103.2",
"@tanstack/react-query-persist-client": "^5.101.4", "@tanstack/react-query-persist-client": "^5.103.2",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^9.2.4",
"@tanstack/react-virtual": "^3.14.13",
"idb-keyval": "^6.3.0", "idb-keyval": "^6.3.0",
"jotai": "^2.20.2", "jotai": "^3.0.0",
"openmeteo": "^1.2.3", "openmeteo": "^1.2.3",
"react": "^19.2.8", "react": "^19.3.0",
"react-dom": "^19.2.8", "react-dom": "^19.3.0",
"react-fontpicker-ts": "^1.3.0", "react-fontpicker-ts": "^1.3.0",
"react-icons": "^5.7.0", "react-icons": "^5.7.0",
"react-router-dom": "^7.18.2", "react-router-dom": "^7.18.4",
"recharts": "^3.10.1" "recharts": "^3.10.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^26.1.2", "@types/node": "^26.6.2",
"@types/react": "^19.2.18", "@types/react": "^19.3.0",
"@types/react-dom": "^19.2.4", "@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.0.5", "@vitejs/plugin-react": "^6.1.1",
"@vitest/coverage-v8": "^4.1.10", "@vitest/coverage-v8": "^5.0.2",
"@vitest/ui": "^4.1.10", "@vitest/ui": "^5.0.2",
"typescript": "^7.0.2", "typescript": "^7.0.2",
"vite": "^8.2.0", "vite": "^8.3.1",
"vite-plugin-dts": "~5.0.3", "vite-plugin-dts": "~5.1.1",
"vite-tsconfig-paths": "^6.1.1", "vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.1.10" "vitest": "^5.0.2"
} }
} }

View File

@@ -1,9 +1,20 @@
import { ActionIcon, Box, Group, Text } from '@mantine/core'; import { ActionIcon, Box, Group, Text } from '@mantine/core';
import { createColumnHelper } from '@tanstack/react-table'; import {
columnFilteringFeature,
createColumnHelper,
filterFn_includesString,
rowSortingFeature,
tableFeatures,
} from '@tanstack/react-table';
import { FaFilter, FaTrash } from 'react-icons/fa'; import { FaFilter, FaTrash } from 'react-icons/fa';
import type { SearchHistoryItem } from 'types'; import type { SearchHistoryItem } from 'types';
const columnHelper = createColumnHelper<SearchHistoryItem>(); const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
filterFns: { includesString: filterFn_includesString },
});
const columnHelper = createColumnHelper<typeof features, SearchHistoryItem>();
export const SearchHistoryColumns = ( export const SearchHistoryColumns = (
setSelectedItem: React.Dispatch<React.SetStateAction<SearchHistoryItem | undefined>>, setSelectedItem: React.Dispatch<React.SetStateAction<SearchHistoryItem | undefined>>,
deleteItemOpen: () => void, deleteItemOpen: () => void,
@@ -26,7 +37,7 @@ export const SearchHistoryColumns = (
</Group> </Group>
), ),
header: () => ( header: () => (
<Group gap={0} justify='center'> <Group gap={0} justify='center' align='center'>
<Box <Box
w={'1.75rem'} w={'1.75rem'}
h={'1.75rem'} h={'1.75rem'}
@@ -48,9 +59,8 @@ export const SearchHistoryColumns = (
), ),
meta: { meta: {
filterVariant: 'counter', filterVariant: 'counter',
width: '32px',
}, },
size: 32,
maxSize: 32,
enableSorting: false, enableSorting: false,
}), }),
columnHelper.accessor('prefix', { columnHelper.accessor('prefix', {
@@ -63,27 +73,28 @@ export const SearchHistoryColumns = (
header: () => <Text fw={600}>Prefix</Text>, header: () => <Text fw={600}>Prefix</Text>,
meta: { meta: {
filterVariant: 'mantineSelect', filterVariant: 'mantineSelect',
width: '72px',
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
if (value === null) return true; if (value === null) return true;
return row.getValue(id) === value; return row.getValue(id) === value;
}, },
size: 32,
maxSize: 32,
}), }),
columnHelper.accessor('provider', { columnHelper.accessor('provider', {
id: 'provider', id: 'provider',
cell: ({ cell }: any) => ( cell: ({ cell }: any) => (
<Text mx={8} style={{ textAlign: 'center' }}> <Text
mx={8}
style={{ textAlign: 'center', overflow: 'ellipsis', whiteSpace: 'nowrap' }}
>
{cell.getValue()} {cell.getValue()}
</Text> </Text>
), ),
header: () => <Text fw={600}>Provider</Text>, header: () => <Text fw={600}>Provider</Text>,
meta: { meta: {
filterVariant: 'mantineSelect', filterVariant: 'mantineSelect',
width: '128px',
}, },
size: 128,
maxSize: 128,
}), }),
columnHelper.accessor('searchTerm', { columnHelper.accessor('searchTerm', {
id: 'searchTerm', id: 'searchTerm',
@@ -93,9 +104,18 @@ export const SearchHistoryColumns = (
overflow: 'hidden', overflow: 'hidden',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
display: 'block',
}} }}
> >
<Text w={'100%'} mx={8} style={{ justifySelf: 'center' }}> <Text
mx={8}
style={{
justifySelf: 'center',
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
}}
>
{cell.getValue()} {cell.getValue()}
</Text> </Text>
</Group> </Group>
@@ -103,8 +123,9 @@ export const SearchHistoryColumns = (
header: () => <Text fw={600}>Search String</Text>, header: () => <Text fw={600}>Search String</Text>,
meta: { meta: {
filterVariant: 'text', filterVariant: 'text',
width: 'calc(100% - 375px)',
}, },
maxSize: 1920, filterFn: 'includesString',
}), }),
columnHelper.accessor('dateTimeStamp', { columnHelper.accessor('dateTimeStamp', {
id: 'dateTimeStamp', id: 'dateTimeStamp',
@@ -126,6 +147,7 @@ export const SearchHistoryColumns = (
header: () => <Text fw={600}>Time Stamp</Text>, header: () => <Text fw={600}>Time Stamp</Text>,
meta: { meta: {
filterVariant: 'range', filterVariant: 'range',
width: '148px',
}, },
filterFn: (row, id, value) => { filterFn: (row, id, value) => {
const startDate = new Date(value[0]); const startDate = new Date(value[0]);
@@ -136,8 +158,5 @@ export const SearchHistoryColumns = (
} }
return true; return true;
}, },
size: 132,
maxSize: 132,
minSize: 132,
}), }),
]; ];

View File

@@ -1,40 +1,41 @@
import { Button, Group, ScrollArea, Text } from '@mantine/core'; import { Button, Group, Table, Text } from '@mantine/core';
import { import { flexRender, type SortingState, useTable } from '@tanstack/react-table';
type ColumnFiltersState, import { useVirtualizer } from '@tanstack/react-virtual';
flexRender, import { type Dispatch, type SetStateAction, useState } from 'react';
getCoreRowModel,
getExpandedRowModel,
getFacetedMinMaxValues,
getFilteredRowModel,
getSortedRowModel,
type SortingState,
useReactTable,
} from '@tanstack/react-table';
import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
import { scrollAreaClasses } from 'styles';
import { Filter } from './components'; import { Filter } from './components';
export function DataTable({ export function DataTable({
data, data,
columns, columns,
features,
sorting, sorting,
setSorting, setSorting,
filtersEnabled, filtersEnabled,
globalFilter, globalFilter,
setGlobalFilter,
isRefreshing, isRefreshing,
}: { }: {
data: any[]; data: any;
columns: any[]; columns: any[];
features: any;
sorting: SortingState; sorting: SortingState;
setSorting: Dispatch<SetStateAction<SortingState>>; setSorting: Dispatch<SetStateAction<SortingState>>;
filtersEnabled: boolean; filtersEnabled: boolean;
globalFilter: string | number; globalFilter: string | number;
setGlobalFilter: Dispatch<SetStateAction<string | number>>;
isRefreshing?: boolean; isRefreshing?: boolean;
}) { }) {
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); const [scrollParent, setScrollParent] = useState<HTMLDivElement | null>(null);
const table = useReactTable({
const virtualizer = useVirtualizer({
count: data.length,
getScrollElement: () => scrollParent,
estimateSize: () => 30,
overscan: 5,
});
const virtualItems = virtualizer.getVirtualItems();
const table = useTable({
features,
data: data, data: data,
columns, columns,
initialState: { initialState: {
@@ -42,48 +43,24 @@ export function DataTable({
}, },
state: { state: {
sorting, sorting,
columnFilters,
globalFilter: globalFilter, globalFilter: globalFilter,
}, },
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getFacetedRowModel: getFilteredRowModel(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
onColumnFiltersChange: setColumnFilters,
enableSorting: true,
enableFilters: true,
enableGlobalFilter: true,
globalFilterFn: 'includesString',
onGlobalFilterChange: setGlobalFilter,
}); });
const { rows } = table.getRowModel();
useEffect(() => {
if (!filtersEnabled) {
setColumnFilters([]);
}
}, [filtersEnabled]);
return ( return (
<ScrollArea <Table.ScrollContainer
className={scrollAreaClasses.default} minWidth={'100%'}
classNames={scrollAreaClasses} scrollAreaProps={{ viewportRef: setScrollParent }}
styles={{
scrollbar: {
paddingTop: '2.5rem',
paddingBottom: '2rem',
},
}}
> >
<table <Table
style={{ style={{
width: '100%', width: '100%',
borderSpacing: '2px 0px', borderSpacing: '2px 0px',
backgroundColor: 'var(--mantine-color-accent-0)', backgroundColor: 'var(--mantine-color-accent-0)',
}} }}
> >
<thead <Table.Thead
style={{ style={{
position: 'sticky', position: 'sticky',
top: 0, top: 0,
@@ -92,133 +69,159 @@ export function DataTable({
}} }}
> >
{table.getHeaderGroups().map((headerGroup) => ( {table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}> <Table.Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => ( {headerGroup.headers.map((header: any) => {
<th return (
key={header.id} <Table.Th
style={{ key={header.id}
width: header.getSize(), style={{
padding: 0, padding: 0,
}} }}
> w={`${header.column.columnDef.meta?.width}`}
{header.isPlaceholder ? null : ( >
<> {header.isPlaceholder ? null : (
<Button <>
{...{ <Button
style: { {...{
cursor: 'pointer', style: {
borderRadius: '0px', cursor: 'pointer',
backgroundColor: 'transparent', borderRadius: '0px',
}, backgroundColor: 'transparent',
px: 0, },
onClick: () => { px: 0,
if (header.column.getCanSort()) { w: '100%',
if (sorting[0].id === header.id) { onClick: () => {
setSorting([ if (header.column.getCanSort()) {
{ if (sorting[0].id === header.id) {
id: header.id, setSorting([
desc: !sorting[0].desc, {
}, id: header.id,
]); desc: !sorting[0].desc,
} else { },
setSorting([ ]);
{ } else {
id: header.id, setSorting([
desc: false, {
}, id: header.id,
]); desc: false,
},
]);
}
} }
} },
}, }}
}} >
> <Group gap={4} align='center' justify='center'>
<Group align='center' justify='center'> {flexRender(
{flexRender( header.column.columnDef.header,
header.column.columnDef.header, header.getContext(),
header.getContext(), )}
)} {{
{{ asc: (
asc: ( <Group gap={0}>
<Group gap={0}> <Text c={'var(--mantine-color-accent-0)'}>🠗</Text>
<Text c={'var(--mantine-color-accent-0)'}>🠗</Text> <Text>🠕</Text>
<Text>🠕</Text> </Group>
</Group> ),
), desc: (
desc: ( <Group gap={0}>
<Group gap={0}> <Text>🠗</Text>
<Text>🠗</Text> <Text c={'var(--mantine-color-accent-0)'}>🠕</Text>
<Text c={'var(--mantine-color-accent-0)'}>🠕</Text> </Group>
</Group> ),
), }[header.column.getIsSorted() as string] ?? null}
}[header.column.getIsSorted() as string] ?? null} </Group>
</Group> </Button>
</Button> {filtersEnabled ? (
{filtersEnabled ? ( <Filter
<Filter column={header.column}
column={header.column} options={[
options={[...new Set(data.map((item: any) => item[header.id]))]} ...new Set(data.map((item: any) => item[header.id])),
resultCount={table.getFilteredRowModel().rows.length} ]}
/> resultCount={table.getFilteredRowModel().rows.length}
) : null} />
</> ) : null}
)} </>
</th> )}
))} </Table.Th>
</tr> );
})}
</Table.Tr>
))} ))}
</thead> </Table.Thead>
<tbody> <Table.Tbody>
{table.getRowModel().rows.map((row, index) => ( {virtualItems.length > 0 && (
<tr <tr aria-hidden>
key={row.id} <td
style={{ aria-hidden
color: 'var(--mantine-color-primary-0)', colSpan={5}
backgroundColor: style={{ height: virtualItems[0].start, padding: 0, border: 'none' }}
index % 2 === 0 />
? 'color-mix(in srgb, var(--mantine-color-background-0), var(--mantine-color-primary-0) 10%)'
: 'color-mix(in srgb, var(--mantine-color-background-0), var(--mantine-color-primary-0) 30%)',
}}
onClick={() => {
row.toggleExpanded();
}}
>
{row.getVisibleCells().map((cell) => (
<td
key={cell.id}
style={{
width: cell.column.getSize(),
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr> </tr>
))} )}
</tbody> {virtualItems.map((virtualItem) => {
<tfoot const row = rows[virtualItem.index];
return (
<Table.Tr
key={row?.id}
style={{
color: 'var(--mantine-color-primary-0)',
backgroundColor:
virtualItem.index % 2 === 0
? 'color-mix(in srgb, var(--mantine-color-background-0), var(--mantine-color-primary-0) 10%)'
: 'color-mix(in srgb, var(--mantine-color-background-0), var(--mantine-color-primary-0) 30%)',
}}
onClick={() => {
row?.toggleExpanded();
}}
>
{row?.getAllCells().map((cell: any) => {
return (
<Table.Td
key={cell.id}
style={{
maxWidth: `${cell.column.columnDef.meta?.width}`,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Td>
);
})}
</Table.Tr>
);
})}
{virtualItems.length > 0 && (
<tr aria-hidden>
<td
aria-hidden
colSpan={5}
style={{
height:
virtualizer.getTotalSize() -
virtualItems[virtualItems.length - 1].end,
padding: 0,
border: 'none',
}}
/>
</tr>
)}
</Table.Tbody>
<Table.Tfoot
style={{ style={{
transition: 'all 1s ease', transition: 'all 1s ease',
}} }}
> >
{table.getFooterGroups().map((footerGroup) => ( <Table.Tr
<tr key={footerGroup.id}>
{footerGroup.headers.map((header) => (
<th key={header.id}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.footer, header.getContext())}
</th>
))}
</tr>
))}
<tr
style={{ style={{
backgroundColor: 'var(--mantine-color-primary-0)', backgroundColor: 'var(--mantine-color-primary-0)',
position: 'sticky', position: 'sticky',
bottom: 0, bottom: 0,
}} }}
> >
<td colSpan={5}> <Table.Td colSpan={5}>
<Text <Text
style={{ style={{
color: 'var(--mantine-color-background-0)', color: 'var(--mantine-color-background-0)',
@@ -228,20 +231,24 @@ export function DataTable({
> >
Total Entries: {table.getCoreRowModel().rows.length} Total Entries: {table.getCoreRowModel().rows.length}
</Text> </Text>
</td> </Table.Td>
</tr> </Table.Tr>
<tr <Table.Tr
style={{ style={{
width: '100%', width: '100%',
backgroundColor: 'var(--mantine-color-accent-0)', backgroundColor: isRefreshing
? 'var(--mantine-color-accent-0)'
: 'transparent',
position: 'absolute', position: 'absolute',
bottom: isRefreshing ? 0 : -30, bottom: isRefreshing ? 0 : -40,
paddingBottom: 6,
paddingTop: 6,
transition: 'all 1.5s ease-in-out', transition: 'all 1.5s ease-in-out',
placeContent: 'center', placeContent: 'center',
display: 'flex', display: 'flex',
}} }}
> >
<td colSpan={5} width={'100%'} style={{}}> <Table.Td colSpan={5} width={'100%'} style={{}}>
<Text <Text
style={{ style={{
color: 'var(--mantine-color-background-0)', color: 'var(--mantine-color-background-0)',
@@ -251,10 +258,10 @@ export function DataTable({
> >
Updating Updating
</Text> </Text>
</td> </Table.Td>
</tr> </Table.Tr>
</tfoot> </Table.Tfoot>
</table> </Table>
</ScrollArea> </Table.ScrollContainer>
); );
} }

View File

@@ -0,0 +1,55 @@
import {
columnFacetingFeature,
columnFilteringFeature,
createExpandedRowModel,
createFacetedMinMaxValues,
createFacetedRowModel,
createFilteredRowModel,
createSortedRowModel,
filterFn_equalsString,
filterFn_includesString,
filterFn_inDateRange,
globalFilteringFeature,
metaHelper,
rowExpandingFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
tableFeatures,
} from '@tanstack/react-table';
interface ColumnMeta {
width?: number | string;
filterVariant?:
| 'text'
| 'range'
| 'select'
| 'mantineSelect'
| 'multiSelect'
| 'slider'
| 'checkbox'
| 'counter'
| 'none';
}
const features = tableFeatures({
filterFns: {
equalsString: filterFn_equalsString,
includesString: filterFn_includesString,
inDateRange: filterFn_inDateRange,
},
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
filteredRowModel: createFilteredRowModel(),
expandedRowModel: createExpandedRowModel(),
sortedRowModel: createSortedRowModel(),
facetedRowModel: createFacetedRowModel(),
facetedMinMaxValues: createFacetedMinMaxValues(),
rowSortingFeature,
rowExpandingFeature,
columnFacetingFeature,
columnFilteringFeature,
globalFilteringFeature,
columnMeta: metaHelper<ColumnMeta>(),
});
export default features;

View File

@@ -11,32 +11,15 @@ import {
Text, Text,
} from '@mantine/core'; } from '@mantine/core';
import { DatePickerInput } from '@mantine/dates'; import { DatePickerInput } from '@mantine/dates';
import type { Column, RowData } from '@tanstack/react-table';
import { DebouncedInput } from 'components'; import { DebouncedInput } from 'components';
import { datePickerInputClasses, selectClasses } from 'styles'; import { datePickerInputClasses, selectClasses } from 'styles';
declare module '@tanstack/react-table' {
//allows us to define custom properties for our columns
interface ColumnMeta<_TData extends RowData, _TValue> {
filterVariant?:
| 'text'
| 'range'
| 'select'
| 'mantineSelect'
| 'multiSelect'
| 'slider'
| 'checkbox'
| 'counter'
| 'none';
}
}
export function Filter({ export function Filter({
column, column,
options, options,
resultCount, resultCount,
}: { }: {
column: Column<unknown>; column: any;
options: any; options: any;
resultCount?: number; resultCount?: number;
}) { }) {

View File

@@ -61,7 +61,9 @@ export default function CurrentWeather({
<Text>Humidity</Text> <Text>Humidity</Text>
</Group> </Group>
} }
rightSide={<Text>{current.current.relative_humidity_2m?.toFixed(0)}%</Text>} rightSide={
<Text>{current.current.relative_humidity_2m?.toFixed(0)}%</Text>
}
highlight='right' highlight='right'
/> />
<WeatherFeaturePill <WeatherFeaturePill

View File

@@ -10,6 +10,7 @@ import { useCommonHooks, useQueryAPI } from 'hooks';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { textClasses } from 'styles'; import { textClasses } from 'styles';
import type { SearchHistoryItem } from 'types'; import type { SearchHistoryItem } from 'types';
import features from '../../../../components/DataTable/components/Features';
export function SearchHistory() { export function SearchHistory() {
const searchEndpoint = 'api/searchHistory'; const searchEndpoint = 'api/searchHistory';
@@ -101,11 +102,11 @@ export function SearchHistory() {
filtersEnabled, filtersEnabled,
setFiltersEnabled, setFiltersEnabled,
)} )}
features={features}
sorting={sorting} sorting={sorting}
setSorting={setSorting} setSorting={setSorting}
filtersEnabled={filtersEnabled} filtersEnabled={filtersEnabled}
globalFilter={globalFilter} globalFilter={globalFilter}
setGlobalFilter={setGlobalFilter}
isRefreshing={historyDataIsFetching} isRefreshing={historyDataIsFetching}
/> />
</Stack> </Stack>