Search History tab
This commit is contained in:
1132
package-lock.json
generated
1132
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
34
package.json
34
package.json
@@ -10,34 +10,36 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gfazioli/mantine-text-animate": "^2.3.1",
|
||||
"@gfazioli/mantine-text-animate": "^2.3.3",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@mantine/core": "^8.3.5",
|
||||
"@mantine/form": "^8.3.5",
|
||||
"@mantine/hooks": "^8.3.5",
|
||||
"@mantine/notifications": "^8.3.5",
|
||||
"@tanstack/react-query": "^5.90.3",
|
||||
"@mantine/core": "^8.3.6",
|
||||
"@mantine/dates": "^8.3.6",
|
||||
"@mantine/form": "^8.3.6",
|
||||
"@mantine/hooks": "^8.3.6",
|
||||
"@mantine/notifications": "^8.3.6",
|
||||
"@tanstack/react-query": "^5.90.7",
|
||||
"@tanstack/react-query-devtools": "^5.90.2",
|
||||
"@tanstack/react-query-persist-client": "^5.90.5",
|
||||
"@tanstack/react-query-persist-client": "^5.90.9",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"jotai": "^2.15.0",
|
||||
"jotai": "^2.15.1",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"recharts": "^3.2.1"
|
||||
"react-router-dom": "^7.9.5",
|
||||
"recharts": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/node": "^24.10.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"@vitejs/plugin-react": "^5.1.0",
|
||||
"@vitest/coverage-v8": "^4.0.7",
|
||||
"@vitest/ui": "^4.0.7",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.10",
|
||||
"vite": "^7.2.1",
|
||||
"vite-plugin-dts": "~4.5.4",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
} from 'react-router-dom';
|
||||
import { Loading, MainLayout } from './components';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
|
||||
import '@gfazioli/mantine-text-animate/styles.css';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { Homepage, Settings } from 'pages';
|
||||
|
||||
119
src/components/Columns/SearchHistoryColumns.tsx
Normal file
119
src/components/Columns/SearchHistoryColumns.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { ActionIcon, Box, Group, Text } from '@mantine/core';
|
||||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { FaFilter, FaTrash } from 'react-icons/fa';
|
||||
|
||||
const columnHelper = createColumnHelper<any>();
|
||||
export const SearchHistoryColumns = (
|
||||
setSelectedItem: React.Dispatch<React.SetStateAction<any>>,
|
||||
deleteItemOpen: () => void,
|
||||
filtersEnabled: boolean,
|
||||
setFiltersEnabled: React.Dispatch<React.SetStateAction<boolean>>,
|
||||
) => [
|
||||
columnHelper.accessor('id', {
|
||||
cell: ({ row }: any) => (
|
||||
<Group gap={0} justify='center'>
|
||||
<ActionIcon
|
||||
color={'red'}
|
||||
c={'black'}
|
||||
onClick={() => {
|
||||
setSelectedItem(row.original);
|
||||
deleteItemOpen();
|
||||
}}
|
||||
>
|
||||
<FaTrash />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
header: () => (
|
||||
<Group gap={0} justify='center'>
|
||||
<Box
|
||||
w={'1.75rem'}
|
||||
h={'1.75rem'}
|
||||
style={{
|
||||
borderRadius: 4,
|
||||
backgroundColor: 'var(--mantine-color-accent-0)',
|
||||
alignContent: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
setFiltersEnabled(!filtersEnabled);
|
||||
}}
|
||||
>
|
||||
<FaFilter />
|
||||
</Box>
|
||||
</Group>
|
||||
),
|
||||
size: 64,
|
||||
maxSize: 64,
|
||||
enableSorting: false,
|
||||
}),
|
||||
columnHelper.accessor('prefix', {
|
||||
id: 'prefix',
|
||||
cell: ({ cell }: any) => (
|
||||
<Group justify='center'>
|
||||
<Text>{cell.getValue()}</Text>
|
||||
</Group>
|
||||
),
|
||||
header: () => <Text fw={600}>Prefix</Text>,
|
||||
meta: {
|
||||
filterVariant: 'mantineSelect',
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
if (value === null) return true;
|
||||
return row.getValue(id) === value;
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('provider', {
|
||||
id: 'provider',
|
||||
cell: ({ cell }: any) => <Text mx={8}>{cell.getValue()}</Text>,
|
||||
header: () => <Text fw={600}>Provider</Text>,
|
||||
meta: {
|
||||
filterVariant: 'mantineSelect',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('searchTerm', {
|
||||
id: 'searchTerm',
|
||||
cell: ({ cell }: any) => (
|
||||
<Text w={'100%'} mx={8} style={{ justifySelf: 'center' }}>
|
||||
{cell.getValue()}
|
||||
</Text>
|
||||
),
|
||||
header: () => <Text fw={600}>Search String</Text>,
|
||||
meta: {
|
||||
filterVariant: 'text',
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('dateTimeStamp', {
|
||||
id: 'dateTimeStamp',
|
||||
cell: ({ cell }: any) => {
|
||||
const m = new Date(cell.getValue());
|
||||
const year = `${m.getUTCFullYear()}`.slice(-2);
|
||||
const month = `0${m.getUTCMonth() + 1}`.slice(-2);
|
||||
const day = `0${m.getUTCDate()}`.slice(-2);
|
||||
const hours = `0${m.getUTCHours()}`.slice(-2);
|
||||
const minutes = `0${m.getUTCMinutes()}`.slice(-2);
|
||||
const dateString = `${month}/${day}/${year} ${hours}:${minutes}`;
|
||||
|
||||
return (
|
||||
<Group justify='center'>
|
||||
<Text mx={8}>{dateString}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
header: () => <Text fw={600}>Time Stamp</Text>,
|
||||
meta: {
|
||||
filterVariant: 'range',
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
const startDate = new Date(value[0]);
|
||||
const endDate = new Date(value[1]);
|
||||
const cellDate = new Date(row.getValue(id));
|
||||
if (value[0] && value[1]) {
|
||||
return cellDate >= startDate && cellDate <= endDate;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
];
|
||||
165
src/components/DataTable/DataTable.tsx
Normal file
165
src/components/DataTable/DataTable.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { Button, Group } from '@mantine/core';
|
||||
import {
|
||||
type ColumnFiltersState,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getExpandedRowModel,
|
||||
getFacetedMinMaxValues,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
type SortingState,
|
||||
useReactTable,
|
||||
} from '@tanstack/react-table';
|
||||
import { type Dispatch, type SetStateAction, useState } from 'react';
|
||||
import { Filter } from './components';
|
||||
|
||||
export function DataTable({
|
||||
data,
|
||||
columns,
|
||||
sorting,
|
||||
setSorting,
|
||||
filtersEnabled,
|
||||
globalFilter,
|
||||
setGlobalFilter,
|
||||
}: {
|
||||
data: any[];
|
||||
columns: any[];
|
||||
subRow?: any;
|
||||
sorting: SortingState;
|
||||
setSorting: Dispatch<SetStateAction<SortingState>>;
|
||||
filtersEnabled: boolean;
|
||||
globalFilter: string;
|
||||
setGlobalFilter: Dispatch<SetStateAction<string>>;
|
||||
}) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const table = useReactTable({
|
||||
data: data,
|
||||
columns,
|
||||
initialState: {
|
||||
sorting,
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
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,
|
||||
});
|
||||
return (
|
||||
<table style={{ width: '100%', backgroundColor: 'var(--mantine-color-primary-0)' }}>
|
||||
<thead
|
||||
style={{
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 1,
|
||||
backgroundColor: 'var(--mantine-color-primary-0)',
|
||||
borderTopLeftRadius: '1rem',
|
||||
borderTopRightRadius: '1rem',
|
||||
}}
|
||||
>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder ? null : (
|
||||
<>
|
||||
<Button
|
||||
{...{
|
||||
style: {
|
||||
cursor: 'pointer',
|
||||
width: '100%',
|
||||
borderRadius: '0px',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
onClick: () => {
|
||||
if (header.column.getCanSort()) {
|
||||
if (sorting[0].id === header.id) {
|
||||
setSorting([
|
||||
{
|
||||
id: header.id,
|
||||
desc: !sorting[0].desc,
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
setSorting([
|
||||
{
|
||||
id: header.id,
|
||||
desc: false,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Group align='center' justify='center'>
|
||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||
{{
|
||||
asc: ' 🔼',
|
||||
desc: ' 🔽',
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</Group>
|
||||
</Button>
|
||||
{filtersEnabled ? (
|
||||
<Filter
|
||||
column={header.column}
|
||||
options={[...new Set(data.map((item: any) => item[header.id]))]}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{table.getRowModel().rows.map((row, index) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
style={{
|
||||
color: 'var(--mantine-color-primary-0)',
|
||||
backgroundColor:
|
||||
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}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
{table.getFooterGroups().map((footerGroup) => (
|
||||
<tr key={footerGroup.id}>
|
||||
{footerGroup.headers.map((header) => (
|
||||
<th key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.footer, header.getContext())}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tfoot>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
161
src/components/DataTable/components/Filter.tsx
Normal file
161
src/components/DataTable/components/Filter.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
type ComboboxItem,
|
||||
Group,
|
||||
MultiSelect,
|
||||
type OptionsFilter,
|
||||
RangeSlider,
|
||||
Select,
|
||||
Stack,
|
||||
} from '@mantine/core';
|
||||
import { DatePickerInput } from '@mantine/dates';
|
||||
import type { Column, RowData } from '@tanstack/react-table';
|
||||
import { DebouncedInput } from 'components';
|
||||
import { selectClasses } from 'styles';
|
||||
|
||||
declare module '@tanstack/react-table' {
|
||||
//allows us to define custom properties for our columns
|
||||
// biome-ignore lint/correctness/noUnusedVariables: ignore
|
||||
interface ColumnMeta<TData extends RowData, TValue> {
|
||||
filterVariant?:
|
||||
| 'text'
|
||||
| 'range'
|
||||
| 'select'
|
||||
| 'mantineSelect'
|
||||
| 'multiSelect'
|
||||
| 'slider'
|
||||
| 'checkbox'
|
||||
| 'none';
|
||||
}
|
||||
}
|
||||
|
||||
export function Filter({ column, options }: { column: Column<unknown>; options: any }) {
|
||||
const columnFilterValue = column.getFilterValue();
|
||||
const { filterVariant } = column.columnDef.meta ?? {};
|
||||
const optionsFilter: OptionsFilter = ({ options, search }) => {
|
||||
const splittedSearch = search.toLowerCase().trim().split(' ');
|
||||
return (options as ComboboxItem[])?.filter((option) => {
|
||||
const words = option.label?.toLowerCase().trim().split(' ');
|
||||
return splittedSearch.every((searchWord) =>
|
||||
words?.some((word) => word.includes(searchWord)),
|
||||
);
|
||||
});
|
||||
};
|
||||
return filterVariant === 'range' ? (
|
||||
<div>
|
||||
<DatePickerInput
|
||||
type='range'
|
||||
clearable
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 0,
|
||||
border: 'none',
|
||||
color: 'var(--mantine-color-primary-0)',
|
||||
},
|
||||
}}
|
||||
value={columnFilterValue as [Date, Date]}
|
||||
onChange={(value) => column.setFilterValue(value)}
|
||||
/>
|
||||
</div>
|
||||
) : filterVariant === 'select' ? (
|
||||
<select
|
||||
onChange={(e) => column.setFilterValue(e.target.value)}
|
||||
value={columnFilterValue?.toString()}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px',
|
||||
}}
|
||||
>
|
||||
{/* See faceted column filters example for dynamic select options */}
|
||||
<option value=''>All</option>
|
||||
{options
|
||||
.sort((a: any, b: any) => a > b)
|
||||
?.map((option: any) => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : filterVariant === 'mantineSelect' ? (
|
||||
<Select
|
||||
className={selectClasses.settingsItem}
|
||||
classNames={{
|
||||
input: selectClasses.menuSelectInput,
|
||||
label: selectClasses.menuSelectLabel,
|
||||
options: selectClasses.menuSelectOptions,
|
||||
option: selectClasses.menuSelectOption,
|
||||
dropdown: selectClasses.dropdown,
|
||||
}}
|
||||
data={options.map((option: ComboboxItem) => {
|
||||
return { value: option, label: option };
|
||||
})}
|
||||
onChange={(e: any) => column.setFilterValue(e)}
|
||||
/>
|
||||
) : filterVariant === 'multiSelect' ? (
|
||||
<MultiSelect
|
||||
data={options
|
||||
.map((option: ComboboxItem) => {
|
||||
return {
|
||||
value: option,
|
||||
label: option,
|
||||
};
|
||||
})
|
||||
.sort((a: ComboboxItem, b: ComboboxItem) => a.label.localeCompare(b.label))}
|
||||
value={columnFilterValue as string[]}
|
||||
onChange={(value) => column.setFilterValue(value)}
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
filter={optionsFilter}
|
||||
searchable
|
||||
clearable
|
||||
/>
|
||||
) : filterVariant === 'text' ? (
|
||||
<DebouncedInput
|
||||
onChange={(value) => column.setFilterValue(value)}
|
||||
placeholder={'Search...'}
|
||||
type='text'
|
||||
value={(columnFilterValue ?? '') as string}
|
||||
/>
|
||||
// See faceted column filters example for datalist search suggestions
|
||||
) : filterVariant === 'slider' ? (
|
||||
<Stack h={36} justify='center'>
|
||||
{column.id === 'elevation' ? (
|
||||
<RangeSlider
|
||||
value={columnFilterValue as [number, number]}
|
||||
onChange={(value) => column.setFilterValue(value)}
|
||||
min={-10}
|
||||
max={20000}
|
||||
defaultValue={[0, 20000]}
|
||||
step={10}
|
||||
/>
|
||||
) : (
|
||||
<RangeSlider
|
||||
value={columnFilterValue as [number, number]}
|
||||
onChange={(value) => column.setFilterValue(value)}
|
||||
min={-180}
|
||||
max={180}
|
||||
defaultValue={[-180, 180]}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
) : filterVariant === 'checkbox' ? (
|
||||
<Group h={36} align='center' justify='center'>
|
||||
<Checkbox onChange={(value) => column.setFilterValue(value.target.checked)} />
|
||||
</Group>
|
||||
// See faceted column filters example for datalist search suggestions
|
||||
) : (
|
||||
<Box
|
||||
style={{
|
||||
width: 'min-content',
|
||||
height: '36px',
|
||||
padding: '4px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
1
src/components/DataTable/components/index.ts
Normal file
1
src/components/DataTable/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { Filter } from './Filter';
|
||||
57
src/components/DebouncedInput.tsx
Normal file
57
src/components/DebouncedInput.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ActionIcon, TextInput } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FiX } from 'react-icons/fi';
|
||||
|
||||
export function DebouncedInput({
|
||||
value: initialValue,
|
||||
onChange,
|
||||
debounce = 500,
|
||||
...props
|
||||
}: {
|
||||
value: string | number;
|
||||
onChange: (value: string | number) => void;
|
||||
debounce?: number;
|
||||
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'>) {
|
||||
const [value, setValue] = useState(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
onChange(value);
|
||||
}, debounce);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [value, debounce, onChange]);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
value={value}
|
||||
size='sm'
|
||||
p={0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
styles={{
|
||||
input: {
|
||||
borderRadius: 0,
|
||||
border: 'none',
|
||||
color: 'var(--mantine-color-primary-0)',
|
||||
},
|
||||
}}
|
||||
rightSection={
|
||||
value && (
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setValue('');
|
||||
}}
|
||||
>
|
||||
<FiX />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export { AddItemModal } from './AddItemModal';
|
||||
export { AppItem } from './Applications/AppItem';
|
||||
export { DataTable } from './DataTable/DataTable';
|
||||
export { DebouncedInput } from './DebouncedInput';
|
||||
export { DeleteItemModal } from './DeleteItemModal';
|
||||
export { HelpModal } from './HelpModal/HelpModal';
|
||||
export { Loading } from './Loading';
|
||||
|
||||
@@ -16,6 +16,7 @@ import Applications from './components/Applications/Applications';
|
||||
import Bookmarks from './components/Bookmarks/Bookmarks';
|
||||
import Messages from './components/Messages/Messages';
|
||||
import Personalization from './components/Personalization/Personalization';
|
||||
import SearchHistory from './components/Search/SearchHistory';
|
||||
import SearchProviders from './components/Search/SearchProviders';
|
||||
import { ThemePlaylists } from './components/Theming/ThemePlaylists';
|
||||
import { Themes } from './components/Theming/Themes';
|
||||
@@ -83,6 +84,9 @@ export function Settings() {
|
||||
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='providers'>
|
||||
{isMobile ? <FiSearch /> : 'Search Providers'}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='history'>
|
||||
{isMobile ? <FiSearch /> : 'Search History'}
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab px={isMobile ? '.9rem' : undefined} value='wolClients'>
|
||||
{isMobile ? <FiMonitor /> : 'WoL Clients'}
|
||||
</Tabs.Tab>
|
||||
@@ -143,6 +147,14 @@ export function Settings() {
|
||||
<SearchProviders />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value='history'>
|
||||
<ScrollArea
|
||||
className={scrollAreaClasses.default}
|
||||
classNames={scrollAreaClasses}
|
||||
>
|
||||
<SearchHistory />
|
||||
</ScrollArea>
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value='wolClients'>
|
||||
<ScrollArea
|
||||
className={scrollAreaClasses.default}
|
||||
|
||||
122
src/pages/settings/components/Search/SearchHistory.tsx
Normal file
122
src/pages/settings/components/Search/SearchHistory.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Group, Stack, Text } from '@mantine/core';
|
||||
import { useDebouncedValue, useDisclosure } from '@mantine/hooks';
|
||||
import { DataTable, DeleteItemModal, SettingsHeader } from 'components';
|
||||
import { SearchHistoryColumns } from 'components/Columns/SearchHistoryColumns';
|
||||
import { useCommonHooks, useQueryAPI } from 'hooks';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { textClasses } from 'styles';
|
||||
import type { SearchHistoryItem } from 'types';
|
||||
|
||||
const SearchHistory = () => {
|
||||
const searchEndpoint = 'api/searchHistory';
|
||||
const { data: historyData, isFetched: historyDataIsFetched } = useQueryAPI({
|
||||
endpoint: searchEndpoint,
|
||||
});
|
||||
const [searchHistory, setSearchHistory] = useState<SearchHistoryItem[]>([]);
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [searchHistoryItem, setSearchHistoryItem] = useState<SearchHistoryItem>();
|
||||
const [debouncedSearch] = useDebouncedValue(searchValue, 500);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { isMobile, handleDelete } = useCommonHooks();
|
||||
const [filtersEnabled, setFiltersEnabled] = useState(false);
|
||||
const [globalFilter, setGlobalFilter] = useState<string>('');
|
||||
const [modalOpened, { open: openModal, close: closeModal }] = useDisclosure(false);
|
||||
const handleClose = () => {
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const [sorting, setSorting] = useState([
|
||||
{
|
||||
id: 'dateTimeStamp',
|
||||
desc: true,
|
||||
},
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (historyData && historyDataIsFetched) {
|
||||
setSearchHistory(historyData);
|
||||
}
|
||||
}, [historyData, historyDataIsFetched]);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalFilter(debouncedSearch);
|
||||
if (debouncedSearch === globalFilter) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [debouncedSearch, globalFilter]);
|
||||
|
||||
return (
|
||||
<Stack w={'100%'}>
|
||||
<DeleteItemModal
|
||||
modalOpened={modalOpened}
|
||||
handleClose={handleClose}
|
||||
handleDelete={() => handleDelete(searchHistoryItem, 'api/searchHistory')}
|
||||
title={'Delete Search History Item'}
|
||||
body={
|
||||
<Stack gap={0} w={'100%'} h={'150px'} align='center'>
|
||||
<Text>Are you sure you want to delete this Search History Item?</Text>
|
||||
<Group>
|
||||
<Text
|
||||
className={textClasses.deleteItem}
|
||||
classNames={textClasses}
|
||||
>{`${searchHistoryItem?.provider}`}</Text>
|
||||
<Text
|
||||
className={textClasses.deleteItem}
|
||||
classNames={textClasses}
|
||||
>{`"${searchHistoryItem?.searchTerm}"`}</Text>
|
||||
</Group>
|
||||
<Text c='red'>This action cannot be undone!</Text>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<SettingsHeader
|
||||
setDragging={undefined}
|
||||
openModal={() => {}}
|
||||
title={'Search History'}
|
||||
tooltip={{
|
||||
title:
|
||||
'All searches are stored in the database and can be found in this table.',
|
||||
highlightList: ['searches', 'search', 'provider', 'prefix'],
|
||||
description:
|
||||
'Searches can be reviewed in the table on this page and can be deleted if necessary.',
|
||||
}}
|
||||
searchValue={searchValue}
|
||||
setSearchValue={setSearchValue}
|
||||
searchPlaceholder={'Search your Search History...'}
|
||||
isLoading={isLoading}
|
||||
setIsLoading={setIsLoading}
|
||||
/>
|
||||
<Stack
|
||||
mt={isMobile ? '10rem' : '6rem'}
|
||||
mb={isMobile ? '6rem' : '1rem'}
|
||||
align='center'
|
||||
>
|
||||
<Stack
|
||||
h={'calc(100vh - 15rem)'}
|
||||
w={'100%'}
|
||||
p={'1rem'}
|
||||
pt={0}
|
||||
mt={'1rem'}
|
||||
style={{ overflow: 'scroll', whiteSpace: 'none' }}
|
||||
>
|
||||
<DataTable
|
||||
data={searchHistory}
|
||||
columns={SearchHistoryColumns(
|
||||
setSearchHistoryItem,
|
||||
openModal,
|
||||
filtersEnabled,
|
||||
setFiltersEnabled,
|
||||
)}
|
||||
sorting={sorting}
|
||||
setSorting={setSorting}
|
||||
filtersEnabled={filtersEnabled}
|
||||
globalFilter={globalFilter}
|
||||
setGlobalFilter={setGlobalFilter}
|
||||
/>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchHistory;
|
||||
Reference in New Issue
Block a user