Create onboarding modal steps
This commit is contained in:
parent
58a1a13bfc
commit
8df2a99724
|
@ -0,0 +1,136 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { Button, Stack, Text, Avatar, Icon, Spinner } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isDefaultAvatar } from 'soapbox/utils/accounts';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
|
||||
interface IAvatarSelectionModal {
|
||||
onClose?(): void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const AvatarSelectionModal: React.FC<IAvatarSelectionModal> = ({ onClose, onNext }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { account } = useOwnAccount();
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>();
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [isDisabled, setDisabled] = React.useState<boolean>(true);
|
||||
const isDefault = account ? isDefaultAvatar(account.avatar) : false;
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const maxPixels = 400 * 400;
|
||||
const rawFile = event.target.files?.item(0);
|
||||
|
||||
if (!rawFile) return;
|
||||
|
||||
resizeImage(rawFile, maxPixels).then((file) => {
|
||||
const url = file ? URL.createObjectURL(file) : account?.avatar as string;
|
||||
|
||||
setSelectedFile(url);
|
||||
setSubmitting(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', rawFile);
|
||||
const credentials = dispatch(patchMe(formData));
|
||||
|
||||
Promise.all([credentials]).then(() => {
|
||||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
};
|
||||
return (
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className='border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.avatar.title' defaultMessage={'Choose a profile picture'} />
|
||||
{/* Colocar o titulo aqui */}
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='onboarding.avatar.subtitle' defaultMessage={'Just have fun with it.'} />
|
||||
{/* Colocar o subtitulo aqui */}
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className='relative mx-auto rounded-full bg-gray-200'>
|
||||
{account && (
|
||||
<Avatar src={selectedFile || account.avatar} size={175} />
|
||||
)}
|
||||
|
||||
{isSubmitting && (
|
||||
<div className='absolute inset-0 flex items-center justify-center rounded-full bg-white/80 dark:bg-primary-900/80'>
|
||||
<Spinner withText={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={openFilePicker}
|
||||
type='button'
|
||||
className={clsx({
|
||||
'absolute bottom-3 right-2 p-1 bg-primary-600 rounded-full ring-2 ring-white dark:ring-primary-900 hover:bg-primary-700': true,
|
||||
'opacity-50 pointer-events-none': isSubmitting,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/outline/plus.svg')} className='h-5 w-5 text-white' />
|
||||
</button>
|
||||
|
||||
<input type='file' className='hidden' ref={fileInput} onChange={handleFileChange} />
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' type='button' onClick={onNext} disabled={isDefault && isDisabled || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isDisabled && (
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default AvatarSelectionModal;
|
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { Button, Text, FormGroup, Stack, Textarea } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
interface IBioStep {
|
||||
onClose(): void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const BioStep: React.FC<IBioStep> = ({ onClose, onNext }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { account } = useOwnAccount();
|
||||
const [value, setValue] = React.useState<string>(account?.source?.note ?? '');
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [errors, setErrors] = React.useState<string[]>([]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ note: value }));
|
||||
|
||||
Promise.all([credentials])
|
||||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className='bg-grey-500 border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.note.title' defaultMessage='Write a short bio' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='onboarding.note.subtitle' defaultMessage='You can always edit this later.' />
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<div className='mx-auto w-2/3'>
|
||||
<FormGroup
|
||||
hintText={<FormattedMessage id='onboarding.bio.hint' defaultMessage='Max 500 characters' />}
|
||||
labelText={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Textarea
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={intl.formatMessage(messages.bioPlaceholder)}
|
||||
value={value}
|
||||
maxLength={500}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' type='button' onClick={handleSubmit} disabled={isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default BioStep;
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
interface ICompletedModal {
|
||||
onClose?(): void;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
const CompletedModal: React.FC<ICompletedModal> = ({ onClose, onComplete }) => {
|
||||
return (
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' onClick={onClose} />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className=''>
|
||||
<Icon strokeWidth={1} src={require('@tabler/icons/outline/confetti.svg')} className='mx-auto h-16 w-16 text-primary-600 dark:text-primary-400' />
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.finished.title' defaultMessage='Onboarding complete' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='onboarding.finished.message'
|
||||
defaultMessage='We are very excited to welcome you to our community! Tap the button below to get started.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' alignItems='center' className='w-full'>
|
||||
<div className='w-2/3' />
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' onClick={onComplete}>
|
||||
<FormattedMessage id='onboarding.view_feed' defaultMessage='View Feed' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CompletedModal;
|
|
@ -0,0 +1,161 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { Button, Stack, Text, Avatar, Icon, Spinner } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
import { isDefaultHeader } from 'soapbox/utils/accounts';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
interface ICoverPhotoSelectionModal {
|
||||
onClose?(): void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const CoverPhotoSelectionModal: React.FC<ICoverPhotoSelectionModal> = ({ onClose, onNext }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { account } = useOwnAccount();
|
||||
|
||||
const fileInput = React.useRef<HTMLInputElement>(null);
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>();
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [isDisabled, setDisabled] = React.useState<boolean>(true);
|
||||
const isDefault = account ? isDefaultHeader(account.avatar) : false;
|
||||
|
||||
const openFilePicker = () => {
|
||||
fileInput.current?.click();
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const maxPixels = 1920 * 1080;
|
||||
const rawFile = event.target.files?.item(0);
|
||||
|
||||
if (!rawFile) return;
|
||||
|
||||
resizeImage(rawFile, maxPixels).then((file) => {
|
||||
const url = file ? URL.createObjectURL(file) : account?.header as string;
|
||||
|
||||
setSelectedFile(url);
|
||||
setSubmitting(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('header', file);
|
||||
const credentials = dispatch(patchMe(formData));
|
||||
|
||||
Promise.all([credentials]).then(() => {
|
||||
setDisabled(false);
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
setSubmitting(false);
|
||||
setDisabled(false);
|
||||
setSelectedFile(null);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
toast.error((error.response.data as any).error.replace('Validation failed: ', ''));
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className='bg-grey-500 border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.header.title' defaultMessage='Pick a cover image' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='onboarding.header.subtitle' defaultMessage='This will be shown at the top of your profile.' />
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full'>
|
||||
<div className='w-2/3 rounded-lg border border-solid border-gray-200 dark:border-gray-800'>
|
||||
<div
|
||||
role='button'
|
||||
className='relative flex h-24 w-full items-center justify-center rounded-t-md bg-gray-200 dark:bg-gray-800'
|
||||
>
|
||||
<div className='flex h-24 w-full overflow-hidden rounded-t-md'>
|
||||
{selectedFile || account?.header && (
|
||||
<StillImage
|
||||
src={selectedFile || account.header}
|
||||
alt={intl.formatMessage(messages.header)}
|
||||
className='absolute inset-0 w-full rounded-t-md object-cover'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isSubmitting && (
|
||||
<div
|
||||
className='absolute inset-0 flex items-center justify-center rounded-t-md bg-white/80 dark:bg-primary-900/80'
|
||||
>
|
||||
<Spinner withText={false} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={openFilePicker}
|
||||
type='button'
|
||||
className={clsx({
|
||||
'absolute -top-3 -right-3 p-1 bg-primary-600 rounded-full ring-2 ring-white dark:ring-primary-900 hover:bg-primary-700': true,
|
||||
'opacity-50 pointer-events-none': isSubmitting,
|
||||
})}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/outline/plus.svg')} className='h-5 w-5 text-white' />
|
||||
</button>
|
||||
|
||||
<input type='file' className='hidden' ref={fileInput} onChange={handleFileChange} />
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col px-4 pb-4'>
|
||||
{account && (
|
||||
<Avatar src={account.avatar} size={64} className='-mt-8 mb-2 ring-2 ring-white dark:ring-primary-800' />
|
||||
)}
|
||||
|
||||
<Text weight='bold' size='sm'>{account?.display_name}</Text>
|
||||
<Text theme='muted' size='sm'>@{account?.username}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' type='button' onClick={onNext} disabled={isDefault && isDisabled || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CoverPhotoSelectionModal;
|
|
@ -0,0 +1,116 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { Button, Stack, Text, FormGroup, Input } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
const messages = defineMessages({
|
||||
usernamePlaceholder: { id: 'onboarding.display_name.placeholder', defaultMessage: 'Eg. John Smith' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
interface IDisplayNameStep {
|
||||
onClose?(): void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const DisplayNameStep: React.FC<IDisplayNameStep> = ({ onClose, onNext }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { account } = useOwnAccount();
|
||||
const [value, setValue] = React.useState<string>(account?.display_name || '');
|
||||
const [isSubmitting, setSubmitting] = React.useState<boolean>(false);
|
||||
const [errors, setErrors] = React.useState<string[]>([]);
|
||||
|
||||
const trimmedValue = value.trim();
|
||||
const isValid = trimmedValue.length > 0;
|
||||
const isDisabled = !isValid || value.length > 30;
|
||||
|
||||
const hintText = React.useMemo(() => {
|
||||
const charsLeft = 30 - value.length;
|
||||
const suffix = charsLeft === 1 ? 'character remaining' : 'characters remaining';
|
||||
|
||||
return `${charsLeft} ${suffix}`;
|
||||
}, [value]);
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitting(true);
|
||||
|
||||
const credentials = dispatch(patchMe({ display_name: value }));
|
||||
|
||||
Promise.all([credentials])
|
||||
.then(() => {
|
||||
setSubmitting(false);
|
||||
onNext();
|
||||
}).catch((error: AxiosError) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
toast.error(messages.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className='bg-grey-500 border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.display_name.title' defaultMessage='Choose a display name' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='onboarding.display_name.subtitle' defaultMessage='You can always edit this later.' />
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Stack space={5} justifyContent='center' alignItems='center' className='w-full'>
|
||||
<div className='w-2/3'>
|
||||
<FormGroup
|
||||
hintText={hintText}
|
||||
labelText={<FormattedMessage id='onboarding.display_name.label' defaultMessage='Display name' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Input
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder={intl.formatMessage(messages.usernamePlaceholder)}
|
||||
type='text'
|
||||
value={value}
|
||||
maxLength={30}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' type='button' onClick={handleSubmit} disabled={isDisabled || isSubmitting}>
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default DisplayNameStep;
|
|
@ -0,0 +1,111 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Stack, Text } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useOnboardingSuggestions } from 'soapbox/queries/suggestions';
|
||||
|
||||
const closeIcon = require('@tabler/icons/outline/x.svg');
|
||||
|
||||
interface ICoverPhotoSelectionModal {
|
||||
onClose?(): void;
|
||||
onNext: () => void;
|
||||
}
|
||||
|
||||
const CoverPhotoSelectionModal: React.FC<ICoverPhotoSelectionModal> = ({ onClose, onNext }) => {
|
||||
const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
|
||||
|
||||
const handleLoadMore = debounce(() => {
|
||||
if (isFetching) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetchNextPage();
|
||||
}, 300);
|
||||
|
||||
const renderSuggestions = () => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className='flex flex-col sm:pb-10 sm:pt-4'>
|
||||
<ScrollableList
|
||||
isLoading={isFetching}
|
||||
scrollKey='suggestions'
|
||||
onLoadMore={handleLoadMore}
|
||||
hasMore={hasNextPage}
|
||||
useWindowScroll={false}
|
||||
style={{ height: 320 }}
|
||||
>
|
||||
{data.map((suggestion) => (
|
||||
<div key={suggestion.account.id} className='py-2'>
|
||||
<AccountContainer
|
||||
id={suggestion.account.id}
|
||||
showProfileHoverCard={false}
|
||||
withLinkToProfile={false}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderEmpty = () => {
|
||||
return (
|
||||
<div className='my-2 rounded-lg bg-primary-50 p-8 text-center dark:bg-gray-800'>
|
||||
<Text>
|
||||
<FormattedMessage id='empty_column.follow_recommendations' defaultMessage='Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.' />
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBody = () => {
|
||||
if (!data || data.length === 0) {
|
||||
return renderEmpty();
|
||||
} else {
|
||||
return renderSuggestions();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<Stack space={10} justifyContent='center' alignItems='center' className='w-full rounded-3xl bg-white px-4 py-8 text-gray-900 shadow-lg black:bg-black sm:p-10 dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'>
|
||||
|
||||
<div className='relative w-full'>
|
||||
<IconButton src={closeIcon} onClick={onClose} className='absolute -top-[6%] right-[2%] text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200' />
|
||||
<Stack space={2} justifyContent='center' alignItems='center' className='bg-grey-500 border-grey-200 -mx-4 mb-4 border-b border-solid pb-4 sm:-mx-10 sm:pb-10 dark:border-gray-800'>
|
||||
<Text size='2xl' align='center' weight='bold'>
|
||||
<FormattedMessage id='onboarding.suggestions.title' defaultMessage='Suggested accounts' />
|
||||
</Text>
|
||||
<Text theme='muted' align='center'>
|
||||
<FormattedMessage id='onboarding.suggestions.subtitle' defaultMessage='Here are a few of the most popular accounts you might like.' />
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' alignItems='center' className='w-full'>
|
||||
<div className='w-2/3'>
|
||||
{renderBody()}
|
||||
</div>
|
||||
|
||||
<Stack justifyContent='center' space={2} className='w-2/3'>
|
||||
<Button block theme='primary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.done' defaultMessage='Done' />
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
<FormattedMessage id='onboarding.skip' defaultMessage='Skip for now' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default CoverPhotoSelectionModal;
|
Loading…
Reference in New Issue