mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-01-23 13:17:54 -05:00
356 lines
9.8 KiB
TypeScript
356 lines
9.8 KiB
TypeScript
import {
|
|
Box,
|
|
BoxedNextImage,
|
|
Button,
|
|
CloseIcon,
|
|
Flex,
|
|
InfoIcon,
|
|
LoadingState,
|
|
MetaButton,
|
|
MetaHeading,
|
|
Spacer,
|
|
Text,
|
|
useBreakpointValue,
|
|
useToast,
|
|
} from '@metafam/ds';
|
|
import { FlexContainer } from 'components/Container';
|
|
import { useSetupFlow } from 'contexts/SetupContext';
|
|
import {
|
|
PlayerRole,
|
|
useUpdatePlayerRolesMutation,
|
|
} from 'graphql/autogen/types';
|
|
import React, { useMemo, useState } from 'react';
|
|
|
|
export type RoleValue = string;
|
|
|
|
export type SetupRolesProps = {
|
|
roleChoices: Array<PlayerRole>;
|
|
roles: RoleValue[];
|
|
fetchingExistingRoles: boolean;
|
|
setRoles: React.Dispatch<React.SetStateAction<RoleValue[]>>;
|
|
};
|
|
|
|
export const SetupRoles: React.FC<SetupRolesProps> = ({
|
|
roleChoices,
|
|
roles,
|
|
fetchingExistingRoles,
|
|
setRoles,
|
|
}) => {
|
|
const { onNextPress, nextButtonLabel } = useSetupFlow();
|
|
const toast = useToast();
|
|
|
|
const computeAvailableRoles = (playerRoles: string[]) =>
|
|
roleChoices.filter((r) => !playerRoles.includes(r.role));
|
|
|
|
const [updateRolesResult, updateRoles] = useUpdatePlayerRolesMutation();
|
|
const [loading, setLoading] = useState(false);
|
|
const [availableRoles, setAvailableRoles] = useState<PlayerRole[]>(
|
|
computeAvailableRoles(roles),
|
|
);
|
|
|
|
useMemo(() => {
|
|
if (roles.length > 0) {
|
|
setAvailableRoles(roleChoices.filter((r) => !roles.includes(r.role)));
|
|
}
|
|
}, [roles, roleChoices]);
|
|
|
|
const handleNextPress = async () => {
|
|
setLoading(true);
|
|
|
|
const { error } = await updateRoles({
|
|
roles: roles.map((r, i) => ({
|
|
rank: i,
|
|
role: r,
|
|
})),
|
|
});
|
|
|
|
if (error) {
|
|
toast({
|
|
title: 'Error',
|
|
description: 'Unable to update roles. The octo is sad 😢',
|
|
status: 'error',
|
|
isClosable: true,
|
|
});
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
onNextPress();
|
|
};
|
|
|
|
const handleSelection = (role: PlayerRole, isPrimary?: boolean) => {
|
|
if (isPrimary === false && roles.length < 1) {
|
|
return;
|
|
}
|
|
let newRoles: RoleValue[] = [];
|
|
const otherRoles = roles.filter((r) => r !== role.role);
|
|
if (isPrimary === true) {
|
|
newRoles = [role.role, ...otherRoles];
|
|
} else {
|
|
newRoles = [...otherRoles, role.role];
|
|
}
|
|
setRoles(newRoles);
|
|
setAvailableRoles(computeAvailableRoles(newRoles));
|
|
};
|
|
|
|
const handleRemoval = (role: PlayerRole) => {
|
|
const newRoles = roles.filter((r) => r !== role.role);
|
|
setRoles(newRoles);
|
|
setAvailableRoles(computeAvailableRoles(newRoles));
|
|
};
|
|
|
|
const roleContainerStyles = {
|
|
width: {
|
|
base: 'calc(100% - 4px)',
|
|
md: 'calc(50% - 16px)',
|
|
lg: 'calc(33% - 20px)',
|
|
},
|
|
mr: { base: 0, md: 4 },
|
|
mb: { base: 2, md: 4 },
|
|
};
|
|
|
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
|
|
|
return (
|
|
<FlexContainer align="center" mx={{ base: 0, md: 8, lg: 16 }}>
|
|
<MetaHeading mb={{ base: 6, sm: 16 }} alignSelf="center">
|
|
Select your role(s)
|
|
</MetaHeading>
|
|
{fetchingExistingRoles && <LoadingState />}
|
|
{!fetchingExistingRoles && roles.length === 0 ? (
|
|
<Text mb={{ base: 6, sm: 10 }}>
|
|
Unlike other role-playing games, in MetaGame, anyone is free to play
|
|
multiple roles at the same time.
|
|
<br />
|
|
Players are required to specify their primary role, whereas any
|
|
secondary roles are optional.
|
|
</Text>
|
|
) : (
|
|
<Flex wrap="wrap" mb={{ base: 4, md: 16 }} w="100%">
|
|
{roles.map((r, i) => {
|
|
const choice = roleChoices.find(
|
|
(roleChoice) => roleChoice.role === r,
|
|
);
|
|
return (
|
|
choice && (
|
|
<>
|
|
<Box key={r} {...roleContainerStyles}>
|
|
<Text
|
|
color="cyan.500"
|
|
fontWeight="bold"
|
|
casing="uppercase"
|
|
my="2"
|
|
>
|
|
{i === 0 && 'Primary Role'}
|
|
{i > 0 && roles.length === 2 && 'Secondary Role'}
|
|
{i === 1 && roles.length > 2 && 'Secondary Roles'}
|
|
{/* we still need a placeholder */}
|
|
{!isMobile && roles.length > 2 && i > 1 && (
|
|
<span> </span>
|
|
)}
|
|
</Text>
|
|
|
|
<Role
|
|
role={choice}
|
|
selectionIndex={i}
|
|
numSelectedRoles={roles.length}
|
|
onSelect={handleSelection}
|
|
onRemove={handleRemoval}
|
|
/>
|
|
</Box>
|
|
{/* wrap after the primary */}
|
|
{i === 0 && <Box flexBasis="100%" />}
|
|
</>
|
|
)
|
|
);
|
|
})}
|
|
</Flex>
|
|
)}
|
|
<Text
|
|
alignSelf="flex-start"
|
|
color="white"
|
|
fontWeight="bold"
|
|
casing="uppercase"
|
|
my="2"
|
|
>
|
|
Available Roles
|
|
</Text>
|
|
<Flex wrap="wrap" mb={{ base: 6, md: 16 }}>
|
|
{availableRoles.map((r) => (
|
|
<Box key={r.role} {...roleContainerStyles}>
|
|
<Role role={r} onSelect={handleSelection} />
|
|
</Box>
|
|
))}
|
|
</Flex>
|
|
|
|
<MetaButton
|
|
onClick={handleNextPress}
|
|
isDisabled={roles.length < 1}
|
|
isLoading={updateRolesResult.fetching || loading}
|
|
loadingText="Saving"
|
|
>
|
|
{nextButtonLabel}
|
|
</MetaButton>
|
|
</FlexContainer>
|
|
);
|
|
};
|
|
|
|
type RoleProps = {
|
|
role: PlayerRole;
|
|
selectionIndex?: number;
|
|
numSelectedRoles?: number;
|
|
onSelect: (role: PlayerRole, isPrimary?: boolean) => void;
|
|
onRemove?: (role: PlayerRole) => void;
|
|
};
|
|
|
|
const Role: React.FC<RoleProps> = ({
|
|
role,
|
|
selectionIndex,
|
|
numSelectedRoles,
|
|
onSelect,
|
|
onRemove,
|
|
}) => {
|
|
const handleContainerClick = () => {
|
|
if (selectionIndex == null) {
|
|
onSelect(role);
|
|
}
|
|
};
|
|
|
|
const handleRemoveClick = () => {
|
|
if (onRemove) {
|
|
onRemove(role);
|
|
}
|
|
};
|
|
|
|
const [showDetails, setShowDetails] = useState(false);
|
|
|
|
const isMobile = useBreakpointValue({ base: true, md: false });
|
|
|
|
return (
|
|
<Box
|
|
p={{ base: 2, lg: 6 }}
|
|
bgColor="purpleBoxLight"
|
|
borderRadius="0.5rem"
|
|
_hover={selectionIndex == null ? { bgColor: 'purpleBoxDark' } : {}}
|
|
cursor={selectionIndex == null ? 'pointer' : 'default'}
|
|
transition="background 0.25s"
|
|
border="2px"
|
|
borderColor="purple.400"
|
|
px={4}
|
|
onClick={handleContainerClick}
|
|
>
|
|
<Flex
|
|
direction={{ base: 'row', md: 'column' }}
|
|
align="center"
|
|
justify={{ base: 'space-between', md: 'stretch' }}
|
|
>
|
|
<BoxedNextImage
|
|
src={`/assets/roles/${role.role.toLowerCase()}.svg`}
|
|
alt={role.label}
|
|
height={{ base: 4, md: 14 }}
|
|
width={{ base: 4, md: 14 }}
|
|
mr={2}
|
|
/>
|
|
<Text
|
|
color="white"
|
|
fontWeight="bold"
|
|
casing="uppercase"
|
|
my={{ base: 0, md: 2 }}
|
|
>
|
|
{role.label}
|
|
</Text>
|
|
{!isMobile && <Text color="white">{role.description}</Text>}
|
|
<Spacer />
|
|
{isMobile && (
|
|
<InfoIcon
|
|
ml={2}
|
|
cursor="pointer"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setShowDetails(!showDetails);
|
|
}}
|
|
/>
|
|
)}
|
|
{selectionIndex != null && (
|
|
<Flex
|
|
w="100%"
|
|
justifyContent={{ base: 'end', md: 'space-between' }}
|
|
mt={{ base: 0, md: 4 }}
|
|
ml={2}
|
|
>
|
|
{numSelectedRoles != null &&
|
|
numSelectedRoles > 1 &&
|
|
(isMobile ? (
|
|
<Button
|
|
variant="solid"
|
|
textTransform="uppercase"
|
|
color="white"
|
|
bgColor="purple.200"
|
|
borderColor="purple.200"
|
|
size="xs"
|
|
whiteSpace="pre-wrap"
|
|
mr={2}
|
|
onClick={() => onSelect(role, selectionIndex !== 0)}
|
|
>
|
|
Make {selectionIndex === 0 ? 'Secondary' : 'Primary'}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
fontWeight="bold"
|
|
textTransform="uppercase"
|
|
color="purple.200"
|
|
borderColor="purple.200"
|
|
_hover={{
|
|
borderColor: 'transparent',
|
|
bgColor: 'blackAlpha.300',
|
|
}}
|
|
fontSize={{ md: '0.875rem', lg: '1rem' }}
|
|
borderWidth={2}
|
|
whiteSpace="pre-wrap"
|
|
onClick={() => onSelect(role, selectionIndex !== 0)}
|
|
>
|
|
Make {selectionIndex === 0 ? 'Secondary' : 'Primary'}
|
|
</Button>
|
|
))}
|
|
{isMobile ? (
|
|
<Button
|
|
variant="solid"
|
|
fontWeight="bold"
|
|
textTransform="uppercase"
|
|
color="white"
|
|
bgColor="red.500"
|
|
size="xs"
|
|
whiteSpace="pre-wrap"
|
|
onClick={handleRemoveClick}
|
|
>
|
|
<CloseIcon />
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
fontWeight="bold"
|
|
textTransform="uppercase"
|
|
color="red.500"
|
|
borderColor="red.500"
|
|
borderWidth={2}
|
|
_hover={{ color: 'white', bgColor: 'red.500' }}
|
|
fontSize={{ md: '0.875rem', lg: '1rem' }}
|
|
whiteSpace="pre-wrap"
|
|
onClick={handleRemoveClick}
|
|
>
|
|
Remove
|
|
</Button>
|
|
)}
|
|
</Flex>
|
|
)}
|
|
</Flex>
|
|
{showDetails && (
|
|
<Text color="white" mt={4}>
|
|
{role.description}
|
|
</Text>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|