Merge branch 'create-group' into 'develop'

Create group

See merge request soapbox-pub/soapbox!2325
This commit is contained in:
Alex Gleason
2023-03-13 18:28:43 +00:00
12 changed files with 210 additions and 22 deletions

View File

@@ -5,6 +5,7 @@ import { submitGroupEditor } from 'soapbox/actions/groups';
import { Modal, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import ConfirmationStep from './steps/confirmation-step';
import DetailsStep from './steps/details-step';
import PrivacyStep from './steps/privacy-step';
@@ -12,16 +13,19 @@ const messages = defineMessages({
next: { id: 'manage_group.next', defaultMessage: 'Next' },
create: { id: 'manage_group.create', defaultMessage: 'Create' },
update: { id: 'manage_group.update', defaultMessage: 'Update' },
done: { id: 'manage_group.done', defaultMessage: 'Done' },
});
enum Steps {
ONE = 'ONE',
TWO = 'TWO',
THREE = 'THREE',
}
const manageGroupSteps = {
ONE: PrivacyStep,
TWO: DetailsStep,
THREE: ConfirmationStep,
};
interface IManageGroupModal {
@@ -33,21 +37,24 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
const dispatch = useAppDispatch();
const id = useAppSelector((state) => state.group_editor.groupId);
const [group, setGroup] = useState<any | null>(null);
const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting);
const [currentStep, setCurrentStep] = useState<Steps>(id ? Steps.TWO : Steps.ONE);
const onClickClose = () => {
const handleClose = () => {
onClose('MANAGE_GROUP');
};
const handleSubmit = () => {
dispatch(submitGroupEditor(true));
return dispatch(submitGroupEditor(true));
};
const confirmationText = useMemo(() => {
switch (currentStep) {
case Steps.THREE:
return intl.formatMessage(messages.done);
case Steps.TWO:
return intl.formatMessage(id ? messages.update : messages.create);
default:
@@ -61,8 +68,15 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
setCurrentStep(Steps.TWO);
break;
case Steps.TWO:
handleSubmit();
onClose();
handleSubmit()
.then((group) => {
setCurrentStep(Steps.THREE);
setGroup(group);
})
.catch(() => {});
break;
case Steps.THREE:
handleClose();
break;
default:
break;
@@ -80,10 +94,11 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
confirmationText={confirmationText}
confirmationDisabled={isSubmitting}
confirmationFullWidth
onClose={onClickClose}
onClose={handleClose}
>
<Stack space={2}>
<StepToRender />
{/* @ts-ignore */}
<StepToRender group={group} />
</Stack>
</Modal>
);

View File

@@ -0,0 +1,119 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Avatar, Divider, HStack, Stack, Text, Button } from 'soapbox/components/ui';
interface IConfirmationStep {
group: any
}
const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
const handleCopyLink = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(group.uri);
}
};
const handleShare = () => {
navigator.share({
text: group.display_name,
url: group.uri,
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
return (
<Stack space={9}>
<Stack space={3}>
<Stack>
<label
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow'
>
{group.header && <img className='h-full w-full object-cover' src={group.header} alt='' />}
</label>
<label className='mx-auto -mt-10 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{group.avatar && <Avatar src={group.avatar} size={80} />}
</label>
</Stack>
<Stack>
<Text size='2xl' weight='bold' align='center'>{group.display_name}</Text>
<Text size='md' className='mx-auto max-w-sm'>{group.note}</Text>
</Stack>
</Stack>
<Divider />
<Stack space={4}>
<Text size='3xl' weight='bold' align='center'>
<FormattedMessage id='manage_group.confirmation.title' defaultMessage='Youre all set!' />
</Text>
<Stack space={5}>
<InfoListItem number={1}>
<FormattedMessage
id='manage_group.confirmation.info_1'
defaultMessage='As the owner of this group, you can assign staff, delete posts and much more.'
/>
</InfoListItem>
<InfoListItem number={2}>
<FormattedMessage
id='manage_group.confirmation.info_2'
defaultMessage="Post the group's first post and get the conversation started."
/>
</InfoListItem>
<InfoListItem number={3}>
<FormattedMessage
id='manage_group.confirmation.info_3'
defaultMessage='Share your new group with friends, family and followers to grow its membership.'
/>
</InfoListItem>
</Stack>
</Stack>
<HStack space={2} justifyContent='center'>
{('share' in navigator) && (
<Button onClick={handleShare} theme='transparent' icon={require('@tabler/icons/share.svg')} className='text-primary-600'>
<FormattedMessage id='manage_group.confirmation.share' defaultMessage='Share this group' />
</Button>
)}
<Button onClick={handleCopyLink} theme='transparent' icon={require('@tabler/icons/link.svg')} className='text-primary-600'>
<FormattedMessage id='manage_group.confirmation.copy' defaultMessage='Copy link' />
</Button>
</HStack>
</Stack>
);
};
interface IInfoListNumber {
number: number
}
const InfoListNumber: React.FC<IInfoListNumber> = ({ number }) => {
return (
<div className='flex h-7 w-7 items-center justify-center rounded-full border border-gray-200'>
<Text theme='primary' size='sm' weight='bold'>{number}</Text>
</div>
);
};
interface IInfoListItem {
number: number
children: React.ReactNode
}
const InfoListItem: React.FC<IInfoListItem> = ({ number, children }) => {
return (
<HStack space={3}>
<div><InfoListNumber number={number} /></div>
<div>{children}</div>
</HStack>
);
};
export default ConfirmationStep;

View File

@@ -9,7 +9,7 @@ import {
} from 'soapbox/actions/groups';
import Icon from 'soapbox/components/icon';
import { Avatar, Form, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
import resizeImage from 'soapbox/utils/resize-image';
@@ -37,17 +37,17 @@ const HeaderPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
className={clsx('absolute top-0 h-full w-full transition-opacity', {
'opacity-0 hover:opacity-90 bg-primary-100 dark:bg-gray-800': src,
})}
space={3}
space={1.5}
alignItems='center'
justifyContent='center'
>
<Icon
src={require('@tabler/icons/photo-plus.svg')}
className='h-7 w-7'
className='h-4.5 w-4.5'
/>
<Text size='sm' theme='primary' weight='semibold' transform='uppercase'>
<FormattedMessage id='compose_event.upload_banner' defaultMessage='Upload photo' />
<Text size='md' theme='primary' weight='semibold'>
<FormattedMessage id='group.upload_banner' defaultMessage='Upload photo' />
</Text>
<input
@@ -65,8 +65,8 @@ const HeaderPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }) => {
return (
<label className='absolute left-1/2 bottom-0 h-[72px] w-[72px] -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{src && <Avatar src={src} size={72} />}
<label className='absolute left-1/2 bottom-0 h-20 w-20 -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{src && <Avatar src={src} size={80} />}
<HStack
alignItems='center'
justifyContent='center'
@@ -77,7 +77,7 @@ const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
>
<Icon
src={require('@tabler/icons/camera-plus.svg')}
className='h-7 w-7 text-white'
className='h-5 w-5 text-white'
/>
</HStack>
<span className='sr-only'>Upload avatar</span>
@@ -96,6 +96,7 @@ const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
const DetailsStep = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const instance = useInstance();
const groupId = useAppSelector((state) => state.group_editor.groupId);
const isUploading = useAppSelector((state) => state.group_editor.isUploading);
@@ -146,7 +147,6 @@ const DetailsStep = () => {
});
}, [groupId]);
return (
<Form>
<div className='relative mb-12 flex'>
@@ -161,6 +161,7 @@ const DetailsStep = () => {
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
value={name}
onChange={onChangeName}
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
/>
</FormGroup>
<FormGroup
@@ -171,6 +172,7 @@ const DetailsStep = () => {
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
/>
</FormGroup>
</Form>

View File

@@ -18,8 +18,13 @@ const PrivacyStep = () => {
return (
<>
<Stack className='mx-auto max-w-sm' space={2}>
<img
className='mx-auto w-32'
src={require('assets/images/group.svg')}
alt=''
/>
<Text size='3xl' weight='bold' align='center'>
<FormattedMessage id='manage_group.get_started' defaultMessage="Let's get started!" />
<FormattedMessage id='manage_group.get_started' defaultMessage='Lets get started!' />
</Text>
<Text size='lg' theme='muted' align='center'>
<FormattedMessage id='manage_group.tagline' defaultMessage='Groups connect you with others based on shared interests.' />