Redesign Poll within Status

environments/review-poll-impro-o4k7hi/deployments/284
Justin 2 years ago
parent f34a5b81f8
commit fb9796b0c4

@ -196,7 +196,7 @@
"expired": false,
"expires_at": "2022-03-12T01:33:18.000Z",
"id": "AHHue67gF2JDqCQGhc",
"multiple": false,
"multiple": true,
"options": [
{
"title": "Regular emoji 😍 ",

@ -6,8 +6,7 @@ import { useDispatch } from 'react-redux';
import { openModal } from 'soapbox/actions/modals';
import { vote, fetchPoll } from 'soapbox/actions/polls';
import Icon from 'soapbox/components/icon';
import { Text, Button, Stack, HStack } from 'soapbox/components/ui';
import { Text, Button, Icon, Stack, HStack } from 'soapbox/components/ui';
import Motion from 'soapbox/features/ui/util/optional_motion';
import { useAppSelector } from 'soapbox/hooks';
@ -21,10 +20,10 @@ const messages = defineMessages({
votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' },
});
const PollPercentageBar: React.FC<{percent: number, leading: boolean}> = ({ percent, leading }): JSX.Element => {
const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ percent, leading }): JSX.Element => {
return (
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
{({ width }) =>(
{({ width }) => (
<span
className={classNames('absolute inset-0 h-full inline-block rounded bg-gray-300 dark:bg-slate-900', {
'bg-primary-300 dark:bg-primary-400': leading,
@ -59,7 +58,12 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
return (
<label
className={classNames('relative', { 'cursor-pointer': !showResults })}
className={classNames({
'relative flex rounded-full border border-solid bg-white p-2 hover:bg-primary-50': true,
'border-primary-600 ring-1 ring-primary-600 bg-primary-50': active,
'border-primary-300': !active,
'cursor-pointer': !showResults,
})}
title={showResults ? message : undefined}
>
<input
@ -71,34 +75,44 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
onChange={handleOptionChange}
/>
<HStack alignItems='center' className='p-1 text-gray-900 dark:text-gray-300'>
{!showResults && (
<span
className={classNames('inline-block w-4 h-4 flex-none mr-2.5 border border-solid border-primary-600 rounded-full', {
'bg-primary-600': active,
'rounded': poll.multiple,
})}
tabIndex={0}
role={poll.multiple ? 'checkbox' : 'radio'}
onKeyPress={handleOptionKeyPress}
aria-checked={active}
aria-label={option.title}
<div className='grid items-center w-full'>
<div className='col-start-1 row-start-1 justify-self-center ml-4 mr-6'>
<Text
theme='primary'
weight='semibold'
dangerouslySetInnerHTML={{ __html: option.title_emojified }}
/>
)}
{showResults && (
<HStack space={2} alignItems='center' className='mr-2.5'>
{voted ? (
<Icon src={require('@tabler/icons/icons/check.svg')} title={intl.formatMessage(messages.voted)} />
) : (
<div className='svg-icon' />
)}
<span className='font-bold'>{Math.round(percent)}%</span>
</HStack>
)}
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
</HStack>
</div>
<div className='col-start-1 row-start-1 justify-self-end flex items-center'>
{showResults ? (
<HStack space={2} alignItems='center' className='mr-2.5'>
{voted ? (
<Icon src={require('@tabler/icons/icons/check.svg')} alt={intl.formatMessage(messages.voted)} />
) : (
<div className='svg-icon' />
)}
<span className='font-bold'>{Math.round(percent)}%</span>
</HStack>
) : (
<span
className={classNames('flex items-center justify-center w-6 h-6 flex-none border border-solid rounded-full', {
'bg-primary-600 border-primary-600': active,
'border-primary-300 bg-white': !active,
})}
tabIndex={0}
role={poll.multiple ? 'checkbox' : 'radio'}
onKeyPress={handleOptionKeyPress}
aria-checked={active}
aria-label={option.title}
>
{active && (
<Icon src={require('@tabler/icons/icons/check.svg')} className='text-white w-4 h-4' />
)}
</span>
)}
</div>
</div>
</label>
);
};
@ -120,7 +134,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count);
return (
<div className='relative mb-2.5' key={option.title}>
<div className='relative' key={option.title}>
{showResults && (
<PollPercentageBar percent={percent} leading={leading} />
)}
@ -130,7 +144,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
);
};
const RefreshButton: React.FC<{poll: PollEntity}> = ({ poll }): JSX.Element => {
const RefreshButton: React.FC<{ poll: PollEntity }> = ({ poll }): JSX.Element => {
const dispatch = useDispatch();
const handleRefresh: React.EventHandler<React.MouseEvent> = (e) => {
@ -148,12 +162,12 @@ const RefreshButton: React.FC<{poll: PollEntity}> = ({ poll }): JSX.Element => {
);
};
const VoteButton: React.FC<{poll: PollEntity, selected: Selected}> = ({ poll, selected }): JSX.Element => {
const VoteButton: React.FC<{ poll: PollEntity, selected: Selected }> = ({ poll, selected }): JSX.Element => {
const dispatch = useDispatch();
const handleVote = () => dispatch(vote(poll.id, Object.keys(selected)));
return (
<Button onClick={handleVote} theme='ghost'>
<Button onClick={handleVote} theme='primary' block>
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
</Button>
);
@ -167,18 +181,35 @@ interface IPollFooter {
const PollFooter: React.FC<IPollFooter> = ({ poll, showResults, selected }): JSX.Element => {
const intl = useIntl();
const timeRemaining = poll.expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.expires_at} futureDate />;
const timeRemaining = poll.expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp weight='medium' timestamp={poll.expires_at} futureDate />;
return (
<Stack space={2}>
<Stack space={4}>
{!showResults && <div><VoteButton poll={poll} selected={selected} /></div>}
<Text>
<HStack space={1.5} alignItems='center'>
{showResults && (
<><RefreshButton poll={poll} /> · </>
<>
<RefreshButton poll={poll} />
<Text theme='muted'>&middot;</Text>
</>
)}
<Text theme='muted' weight='medium'>
<FormattedMessage
id='poll.total_votes'
defaultMessage='{count, plural, one {# vote} other {# votes}}'
values={{ count: poll.votes_count }}
/>
</Text>
{poll.expires_at && (
<>
<Text theme='muted'>&middot;</Text>
<Text weight='medium' theme='muted'>{timeRemaining}</Text>
</>
)}
<FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.votes_count }} />
{poll.expires_at && <span> · {timeRemaining}</span>}
</Text>
</HStack>
</Stack>
);
};
@ -231,8 +262,8 @@ const Poll: React.FC<IPoll> = ({ id, status }): JSX.Element | null => {
return (
<div onClick={e => e.stopPropagation()}>
<Stack className={classNames('my-2', { voted: poll.voted })}>
<Stack>
<Stack space={4} className={classNames('mt-4', { voted: poll.voted })}>
<Stack space={2}>
{poll.options.map((option, i) => (
<PollOption
key={i}

@ -33,8 +33,8 @@ const shortDateFormatOptions = {
const SECOND = 1000;
const MINUTE = 1000 * 60;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;
const HOUR = 1000 * 60 * 60;
const DAY = 1000 * 60 * 60 * 24;
const MAX_DELAY = 2147483647;
@ -160,12 +160,12 @@ class RelativeTimestamp extends React.Component {
_scheduleNextUpdate() {
clearTimeout(this._timer);
const { timestamp } = this.props;
const delta = (new Date(timestamp)).getTime() - this.state.now;
const unitDelay = getUnitDelay(selectUnits(delta));
const unitRemainder = Math.abs(delta % unitDelay);
const { timestamp } = this.props;
const delta = (new Date(timestamp)).getTime() - this.state.now;
const unitDelay = getUnitDelay(selectUnits(delta));
const unitRemainder = Math.abs(delta % unitDelay);
const updateInterval = 1000 * 10;
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
const delay = delta < 0 ? Math.max(updateInterval, unitDelay - unitRemainder) : Math.max(updateInterval, unitRemainder);
this._timer = setTimeout(() => {
this.setState({ now: Date.now() });
@ -175,11 +175,11 @@ class RelativeTimestamp extends React.Component {
render() {
const { timestamp, intl, year, futureDate, ...textProps } = this.props;
const date = new Date(timestamp);
const date = new Date(timestamp);
const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year);
return (
<Text {...textProps} color='inherit' tag='time' title={intl.formatDate(date, dateFormatOptions)}>
<Text {...textProps} theme='inherit' tag='time' title={intl.formatDate(date, dateFormatOptions)}>
{relativeTime}
</Text>
);

@ -174,8 +174,8 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
const target = e.target as HTMLElement;
const parentNode = target.parentNode as HTMLElement;
const [ startX, startY ] = startXY.current;
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
const [startX, startY] = startXY.current;
const [deltaX, deltaY] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
if (target.localName === 'button' || target.localName === 'a' || (parentNode && (parentNode.localName === 'button' || parentNode.localName === 'a'))) {
return;
@ -273,11 +273,12 @@ const StatusContent: React.FC<IStatusContent> = ({ status, expanded = false, onE
output.push(<ReadMoreButton onClick={onClick} key='read-more' />);
}
if (status.poll && typeof status.poll === 'string') {
const hasPoll = status.poll && typeof status.poll === 'string';
if (hasPoll) {
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
}
return <>{output}</>;
return <div className={classNames({ 'bg-gray-100 rounded-md p-4': hasPoll })}>{output}</div>;
} else {
const output = [
<div

@ -18,9 +18,9 @@ const messages = defineMessages({
* These get embedded into the build, but only in this chunk, so it's okay.
*/
const MOCK_STATUSES: any[] = [
require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json'),
require('soapbox/__fixtures__/pleroma-status-with-poll.json'),
require('soapbox/__fixtures__/pleroma-status-vertical-video-without-metadata.json'),
require('soapbox/__fixtures__/pleroma-status-with-poll-with-emojis.json'),
require('soapbox/__fixtures__/pleroma-quote-of-quote-post.json'),
require('soapbox/__fixtures__/truthsocial-status-with-external-video.json'),
];

@ -1,30 +1,44 @@
import classNames from 'classnames';
import React from 'react';
import { Stack } from 'soapbox/components/ui';
import { Poll as PollEntity, PollOption as PollOptionEntity } from 'soapbox/types/entities';
interface IPollPreview {
poll: PollEntity,
}
// {/* <li key={index}>
// <label className={classNames('poll__text', { selectable: !showResults })}>
// <input
// name='vote-options'
// type={poll.multiple ? 'checkbox' : 'radio'}
// disabled
// />
// <span className={classNames('poll__input', { checkbox: poll.multiple })} />
// <span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
// </label>
// </li> */}
const PollPreview: React.FC<IPollPreview> = ({ poll }) => {
const renderOption = (option: PollOptionEntity, index: number) => {
const showResults = poll.voted || poll.expired;
return (
<li key={index}>
<label className={classNames('poll__text', { selectable: !showResults })}>
<input
name='vote-options'
type={poll.multiple ? 'checkbox' : 'radio'}
disabled
/>
<span className={classNames('poll__input', { checkbox: poll.multiple })} />
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
</label>
</li>
<label className='rounded-tl-md rounded-tr-md relative border p-4 flex flex-col cursor-pointer md:pl-4 md:pr-6 md:grid md:grid-cols-3 focus:outline-none'>
<span className='flex items-center text-sm'>
<input type='radio' name='pricing-plan' value='Startup' className='h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500' aria-labelledby='pricing-plans-0-label' aria-describedby='pricing-plans-0-description-0 pricing-plans-0-description-1' />
<span id='pricing-plans-0-label' className='ml-3 font-medium'>Startup</span>
</span>
<span id='pricing-plans-0-description-0' className='ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-center'>
<span className='font-medium'>$29 / mo</span>
<span>($290 / yr)</span>
</span>
<span id='pricing-plans-0-description-1' className='ml-6 pl-1 text-sm md:ml-0 md:pl-0 md:text-right'>Up to 5 active job postings</span>
</label>
);
};
@ -33,11 +47,9 @@ const PollPreview: React.FC<IPollPreview> = ({ poll }) => {
}
return (
<div className='poll'>
<ul>
{poll.options.map((option, i) => renderOption(option, i))}
</ul>
</div>
<Stack>
{poll.options.map((option, i) => renderOption(option, i))}
</Stack>
);
};

@ -780,7 +780,7 @@
"poll.closed": "Closed",
"poll.refresh": "Refresh",
"poll.total_votes": "{count, plural, one {# vote} other {# votes}}",
"poll.vote": "Vote",
"poll.vote": "Submit Vote",
"poll.voted": "You voted for this answer",
"poll.votes": "{votes, plural, one {# vote} other {# votes}}",
"poll_button.add_poll": "Add a poll",

Loading…
Cancel
Save