soapbox/app/soapbox/components/polls/poll-option.tsx

164 wiersze
5.5 KiB
TypeScript

import clsx from 'clsx';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Motion, presets, spring } from 'react-motion';
import { HStack, Icon, Text } from '../ui';
import type {
Poll as PollEntity,
PollOption as PollOptionEntity,
} from 'soapbox/types/entities';
const messages = defineMessages({
voted: { id: 'poll.voted', defaultMessage: 'You voted for this answer' },
votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' },
});
const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ percent, leading }): JSX.Element => {
return (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
{({ width }) => (
<span
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
style={{ width: `${width}%` }}
/>
)}
</Motion>
);
};
interface IPollOptionText extends IPollOption {
percent: number
}
const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active, onToggle }) => {
const handleOptionChange: React.EventHandler<React.ChangeEvent> = () => onToggle(index);
const handleOptionKeyPress: React.EventHandler<React.KeyboardEvent> = e => {
if (e.key === 'Enter' || e.key === ' ') {
onToggle(index);
e.stopPropagation();
e.preventDefault();
}
};
return (
<label
className={
clsx('relative flex cursor-pointer rounded-3xl border border-solid bg-white p-2 hover:bg-primary-50 dark:bg-primary-900 dark:hover:bg-primary-800/50', {
'border-primary-600 ring-1 ring-primary-600 bg-primary-50 dark:bg-primary-800/50 dark:border-primary-300 dark:ring-primary-300': active,
'border-primary-300 dark:border-primary-500': !active,
})
}
>
<input
className='hidden'
name='vote-options'
type={poll.multiple ? 'checkbox' : 'radio'}
value={index}
checked={active}
onChange={handleOptionChange}
/>
<div className='grid w-full items-center'>
<div className='col-start-1 row-start-1 ml-4 mr-6 justify-self-center'>
<div className='text-primary-600 dark:text-white'>
<Text
theme='inherit'
weight='medium'
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
/>
</div>
</div>
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
<span
className={clsx('flex h-6 w-6 flex-none items-center justify-center rounded-full border border-solid', {
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
})}
tabIndex={0}
role={poll.multiple ? 'checkbox' : 'radio'}
onKeyPress={handleOptionKeyPress}
aria-checked={active}
aria-label={option.title}
>
{active && (
<Icon src={require('@tabler/icons/check.svg')} className='h-4 w-4 text-white dark:text-primary-900' />
)}
</span>
</div>
</div>
</label>
);
};
interface IPollOption {
poll: PollEntity
option: PollOptionEntity
index: number
showResults?: boolean
active: boolean
onToggle: (value: number) => void
}
const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
const { index, poll, option, showResults } = props;
const intl = useIntl();
if (!poll) return null;
const pollVotesCount = poll.voters_count || poll.votes_count;
const percent = pollVotesCount === 0 ? 0 : (option.votes_count / pollVotesCount) * 100;
const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count);
const voted = poll.own_votes?.includes(index);
const message = intl.formatMessage(messages.votes, { votes: option.votes_count });
return (
<div key={option.title}>
{showResults ? (
<div title={voted ? message : undefined}>
<HStack
justifyContent='between'
alignItems='center'
className='relative w-full overflow-hidden rounded-md bg-white p-2 dark:bg-primary-800'
>
<PollPercentageBar percent={percent} leading={leading} />
<div className='text-primary-600 dark:text-white'>
<Text
theme='inherit'
weight='medium'
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
className='relative'
/>
</div>
<HStack space={2} alignItems='center' className='relative'>
{voted ? (
<Icon
src={require('@tabler/icons/circle-check.svg')}
alt={intl.formatMessage(messages.voted)}
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
/>
) : (
<div className='svg-icon' />
)}
<div className='text-primary-600 dark:text-white'>
<Text weight='medium' theme='inherit'>{Math.round(percent)}%</Text>
</div>
</HStack>
</HStack>
</div>
) : (
<PollOptionText percent={percent} {...props} />
)}
</div>
);
};
export default PollOption;