Add search state to URL params

This commit is contained in:
Corey 2024-05-13 00:13:03 -04:00
parent 1c0f3582cc
commit 9e598dc280
Signed by: coco
GPG Key ID: 051138DA6AEE3E60
4 changed files with 162 additions and 52 deletions

View File

@ -1,6 +1,7 @@
import { Refresh, Search } from '@mui/icons-material'; import { Refresh, Search } from '@mui/icons-material';
import { import {
AppBar, AppBar,
Badge,
Box, Box,
LinearProgress, LinearProgress,
Toolbar, Toolbar,
@ -9,11 +10,11 @@ import {
} from '@mui/material'; } from '@mui/material';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Link } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { fetchDataFromBackend } from '../../apiUtil'; import { fetchDataFromBackend } from '../../apiUtil';
import { useLoadingContext } from '../../context/LoadingContext'; import { useLoadingContext } from '../../context/LoadingContext';
import { useThemeModeContext } from '../../context/ThemeContext'; import { useThemeModeContext } from '../../context/ThemeContext';
import { SearchState } from '../../data/SearchState'; import { SearchState, SearchStateDefaults } from '../../data/SearchState';
import { ServerData } from '../../data/ServerData'; import { ServerData } from '../../data/ServerData';
import AlertComponent from '../Alert'; import AlertComponent from '../Alert';
import AddServer from './AddServer'; import AddServer from './AddServer';
@ -32,9 +33,12 @@ export default function Header({
searchState, searchState,
setSearchState, setSearchState,
}: HeaderProps) { }: HeaderProps) {
const location = useLocation();
const { themeMode } = useThemeModeContext(); const { themeMode } = useThemeModeContext();
const { progress, setProgress, showAlert } = useLoadingContext(); const { progress, setProgress, showAlert } = useLoadingContext();
const [showSearchServer, setShowSearchServer] = useState(false); const [showSearchServer, setShowSearchServer] = useState(false);
const [filtersApplied, setFiltersApplied] = useState(false);
const [initialFetch, setInitialFetch] = useState(false);
const toggleShowSearchServer = () => { const toggleShowSearchServer = () => {
setShowSearchServer((prev) => !prev); setShowSearchServer((prev) => !prev);
}; };
@ -65,8 +69,14 @@ export default function Header({
}, [showAlert, setServers, setProgress]); }, [showAlert, setServers, setProgress]);
useEffect(() => { useEffect(() => {
if (!initialFetch) {
fetchServerData(); fetchServerData();
}, [fetchServerData]); setInitialFetch(true);
}
setFiltersApplied(
JSON.stringify(searchState) !== JSON.stringify(SearchStateDefaults)
);
}, [initialFetch, fetchServerData, searchState]);
return ( return (
<Box> <Box>
@ -83,6 +93,11 @@ export default function Header({
sx={{ sx={{
flexGrow: 1, flexGrow: 1,
}} }}
onClick={(event) => {
if (location.pathname === '/') {
event.preventDefault();
}
}}
> >
IXION IXION
</Typography> </Typography>
@ -94,7 +109,9 @@ export default function Header({
</Tooltip> </Tooltip>
<Tooltip arrow disableInteractive title="Filter servers."> <Tooltip arrow disableInteractive title="Filter servers.">
<IconButton onClick={toggleShowSearchServer}> <IconButton onClick={toggleShowSearchServer}>
<Badge color="error" variant="dot" invisible={!filtersApplied}>
<Search /> <Search />
</Badge>
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</Toolbar> </Toolbar>

View File

@ -13,6 +13,7 @@ import {
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { SearchState, SearchStateDefaults } from '../../data/SearchState'; import { SearchState, SearchStateDefaults } from '../../data/SearchState';
type SearchServersProps = { type SearchServersProps = {
@ -27,15 +28,51 @@ export default function SearchServers({
setSearchState, setSearchState,
}: SearchServersProps) { }: SearchServersProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const location = useLocation();
const [initialPath] = useState(location.pathname);
const [contentHeight, setContentHeight] = useState(0); const [contentHeight, setContentHeight] = useState(0);
function parseSearchParams(searchParams: string): SearchState {
const params = new URLSearchParams(searchParams);
const parsedState = SearchStateDefaults;
const keys = Object.keys(SearchStateDefaults) as Array<keyof SearchState>;
keys.forEach((key) => {
if (params.has(key)) {
const value = params.get(key) as string;
const decodedValue = decodeURIComponent(value);
if (key === 'maxLevel') {
const maxLevelArray = decodedValue.split(',').map(Number);
parsedState[key] =
maxLevelArray.length === 1 ? [1, maxLevelArray[0]] : maxLevelArray;
} else if (key === 'expansionsEnabled') {
parsedState[key] = decodedValue.split(',') as string[];
} else {
parsedState[key] = decodedValue;
}
}
});
return parsedState as SearchState;
}
useEffect(() => { useEffect(() => {
// Populate search state with URL params only if loading the home page
// Home page manages updating URL params
if (initialPath === '/') {
const params = parseSearchParams(location.search);
if (params !== SearchStateDefaults) {
setSearchState(params);
}
}
if (showSearchServer && containerRef.current) { if (showSearchServer && containerRef.current) {
setContentHeight(containerRef.current.scrollHeight); setContentHeight(containerRef.current.scrollHeight);
} else { } else {
setContentHeight(0); setContentHeight(0);
} }
}, [showSearchServer]); }, [showSearchServer, location.search, setSearchState, initialPath]);
const handleChange = ( const handleChange = (
name: string, name: string,
@ -67,24 +104,29 @@ export default function SearchServers({
newSearchExpansions: string[] newSearchExpansions: string[]
) => { ) => {
if (newSearchExpansions.length === 0) { if (newSearchExpansions.length === 0) {
setSearchState({ ...searchState, expansions: null }); setSearchState({ ...searchState, expansionsEnabled: null });
} else if ( } else if (
searchState.expansions && searchState.expansionsEnabled &&
searchState.expansions.includes('none') && searchState.expansionsEnabled.includes('none') &&
newSearchExpansions.length > 1 newSearchExpansions.length > 1
) { ) {
setSearchState({ setSearchState({
...searchState, ...searchState,
expansions: newSearchExpansions.filter((item) => item !== 'none'), expansionsEnabled: newSearchExpansions.filter(
(item) => item !== 'none'
),
}); });
} else if ( } else if (
searchState.expansions && searchState.expansionsEnabled &&
!searchState.expansions.includes('none') && !searchState.expansionsEnabled.includes('none') &&
newSearchExpansions.includes('none') newSearchExpansions.includes('none')
) { ) {
setSearchState({ ...searchState, expansions: ['none'] }); setSearchState({ ...searchState, expansionsEnabled: ['none'] });
} else { } else {
setSearchState({ ...searchState, expansions: newSearchExpansions }); setSearchState({
...searchState,
expansionsEnabled: newSearchExpansions,
});
} }
}; };
@ -214,7 +256,7 @@ export default function SearchServers({
</Typography> </Typography>
</Tooltip> </Tooltip>
<ToggleButtonGroup <ToggleButtonGroup
value={searchState.expansions} value={searchState.expansionsEnabled}
onChange={handleSearchExpansions} onChange={handleSearchExpansions}
size="small" size="small"
aria-labelledby="expansions-button-group" aria-labelledby="expansions-button-group"
@ -509,9 +551,9 @@ export default function SearchServers({
Fields of Valor Fields of Valor
</Typography> </Typography>
<ToggleButtonGroup <ToggleButtonGroup
value={searchState.fieldsOfValor} value={searchState.fov}
onChange={(_event, value) => { onChange={(_event, value) => {
handleChange('fieldsOfValor', value); handleChange('fov', value);
}} }}
size="small" size="small"
aria-labelledby="fields-of-valor-button-group" aria-labelledby="fields-of-valor-button-group"
@ -551,9 +593,9 @@ export default function SearchServers({
Grounds of Valor Grounds of Valor
</Typography> </Typography>
<ToggleButtonGroup <ToggleButtonGroup
value={searchState.groundsOfValor} value={searchState.gov}
onChange={(_event, value) => { onChange={(_event, value) => {
handleChange('groundsOfValor', value); handleChange('gov', value);
}} }}
size="small" size="small"
aria-labelledby="grounds-of-valor-button-group" aria-labelledby="grounds-of-valor-button-group"
@ -593,9 +635,9 @@ export default function SearchServers({
Records of Eminence Records of Eminence
</Typography> </Typography>
<ToggleButtonGroup <ToggleButtonGroup
value={searchState.recordsOfEminence} value={searchState.roe}
onChange={(_event, value) => { onChange={(_event, value) => {
handleChange('recordsOfEminence', value); handleChange('roe', value);
}} }}
size="small" size="small"
aria-labelledby="records-of-eminence-button-group" aria-labelledby="records-of-eminence-button-group"
@ -648,6 +690,9 @@ export default function SearchServers({
> >
<IconButton <IconButton
onClick={() => { onClick={() => {
if (location.pathname === '/') {
window.history.replaceState({}, '', '/');
}
setSearchState(SearchStateDefaults); setSearchState(SearchStateDefaults);
}} }}
> >

View File

@ -1,27 +1,27 @@
export type SearchState = { export type SearchState = {
name: string; name: string;
multibox: string[] | null;
trusts: string[] | null;
levelSync: string[] | null;
homePoint: string[] | null;
survivalGuide: string[] | null;
recordsOfEminence: string[] | null;
fieldsOfValor: string[] | null;
groundsOfValor: string[] | null;
maxLevel: number[]; maxLevel: number[];
expansions: string[] | null; expansionsEnabled: string[] | null;
multibox: string | null;
trusts: string | null;
levelSync: string | null;
homePoint: string | null;
survivalGuide: string | null;
fov: string | null;
gov: string | null;
roe: string | null;
}; };
export const SearchStateDefaults: SearchState = { export const SearchStateDefaults: SearchState = {
name: '', name: '',
maxLevel: [1, 99],
expansionsEnabled: null,
multibox: null, multibox: null,
trusts: null, trusts: null,
levelSync: null, levelSync: null,
homePoint: null, homePoint: null,
recordsOfEminence: null,
fieldsOfValor: null,
groundsOfValor: null,
survivalGuide: null, survivalGuide: null,
maxLevel: [1, 99], fov: null,
expansions: null, gov: null,
roe: null,
}; };

View File

@ -1,8 +1,9 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { useEffect } from 'react';
import ErrorCard from '../components/ErrorCard'; import ErrorCard from '../components/ErrorCard';
import ServerCard from '../components/Server/ServerCard'; import ServerCard from '../components/Server/ServerCard';
import SettingsChipCloud from '../components/Server/SettingsChipCloud'; import SettingsChipCloud from '../components/Server/SettingsChipCloud';
import { SearchState } from '../data/SearchState'; import { SearchState, SearchStateDefaults } from '../data/SearchState';
import { ServerData } from '../data/ServerData'; import { ServerData } from '../data/ServerData';
export default function Home({ export default function Home({
@ -12,6 +13,40 @@ export default function Home({
servers: ServerData[]; servers: ServerData[];
searchState: SearchState; searchState: SearchState;
}) { }) {
function serializeSearchState(search: SearchState): string {
const params = new URLSearchParams();
Object.entries(search).forEach(([key, value]) => {
if (
value !== null &&
value !== undefined &&
!(key === 'name' && value === '') &&
!(
key === 'maxLevel' &&
JSON.stringify(value) === JSON.stringify(SearchStateDefaults.maxLevel)
)
) {
if (Array.isArray(value)) {
params.append(key, value.join(','));
} else {
params.append(key, value);
}
}
});
return params.toString();
}
useEffect(() => {
// Update URL params whenever search state changes
const searchParams = serializeSearchState(searchState);
if (searchParams.length !== 0) {
window.history.replaceState({}, '', `?${searchParams}`);
} else {
window.history.replaceState({}, '', '/');
}
}, [searchState]);
const filterServers = (server: ServerData): boolean => { const filterServers = (server: ServerData): boolean => {
// Name // Name
if (searchState.name.length > 0) { if (searchState.name.length > 0) {
@ -92,12 +127,12 @@ export default function Home({
} }
// Records of Eminence // Records of Eminence
if ( if (
searchState.recordsOfEminence && searchState.roe &&
typeof server.settings_summary['MAIN.ENABLE_ROE'] === 'number' typeof server.settings_summary['MAIN.ENABLE_ROE'] === 'number'
) { ) {
const serverEnabled = server.settings_summary['MAIN.ENABLE_ROE'] === 1; const serverEnabled = server.settings_summary['MAIN.ENABLE_ROE'] === 1;
const searchEnabled = searchState.recordsOfEminence.includes('enabled'); const searchEnabled = searchState.roe.includes('enabled');
const searchDisabled = searchState.recordsOfEminence.includes('disabled'); const searchDisabled = searchState.roe.includes('disabled');
if ( if (
(serverEnabled && searchDisabled && !searchEnabled) || (serverEnabled && searchDisabled && !searchEnabled) ||
(!serverEnabled && searchEnabled && !searchDisabled) (!serverEnabled && searchEnabled && !searchDisabled)
@ -107,13 +142,13 @@ export default function Home({
} }
// Fields of Valor // Fields of Valor
if ( if (
searchState.fieldsOfValor && searchState.fov &&
typeof server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 'number' typeof server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 'number'
) { ) {
const serverEnabled = const serverEnabled =
server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 1; server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 1;
const searchEnabled = searchState.fieldsOfValor.includes('enabled'); const searchEnabled = searchState.fov.includes('enabled');
const searchDisabled = searchState.fieldsOfValor.includes('disabled'); const searchDisabled = searchState.fov.includes('disabled');
if ( if (
(serverEnabled && searchDisabled && !searchEnabled) || (serverEnabled && searchDisabled && !searchEnabled) ||
(!serverEnabled && searchEnabled && !searchDisabled) (!serverEnabled && searchEnabled && !searchDisabled)
@ -123,13 +158,13 @@ export default function Home({
} }
// Grounds of Valor // Grounds of Valor
if ( if (
searchState.groundsOfValor && searchState.gov &&
typeof server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 'number' typeof server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 'number'
) { ) {
const serverEnabled = const serverEnabled =
server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 1; server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 1;
const searchEnabled = searchState.groundsOfValor.includes('enabled'); const searchEnabled = searchState.gov.includes('enabled');
const searchDisabled = searchState.groundsOfValor.includes('disabled'); const searchDisabled = searchState.gov.includes('disabled');
if ( if (
(serverEnabled && searchDisabled && !searchEnabled) || (serverEnabled && searchDisabled && !searchEnabled) ||
(!serverEnabled && searchEnabled && !searchDisabled) (!serverEnabled && searchEnabled && !searchDisabled)
@ -138,12 +173,12 @@ export default function Home({
} }
} }
// Expansions // Expansions
if (searchState.expansions) { if (searchState.expansionsEnabled) {
if (!server.expansions) { if (!server.expansions) {
return false; return false;
} }
if ( if (
searchState.expansions.includes('none') && searchState.expansionsEnabled.includes('none') &&
(server.expansions.rotz || (server.expansions.rotz ||
server.expansions.cop || server.expansions.cop ||
server.expansions.toau || server.expansions.toau ||
@ -152,19 +187,32 @@ export default function Home({
) { ) {
return false; return false;
} }
if (searchState.expansions.includes('rotz') !== server.expansions.rotz) { if (
searchState.expansionsEnabled.includes('rotz') !==
server.expansions.rotz
) {
return false; return false;
} }
if (searchState.expansions.includes('cop') !== server.expansions.cop) { if (
searchState.expansionsEnabled.includes('cop') !== server.expansions.cop
) {
return false; return false;
} }
if (searchState.expansions.includes('toau') !== server.expansions.toau) { if (
searchState.expansionsEnabled.includes('toau') !==
server.expansions.toau
) {
return false; return false;
} }
if (searchState.expansions.includes('wotg') !== server.expansions.wotg) { if (
searchState.expansionsEnabled.includes('wotg') !==
server.expansions.wotg
) {
return false; return false;
} }
if (searchState.expansions.includes('soa') !== server.expansions.soa) { if (
searchState.expansionsEnabled.includes('soa') !== server.expansions.soa
) {
return false; return false;
} }
} }