From 334b45ec7430c6fbc194ea00d3f23af31b116fa0 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 26 Mar 2022 18:52:48 -0500 Subject: [PATCH 1/5] Polls: break out PollPercentageBar into separate component --- app/soapbox/components/poll.tsx | 196 +++++++++++++++++++------------- 1 file changed, 118 insertions(+), 78 deletions(-) diff --git a/app/soapbox/components/poll.tsx b/app/soapbox/components/poll.tsx index b11925fff..3f7139c30 100644 --- a/app/soapbox/components/poll.tsx +++ b/app/soapbox/components/poll.tsx @@ -1,7 +1,7 @@ import classNames from 'classnames'; import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage, IntlShape } from 'react-intl'; +import { defineMessages, injectIntl, useIntl, FormattedMessage, IntlShape } from 'react-intl'; import { spring } from 'react-motion'; import { openModal } from 'soapbox/actions/modals'; @@ -12,7 +12,7 @@ import Motion from 'soapbox/features/ui/util/optional_motion'; import RelativeTimestamp from './relative_timestamp'; -import type { Poll as PollEntity, PollOption } from 'soapbox/types/entities'; +import type { Poll as PollEntity, PollOption as PollOptionEntity } from 'soapbox/types/entities'; const messages = defineMessages({ closed: { id: 'poll.closed', defaultMessage: 'Closed' }, @@ -20,6 +20,106 @@ const messages = defineMessages({ votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' }, }); +interface IPollPercentageBar { + percent: number, + leading: boolean, +} + +const PollPercentageBar: React.FC = ({ percent, leading }): JSX.Element => { + return ( + + {({ width }) =>( + + )} + + ); +}; + +interface IPollOption { + poll: PollEntity, + option: PollOptionEntity, + index: number, + showResults?: boolean, + disabled?: boolean, + active: boolean, + onToggle: (value: number) => void, +} + +const PollOption: React.FC = ({ + poll, + option, + index, + showResults, + disabled, + active, + onToggle, +}): JSX.Element | null => { + const intl = useIntl(); + + if (!poll) return null; + + const percent = poll.votes_count === 0 ? 0 : (option.votes_count / poll.votes_count) * 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 handleOptionChange = (): void => { + onToggle(index); + }; + + const handleOptionKeyPress = (e: React.KeyboardEvent): void => { + if (e.key === 'Enter' || e.key === ' ') { + onToggle(index); + e.stopPropagation(); + e.preventDefault(); + } + }; + + return ( +
  • + {showResults && ( + + )} + + +
  • + ); +}; + interface IPoll { poll?: PollEntity, intl: IntlShape, @@ -30,16 +130,16 @@ interface IPoll { } interface IPollState { - selected: Record, + selected: Record, } class Poll extends ImmutablePureComponent { state = { - selected: {} as Record, + selected: {} as Record, }; - _toggleOption = (value: string) => { + toggleOption = (value: number) => { const { me, poll } = this.props; if (me) { @@ -52,7 +152,7 @@ class Poll extends ImmutablePureComponent { } this.setState({ selected: tmp }); } else { - const tmp: Record = {}; + const tmp: Record = {}; tmp[value] = true; this.setState({ selected: tmp }); } @@ -61,23 +161,6 @@ class Poll extends ImmutablePureComponent { } } - handleOptionChange = (e: React.ChangeEvent): void => { - this._toggleOption(e.currentTarget.value); - }; - - handleOptionKeyPress = (e: React.KeyboardEvent): void => { - if (e.key === 'Enter' || e.key === ' ') { - const dataIndex = e.currentTarget.getAttribute('data-index'); - - if (dataIndex) { - this._toggleOption(dataIndex); - } - - e.stopPropagation(); - e.preventDefault(); - } - } - handleVote = () => { const { disabled, dispatch, poll } = this.props; if (disabled || !dispatch || !poll) return; @@ -99,60 +182,6 @@ class Poll extends ImmutablePureComponent { dispatch(fetchPoll(poll.id)); }; - renderOption(option: PollOption, optionIndex: number, showResults: boolean): JSX.Element | null { - const { poll, disabled, intl } = this.props; - if (!poll) return null; - - const percent = poll.votes_count === 0 ? 0 : (option.votes_count / poll.votes_count) * 100; - const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count); - const active = !!this.state.selected[`${optionIndex}`]; - const voted = poll.own_votes?.includes(optionIndex); - const titleEmojified = option.title_emojified; - - return ( -
  • - {showResults && ( - - {({ width }) => - - } - - )} - - -
  • - ); - } - render() { const { poll, intl } = this.props; @@ -167,7 +196,18 @@ class Poll extends ImmutablePureComponent { return (
      - {poll.options.map((option, i) => this.renderOption(option, i, showResults))} + {poll.options.map((option, i) => ( + + ))}
    From e78ea4aaf3bc6db2177803bc8fe06c479f827e6b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 26 Mar 2022 19:05:16 -0500 Subject: [PATCH 2/5] Polls: break out PollOptionText into a React.FC --- app/soapbox/components/poll.tsx | 111 ++++++++++++++++---------------- 1 file changed, 57 insertions(+), 54 deletions(-) diff --git a/app/soapbox/components/poll.tsx b/app/soapbox/components/poll.tsx index 3f7139c30..0eae7c93e 100644 --- a/app/soapbox/components/poll.tsx +++ b/app/soapbox/components/poll.tsx @@ -40,32 +40,13 @@ const PollPercentageBar: React.FC = ({ percent, leading }): ); }; -interface IPollOption { - poll: PollEntity, - option: PollOptionEntity, - index: number, - showResults?: boolean, - disabled?: boolean, - active: boolean, - onToggle: (value: number) => void, +interface IPollOptionText extends IPollOption { + percent: number, } -const PollOption: React.FC = ({ - poll, - option, - index, - showResults, - disabled, - active, - onToggle, -}): JSX.Element | null => { +const PollOptionText: React.FC = ({ poll, option, index, active, disabled, percent, showResults, onToggle }) => { const intl = useIntl(); - - if (!poll) return null; - - const percent = poll.votes_count === 0 ? 0 : (option.votes_count / poll.votes_count) * 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 voted = poll.own_votes?.includes(index); const handleOptionChange = (): void => { onToggle(index); @@ -79,43 +60,65 @@ const PollOption: React.FC = ({ } }; + return ( + + ); +}; + +interface IPollOption { + poll: PollEntity, + option: PollOptionEntity, + index: number, + showResults?: boolean, + disabled?: boolean, + active: boolean, + onToggle: (value: number) => void, +} + +const PollOption: React.FC = (props): JSX.Element | null => { + const { poll, option, showResults } = props; + if (!poll) return null; + + const percent = poll.votes_count === 0 ? 0 : (option.votes_count / poll.votes_count) * 100; + const leading = poll.options.filterNot(other => other.title === option.title).every(other => option.votes_count >= other.votes_count); + return (
  • {showResults && ( )} - +
  • ); }; From 9262d5c26be585984b191ff631d0e036a6bd5673 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 26 Mar 2022 19:53:51 -0500 Subject: [PATCH 3/5] Poll: break into smaller components --- app/soapbox/components/poll.tsx | 109 ++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 42 deletions(-) diff --git a/app/soapbox/components/poll.tsx b/app/soapbox/components/poll.tsx index 0eae7c93e..4b0628d2d 100644 --- a/app/soapbox/components/poll.tsx +++ b/app/soapbox/components/poll.tsx @@ -3,6 +3,7 @@ import React from 'react'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { defineMessages, injectIntl, useIntl, FormattedMessage, IntlShape } from 'react-intl'; import { spring } from 'react-motion'; +import { useDispatch } from 'react-redux'; import { openModal } from 'soapbox/actions/modals'; import { vote, fetchPoll } from 'soapbox/actions/polls'; @@ -20,12 +21,7 @@ const messages = defineMessages({ votes: { id: 'poll.votes', defaultMessage: '{votes, plural, one {# vote} other {# votes}}' }, }); -interface IPollPercentageBar { - percent: number, - leading: boolean, -} - -const PollPercentageBar: React.FC = ({ percent, leading }): JSX.Element => { +const PollPercentageBar: React.FC<{percent: number, leading: boolean}> = ({ percent, leading }): JSX.Element => { return ( {({ width }) =>( @@ -44,7 +40,7 @@ interface IPollOptionText extends IPollOption { percent: number, } -const PollOptionText: React.FC = ({ poll, option, index, active, disabled, percent, showResults, onToggle }) => { +const PollOptionText: React.FC = ({ poll, option, index, active, percent, showResults, onToggle }) => { const intl = useIntl(); const voted = poll.own_votes?.includes(index); @@ -68,7 +64,6 @@ const PollOptionText: React.FC = ({ poll, option, index, active value={index} checked={active} onChange={handleOptionChange} - disabled={disabled} /> {!showResults && ( @@ -100,7 +95,6 @@ interface IPollOption { option: PollOptionEntity, index: number, showResults?: boolean, - disabled?: boolean, active: boolean, onToggle: (value: number) => void, } @@ -127,19 +121,68 @@ interface IPoll { poll?: PollEntity, intl: IntlShape, dispatch?: Function, - disabled?: boolean, me?: string | null | false | undefined, status?: string, } interface IPollState { - selected: Record, + selected: Selected, } +const RefreshButton: React.FC<{poll: PollEntity}> = ({ poll }): JSX.Element => { + const dispatch = useDispatch(); + const handleRefresh = () => dispatch(fetchPoll(poll.id)); + + return ( + + + + ); +}; + +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 ( + + ); +}; + +interface IPollFooter { + poll: PollEntity, + showResults: boolean, + selected: Selected, +} + +const PollFooter: React.FC = ({ poll, showResults, selected }): JSX.Element => { + const intl = useIntl(); + const timeRemaining = poll.expired ? intl.formatMessage(messages.closed) : ; + + return ( +
    + {!showResults && } + + {showResults && ( + <> · + )} + + {poll.expires_at && · {timeRemaining}} + +
    + ); +}; + +type Selected = Record; + class Poll extends ImmutablePureComponent { state = { - selected: {} as Record, + selected: {} as Selected, }; toggleOption = (value: number) => { @@ -155,7 +198,7 @@ class Poll extends ImmutablePureComponent { } this.setState({ selected: tmp }); } else { - const tmp: Record = {}; + const tmp: Selected = {}; tmp[value] = true; this.setState({ selected: tmp }); } @@ -165,8 +208,8 @@ class Poll extends ImmutablePureComponent { } handleVote = () => { - const { disabled, dispatch, poll } = this.props; - if (disabled || !dispatch || !poll) return; + const { dispatch, poll } = this.props; + if (!dispatch || !poll) return; dispatch(vote(poll.id, Object.keys(this.state.selected))); }; @@ -179,22 +222,12 @@ class Poll extends ImmutablePureComponent { })); } - handleRefresh = () => { - const { disabled, dispatch, poll } = this.props; - if (disabled || !poll || !dispatch) return; - dispatch(fetchPoll(poll.id)); - }; - render() { - const { poll, intl } = this.props; + const { poll } = this.props; + if (!poll) return null; - if (!poll) { - return null; - } - - const timeRemaining = poll.expired ? intl.formatMessage(messages.closed) : ; - const showResults = poll.voted || poll.expired; - const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item); + const { selected } = this.state; + const showResults = poll.voted || poll.expired; return (
    @@ -206,25 +239,17 @@ class Poll extends ImmutablePureComponent { option={option} index={i} showResults={showResults} - disabled={disabled} active={!!this.state.selected[i]} onToggle={this.toggleOption} /> ))} -
    - {!showResults && } - - {showResults && !this.props.disabled && ( - · - )} - - {poll.expires_at && · {timeRemaining}} - -
    +
    ); } From cc1df1bd09d939b834053da33174c8e63ee212e9 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Sat, 26 Mar 2022 22:05:41 -0500 Subject: [PATCH 4/5] Polls: style with Tailwind --- app/soapbox/components/poll.tsx | 130 ++++++++-------- app/styles/application.scss | 1 - app/styles/polls.scss | 254 -------------------------------- 3 files changed, 72 insertions(+), 313 deletions(-) delete mode 100644 app/styles/polls.scss diff --git a/app/soapbox/components/poll.tsx b/app/soapbox/components/poll.tsx index 4b0628d2d..52518172e 100644 --- a/app/soapbox/components/poll.tsx +++ b/app/soapbox/components/poll.tsx @@ -8,7 +8,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 } from 'soapbox/components/ui'; +import { Text, Button, Stack, HStack } from 'soapbox/components/ui'; import Motion from 'soapbox/features/ui/util/optional_motion'; import RelativeTimestamp from './relative_timestamp'; @@ -26,7 +26,7 @@ const PollPercentageBar: React.FC<{percent: number, leading: boolean}> = ({ perc {({ width }) =>( = ({ poll, option, index, active, percent, showResults, onToggle }) => { const intl = useIntl(); const voted = poll.own_votes?.includes(index); + const message = intl.formatMessage(messages.votes, { votes: option.votes_count }); - const handleOptionChange = (): void => { + const handleOptionChange: React.EventHandler = () => { onToggle(index); }; - const handleOptionKeyPress = (e: React.KeyboardEvent): void => { + const handleOptionKeyPress: React.EventHandler = e => { if (e.key === 'Enter' || e.key === ' ') { onToggle(index); e.stopPropagation(); @@ -57,8 +58,12 @@ const PollOptionText: React.FC = ({ poll, option, index, active }; return ( -