diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 6d1d80b..199ebdf 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,6 +1,7 @@ import { Refresh, Search } from '@mui/icons-material'; import { AppBar, + Badge, Box, LinearProgress, Toolbar, @@ -9,11 +10,11 @@ import { } from '@mui/material'; import IconButton from '@mui/material/IconButton'; import { useCallback, useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; +import { Link, useLocation } from 'react-router-dom'; import { fetchDataFromBackend } from '../../apiUtil'; import { useLoadingContext } from '../../context/LoadingContext'; import { useThemeModeContext } from '../../context/ThemeContext'; -import { SearchState } from '../../data/SearchState'; +import { SearchState, SearchStateDefaults } from '../../data/SearchState'; import { ServerData } from '../../data/ServerData'; import AlertComponent from '../Alert'; import AddServer from './AddServer'; @@ -32,9 +33,12 @@ export default function Header({ searchState, setSearchState, }: HeaderProps) { + const location = useLocation(); const { themeMode } = useThemeModeContext(); const { progress, setProgress, showAlert } = useLoadingContext(); const [showSearchServer, setShowSearchServer] = useState(false); + const [filtersApplied, setFiltersApplied] = useState(false); + const [initialFetch, setInitialFetch] = useState(false); const toggleShowSearchServer = () => { setShowSearchServer((prev) => !prev); }; @@ -65,8 +69,14 @@ export default function Header({ }, [showAlert, setServers, setProgress]); useEffect(() => { - fetchServerData(); - }, [fetchServerData]); + if (!initialFetch) { + fetchServerData(); + setInitialFetch(true); + } + setFiltersApplied( + JSON.stringify(searchState) !== JSON.stringify(SearchStateDefaults) + ); + }, [initialFetch, fetchServerData, searchState]); return ( @@ -83,6 +93,11 @@ export default function Header({ sx={{ flexGrow: 1, }} + onClick={(event) => { + if (location.pathname === '/') { + event.preventDefault(); + } + }} > IXION @@ -94,7 +109,9 @@ export default function Header({ - + + + diff --git a/client/src/components/Header/SearchServers.tsx b/client/src/components/Header/SearchServers.tsx index 9e14f98..60fcd41 100644 --- a/client/src/components/Header/SearchServers.tsx +++ b/client/src/components/Header/SearchServers.tsx @@ -13,6 +13,7 @@ import { Typography, } from '@mui/material'; import { useEffect, useRef, useState } from 'react'; +import { useLocation } from 'react-router-dom'; import { SearchState, SearchStateDefaults } from '../../data/SearchState'; type SearchServersProps = { @@ -27,15 +28,51 @@ export default function SearchServers({ setSearchState, }: SearchServersProps) { const containerRef = useRef(null); + const location = useLocation(); + const [initialPath] = useState(location.pathname); 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; + + 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(() => { + // 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) { setContentHeight(containerRef.current.scrollHeight); } else { setContentHeight(0); } - }, [showSearchServer]); + }, [showSearchServer, location.search, setSearchState, initialPath]); const handleChange = ( name: string, @@ -67,24 +104,29 @@ export default function SearchServers({ newSearchExpansions: string[] ) => { if (newSearchExpansions.length === 0) { - setSearchState({ ...searchState, expansions: null }); + setSearchState({ ...searchState, expansionsEnabled: null }); } else if ( - searchState.expansions && - searchState.expansions.includes('none') && + searchState.expansionsEnabled && + searchState.expansionsEnabled.includes('none') && newSearchExpansions.length > 1 ) { setSearchState({ ...searchState, - expansions: newSearchExpansions.filter((item) => item !== 'none'), + expansionsEnabled: newSearchExpansions.filter( + (item) => item !== 'none' + ), }); } else if ( - searchState.expansions && - !searchState.expansions.includes('none') && + searchState.expansionsEnabled && + !searchState.expansionsEnabled.includes('none') && newSearchExpansions.includes('none') ) { - setSearchState({ ...searchState, expansions: ['none'] }); + setSearchState({ ...searchState, expansionsEnabled: ['none'] }); } else { - setSearchState({ ...searchState, expansions: newSearchExpansions }); + setSearchState({ + ...searchState, + expansionsEnabled: newSearchExpansions, + }); } }; @@ -214,7 +256,7 @@ export default function SearchServers({ { - handleChange('fieldsOfValor', value); + handleChange('fov', value); }} size="small" aria-labelledby="fields-of-valor-button-group" @@ -551,9 +593,9 @@ export default function SearchServers({ Grounds of Valor { - handleChange('groundsOfValor', value); + handleChange('gov', value); }} size="small" aria-labelledby="grounds-of-valor-button-group" @@ -593,9 +635,9 @@ export default function SearchServers({ Records of Eminence { - handleChange('recordsOfEminence', value); + handleChange('roe', value); }} size="small" aria-labelledby="records-of-eminence-button-group" @@ -648,6 +690,9 @@ export default function SearchServers({ > { + if (location.pathname === '/') { + window.history.replaceState({}, '', '/'); + } setSearchState(SearchStateDefaults); }} > diff --git a/client/src/data/SearchState.tsx b/client/src/data/SearchState.tsx index 308a31d..0d35abc 100644 --- a/client/src/data/SearchState.tsx +++ b/client/src/data/SearchState.tsx @@ -1,27 +1,27 @@ export type SearchState = { 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[]; - 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 = { name: '', + maxLevel: [1, 99], + expansionsEnabled: null, multibox: null, trusts: null, levelSync: null, homePoint: null, - recordsOfEminence: null, - fieldsOfValor: null, - groundsOfValor: null, survivalGuide: null, - maxLevel: [1, 99], - expansions: null, + fov: null, + gov: null, + roe: null, }; diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx index a5aa2da..c440461 100644 --- a/client/src/pages/Home.tsx +++ b/client/src/pages/Home.tsx @@ -1,8 +1,9 @@ import { Box } from '@mui/material'; +import { useEffect } from 'react'; import ErrorCard from '../components/ErrorCard'; import ServerCard from '../components/Server/ServerCard'; import SettingsChipCloud from '../components/Server/SettingsChipCloud'; -import { SearchState } from '../data/SearchState'; +import { SearchState, SearchStateDefaults } from '../data/SearchState'; import { ServerData } from '../data/ServerData'; export default function Home({ @@ -12,6 +13,40 @@ export default function Home({ servers: ServerData[]; 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 => { // Name if (searchState.name.length > 0) { @@ -92,12 +127,12 @@ export default function Home({ } // Records of Eminence if ( - searchState.recordsOfEminence && + searchState.roe && typeof server.settings_summary['MAIN.ENABLE_ROE'] === 'number' ) { const serverEnabled = server.settings_summary['MAIN.ENABLE_ROE'] === 1; - const searchEnabled = searchState.recordsOfEminence.includes('enabled'); - const searchDisabled = searchState.recordsOfEminence.includes('disabled'); + const searchEnabled = searchState.roe.includes('enabled'); + const searchDisabled = searchState.roe.includes('disabled'); if ( (serverEnabled && searchDisabled && !searchEnabled) || (!serverEnabled && searchEnabled && !searchDisabled) @@ -107,13 +142,13 @@ export default function Home({ } // Fields of Valor if ( - searchState.fieldsOfValor && + searchState.fov && typeof server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 'number' ) { const serverEnabled = server.settings_summary['MAIN.ENABLE_FIELD_MANUALS'] === 1; - const searchEnabled = searchState.fieldsOfValor.includes('enabled'); - const searchDisabled = searchState.fieldsOfValor.includes('disabled'); + const searchEnabled = searchState.fov.includes('enabled'); + const searchDisabled = searchState.fov.includes('disabled'); if ( (serverEnabled && searchDisabled && !searchEnabled) || (!serverEnabled && searchEnabled && !searchDisabled) @@ -123,13 +158,13 @@ export default function Home({ } // Grounds of Valor if ( - searchState.groundsOfValor && + searchState.gov && typeof server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 'number' ) { const serverEnabled = server.settings_summary['MAIN.ENABLE_GROUNDS_TOMES'] === 1; - const searchEnabled = searchState.groundsOfValor.includes('enabled'); - const searchDisabled = searchState.groundsOfValor.includes('disabled'); + const searchEnabled = searchState.gov.includes('enabled'); + const searchDisabled = searchState.gov.includes('disabled'); if ( (serverEnabled && searchDisabled && !searchEnabled) || (!serverEnabled && searchEnabled && !searchDisabled) @@ -138,12 +173,12 @@ export default function Home({ } } // Expansions - if (searchState.expansions) { + if (searchState.expansionsEnabled) { if (!server.expansions) { return false; } if ( - searchState.expansions.includes('none') && + searchState.expansionsEnabled.includes('none') && (server.expansions.rotz || server.expansions.cop || server.expansions.toau || @@ -152,19 +187,32 @@ export default function Home({ ) { return false; } - if (searchState.expansions.includes('rotz') !== server.expansions.rotz) { + if ( + searchState.expansionsEnabled.includes('rotz') !== + server.expansions.rotz + ) { return false; } - if (searchState.expansions.includes('cop') !== server.expansions.cop) { + if ( + searchState.expansionsEnabled.includes('cop') !== server.expansions.cop + ) { return false; } - if (searchState.expansions.includes('toau') !== server.expansions.toau) { + if ( + searchState.expansionsEnabled.includes('toau') !== + server.expansions.toau + ) { return false; } - if (searchState.expansions.includes('wotg') !== server.expansions.wotg) { + if ( + searchState.expansionsEnabled.includes('wotg') !== + server.expansions.wotg + ) { return false; } - if (searchState.expansions.includes('soa') !== server.expansions.soa) { + if ( + searchState.expansionsEnabled.includes('soa') !== server.expansions.soa + ) { return false; } }