kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Delete onboarding-wizard
rodzic
f71abb4543
commit
2146d5ea03
|
@ -1,119 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import LandingGradient from 'soapbox/components/landing-gradient';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
import AvatarSelectionStep from './steps/avatar-selection-step';
|
||||
import BioStep from './steps/bio-step';
|
||||
import CompletedStep from './steps/completed-step';
|
||||
import CoverPhotoSelectionStep from './steps/cover-photo-selection-step';
|
||||
import DisplayNameStep from './steps/display-name-step';
|
||||
import FediverseStep from './steps/fediverse-step';
|
||||
import SuggestedAccountsStep from './steps/suggested-accounts-step';
|
||||
|
||||
const OnboardingWizard = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
|
||||
const [currentStep, setCurrentStep] = React.useState<number>(0);
|
||||
|
||||
const handleSwipe = (nextStep: number) => {
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
const handlePreviousStep = () => {
|
||||
setCurrentStep((prevStep) => Math.max(0, prevStep - 1));
|
||||
};
|
||||
|
||||
const handleNextStep = () => {
|
||||
setCurrentStep((prevStep) => Math.min(prevStep + 1, steps.length - 1));
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
const steps = [
|
||||
<AvatarSelectionStep onNext={handleNextStep} />,
|
||||
<DisplayNameStep onNext={handleNextStep} />,
|
||||
<BioStep onNext={handleNextStep} />,
|
||||
<CoverPhotoSelectionStep onNext={handleNextStep} />,
|
||||
<SuggestedAccountsStep onNext={handleNextStep} />,
|
||||
];
|
||||
|
||||
if (features.federating && !features.nostr) {
|
||||
steps.push(<FediverseStep onNext={handleNextStep} />);
|
||||
}
|
||||
|
||||
steps.push(<CompletedStep onComplete={handleComplete} />);
|
||||
|
||||
const handleKeyUp = ({ key }: KeyboardEvent): void => {
|
||||
switch (key) {
|
||||
case 'ArrowLeft':
|
||||
handlePreviousStep();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
handleNextStep();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDotClick = (nextStep: number) => {
|
||||
setCurrentStep(nextStep);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener('keyup', handleKeyUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', handleKeyUp);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-testid='onboarding-wizard'>
|
||||
<LandingGradient />
|
||||
|
||||
<main className='flex h-screen flex-col overflow-x-hidden'>
|
||||
<div className='flex h-full flex-col items-center justify-center'>
|
||||
<ReactSwipeableViews animateHeight index={currentStep} onChangeIndex={handleSwipe}>
|
||||
{steps.map((step, i) => (
|
||||
<div key={i} className='w-full max-w-[100vw] py-6 sm:mx-auto sm:max-w-lg md:max-w-2xl'>
|
||||
<div
|
||||
className={clsx({
|
||||
'transition-opacity ease-linear': true,
|
||||
'opacity-0 duration-500': currentStep !== i,
|
||||
'opacity-100 duration-75': currentStep === i,
|
||||
})}
|
||||
>
|
||||
{step}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ReactSwipeableViews>
|
||||
|
||||
<HStack space={3} alignItems='center' justifyContent='center' className='relative'>
|
||||
{steps.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
tabIndex={0}
|
||||
onClick={() => handleDotClick(i)}
|
||||
className={clsx({
|
||||
'w-5 h-5 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
||||
'bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-700/75 hover:bg-gray-400': i !== currentStep,
|
||||
'bg-primary-600': i === currentStep,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OnboardingWizard;
|
|
@ -1,125 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { BigCard } from 'soapbox/components/big-card';
|
||||
import { Avatar, Button, Icon, Spinner, Stack } from 'soapbox/components/ui';
|
||||
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 messages = defineMessages({
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
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);
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.avatar.title' defaultMessage='Choose a profile picture' />}
|
||||
subtitle={<FormattedMessage id='onboarding.avatar.subtitle' defaultMessage='Just have fun with it.' />}
|
||||
onClose={handleComplete}
|
||||
>
|
||||
<Stack space={10}>
|
||||
<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}>
|
||||
<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>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarSelectionStep;
|
|
@ -1,99 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { BigCard } from 'soapbox/components/big-card';
|
||||
import { Button, FormGroup, Stack, Textarea } from 'soapbox/components/ui';
|
||||
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 BioStep = ({ onNext }: { onNext: () => void }) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.note.title' defaultMessage='Write a short bio' />}
|
||||
subtitle={<FormattedMessage id='onboarding.note.subtitle' defaultMessage='You can always edit this later.' />}
|
||||
onClose={handleComplete}
|
||||
>
|
||||
<Stack space={5}>
|
||||
<div>
|
||||
<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>
|
||||
|
||||
<div>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{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>
|
||||
</div>
|
||||
</Stack>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default BioStep;
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
const CompletedStep = ({ onComplete }: { onComplete: () => void }) => (
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<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 className='mx-auto pt-10 sm:w-2/3 md:w-1/2'>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
onClick={onComplete}
|
||||
>
|
||||
<FormattedMessage id='onboarding.view_feed' defaultMessage='View Feed' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export default CompletedStep;
|
|
@ -1,149 +0,0 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { BigCard } from 'soapbox/components/big-card';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { Avatar, Button, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
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 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.' },
|
||||
});
|
||||
|
||||
const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
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.header) : 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);
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.header.title' defaultMessage='Pick a cover image' />}
|
||||
subtitle={<FormattedMessage id='onboarding.header.subtitle' defaultMessage='This will be shown at the top of your profile.' />}
|
||||
onClose={handleComplete}
|
||||
>
|
||||
<Stack space={10}>
|
||||
<div className='rounded-lg border border-solid border-gray-200 dark:border-gray-800'>
|
||||
<div
|
||||
role='button'
|
||||
className='relative flex h-24 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}>
|
||||
<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>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default CoverPhotoSelectionStep;
|
|
@ -1,107 +0,0 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { BigCard } from 'soapbox/components/big-card';
|
||||
import { Button, FormGroup, Input, Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
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.' },
|
||||
});
|
||||
|
||||
const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
return (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.display_name.title' defaultMessage='Choose a display name' />}
|
||||
subtitle={<FormattedMessage id='onboarding.display_name.subtitle' defaultMessage='You can always edit this later.' />}
|
||||
onClose={handleComplete}
|
||||
>
|
||||
<Stack space={5}>
|
||||
<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>
|
||||
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
type='submit'
|
||||
disabled={isDisabled || isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{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>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayNameStep;
|
|
@ -1,88 +0,0 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'soapbox/components/account';
|
||||
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useInstance, useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
const FediverseStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const { account } = useOwnAccount();
|
||||
const instance = useInstance();
|
||||
|
||||
return (
|
||||
<Card variant='rounded' size='xl'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<Icon strokeWidth={1} src={require('@tabler/icons/outline/affiliate.svg')} className='mx-auto h-16 w-16 text-primary-600 dark:text-primary-400' />
|
||||
|
||||
<Text size='2xl' weight='bold'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.title'
|
||||
defaultMessage='{siteTitle} is just one part of the Fediverse'
|
||||
values={{
|
||||
siteTitle: instance.title,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Stack space={4}>
|
||||
<div className='border-b border-solid border-gray-200 pb-2 sm:pb-5 dark:border-gray-800'>
|
||||
<Stack space={4}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.message'
|
||||
defaultMessage='The Fediverse is a social network made up of thousands of diverse and independently-run social media sites (aka "servers"). You can follow users — and like, repost, and reply to posts — from most other Fediverse servers, because they can communicate with {siteTitle}.'
|
||||
values={{
|
||||
siteTitle: instance.title,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.trailer'
|
||||
defaultMessage='Because it is distributed and anyone can run their own server, the Fediverse is resilient and open. If you choose to join another server or set up your own, you can interact with the same people and continue on the same social graph.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
{account && (
|
||||
<div className='rounded-lg bg-primary-50 p-4 text-center dark:bg-gray-800'>
|
||||
<Account account={account} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.its_you'
|
||||
defaultMessage='This is you! Other people can follow you from other servers by using your full @-handle.'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='onboarding.fediverse.other_instances'
|
||||
defaultMessage='When browsing your timeline, pay attention to the full username after the second @ symbol to know which server a post is from.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<div className='mx-auto pt-10 sm:w-2/3 md:w-1/2'>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
onClick={onNext}
|
||||
>
|
||||
<FormattedMessage id='onboarding.fediverse.next' defaultMessage='Next' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default FediverseStep;
|
|
@ -1,103 +0,0 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { endOnboarding } from 'soapbox/actions/onboarding';
|
||||
import { BigCard } from 'soapbox/components/big-card';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
import { useOnboardingSuggestions } from 'soapbox/queries/suggestions';
|
||||
|
||||
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
|
||||
|
||||
const handleLoadMore = debounce(() => {
|
||||
if (isFetching) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return fetchNextPage();
|
||||
}, 300);
|
||||
|
||||
const handleComplete = () => {
|
||||
dispatch(endOnboarding());
|
||||
};
|
||||
|
||||
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 (
|
||||
<BigCard
|
||||
title={<FormattedMessage id='onboarding.suggestions.title' defaultMessage='Suggested accounts' />}
|
||||
subtitle={<FormattedMessage id='onboarding.suggestions.subtitle' defaultMessage='Here are a few of the most popular accounts you might like.' />}
|
||||
onClose={handleComplete}
|
||||
>
|
||||
{renderBody()}
|
||||
|
||||
<Stack>
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button
|
||||
block
|
||||
theme='primary'
|
||||
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>
|
||||
</BigCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestedAccountsStep;
|
Ładowanie…
Reference in New Issue