Finished upgrading profile setup components

This commit is contained in:
Alec LaLonde
2023-02-07 13:28:31 -07:00
committed by Alec LaLonde
parent 9ea06eb49b
commit b2cd99a477
10 changed files with 476 additions and 334 deletions

View File

@@ -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>;

View File

@@ -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&#xAD;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: 'Its not possible to be available for negative time.',
},
max: {
value: 24 * 7,
message: `Theres 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&#xAD;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: 'Its 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>
);
};

View File

@@ -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)' }}

View File

@@ -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>
);
};

View File

@@ -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>
);
};

View File

@@ -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&#xAD;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>
);

View File

@@ -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>
);
};

View File

@@ -44,3 +44,15 @@ export const mutationComposeDBCreateProfileTimeZone = /* GraphQL */ `
}
}
`;
export const mutationComposeDBCreateProfileAvailability = /* GraphQL */ `
mutation ComposeDBCreateProfileAvailability(
$input: CreateProfileAvailabilityInput!
) {
createProfileAvailability(input: $input) {
document {
weeklyHours
}
}
}
`;

View File

@@ -2,3 +2,4 @@ export const composeDBDocumentProfileName = 'profileNameIndex';
export const composeDBDocumentProfileDescription = 'profileDescriptionIndex';
export const composeDBDocumentProfileDisposition = 'profileDispositionIndex';
export const composeDBDocumentProfileTimeZone = 'profileTimeZoneIndex';
export const composeDBDocumentProfileAvailability = 'profileAvailabilityIndex';

View File

@@ -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,
}: {