349 lines
13 KiB
TypeScript
349 lines
13 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
import Toggle from 'react-toggle';
|
|
|
|
import {
|
|
changeEditEventApprovalRequired,
|
|
changeEditEventDescription,
|
|
changeEditEventEndTime,
|
|
changeEditEventHasEndTime,
|
|
changeEditEventName,
|
|
changeEditEventStartTime,
|
|
changeEditEventLocation,
|
|
uploadEventBanner,
|
|
undoUploadEventBanner,
|
|
submitEvent,
|
|
fetchEventParticipationRequests,
|
|
rejectEventParticipationRequest,
|
|
authorizeEventParticipationRequest,
|
|
cancelEventCompose,
|
|
} from 'soapbox/actions/events';
|
|
import { closeModal, openModal } from 'soapbox/actions/modals';
|
|
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
|
|
import LocationSearch from 'soapbox/components/location-search';
|
|
import { checkEventComposeContent } from 'soapbox/components/modal-root';
|
|
import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Textarea } from 'soapbox/components/ui';
|
|
import AccountContainer from 'soapbox/containers/account-container';
|
|
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule-form';
|
|
import BundleContainer from 'soapbox/features/ui/containers/bundle-container';
|
|
import { DatePicker } from 'soapbox/features/ui/util/async-components';
|
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
|
|
|
import UploadButton from './upload-button';
|
|
|
|
const messages = defineMessages({
|
|
eventNamePlaceholder: { id: 'compose_event.fields.name_placeholder', defaultMessage: 'Name' },
|
|
eventDescriptionPlaceholder: { id: 'compose_event.fields.description_placeholder', defaultMessage: 'Description' },
|
|
eventStartTimePlaceholder: { id: 'compose_event.fields.start_time_placeholder', defaultMessage: 'Event begins on…' },
|
|
eventEndTimePlaceholder: { id: 'compose_event.fields.end_time_placeholder', defaultMessage: 'Event ends on…' },
|
|
resetLocation: { id: 'compose_event.reset_location', defaultMessage: 'Reset location' },
|
|
edit: { id: 'compose_event.tabs.edit', defaultMessage: 'Edit details' },
|
|
pending: { id: 'compose_event.tabs.pending', defaultMessage: 'Manage requests' },
|
|
authorize: { id: 'compose_event.participation_requests.authorize', defaultMessage: 'Authorize' },
|
|
reject: { id: 'compose_event.participation_requests.reject', defaultMessage: 'Reject' },
|
|
confirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
|
|
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
|
|
});
|
|
|
|
interface IAccount {
|
|
eventId: string,
|
|
id: string,
|
|
participationMessage: string | null,
|
|
}
|
|
|
|
const Account: React.FC<IAccount> = ({ eventId, id, participationMessage }) => {
|
|
const intl = useIntl();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const handleAuthorize = () => {
|
|
dispatch(authorizeEventParticipationRequest(eventId, id));
|
|
};
|
|
|
|
const handleReject = () => {
|
|
dispatch(rejectEventParticipationRequest(eventId, id));
|
|
};
|
|
|
|
return (
|
|
<AccountContainer
|
|
id={id}
|
|
note={participationMessage || undefined}
|
|
action={
|
|
<HStack space={2}>
|
|
<Button
|
|
theme='secondary'
|
|
size='sm'
|
|
text={intl.formatMessage(messages.authorize)}
|
|
onClick={handleAuthorize}
|
|
/>
|
|
<Button
|
|
theme='danger'
|
|
size='sm'
|
|
text={intl.formatMessage(messages.reject)}
|
|
onClick={handleReject}
|
|
/>
|
|
</HStack>
|
|
}
|
|
/>
|
|
);
|
|
};
|
|
|
|
interface IComposeEventModal {
|
|
onClose: (type?: string) => void,
|
|
}
|
|
|
|
const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
|
|
const intl = useIntl();
|
|
const dispatch = useAppDispatch();
|
|
|
|
const [tab, setTab] = useState<'edit' | 'pending'>('edit');
|
|
|
|
const banner = useAppSelector((state) => state.compose_event.banner);
|
|
const isUploading = useAppSelector((state) => state.compose_event.is_uploading);
|
|
|
|
const name = useAppSelector((state) => state.compose_event.name);
|
|
const description = useAppSelector((state) => state.compose_event.status);
|
|
const startTime = useAppSelector((state) => state.compose_event.start_time);
|
|
const endTime = useAppSelector((state) => state.compose_event.end_time);
|
|
const approvalRequired = useAppSelector((state) => state.compose_event.approval_required);
|
|
const location = useAppSelector((state) => state.compose_event.location);
|
|
|
|
const id = useAppSelector((state) => state.compose_event.id);
|
|
|
|
const isSubmitting = useAppSelector((state) => state.compose_event.is_submitting);
|
|
|
|
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
|
dispatch(changeEditEventName(target.value));
|
|
};
|
|
|
|
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
|
|
dispatch(changeEditEventDescription(target.value));
|
|
};
|
|
|
|
const onChangeStartTime = (date: Date) => {
|
|
dispatch(changeEditEventStartTime(date));
|
|
};
|
|
|
|
const onChangeEndTime = (date: Date) => {
|
|
dispatch(changeEditEventEndTime(date));
|
|
};
|
|
|
|
const onChangeHasEndTime: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
|
dispatch(changeEditEventHasEndTime(target.checked));
|
|
};
|
|
|
|
const onChangeApprovalRequired: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
|
dispatch(changeEditEventApprovalRequired(target.checked));
|
|
};
|
|
|
|
const onChangeLocation = (value: string | null) => {
|
|
dispatch(changeEditEventLocation(value));
|
|
};
|
|
|
|
const onClickClose = () => {
|
|
dispatch((dispatch, getState) => {
|
|
if (checkEventComposeContent(getState().compose_event)) {
|
|
dispatch(openModal('CONFIRM', {
|
|
icon: require('@tabler/icons/trash.svg'),
|
|
heading: id
|
|
? <FormattedMessage id='confirmations.cancel_event_editing.heading' defaultMessage='Cancel event editing' />
|
|
: <FormattedMessage id='confirmations.delete_event.heading' defaultMessage='Delete event' />,
|
|
message: id
|
|
? <FormattedMessage id='confirmations.cancel_event_editing.message' defaultMessage='Are you sure you want to cancel editing this event? All changes will be lost.' />
|
|
: <FormattedMessage id='confirmations.delete_event.message' defaultMessage='Are you sure you want to delete this event?' />,
|
|
confirm: intl.formatMessage(messages.confirm),
|
|
onConfirm: () => {
|
|
dispatch(closeModal('COMPOSE_EVENT'));
|
|
dispatch(cancelEventCompose());
|
|
},
|
|
}));
|
|
} else {
|
|
onClose('COMPOSE_EVENT');
|
|
}
|
|
});
|
|
};
|
|
|
|
const handleFiles = (files: FileList) => {
|
|
dispatch(uploadEventBanner(files[0], intl));
|
|
};
|
|
|
|
const handleClearBanner = () => {
|
|
dispatch(undoUploadEventBanner());
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
dispatch(submitEvent());
|
|
};
|
|
|
|
const accounts = useAppSelector((state) => state.user_lists.event_participation_requests.get(id!)?.items);
|
|
|
|
useEffect(() => {
|
|
if (id) dispatch(fetchEventParticipationRequests(id));
|
|
}, []);
|
|
|
|
const renderLocation = () => location && (
|
|
<HStack className='h-[38px] text-gray-700 dark:text-gray-500' alignItems='center' space={2}>
|
|
<Icon src={ADDRESS_ICONS[location.type] || require('@tabler/icons/map-pin.svg')} />
|
|
<Stack className='flex-grow'>
|
|
<Text>{location.description}</Text>
|
|
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
|
|
</Stack>
|
|
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@tabler/icons/x.svg')} onClick={() => onChangeLocation(null)} />
|
|
</HStack>
|
|
);
|
|
|
|
const renderTabs = () => {
|
|
const items = [
|
|
{
|
|
text: intl.formatMessage(messages.edit),
|
|
action: () => setTab('edit'),
|
|
name: 'edit',
|
|
},
|
|
{
|
|
text: intl.formatMessage(messages.pending),
|
|
action: () => setTab('pending'),
|
|
name: 'pending',
|
|
},
|
|
];
|
|
|
|
return <Tabs items={items} activeItem={tab} />;
|
|
};
|
|
|
|
let body;
|
|
if (tab === 'edit') body = (
|
|
<Form>
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
|
|
>
|
|
<div className='flex items-center justify-center bg-gray-200 dark:bg-gray-900/50 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden h-24 sm:h-32 relative'>
|
|
{banner ? (
|
|
<>
|
|
<img className='h-full w-full object-cover' src={banner.url} alt='' />
|
|
<IconButton className='absolute top-2 right-2' src={require('@tabler/icons/x.svg')} onClick={handleClearBanner} />
|
|
</>
|
|
) : (
|
|
<UploadButton disabled={isUploading} onSelectFile={handleFiles} />
|
|
)}
|
|
|
|
</div>
|
|
</FormGroup>
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.name_label' defaultMessage='Event name' />}
|
|
>
|
|
<Input
|
|
type='text'
|
|
placeholder={intl.formatMessage(messages.eventNamePlaceholder)}
|
|
value={name}
|
|
onChange={onChangeName}
|
|
/>
|
|
</FormGroup>
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
|
|
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
|
|
>
|
|
<Textarea
|
|
autoComplete='off'
|
|
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
|
|
value={description}
|
|
onChange={onChangeDescription}
|
|
/>
|
|
</FormGroup>
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.location_label' defaultMessage='Event location' />}
|
|
>
|
|
{location ? renderLocation() : (
|
|
<LocationSearch
|
|
onSelected={onChangeLocation}
|
|
/>
|
|
)}
|
|
</FormGroup>
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.start_time_label' defaultMessage='Event start date' />}
|
|
>
|
|
<BundleContainer fetchComponent={DatePicker}>
|
|
{Component => (<Component
|
|
showTimeSelect
|
|
dateFormat='MMMM d, yyyy h:mm aa'
|
|
timeIntervals={15}
|
|
wrapperClassName='react-datepicker-wrapper'
|
|
placeholderText={intl.formatMessage(messages.eventStartTimePlaceholder)}
|
|
filterDate={isCurrentOrFutureDate}
|
|
selected={startTime}
|
|
onChange={onChangeStartTime}
|
|
/>)}
|
|
</BundleContainer>
|
|
</FormGroup>
|
|
<HStack alignItems='center' space={2}>
|
|
<Toggle
|
|
icons={false}
|
|
checked={!!endTime}
|
|
onChange={onChangeHasEndTime}
|
|
/>
|
|
<Text tag='span' theme='muted'>
|
|
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has end date' />
|
|
</Text>
|
|
</HStack>
|
|
{endTime && (
|
|
<FormGroup
|
|
labelText={<FormattedMessage id='compose_event.fields.end_time_label' defaultMessage='Event end date' />}
|
|
>
|
|
<BundleContainer fetchComponent={DatePicker}>
|
|
{Component => (<Component
|
|
showTimeSelect
|
|
dateFormat='MMMM d, yyyy h:mm aa'
|
|
timeIntervals={15}
|
|
wrapperClassName='react-datepicker-wrapper'
|
|
placeholderText={intl.formatMessage(messages.eventEndTimePlaceholder)}
|
|
filterDate={isCurrentOrFutureDate}
|
|
selected={endTime}
|
|
onChange={onChangeEndTime}
|
|
/>)}
|
|
</BundleContainer>
|
|
</FormGroup>
|
|
)}
|
|
{!id && (
|
|
<HStack alignItems='center' space={2}>
|
|
<Toggle
|
|
icons={false}
|
|
checked={approvalRequired}
|
|
onChange={onChangeApprovalRequired}
|
|
/>
|
|
<Text tag='span' theme='muted'>
|
|
<FormattedMessage id='compose_event.fields.approval_required' defaultMessage='I want to approve participation requests manually' />
|
|
</Text>
|
|
</HStack>
|
|
)}
|
|
</Form>
|
|
);
|
|
else body = accounts ? (
|
|
<Stack space={3}>
|
|
{accounts.size > 0 ? (
|
|
accounts.map(({ account, participation_message }) =>
|
|
<Account key={account} eventId={id!} id={account} participationMessage={participation_message} />,
|
|
)
|
|
) : (
|
|
<FormattedMessage id='empty_column.event_participant_requests' defaultMessage='There are no pending event participation requests.' />
|
|
)}
|
|
</Stack>
|
|
) : <Spinner />;
|
|
|
|
return (
|
|
<Modal
|
|
title={id
|
|
? <FormattedMessage id='navigation_bar.compose_event' defaultMessage='Manage event' />
|
|
: <FormattedMessage id='navigation_bar.create_event' defaultMessage='Create new event' />}
|
|
confirmationAction={tab === 'edit' ? handleSubmit : undefined}
|
|
confirmationText={id
|
|
? <FormattedMessage id='compose_event.update' defaultMessage='Update' />
|
|
: <FormattedMessage id='compose_event.create' defaultMessage='Create' />}
|
|
confirmationDisabled={isSubmitting}
|
|
onClose={onClickClose}
|
|
>
|
|
<Stack space={2}>
|
|
{id && renderTabs()}
|
|
{body}
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default ComposeEventModal;
|