Files
TheGame/packages/web/components/MegaMenu/MegaMenuHeader.tsx
Udit Takkar e29b46cee4 feat: global search (#1587)
* feat: global search

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* add: name field

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: bugs

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: err

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: add mobile support

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* GitHub Actions use node 18 to run the seeding script 🍬

* feat: add cmd +k search

* fix: improvements

* fix: improvements

* chore: name

* 🐌 `yarn lint --fix`

* fix: improvements

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: body empty

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: guild search limit

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore: typo

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: vidvidvid <35112939+vidvidvid@users.noreply.github.com>
Co-authored-by: Sero <69639595+Seroxdesign@users.noreply.github.com>
Co-authored-by: δυς <dys@dhappy.org>
2023-10-03 18:38:57 -04:00

602 lines
16 KiB
TypeScript

import {
Avatar,
Box,
BoxedNextImage as Image,
BoxProps,
CloseIcon,
ExternalLinkIcon,
Flex,
FlexProps,
HamburgerIcon,
Input,
InputGroup,
InputLeftElement,
Link,
MetaButton,
Modal,
ModalBody,
ModalContent,
ModalOverlay,
SimpleGrid,
Stack,
Text,
Tooltip,
useBreakpointValue,
useDisclosure,
} from '@metafam/ds';
import { httpLink, Maybe } from '@metafam/utils';
import LogoImage from 'assets/logo.webp';
import SearchIcon from 'assets/search-icon.svg';
import { MetaLink } from 'components/Link';
import { DesktopNavLinks } from 'components/MegaMenu/DesktopNavLinks';
import { DesktopPlayerStats } from 'components/MegaMenu/DesktopPlayerStats';
import { GuildFragment, Player, PlayerFragment } from 'graphql/autogen/types';
import { searchPlayers } from 'graphql/getPlayers';
import { searchGuilds } from 'graphql/queries/guild';
import { useMounted, useUser, useWeb3 } from 'lib/hooks';
import { useRouter } from 'next/router';
import React, {
FormEventHandler,
ReactNode,
useEffect,
useRef,
useState,
} from 'react';
import { distinctUntilChanged, forkJoin, from, Subject } from 'rxjs';
import { debounceTime, filter, shareReplay, switchMap } from 'rxjs/operators';
import { menuIcons } from 'utils/menuIcons';
import { MenuSectionLinks } from 'utils/menuLinks';
import {
getPlayerImage,
getPlayerName,
getPlayerURL,
getPlayerUsername,
} from 'utils/playerHelpers';
type LogoProps = {
link: string;
} & BoxProps;
const Logo: React.FC<LogoProps> = ({ link, ...props }) => {
const w = useBreakpointValue({ base: 9, lg: 10 }) ?? 9;
const h = useBreakpointValue({ base: 12, lg: 14 }) ?? 12;
return (
<Box {...props}>
<MetaLink
href={link}
_focus={{ outline: 'none', bg: 'transparent' }}
_hover={{ bg: 'transparent' }}
_active={{ bg: 'transparent' }}
>
<Image
src={LogoImage}
transition="0.25s"
{...{ w, h }}
_hover={{ transform: 'scale(1.1)' }}
/>
</MetaLink>
</Box>
);
};
interface OptionProps {
text: string;
name: string;
image?: string;
onClick: () => void;
}
const Option = ({ onClick, name, image, text }: OptionProps) => (
<Box {...{ onClick }} as="li" role="option" sx={{ listStyleType: 'none' }}>
<Flex
_hover={{
background: 'purple.50',
}}
align="center"
px={3}
py={2}
cursor="pointer"
rounded="lg"
>
<Avatar name={name} src={httpLink(image)} w={6} h={6} />
<Text
px={2}
color="black"
fontFamily="Exo 2"
fontWeight={400}
textOverflow="ellipsis"
overflow="hidden"
whiteSpace="nowrap"
>
{text}
</Text>
</Flex>
</Box>
);
const ResultsTitle = ({ children }: { children: ReactNode }) => (
<Text
fontWeight={600}
color="black"
w="100%"
px={3}
pt={1}
fontFamily="Exo 2"
fontSize="1rem"
>
{children}
</Text>
);
const SeeAllOption = ({
type,
onClick,
}: {
type: string;
onClick: () => void;
}) => (
<Box {...{ onClick }} cursor="pointer">
<Text
fontFamily="Exo 2"
fontWeight={600}
color="landing450"
px={3}
fontSize="0.875rem"
>
See All {type}
</Text>
</Box>
);
const LIMIT = 3;
interface SearchResults {
players: PlayerFragment[];
guilds: GuildFragment[];
}
const SearchModal = ({
isOpen,
onClose,
}: {
isOpen: boolean;
onClose: () => void;
}) => {
const router = useRouter();
const searchInputSubjectRef = useRef(new Subject<string>());
const searchBarRef = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState<string>('');
const [{ players, guilds }, setSearchResults] = useState<SearchResults>({
players: [],
guilds: [],
});
const resetResults = () => {
setSearchResults({
players: [],
guilds: [],
});
};
const dropdown = useRef<Maybe<HTMLDivElement>>(null);
const handleSubmit: FormEventHandler<HTMLFormElement> = (e) => {
e.preventDefault();
onClose();
// Default Show Players Matching With Query
router.push(`/search/players?q=${query}`);
};
useEffect(() => {
searchInputSubjectRef.current.next(query);
}, [query]);
useEffect(() => {
const searchSubscription = searchInputSubjectRef.current
.pipe(
filter((searchValue: string) => {
if (searchValue.length >= 1) return true;
resetResults();
return false;
}),
debounceTime(300),
distinctUntilChanged(),
switchMap((queryString) =>
forkJoin([
from(searchPlayers(queryString)),
from(searchGuilds({ search: queryString, limit: LIMIT })),
]),
),
shareReplay(1),
)
.subscribe(([{ players: p }, { guilds: g }]) => {
setSearchResults({ players: p, guilds: g });
});
return () => searchSubscription?.unsubscribe();
}, []);
const isBodyEmpty = players.length + guilds.length === 0;
return (
<Modal {...{ isOpen }} {...{ onClose }} scrollBehavior="inside">
<ModalOverlay />
<ModalContent
overflow="hidden"
top="max(4rem, 8vh)"
shadow="lg"
maxH="700px"
maxW="500px"
aria-expanded="true"
marginTop={1}
p={0}
>
<Flex
flexDirection="column"
alignItems="center"
minWidth={40}
pos="relative"
align="stretch"
ref={dropdown}
>
<Box as="form" onSubmit={handleSubmit} w="full" color="white">
<InputGroup
justifyContent="flex-start"
h="fit-content"
p={2}
my="auto"
bg={{
base: '#FFFFFF05',
}}
border={{ base: '1px solid #2B2244' }}
borderRadius={4}
>
<InputLeftElement
pointerEvents="none"
children={
<Image src={SearchIcon} alt="search" height={4} width={4} />
}
/>
<Input
variant="unstyled"
color="white"
w="100%"
placeholder="Find Players or Guilds"
_placeholder={{ color: 'whiteAlpha.500' }}
value={query}
onChange={({ target: { value } }) => setQuery(value)}
size="sm"
fontSize="md"
ref={searchBarRef}
/>
</InputGroup>
</Box>
<ModalBody
sx={{
'&::-webkit-scrollbar': {
width: '5px',
},
bg: !isBodyEmpty ? 'white' : 'transparent',
}}
w="100%"
maxH="66vh"
p={0}
>
{!isBodyEmpty && (
<Box
w="100%"
borderRadius="0.25rem"
css={{
transform: 'translate3d(0px, 15px, 0px)',
}}
>
<Box as="ul" role="listbox" pb={8}>
{players.length > 0 && <ResultsTitle>Players</ResultsTitle>}
{players?.map((player: PlayerFragment) => (
<Option
key={player.id}
onClick={() => {
router.push(getPlayerURL(player) as string);
onClose();
}}
name={getPlayerName(player) ?? 'Unknown'}
image={getPlayerImage(player)}
text={
(getPlayerUsername(player as Maybe<Player>) ||
getPlayerName(player)) ??
'Unknown'
}
/>
))}
{players.length >= LIMIT && (
<SeeAllOption
type="Players"
onClick={() => {
router.push(`/search/players?q=${encodeURI(query)}`);
onClose();
}}
/>
)}
{guilds.length > 0 && <ResultsTitle>Guilds</ResultsTitle>}
{guilds?.map((guild: GuildFragment) => (
<Option
key={guild.id}
onClick={() => {
router.push(`/guild/${guild.guildname}`);
onClose();
}}
name={guild.name}
image={guild?.logo as string | undefined}
text={guild.name}
/>
))}
{guilds.length >= LIMIT && (
<SeeAllOption
type="Guilds"
onClick={() => {
router.push(`/search/guilds?q=${encodeURI(query)}`);
onClose();
}}
/>
)}
</Box>
</Box>
)}
</ModalBody>
</Flex>
</ModalContent>
</Modal>
);
};
type HeaderSearchBarProps = BoxProps & { onOpen: any };
const HeaderSearchBar = (props: HeaderSearchBarProps) => {
const { onOpen, ...restProps } = props;
return (
<Tooltip label="Quick Search… ⌘K">
<Box
cursor="pointer"
display="flex"
flexDirection="row"
gap={2}
h="fit-content"
maxW="fit-content"
alignItems="center"
p={2}
mx={2}
my="auto"
bg={{
base: '#FFFFFF05',
}}
border={{ base: '1px solid #2B2244' }}
borderRadius={8}
pos="relative"
onClick={onOpen}
{...restProps}
>
<Image src={SearchIcon} alt="search" height={4} width={4} />
</Box>
</Tooltip>
);
};
export const MegaMenuHeader: React.FC = () => {
const { connected, connect, connecting } = useWeb3();
const router = useRouter();
const { user, fetching } = useUser();
const mounted = useMounted();
const { isOpen, onOpen, onClose } = useDisclosure();
const menuToggle = () => (isOpen ? onClose() : onOpen());
const {
isOpen: isSearchOpen,
onOpen: onSearchOpen,
onClose: onSearchClose,
} = useDisclosure();
// Toggle the menu when ⌘K is pressed
React.useEffect(() => {
const down = (e: any) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
if (isSearchOpen) {
onSearchClose();
} else {
onSearchOpen();
}
}
};
document.addEventListener('keydown', down);
return () => document.removeEventListener('keydown', down);
}, [isSearchOpen, onSearchClose, onSearchOpen]);
return (
<>
<SearchModal isOpen={isSearchOpen} onClose={onSearchClose} />
<Stack
position={router.pathname === '/players' ? 'relative' : 'sticky'}
top={0}
id="MegaMenu"
zIndex={11}
>
<Flex
borderBottom="1px"
bg="rgba(0, 0, 0, 0.5)"
borderColor="#2B2244"
backdropFilter="blur(10px)"
px={4}
py={1.5}
h={20}
justify="space-between"
w="100%"
>
<Flex
onClick={menuToggle}
flexWrap="nowrap"
alignItems="center"
cursor="pointer"
h={8}
w={8}
display={{ base: 'flex', lg: 'flex', xl: 'none' }}
p={2}
my="auto"
>
{isOpen ? (
<CloseIcon fontSize="2xl" color="#FFF" />
) : (
// max-width set in style attribute because the unstyled version
// is flashing at 100% width on load
<HamburgerIcon
fontSize="3xl"
color="#FFF"
style={{ maxWidth: '2rem' }}
/>
)}
</Flex>
<Flex
w={{ base: 'auto', lg: '100%' }}
align="center"
justify="center"
pos="relative"
display={{
base: 'none',
sm: 'none',
md: 'none',
lg: 'none',
xl: 'flex',
}}
>
<Logo
link={user ? '/dashboard' : '/'}
pos={{ base: 'initial', lg: 'relative' }}
left={0}
top="auto"
bottom="auto"
/>
<DesktopNavLinks />
<HeaderSearchBar onOpen={onSearchOpen} />
<Box
textAlign="right"
display={{ base: 'none', lg: 'block' }}
pos="relative"
right="0"
left="1"
top="auto"
bottom="auto"
>
{connected && !!user && !fetching && !connecting ? (
<DesktopPlayerStats player={user} />
) : (
<Stack
fontWeight="bold"
fontFamily="Exo 2, san-serif"
align="center"
>
<MetaButton
h={10}
px={12}
onClick={connect}
isLoading={!mounted || connecting || fetching}
>
Connect
</MetaButton>
</Stack>
)}
</Box>
</Flex>
<Flex align="center" justify="center" pos="relative" display="flex">
<HeaderSearchBar
display={{ base: 'none', sm: 'flex', xl: 'none' }}
right={5}
onOpen={onSearchOpen}
/>
<Logo
display={{ lg: 'flex', xl: 'none' }}
link={user ? '/dashboard' : '/'}
pos={{ base: 'initial', lg: 'relative' }}
pt={1}
right={4}
/>
</Flex>
</Flex>
<Stack
display={{ base: isOpen ? 'block' : 'none', xl: 'none' }}
position="fixed"
top="4.5rem"
zIndex={1}
overflowX="hidden"
w="100vw"
bg="alphaBlack.200"
h="calc(100vh - 10rem)"
backdropFilter="blur(10px)"
p="1rem"
border="none"
>
<HeaderSearchBar onOpen={onSearchOpen} />
{MenuSectionLinks.map((section) => (
<Stack pt={1} key={section.label}>
<Link
href={section?.url}
target={section.type === 'external-link' ? '_blank' : ''}
display={'flex'}
flexDir={'row'}
alignItems={'center'}
>
<Text
fontSize={18}
fontWeight={600}
textTransform="capitalize"
color={section.type === 'external-link' ? '#79F8FB' : 'white'}
>
{section.label}
</Text>
{section.type === 'external-link' && (
<ExternalLinkIcon color="#79F8FB" ml="10px" />
)}
</Link>
<SimpleGrid columns={2}>
{section?.menuItems?.length &&
section.menuItems.map(({ title, icon, url }) => (
<Link
key={title}
display="flex"
alignItems="center"
href={url}
border="1px"
_odd={{ marginRight: '-1px' }}
marginBottom="-1px"
borderColor="purple.400"
bg="alphaBlack.50"
px={2}
py={1.5}
_hover={{
bg: 'alphaWhite.50',
}}
isExternal={/^https?:\/\//.test(url)}
>
<Avatar
name={title}
src={menuIcons[icon]}
p={0}
w={7}
h={7}
mr={1}
bg="linear-gradient(180deg, #170B23 0%, #350C58 100%)"
/>
<Text fontSize={20}>{title}</Text>
</Link>
))}
</SimpleGrid>
</Stack>
))}
</Stack>
</Stack>
</>
);
};