mirror of
https://github.com/MetaFam/TheGame.git
synced 2026-04-24 03:00:09 -04:00
Finished upgrading profile setup components
This commit is contained in:
committed by
Alec LaLonde
parent
9ea06eb49b
commit
b2cd99a477
@@ -4,14 +4,19 @@ export const composeDBProfileFieldName = 'name';
|
||||
export const composeDBProfileFieldDescription = 'description';
|
||||
export const composeDBProfileFieldFiveColorDisposition = 'fiveColorDisposition';
|
||||
export const composeDBProfileFieldTimeZone = 'iana';
|
||||
export const composeDBProfileFieldAvailability = 'weeklyHours';
|
||||
export const composeDBProfileFieldExplorerType = 'explorerType';
|
||||
|
||||
export type ComposeDBFieldValue = string | number;
|
||||
|
||||
// Hasura to ComposeDB field mapping
|
||||
export const ProfileMapping = {
|
||||
name: composeDBProfileFieldName,
|
||||
description: composeDBProfileFieldDescription,
|
||||
colorMask: composeDBProfileFieldFiveColorDisposition,
|
||||
timeZone: composeDBProfileFieldTimeZone,
|
||||
availableHours: composeDBProfileFieldAvailability,
|
||||
explorerType: composeDBProfileFieldExplorerType,
|
||||
} as const;
|
||||
|
||||
export type ComposeDBField = Values<typeof ProfileMapping>;
|
||||
@@ -22,5 +27,13 @@ export type ComposeDBTimeZoneFullValue = {
|
||||
abbreviation?: string;
|
||||
};
|
||||
|
||||
export type ComposeDBProfileFieldMutationValue = ComposeDBField &
|
||||
ComposeDBTimeZoneFullValue;
|
||||
export type ComposeDBPayload = {
|
||||
[composeDBProfileFieldName]?: string;
|
||||
[composeDBProfileFieldDescription]?: string;
|
||||
[composeDBProfileFieldFiveColorDisposition]?: string;
|
||||
[composeDBProfileFieldTimeZone]?: ComposeDBTimeZoneFullValue;
|
||||
[composeDBProfileFieldAvailability]?: number;
|
||||
[composeDBProfileFieldExplorerType]?: string;
|
||||
};
|
||||
|
||||
export type ComposeDBPayloadValue = Values<ComposeDBPayload>;
|
||||
|
||||
@@ -5,72 +5,113 @@ import {
|
||||
InputRightAddon,
|
||||
Text,
|
||||
} from '@metafam/ds';
|
||||
import React from 'react';
|
||||
import { composeDBProfileFieldAvailability } from '@metafam/utils';
|
||||
import { mutationComposeDBCreateProfileAvailability } from 'graphql/composeDB/mutations/profile';
|
||||
import { composeDBDocumentProfileAvailability } from 'graphql/composeDB/queries/profile';
|
||||
import { usePlayerSetupSaveToComposeDB } from 'lib/hooks/usePlayerSetupSaveToComposeDB';
|
||||
import { useQueryFromComposeDB } from 'lib/hooks/useQueryFromComposeDB';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { ProfileWizardPane } from './ProfileWizardPane';
|
||||
import { WizardPaneCallbackProps } from './WizardPane';
|
||||
import { useShowToastOnQueryError } from './SetupProfile';
|
||||
import { WizardPane } from './WizardPane';
|
||||
|
||||
const field = composeDBProfileFieldAvailability;
|
||||
|
||||
export const SetupAvailability: React.FC = () => {
|
||||
const field = 'availableHours';
|
||||
const { error, result: existing } = useQueryFromComposeDB<string>({
|
||||
indexName: composeDBDocumentProfileAvailability,
|
||||
field,
|
||||
});
|
||||
|
||||
useShowToastOnQueryError(error);
|
||||
|
||||
const formMethods = useForm<{ [field]: string | undefined }>();
|
||||
const {
|
||||
watch,
|
||||
setValue,
|
||||
formState: { dirtyFields },
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
setValue(field, existing);
|
||||
}, [existing, setValue]);
|
||||
|
||||
const current = watch(field, existing);
|
||||
const dirty = current !== existing || !!dirtyFields[field];
|
||||
|
||||
const { onSubmit, status } = usePlayerSetupSaveToComposeDB<number>({
|
||||
mutationQuery: mutationComposeDBCreateProfileAvailability,
|
||||
isChanged: dirty,
|
||||
});
|
||||
|
||||
return (
|
||||
<ProfileWizardPane
|
||||
{...{ field }}
|
||||
title="Avail­ability"
|
||||
prompt="What is your weekly availability for any kind of freelance work?"
|
||||
>
|
||||
{({ register, errored = false }: WizardPaneCallbackProps<number>) => {
|
||||
const { ref: registerRef, ...props } = register(field, {
|
||||
valueAsNumber: true,
|
||||
min: {
|
||||
value: 0,
|
||||
message: 'It’s not possible to be available for negative time.',
|
||||
},
|
||||
max: {
|
||||
value: 24 * 7,
|
||||
message: `There’s only ${24 * 7} hours in a week.`,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
mb={10}
|
||||
maxW="10rem"
|
||||
margin="auto"
|
||||
borderColor="purple.700"
|
||||
sx={{
|
||||
':hover, :focus-within': {
|
||||
borderColor: errored ? 'red' : 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InputLeftElement>
|
||||
<Text as="span" role="img" aria-label="clock">
|
||||
🕛
|
||||
</Text>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="23…"
|
||||
pl={9}
|
||||
background="dark"
|
||||
borderTopEndRadius={0}
|
||||
borderBottomEndRadius={0}
|
||||
borderRight={0}
|
||||
_focus={errored ? { borderColor: 'red' } : undefined}
|
||||
autoFocus
|
||||
ref={(ref) => {
|
||||
ref?.focus();
|
||||
registerRef(ref);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<InputRightAddon bg="purpleBoxDark" color="white">
|
||||
<Text as="sup">hr</Text> ⁄ <Text as="sub">week</Text>
|
||||
</InputRightAddon>
|
||||
</InputGroup>
|
||||
);
|
||||
}}
|
||||
</ProfileWizardPane>
|
||||
<FormProvider {...formMethods}>
|
||||
<WizardPane<number>
|
||||
{...{ field, onSubmit, status }}
|
||||
title="Avail­ability"
|
||||
prompt="What is your weekly availability for any kind of freelance work?"
|
||||
>
|
||||
<SetupAvailabilityInput />
|
||||
</WizardPane>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const SetupAvailabilityInput: React.FC = () => {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
|
||||
const { ref: registerRef, ...props } = register(field, {
|
||||
valueAsNumber: true,
|
||||
min: {
|
||||
value: 0,
|
||||
message: 'It’s not possible to be available for negative time.',
|
||||
},
|
||||
max: {
|
||||
value: 24 * 7,
|
||||
message: `More than 24 * 7 hours a week? Wow! Care to share your secret? 😉`,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<InputGroup
|
||||
mb={10}
|
||||
maxW="10rem"
|
||||
margin="auto"
|
||||
borderColor="purple.700"
|
||||
sx={{
|
||||
':hover, :focus-within': {
|
||||
borderColor: errors[field] ? 'red' : 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<InputLeftElement>
|
||||
<Text as="span" role="img" aria-label="clock">
|
||||
🕛
|
||||
</Text>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="23…"
|
||||
pl={9}
|
||||
background="dark"
|
||||
borderTopEndRadius={0}
|
||||
borderBottomEndRadius={0}
|
||||
borderRight={0}
|
||||
_focus={errors[field] ? { borderColor: 'red' } : undefined}
|
||||
autoFocus
|
||||
ref={(ref) => {
|
||||
ref?.focus();
|
||||
registerRef(ref);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
<InputRightAddon bg="purpleBoxDark" color="white">
|
||||
<Text as="sup">hr</Text> ⁄ <Text as="sub">week</Text>
|
||||
</InputRightAddon>
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ const ColorButtons: React.FC<ColorButtonsProps> = ({
|
||||
<Wrap spacing={[3, 7]} maxW="60rem" w="100%" justify="center">
|
||||
{Object.entries(MaskImages)
|
||||
.reverse()
|
||||
.map(([bitString, image], idx) => {
|
||||
.map(([bitString, image]) => {
|
||||
const type = types[bitString];
|
||||
|
||||
if (!type) {
|
||||
@@ -100,12 +100,6 @@ const ColorButtons: React.FC<ColorButtonsProps> = ({
|
||||
);
|
||||
})
|
||||
}
|
||||
ref={(input) => {
|
||||
if (idx === 0 && !input?.getAttribute('focused-once')) {
|
||||
input?.focus();
|
||||
input?.setAttribute('focused-once', 'true');
|
||||
}
|
||||
}}
|
||||
transition="background 0.25s, filter 0.75s"
|
||||
bg={selected ? 'purpleBoxDark' : 'purpleBoxLight'}
|
||||
_hover={{ filter: 'hue-rotate(25deg)' }}
|
||||
|
||||
@@ -7,19 +7,80 @@ import {
|
||||
Stack,
|
||||
Text,
|
||||
} from '@metafam/ds';
|
||||
import { Maybe, Optional } from '@metafam/utils';
|
||||
import { composeDBProfileFieldExplorerType } from '@metafam/utils';
|
||||
import { ExplorerType } from 'graphql/autogen/types';
|
||||
import { mutationComposeDBCreateProfileDisposition } from 'graphql/composeDB/mutations/profile';
|
||||
import { composeDBDocumentProfileDisposition } from 'graphql/composeDB/queries/profile';
|
||||
import { getExplorerTypes } from 'graphql/queries/enums/getExplorerTypes';
|
||||
import { useWeb3 } from 'lib/hooks';
|
||||
import { usePlayerSetupSaveToComposeDB } from 'lib/hooks/usePlayerSetupSaveToComposeDB';
|
||||
import { useQueryFromComposeDB } from 'lib/hooks/useQueryFromComposeDB';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import { ProfileWizardPane } from './ProfileWizardPane';
|
||||
import { MaybeModalProps, WizardPaneCallbackProps } from './WizardPane';
|
||||
import { useShowToastOnQueryError } from './SetupProfile';
|
||||
import { MaybeModalProps, WizardPane } from './WizardPane';
|
||||
|
||||
const field = composeDBProfileFieldExplorerType;
|
||||
|
||||
export const SetupPlayerType: React.FC<MaybeModalProps> = ({
|
||||
onClose,
|
||||
buttonLabel,
|
||||
title = 'Player Type',
|
||||
}) => {
|
||||
const { connected } = useWeb3();
|
||||
const {
|
||||
error,
|
||||
result: existing,
|
||||
fetching,
|
||||
} = useQueryFromComposeDB<string>({
|
||||
indexName: composeDBDocumentProfileDisposition,
|
||||
field,
|
||||
});
|
||||
|
||||
useShowToastOnQueryError(error);
|
||||
|
||||
const formMethods = useForm<{ [field]: string | undefined }>();
|
||||
const {
|
||||
watch,
|
||||
setValue,
|
||||
formState: { dirtyFields },
|
||||
register,
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
setValue(field, existing);
|
||||
}, [existing, setValue]);
|
||||
|
||||
const current = watch(field, existing);
|
||||
const dirty = current !== existing || !!dirtyFields[field];
|
||||
|
||||
const { onSubmit, status } = usePlayerSetupSaveToComposeDB<string>({
|
||||
mutationQuery: mutationComposeDBCreateProfileDisposition,
|
||||
isChanged: dirty,
|
||||
});
|
||||
|
||||
return (
|
||||
<WizardPane
|
||||
{...{ field, onClose, onSubmit, status, buttonLabel }}
|
||||
title={title}
|
||||
prompt="Which one suits you best?"
|
||||
>
|
||||
<Center mt={5}>
|
||||
<Input type="hidden" {...register(field, {})} />
|
||||
<ExplorerTypes
|
||||
selectedType={current}
|
||||
setSelectedType={(newValue) => setValue(field, newValue)}
|
||||
disabled={!connected || fetching}
|
||||
/>
|
||||
</Center>
|
||||
</WizardPane>
|
||||
);
|
||||
};
|
||||
|
||||
export type ExplorerTypesType = {
|
||||
selectedType: Maybe<string>;
|
||||
setSelectedType: (
|
||||
arg: string | ((type: Optional<Maybe<string>>) => Maybe<string>),
|
||||
) => void;
|
||||
selectedType?: string;
|
||||
setSelectedType: (arg: string) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
@@ -82,29 +143,3 @@ export const ExplorerTypes: React.FC<ExplorerTypesType> = ({
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
export const SetupPlayerType: React.FC<MaybeModalProps> = ({
|
||||
onClose,
|
||||
buttonLabel,
|
||||
title = 'Player Type',
|
||||
}) => {
|
||||
const field = 'explorerTypeTitle';
|
||||
|
||||
return (
|
||||
<ProfileWizardPane
|
||||
{...{ field, onClose, buttonLabel }}
|
||||
title={title}
|
||||
prompt="Which one suits you best?"
|
||||
>
|
||||
{({ register, loading, current, setter }: WizardPaneCallbackProps) => (
|
||||
<Center mt={5}>
|
||||
<Input type="hidden" {...register(field, {})} />
|
||||
<ExplorerTypes
|
||||
selectedType={current}
|
||||
setSelectedType={setter}
|
||||
disabled={loading}
|
||||
/>
|
||||
</Center>
|
||||
)}
|
||||
</ProfileWizardPane>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -13,16 +13,24 @@ import {
|
||||
useBreakpointValue,
|
||||
} from '@metafam/ds';
|
||||
import { Maybe, Optional } from '@metafam/utils';
|
||||
import { useSetupFlow } from 'contexts/SetupContext';
|
||||
import {
|
||||
PlayerRole,
|
||||
useUpdatePlayerRolesMutation as useUpdateRoles,
|
||||
} from 'graphql/autogen/types';
|
||||
import { getPlayerRoles } from 'graphql/queries/enums/getRoles';
|
||||
import { useOverridableField, useUser } from 'lib/hooks';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import { useUser } from 'lib/hooks';
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||
import { isEmpty } from 'utils/objectHelpers';
|
||||
|
||||
import { WizardPane, WizardPaneCallbackProps } from './WizardPane';
|
||||
import { WizardPane } from './WizardPane';
|
||||
|
||||
export type RoleValue = string;
|
||||
|
||||
@@ -34,24 +42,19 @@ export type SetupRolesProps = {
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const field = 'roles';
|
||||
|
||||
export const SetupRoles: React.FC<SetupRolesProps> = ({
|
||||
choices: inputChoices = null,
|
||||
onClose,
|
||||
buttonLabel,
|
||||
title = 'Roles',
|
||||
}) => {
|
||||
const field = 'roles';
|
||||
const { user } = useUser();
|
||||
const [choices, setChoices] =
|
||||
useState<Maybe<Array<PlayerRole>>>(inputChoices);
|
||||
const { onNextPress } = useSetupFlow();
|
||||
const [choices, setChoices] = useState<Maybe<PlayerRole[]>>(inputChoices);
|
||||
const [, updateRoles] = useUpdateRoles();
|
||||
const { value: roles, setter: setRoles } = useOverridableField<Array<string>>(
|
||||
{
|
||||
field,
|
||||
loaded: !!user,
|
||||
},
|
||||
);
|
||||
const mobile = useBreakpointValue({ base: true, sm: false }) ?? false;
|
||||
const [status, setStatus] = useState<string | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchRoles = async () => {
|
||||
@@ -64,113 +67,121 @@ export const SetupRoles: React.FC<SetupRolesProps> = ({
|
||||
}
|
||||
}, [choices]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && setRoles && !roles) {
|
||||
setRoles(user.roles.map(({ role }) => role));
|
||||
const roles = useMemo(() => user?.roles.map(({ role }) => role), [user]);
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (values: Record<string, string[]>) => {
|
||||
if (values.roles) {
|
||||
setStatus('Writing to Hasura…');
|
||||
|
||||
const { error } = await updateRoles({
|
||||
[field]: values.roles.map((role, rank) => ({ rank, role })),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Unable to update roles. Error: ${error}`);
|
||||
}
|
||||
} else {
|
||||
setStatus('No Change. Skipping Save…');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
(onClose ?? onNextPress)();
|
||||
},
|
||||
[onClose, onNextPress, updateRoles],
|
||||
);
|
||||
|
||||
const formMethods = useForm<{ [field]: string[] }>();
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<WizardPane
|
||||
{...{ field, onClose, onSubmit, status, buttonLabel }}
|
||||
title={title}
|
||||
prompt={
|
||||
<Text mb={[4, 6]} textAlign="center">
|
||||
Unlike other role-playing games, in MetaGame a player is free to
|
||||
take multiple roles at the same time.
|
||||
</Text>
|
||||
}
|
||||
fetching={!user}
|
||||
>
|
||||
<SetupRolesInput {...{ choices, roles }} />
|
||||
</WizardPane>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
type SetupRolesInputProps = {
|
||||
choices: Maybe<PlayerRole[]>;
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
const SetupRolesInput: React.FC<SetupRolesInputProps> = ({
|
||||
choices,
|
||||
roles,
|
||||
}) => {
|
||||
const { register, setValue: setter, watch } = useFormContext();
|
||||
const mobile = useBreakpointValue({ base: true, sm: false }) ?? false;
|
||||
|
||||
const current = watch(field, roles) as Maybe<string[]>;
|
||||
|
||||
if (!choices) {
|
||||
return <Text>Loading Role Choices…</Text>;
|
||||
}
|
||||
|
||||
const availableRoles =
|
||||
choices
|
||||
?.filter(({ role, basic }) => !current?.includes(role) && basic)
|
||||
.map(({ role }) => role) ?? [];
|
||||
|
||||
const select = ({ role }: PlayerRole, isPrimary?: boolean) => {
|
||||
let out = null;
|
||||
const otherRoles = current?.filter((r) => r !== role) ?? [];
|
||||
if (isPrimary || isEmpty(otherRoles)) {
|
||||
out = [role, ...otherRoles];
|
||||
} else {
|
||||
out = [...otherRoles, role];
|
||||
}
|
||||
}, [user, setRoles, roles]);
|
||||
setter(field, out);
|
||||
};
|
||||
|
||||
const onSave = async ({
|
||||
values,
|
||||
setStatus,
|
||||
}: {
|
||||
values: Record<string, unknown>;
|
||||
setStatus: (msg: string) => void;
|
||||
}) => {
|
||||
const { roles: toSet } = values as { ['roles']: Array<string> };
|
||||
|
||||
setStatus('Writing to Hasura…');
|
||||
|
||||
const { error } = await updateRoles({
|
||||
[field]: toSet.map((role, rank) => ({ rank, role })),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Unable to update roles. Error: ${error}`);
|
||||
}
|
||||
|
||||
if (setRoles) {
|
||||
setStatus('Setting Local State…');
|
||||
setRoles(toSet);
|
||||
const remove = ({ role }: PlayerRole) => {
|
||||
if (current) {
|
||||
const out = current.filter((r) => r !== role);
|
||||
setter(field, out);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<WizardPane<Array<string>>
|
||||
{...{ field, onClose, onSave, buttonLabel }}
|
||||
value={roles}
|
||||
title={title}
|
||||
prompt={
|
||||
<Text mb={[4, 6]} textAlign="center">
|
||||
Unlike other role-playing games, in MetaGame a player is free to take
|
||||
multiple roles at the same time.
|
||||
</Text>
|
||||
}
|
||||
fetching={!user}
|
||||
>
|
||||
{({
|
||||
register,
|
||||
current,
|
||||
setter,
|
||||
}: WizardPaneCallbackProps<Array<string>>) => {
|
||||
if (!choices) {
|
||||
return <Text>Loading Role Choices…</Text>;
|
||||
}
|
||||
|
||||
if (!current) return null;
|
||||
|
||||
const availableRoles =
|
||||
choices
|
||||
?.filter(({ role, basic }) => !current?.includes(role) && basic)
|
||||
.map(({ role }) => role) ?? [];
|
||||
|
||||
const select = ({ role }: PlayerRole, isPrimary?: boolean) => {
|
||||
if (current) {
|
||||
let out = null;
|
||||
const otherRoles = current.filter((r) => r !== role);
|
||||
if (isPrimary || isEmpty(otherRoles)) {
|
||||
out = [role, ...otherRoles];
|
||||
} else {
|
||||
out = [...otherRoles, role];
|
||||
}
|
||||
setter(out);
|
||||
}
|
||||
};
|
||||
|
||||
const remove = ({ role }: PlayerRole) => {
|
||||
if (current) {
|
||||
const out = current.filter((r) => r !== role);
|
||||
setter(out);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack mb={[4, 8]} align="center" w="100%">
|
||||
<Input type="hidden" {...register(field, {})} />
|
||||
<RoleGroup
|
||||
title="Primary Role"
|
||||
active={true}
|
||||
primary={true}
|
||||
roles={current.slice(0, 1)}
|
||||
numSelectedRoles={current.length}
|
||||
{...{ mobile, choices, select, remove }}
|
||||
/>
|
||||
<RoleGroup
|
||||
title="Secondary Role"
|
||||
active={true}
|
||||
roles={current.slice(1)}
|
||||
numSelectedRoles={current.length}
|
||||
{...{ mobile, choices, select, remove }}
|
||||
/>
|
||||
<RoleGroup
|
||||
title="Available Role"
|
||||
roles={availableRoles}
|
||||
{...{ mobile, choices, select }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
}}
|
||||
</WizardPane>
|
||||
<Stack mb={[4, 8]} align="center" w="100%">
|
||||
<Input type="hidden" {...register(field, {})} />
|
||||
{current ? (
|
||||
<>
|
||||
<RoleGroup
|
||||
title="Primary Role"
|
||||
active={true}
|
||||
primary={true}
|
||||
roles={current.slice(0, 1)}
|
||||
numSelectedRoles={current.length}
|
||||
{...{ mobile, choices, select, remove }}
|
||||
/>
|
||||
<RoleGroup
|
||||
title="Secondary Role"
|
||||
active={true}
|
||||
roles={current.slice(1)}
|
||||
numSelectedRoles={current.length}
|
||||
{...{ mobile, choices, select, remove }}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<RoleGroup
|
||||
title="Available Role"
|
||||
roles={availableRoles}
|
||||
{...{ mobile, choices, select }}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -8,15 +8,23 @@ import {
|
||||
Spinner,
|
||||
Text,
|
||||
} from '@metafam/ds';
|
||||
import { Maybe } from '@metafam/utils';
|
||||
import { useSetupFlow } from 'contexts/SetupContext';
|
||||
import {
|
||||
Player,
|
||||
SkillCategory_Enum,
|
||||
useUpdatePlayerSkillsMutation,
|
||||
} from 'graphql/autogen/types';
|
||||
import { getSkills } from 'graphql/queries/enums/getSkills';
|
||||
import { SkillColors } from 'graphql/types';
|
||||
import { useMounted, useOverridableField, useUser } from 'lib/hooks';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { FormProvider, useForm, useFormContext } from 'react-hook-form';
|
||||
import { useMounted, useUser } from 'lib/hooks';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
FormProvider,
|
||||
useForm,
|
||||
useFormContext,
|
||||
UseFormSetValue,
|
||||
} from 'react-hook-form';
|
||||
import { CategoryOption, parseSkills, SkillOption } from 'utils/skillHelpers';
|
||||
|
||||
import { MaybeModalProps, WizardPane } from './WizardPane';
|
||||
@@ -75,23 +83,18 @@ export const SetupSkills: React.FC<MaybeModalProps> = ({
|
||||
buttonLabel,
|
||||
title = 'Skills',
|
||||
}) => {
|
||||
const mounted = useMounted();
|
||||
const [choices, setChoices] = useState<Array<CategoryOption>>();
|
||||
const { user } = useUser();
|
||||
const { value: strippedSkills, setter: setValue } = useOverridableField<
|
||||
Array<SkillOption>
|
||||
>({
|
||||
field,
|
||||
loaded: !!user,
|
||||
});
|
||||
const { onNextPress } = useSetupFlow();
|
||||
const modal = !!onClose;
|
||||
const [, updateSkills] = useUpdatePlayerSkillsMutation();
|
||||
const [status, setStatus] = useState<string | undefined>();
|
||||
const [choices, setChoices] = useState<CategoryOption[]>();
|
||||
const skills = useMemo(
|
||||
() =>
|
||||
strippedSkills?.map(
|
||||
user?.skills?.map(
|
||||
(skill) =>
|
||||
({
|
||||
...skill,
|
||||
...skill.Skill,
|
||||
get label() {
|
||||
return this.name;
|
||||
},
|
||||
@@ -100,22 +103,9 @@ export const SetupSkills: React.FC<MaybeModalProps> = ({
|
||||
},
|
||||
} as SkillOption),
|
||||
),
|
||||
[strippedSkills],
|
||||
[user],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && setValue && choices && !skills) {
|
||||
if (user.skills.length > 0) {
|
||||
const options = choices.map(({ options: opts }) => opts).flat();
|
||||
setValue(
|
||||
user.skills.map(({ Skill: { id: sid } }) =>
|
||||
options.find(({ id: cid }) => sid === cid),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [choices, setValue, user, skills]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSkills = async () => {
|
||||
const skillChoices = await getSkills();
|
||||
@@ -125,56 +115,83 @@ export const SetupSkills: React.FC<MaybeModalProps> = ({
|
||||
fetchSkills();
|
||||
}, []);
|
||||
|
||||
const onSubmit = async ({
|
||||
values: { skills: skillList },
|
||||
setStatus,
|
||||
}: {
|
||||
values: Record<string, unknown>;
|
||||
setStatus?: (msg: string) => void;
|
||||
}) => {
|
||||
setStatus?.('Writing to Hasura…');
|
||||
const onSubmit = useCallback(
|
||||
async (values: Record<string, SkillOption[]>) => {
|
||||
if (values.skills) {
|
||||
setStatus?.('Writing to Hasura…');
|
||||
|
||||
const { error } = await updateSkills({
|
||||
skills: (skillList as Array<SkillOption>).map(({ id }) => ({
|
||||
skill_id: id,
|
||||
})),
|
||||
});
|
||||
const { error } = await updateSkills({
|
||||
skills: values.skills.map(({ id }) => ({
|
||||
skill_id: id,
|
||||
})),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`Unable to update skills. Error: ${error}`);
|
||||
}
|
||||
if (error) {
|
||||
throw new Error(`Unable to update skills. Error: ${error}`);
|
||||
}
|
||||
} else {
|
||||
setStatus('No Change. Skipping Save…');
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10);
|
||||
});
|
||||
}
|
||||
(onClose ?? onNextPress)();
|
||||
},
|
||||
[onClose, onNextPress, updateSkills],
|
||||
);
|
||||
|
||||
if (setValue) {
|
||||
setStatus?.('Setting Local State…');
|
||||
setValue(skillList);
|
||||
}
|
||||
};
|
||||
|
||||
const formMethods = useForm<{ [field]: string | undefined }>();
|
||||
const formMethods = useForm<{ [field]: SkillOption[] }>();
|
||||
const { setValue } = formMethods;
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<WizardPane
|
||||
{...{ field, onClose, onSubmit, buttonLabel }}
|
||||
title={title}
|
||||
<WizardPane<SkillOption[]>
|
||||
{...{ field, onSubmit, status, title, buttonLabel }}
|
||||
prompt="What are your super­powers?"
|
||||
fetching={!user}
|
||||
>
|
||||
<SetupSkillsInput />
|
||||
<SetupSkillsInput {...{ user, skills, setValue, modal, choices }} />
|
||||
</WizardPane>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const SetupSkillsInput: React.FC = () => {
|
||||
const { register } = useFormContext();
|
||||
const { ref: registerRef, onChange, ...props } = register(field, {});
|
||||
type SetupSkillInputProps = {
|
||||
user: Maybe<Player>;
|
||||
setValue: UseFormSetValue<{ skills: SkillOption[] }>;
|
||||
modal: boolean;
|
||||
choices?: CategoryOption[];
|
||||
skills?: SkillOption[];
|
||||
};
|
||||
|
||||
if (choices == null || !mounted) {
|
||||
const SetupSkillsInput: React.FC<SetupSkillInputProps> = ({
|
||||
user,
|
||||
skills,
|
||||
setValue,
|
||||
modal,
|
||||
choices,
|
||||
}) => {
|
||||
const mounted = useMounted();
|
||||
const { watch } = useFormContext();
|
||||
|
||||
const current = watch(field, skills);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && setValue && choices && !skills) {
|
||||
if (user.skills.length > 0) {
|
||||
const options = choices.map(({ options: opts }) => opts).flat();
|
||||
const selections = user.skills.map(({ Skill: { id: sid } }) =>
|
||||
options.find(({ id: cid }) => sid === cid),
|
||||
) as SkillOption[];
|
||||
setValue(field, selections);
|
||||
}
|
||||
}
|
||||
}, [choices, setValue, user, skills]);
|
||||
|
||||
if (user == null || choices == null || !mounted) {
|
||||
return (
|
||||
<Flex w="full" align="center" justify="center">
|
||||
<Spinner />
|
||||
<Text>Loading Options…</Text>
|
||||
<Text>Loading Skills…</Text>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -185,8 +202,10 @@ const SetupSkillsInput: React.FC = () => {
|
||||
isMulti
|
||||
{...{ styles }}
|
||||
onChange={(newValue) => {
|
||||
const values = newValue as unknown as Array<SkillOption>;
|
||||
setter(values);
|
||||
if (setValue) {
|
||||
const values = newValue as unknown as Array<SkillOption>;
|
||||
setValue(field, values);
|
||||
}
|
||||
}}
|
||||
options={choices as LabeledOptions<string>[]}
|
||||
value={current}
|
||||
@@ -195,7 +214,7 @@ const SetupSkillsInput: React.FC = () => {
|
||||
placeholder="Add your skills…"
|
||||
menuShouldScrollIntoView={true}
|
||||
menuPlacement={modal ? 'auto' : 'top'}
|
||||
{...props}
|
||||
// {...props}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Wrap,
|
||||
WrapItem,
|
||||
} from '@metafam/ds';
|
||||
import { ComposeDBProfileFieldMutationValue, Maybe } from '@metafam/utils';
|
||||
import { ComposeDBPayloadValue, Maybe } from '@metafam/utils';
|
||||
import { ConnectToProgress } from 'components/ConnectToProgress';
|
||||
import { FlexContainer } from 'components/Container';
|
||||
import { HeadComponent } from 'components/Seo';
|
||||
@@ -32,13 +32,16 @@ export type WizardPanePromptProps = {
|
||||
prompt?: string | ReactElement;
|
||||
};
|
||||
|
||||
export type WizardPaneProps<T = ComposeDBProfileFieldMutationValue> =
|
||||
WizardPanePromptProps & {
|
||||
export type WizardPaneSubmitProps = {
|
||||
status?: Maybe<string | ReactElement>;
|
||||
buttonLabel?: string | ReactElement;
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
export type WizardPaneProps<T> = WizardPanePromptProps &
|
||||
WizardPaneSubmitProps & {
|
||||
field: string;
|
||||
buttonLabel?: string | ReactElement;
|
||||
onSubmit: (values: Record<string, T>) => Promise<void>;
|
||||
status?: Maybe<string | ReactElement>;
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -48,43 +51,37 @@ export type WizardPaneOnSaveProps = {
|
||||
setStatus: (msg: string) => void;
|
||||
};
|
||||
|
||||
export type PaneProps<T = ComposeDBProfileFieldMutationValue> =
|
||||
WizardPaneProps<T> & {
|
||||
fetching?: boolean;
|
||||
authenticating?: boolean;
|
||||
onSave?: ({
|
||||
query,
|
||||
values,
|
||||
setStatus,
|
||||
}: WizardPaneOnSaveProps) => Promise<void>;
|
||||
children: ReactNode;
|
||||
};
|
||||
export type PaneProps<T> = WizardPaneProps<T> & {
|
||||
fetching?: boolean;
|
||||
authenticating?: boolean;
|
||||
onSave?: ({
|
||||
query,
|
||||
values,
|
||||
setStatus,
|
||||
}: WizardPaneOnSaveProps) => Promise<void>;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const WizardPane = <T,>({
|
||||
field,
|
||||
title,
|
||||
prompt,
|
||||
buttonLabel,
|
||||
onSubmit,
|
||||
status,
|
||||
onClose,
|
||||
buttonLabel,
|
||||
onSubmit,
|
||||
fetching = false,
|
||||
children,
|
||||
}: PaneProps<T>) => {
|
||||
const { nextButtonLabel } = useSetupFlow();
|
||||
const { connecting, connected, chainId } = useWeb3();
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { errors, isValidating: validating },
|
||||
} = useFormContext();
|
||||
|
||||
if ((!connecting && !connected) || (chainId != null && chainId !== '0x1')) {
|
||||
return (
|
||||
<FlexContainer>
|
||||
<MetaHeading color="white">Wrong Chain</MetaHeading>
|
||||
<ConnectToProgress header="" />
|
||||
</FlexContainer>
|
||||
);
|
||||
const wrongChain = chainId != null && chainId !== '0x1';
|
||||
if ((!connecting && !connected) || wrongChain) {
|
||||
return <WalletNotConnected {...{ wrongChain }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -105,7 +102,7 @@ export const WizardPane = <T,>({
|
||||
<Spinner thickness="4px" speed="1.25s" size="lg" mr={4} />
|
||||
<Text>
|
||||
{(() => {
|
||||
if (!connected) return 'Connecting to Ceramic…';
|
||||
if (!connected) return 'Connecting wallet…';
|
||||
if (validating) return 'Validating…';
|
||||
return 'Loading Current Value…';
|
||||
})()}
|
||||
@@ -121,33 +118,20 @@ export const WizardPane = <T,>({
|
||||
</>
|
||||
</Box>
|
||||
</FormControl>
|
||||
|
||||
<Wrap align="center">
|
||||
<WrapItem>
|
||||
<StatusedSubmitButton
|
||||
px={[8, 12]}
|
||||
label={buttonLabel ?? nextButtonLabel}
|
||||
{...{ status }}
|
||||
/>
|
||||
</WrapItem>
|
||||
{onClose && (
|
||||
<WrapItem>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color="white"
|
||||
_hover={{ bg: '#FFFFFF11' }}
|
||||
_active={{ bg: '#FF000011' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
<WizardPaneSubmit {...{ status, onClose, buttonLabel }} />
|
||||
</FlexContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const WalletNotConnected: React.FC<{ wrongChain: boolean }> = ({
|
||||
wrongChain,
|
||||
}) => (
|
||||
<FlexContainer>
|
||||
{wrongChain ? <MetaHeading color="white">Wrong Chain</MetaHeading> : null}
|
||||
<ConnectToProgress header="" />
|
||||
</FlexContainer>
|
||||
);
|
||||
|
||||
export const WizardPanePrompt: React.FC<WizardPanePromptProps> = ({
|
||||
title,
|
||||
prompt,
|
||||
@@ -168,3 +152,35 @@ export const WizardPanePrompt: React.FC<WizardPanePromptProps> = ({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export const WizardPaneSubmit: React.FC<WizardPaneSubmitProps> = ({
|
||||
status,
|
||||
buttonLabel,
|
||||
onClose,
|
||||
}) => {
|
||||
const { nextButtonLabel } = useSetupFlow();
|
||||
return (
|
||||
<Wrap align="center">
|
||||
<WrapItem>
|
||||
<StatusedSubmitButton
|
||||
px={[8, 12]}
|
||||
label={buttonLabel ?? nextButtonLabel}
|
||||
{...{ status }}
|
||||
/>
|
||||
</WrapItem>
|
||||
{onClose && (
|
||||
<WrapItem>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClose}
|
||||
color="white"
|
||||
_hover={{ bg: '#FFFFFF11' }}
|
||||
_active={{ bg: '#FF000011' }}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,3 +44,15 @@ export const mutationComposeDBCreateProfileTimeZone = /* GraphQL */ `
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const mutationComposeDBCreateProfileAvailability = /* GraphQL */ `
|
||||
mutation ComposeDBCreateProfileAvailability(
|
||||
$input: CreateProfileAvailabilityInput!
|
||||
) {
|
||||
createProfileAvailability(input: $input) {
|
||||
document {
|
||||
weeklyHours
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -2,3 +2,4 @@ export const composeDBDocumentProfileName = 'profileNameIndex';
|
||||
export const composeDBDocumentProfileDescription = 'profileDescriptionIndex';
|
||||
export const composeDBDocumentProfileDisposition = 'profileDispositionIndex';
|
||||
export const composeDBDocumentProfileTimeZone = 'profileTimeZoneIndex';
|
||||
export const composeDBDocumentProfileAvailability = 'profileAvailabilityIndex';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComposeDBField, Optional } from '@metafam/utils';
|
||||
import { ComposeDBField, ComposeDBFieldValue, Optional } from '@metafam/utils';
|
||||
import { useComposeDB } from 'contexts/ComposeDBContext';
|
||||
import { ComposeDBDocumentQueryResult } from 'graphql/types';
|
||||
import { CeramicError } from 'lib/errors';
|
||||
@@ -9,7 +9,7 @@ const genericFetchError = new CeramicError(
|
||||
'An unexpected error occurred when querying Ceramic.',
|
||||
);
|
||||
// todo load from hasura as a fallback ?
|
||||
export const useQueryFromComposeDB = <T>({
|
||||
export const useQueryFromComposeDB = <T extends ComposeDBFieldValue>({
|
||||
indexName,
|
||||
field,
|
||||
}: {
|
||||
|
||||
Reference in New Issue
Block a user