Files
ncd-fe/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx
nicole mikołajczyk 61a5dbab2e pl-fe: more icons changes
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
2025-10-13 11:48:19 +02:00

285 lines
11 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { resetCompose } from 'pl-fe/actions/compose';
import {
cancelEventCompose,
initEventEdit,
submitEvent,
} from 'pl-fe/actions/events';
import { uploadFile } from 'pl-fe/actions/media';
import { fetchStatus } from 'pl-fe/actions/statuses';
import { ADDRESS_ICONS } from 'pl-fe/components/autosuggest-location';
import LocationSearch from 'pl-fe/components/location-search';
import Button from 'pl-fe/components/ui/button';
import Form from 'pl-fe/components/ui/form';
import FormActions from 'pl-fe/components/ui/form-actions';
import FormGroup from 'pl-fe/components/ui/form-group';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import IconButton from 'pl-fe/components/ui/icon-button';
import Input from 'pl-fe/components/ui/input';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import Toggle from 'pl-fe/components/ui/toggle';
import { isCurrentOrFutureDate } from 'pl-fe/features/compose/components/schedule-form';
import { ComposeEditor, DatePicker } from 'pl-fe/features/ui/util/async-components';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import toast from 'pl-fe/toast';
import UploadButton from '../components/upload-button';
import type { Location } from 'pl-api';
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' },
eventFetchFail: { id: 'compose_event.fetch_fail', defaultMessage: 'Failed to fetch edited event information' },
});
interface IEditEvent {
statusId: string | null;
}
const EditEvent: React.FC<IEditEvent> = ({ statusId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const history = useHistory();
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector((state) => statusId ? getStatus(state, { id: statusId }) : undefined);
const [name, setName] = useState(status?.event?.name || '');
const [text, setText] = useState('');
const [startTime, setStartTime] = useState(status?.event?.start_time ? new Date(status.event.start_time) : new Date());
const [endTime, setEndTime] = useState(status?.event?.end_time ? new Date(status.event.end_time) : null);
const [approvalRequired, setApprovalRequired] = useState(status?.event?.join_mode !== 'free');
const [banner, setBanner] = useState(status?.event?.banner || null);
const [location, setLocation] = useState<Location | null>(null);
const [isDisabled, setIsDisabled] = useState(!!statusId);
const [isUploading, setIsUploading] = useState(false);
const composeId = statusId ? `compose-event-modal-${statusId}` : 'compose-event-modal';
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
setName(target.value);
};
const onChangeStartTime = (date: Date | null) => {
setStartTime(date === null ? new Date : date);
};
const onChangeEndTime = (date: Date | null) => {
setEndTime(date);
};
const onChangeHasEndTime: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
if (target.checked) {
const endTime = new Date(startTime);
endTime.setHours(endTime.getHours() + 2);
setEndTime(endTime);
} else setEndTime(null);
};
const onChangeApprovalRequired: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
setApprovalRequired(target.checked);
};
const onChangeLocation = (location: Location | null) => {
setLocation(location);
};
const handleFiles = (files: FileList) => {
setIsUploading(true);
dispatch(uploadFile(
files[0],
intl,
(data) => {
setBanner(data);
setIsUploading(false);
},
() => setIsUploading(false),
));
};
const handleClearBanner = () => {
setBanner(null);
};
const handleSubmit = () => {
setIsDisabled(true);
dispatch(submitEvent({
statusId,
name,
status: text,
banner,
startTime,
endTime,
joinMode: approvalRequired ? 'restricted' : 'free',
location,
})).then((status) => {
if (status) history.push(`/@${status.account.acct}/events/${status.id}`);
dispatch(resetCompose(composeId));
}).catch(() => {
});
};
useEffect(() => {
if (statusId) {
Promise.all([dispatch(initEventEdit(statusId)), dispatch(fetchStatus(statusId))])
.then(([source, status]) => {
if (!source || !status) throw new Error();
setText(source.text);
setLocation(source.location);
setName(status?.event?.name || '');
setStartTime(status?.event?.start_time ? new Date(status.event.start_time) : new Date());
setEndTime(status?.event?.end_time ? new Date(status.event.end_time) : null);
setApprovalRequired(status?.event?.join_mode !== 'free');
setBanner(status?.media_attachments[0] || null);
setIsDisabled(false);
}).catch(() => {
toast.error(messages.eventFetchFail);
});
}
return () => {
dispatch(cancelEventCompose());
};
}, [statusId]);
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('@phosphor-icons/core/regular/map-pin.svg')} />
<Stack className='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('@phosphor-icons/core/regular/x.svg')} onClick={() => onChangeLocation(null)} />
</HStack>
);
return (
<Form onSubmit={handleSubmit}>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
hintText={<FormattedMessage id='compose_event.fields.banner_hint' defaultMessage='PNG, GIF or JPG. Landscape format is preferred.' />}
>
<div className='dark:sm:shadow-inset relative flex h-24 items-center justify-center overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-white sm:h-32 sm:shadow'>
{banner ? (
<>
<img className='size-full object-cover' src={banner.url} alt='' />
<IconButton className='absolute right-2 top-2' src={require('@phosphor-icons/core/regular/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' />}
>
<ComposeEditor
key={String(isDisabled)}
className='block w-full rounded-md border border-gray-400 bg-white px-3 py-2 text-base text-gray-900 ring-1 placeholder:text-gray-600 focus-within:border-primary-500 focus-within:ring-primary-500 black:bg-black dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500 sm:text-sm'
placeholderClassName='pt-2'
composeId={composeId}
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
handleSubmit={handleSubmit}
onChange={setText}
/>
</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' />}
>
<DatePicker
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}
/>
</FormGroup>
<HStack alignItems='center' space={2}>
<Toggle
checked={!!endTime}
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has an end date' />
</Text>
</HStack>
{endTime && (
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.end_time_label' defaultMessage='Event end date' />}
>
<DatePicker
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}
/>
</FormGroup>
)}
{!statusId && (
<HStack alignItems='center' space={2}>
<Toggle
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>
)}
<FormActions>
<Button disabled={isDisabled} theme='primary' type='submit'>
{statusId
? <FormattedMessage id='compose_event.update' defaultMessage='Update' />
: <FormattedMessage id='compose_event.create' defaultMessage='Create' />}
</Button>
</FormActions>
</Form>
);
};
export { EditEvent };