mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-01-14 00:48:18 -05:00
737 lines
21 KiB
TypeScript
737 lines
21 KiB
TypeScript
import {
|
|
Avatar,
|
|
Box,
|
|
BoxedNextImage as Image,
|
|
BoxProps,
|
|
Button,
|
|
CloseIcon,
|
|
ExternalLinkIcon,
|
|
Flex,
|
|
HamburgerIcon,
|
|
HStack,
|
|
Input,
|
|
InputGroup,
|
|
InputLeftElement,
|
|
Link,
|
|
Modal,
|
|
ModalBody,
|
|
ModalContent,
|
|
ModalOverlay,
|
|
SimpleGrid,
|
|
Stack,
|
|
Text,
|
|
Tooltip,
|
|
useBreakpointValue,
|
|
useDisclosure,
|
|
} from '@metafam/ds';
|
|
import { Maybe } from '@metafam/utils';
|
|
import { ConnectKitButton } from 'connectkit';
|
|
import {
|
|
GuildFragment,
|
|
Player,
|
|
PlayerFragment,
|
|
SearchQuestsQuery,
|
|
} from 'graphql/autogen/hasura-sdk';
|
|
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 {
|
|
getPlayerName,
|
|
getPlayerURL,
|
|
getPlayerUsername,
|
|
} from 'utils/playerHelpers';
|
|
import { useAccount } from 'wagmi';
|
|
|
|
import LogoImage from '#assets/new_logo_svg.svg';
|
|
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 { searchPatrons } from '#graphql/getPatrons';
|
|
import { searchPlayers } from '#graphql/getPlayers';
|
|
import { searchQuests } from '#graphql/getQuests';
|
|
import { searchGuilds } from '#graphql/queries/guild';
|
|
import { Patron } from '#graphql/types';
|
|
import { useMounted, useUser, useWeb3 } from '#lib/hooks';
|
|
import { useProfileImageOnload } from '#lib/hooks/useProfileImageOnload';
|
|
import { menuIcons } from '#utils/menuIcons';
|
|
import { MenuSectionLinks } from '#utils/menuLinks';
|
|
import { authenticateWallet } from '#contexts/Web3Context';
|
|
import { useViemClients } from '#lib/hooks/useEthersProvider';
|
|
|
|
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;
|
|
player?: Player;
|
|
image?: string;
|
|
onClick: () => void;
|
|
}
|
|
|
|
const Option = ({ onClick, name, player, text }: OptionProps) => {
|
|
const imageURL = useProfileImageOnload({ player });
|
|
|
|
return (
|
|
<Box {...{ onClick }} as="li" role="option" sx={{ listStyleType: 'none' }}>
|
|
<Flex
|
|
_hover={{
|
|
background: 'rgba(0,0,0,0.56)',
|
|
}}
|
|
align="center"
|
|
px={3}
|
|
py={2}
|
|
cursor="pointer"
|
|
rounded="lg"
|
|
>
|
|
<Avatar
|
|
{...{ name }}
|
|
src={imageURL}
|
|
w={6}
|
|
h={6}
|
|
sx={{
|
|
'& > div': {
|
|
fontSize: 'xs',
|
|
},
|
|
}}
|
|
/>
|
|
<Text
|
|
px={2}
|
|
color="white"
|
|
fontFamily="Exo 2"
|
|
fontWeight={400}
|
|
textOverflow="ellipsis"
|
|
overflow="hidden"
|
|
whiteSpace="nowrap"
|
|
>
|
|
{text}
|
|
</Text>
|
|
</Flex>
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
const ResultsTitle = ({ children }: { children: ReactNode }) => (
|
|
<Text
|
|
fontWeight={600}
|
|
color="white"
|
|
w="100%"
|
|
px={6}
|
|
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[];
|
|
patrons: Patron[];
|
|
quests: SearchQuestsQuery['quest'];
|
|
}
|
|
|
|
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, patrons, quests }, setSearchResults] =
|
|
useState<SearchResults>({
|
|
players: [],
|
|
guilds: [],
|
|
patrons: [],
|
|
quests: [],
|
|
});
|
|
|
|
const resetResults = () => {
|
|
setSearchResults({
|
|
players: [],
|
|
guilds: [],
|
|
patrons: [],
|
|
quests: [],
|
|
});
|
|
};
|
|
|
|
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 })),
|
|
from(searchPatrons(queryString, LIMIT)),
|
|
from(searchQuests(queryString, LIMIT)),
|
|
]),
|
|
),
|
|
shareReplay(1),
|
|
)
|
|
.subscribe(
|
|
([
|
|
{ players: p },
|
|
{ guilds: g },
|
|
searchPatronsRes,
|
|
searchQuestsRes,
|
|
]) => {
|
|
setSearchResults({
|
|
players: p,
|
|
guilds: g,
|
|
patrons: searchPatronsRes,
|
|
quests: searchQuestsRes,
|
|
});
|
|
},
|
|
);
|
|
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="2xl"
|
|
maxH="700px"
|
|
maxW="500px"
|
|
aria-expanded="true"
|
|
marginTop={1}
|
|
bgColor="transparent"
|
|
p={0}
|
|
>
|
|
<Flex
|
|
flexDirection="column"
|
|
alignItems="center"
|
|
minWidth={40}
|
|
pos="relative"
|
|
align="stretch"
|
|
bg="transparent"
|
|
backdropFilter="blur(10px)"
|
|
ref={dropdown}
|
|
>
|
|
<Box as="form" onSubmit={handleSubmit} w="full" color="white">
|
|
<InputGroup
|
|
justifyContent="flex-start"
|
|
h="fit-content"
|
|
p={2}
|
|
my="auto"
|
|
bg={{
|
|
base: '#FFFFFF25',
|
|
}}
|
|
border={{ base: '1px solid #2B2244' }}
|
|
borderTopRadius={4}
|
|
borderBottomRadius={query.length > 0 ? 0 : 4}
|
|
overflow="hidden"
|
|
>
|
|
<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.700' }}
|
|
value={query}
|
|
onChange={({ target: { value } }) => setQuery(value)}
|
|
size="sm"
|
|
fontSize="md"
|
|
ref={searchBarRef}
|
|
/>
|
|
</InputGroup>
|
|
</Box>
|
|
<ModalBody
|
|
sx={{
|
|
'&::-webkit-scrollbar': {
|
|
width: '8px',
|
|
},
|
|
bg: 'linear-gradient(180deg, rgba(42, 31, 71, 0.9) 6.18%, rgba(17, 3, 32, 0.86) 140%)',
|
|
}}
|
|
borderColor="#2B2244"
|
|
backdropFilter="blur(10px)"
|
|
w="100%"
|
|
maxH="67vh"
|
|
p={0}
|
|
>
|
|
{!isBodyEmpty && (
|
|
<Box w="100%" color="white" py={4}>
|
|
{players.length > 0 && <ResultsTitle>Players</ResultsTitle>}
|
|
{players.length > 0 && (
|
|
<Box as="ul" role="listbox" mb={2} px={3} color="white">
|
|
{players?.map((player: PlayerFragment) => (
|
|
<Option
|
|
key={player.id}
|
|
onClick={() => {
|
|
router.push(getPlayerURL(player) as string);
|
|
onClose();
|
|
}}
|
|
name={getPlayerName(player) ?? 'Unknown'}
|
|
player={player as 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();
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{guilds.length > 0 && <ResultsTitle>Guilds</ResultsTitle>}
|
|
{guilds.length > 0 && (
|
|
<Box as="ul" role="listbox" mb={2} px={3} color="white">
|
|
{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>
|
|
)}
|
|
|
|
{patrons.length > 0 && <ResultsTitle>Patrons</ResultsTitle>}
|
|
{patrons.length > 0 && (
|
|
<Box as="ul" role="listbox" mb={2} px={3} color="white">
|
|
{patrons?.map((patron: Patron) => (
|
|
<Option
|
|
key={patron.id}
|
|
onClick={() => {
|
|
router.push(getPlayerURL(patron) as string);
|
|
onClose();
|
|
}}
|
|
name={getPlayerName(patron) ?? 'Unknown'}
|
|
player={patron as Player}
|
|
text={
|
|
(getPlayerUsername(patron as Maybe<Patron>) ||
|
|
getPlayerName(patron)) ??
|
|
'Unknown'
|
|
}
|
|
/>
|
|
))}
|
|
{patrons.length >= LIMIT && (
|
|
<SeeAllOption
|
|
type="Patrons"
|
|
onClick={() => {
|
|
router.push(`/search/patrons?q=${encodeURI(query)}`);
|
|
onClose();
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
{quests.length > 0 && <ResultsTitle>Quests</ResultsTitle>}
|
|
{quests.length > 0 && (
|
|
<Box as="ul" role="listbox" mb={2} px={3} color="white">
|
|
{quests.map((quest: SearchQuestsQuery['quest'][number]) => (
|
|
<Option
|
|
key={quest.id}
|
|
onClick={() => {
|
|
router.push(`/quest/${quest.id}`);
|
|
onClose();
|
|
}}
|
|
name={quest.title}
|
|
image={quest.image ?? undefined}
|
|
text={quest.title}
|
|
/>
|
|
))}
|
|
{quests.length >= LIMIT && (
|
|
<SeeAllOption
|
|
type="Quests"
|
|
onClick={() => {
|
|
router.push(`/search/quests?q=${encodeURI(query)}`);
|
|
onClose();
|
|
}}
|
|
/>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)}
|
|
</ModalBody>
|
|
</Flex>
|
|
</ModalContent>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
type HeaderSearchBarProps = BoxProps & { onOpen: () => void };
|
|
|
|
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 router = useRouter();
|
|
const { user, fetching } = useUser();
|
|
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: KeyboardEvent) => {
|
|
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}
|
|
pb="0"
|
|
h={20}
|
|
justify={{ base: 'space-between', lg: 'center' }}
|
|
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="space-between"
|
|
pos="relative"
|
|
display={{
|
|
base: 'none',
|
|
sm: 'none',
|
|
md: 'none',
|
|
lg: 'none',
|
|
xl: 'flex',
|
|
}}
|
|
>
|
|
<HStack w="15%" flexShrink={1}>
|
|
<Logo
|
|
link={'/dashboard'}
|
|
pos={{ base: 'initial', lg: 'relative' }}
|
|
left={0}
|
|
top="auto"
|
|
bottom="auto"
|
|
/>
|
|
</HStack>
|
|
<HStack
|
|
flex="0 1 auto"
|
|
align="center"
|
|
justify="center"
|
|
alignSelf="center"
|
|
spacing={0}
|
|
height="100%"
|
|
>
|
|
<DesktopNavLinks />
|
|
|
|
<HeaderSearchBar onOpen={onSearchOpen} />
|
|
</HStack>
|
|
<Box
|
|
textAlign="right"
|
|
display={{ base: 'none', lg: 'block' }}
|
|
pos="relative"
|
|
right="0"
|
|
left="1"
|
|
top="auto"
|
|
bottom="auto"
|
|
w="15%"
|
|
>
|
|
<ConnectKitButton.Custom>
|
|
{({ isConnected, isConnecting, show }) => (
|
|
isConnected ? (
|
|
!!user ? (
|
|
<DesktopPlayerStats player={user} />
|
|
) : (
|
|
<Button onClick={show}>Missing User Record</Button>
|
|
)
|
|
) : (
|
|
<Button
|
|
w="100%"
|
|
size="md"
|
|
textTransform="uppercase"
|
|
fontWeight="600"
|
|
onClick={show}
|
|
isLoading={isConnecting || fetching}
|
|
colorScheme="pink"
|
|
fontSize="1rem"
|
|
>
|
|
Connect Wallet
|
|
</Button>
|
|
)
|
|
)}
|
|
</ConnectKitButton.Custom>
|
|
</Box>
|
|
</Flex>
|
|
<Flex
|
|
align="center"
|
|
justify="end"
|
|
pos="relative"
|
|
w={{ base: '100%', xl: 'auto' }}
|
|
display="flex"
|
|
>
|
|
<HeaderSearchBar
|
|
display={{ sm: 'flex', xl: 'none' }}
|
|
right={{ sm: 2, lg: 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>
|
|
</>
|
|
);
|
|
};
|