Progress
This commit is contained in:
236
app/javascript/gabsocial/components/poll.js
Normal file
236
app/javascript/gabsocial/components/poll.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import { Fragment } from 'react'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'
|
||||
import classNames from 'classnames/bind'
|
||||
import escapeTextContentForBrowser from 'escape-html'
|
||||
import spring from 'react-motion/lib/spring'
|
||||
import Motion from '../features/ui/util/optional_motion'
|
||||
import { vote } from '../actions/polls'
|
||||
import emojify from './emoji/emoji'
|
||||
import RelativeTimestamp from './relative_timestamp'
|
||||
import Button from './button'
|
||||
import DotTextSeperator from './dot_text_seperator'
|
||||
import Text from './text'
|
||||
|
||||
const cx = classNames.bind(_s)
|
||||
|
||||
const mapStateToProps = (state, { pollId }) => ({
|
||||
poll: state.getIn(['polls', pollId]),
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
closed: { id: 'poll.closed', defaultMessage: 'Closed' },
|
||||
vote: { id: 'poll.vote', defaultMessage: 'Vote' },
|
||||
refresh: { id: 'poll.refresh', defaultMessage: 'Refresh' },
|
||||
})
|
||||
|
||||
const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
|
||||
obj[`:${emoji.get('shortcode')}:`] = emoji.toJS()
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
export default
|
||||
@connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Poll extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
poll: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
}
|
||||
|
||||
state = {
|
||||
selected: {},
|
||||
}
|
||||
|
||||
handleOptionChange = e => {
|
||||
const { target: { value } } = e
|
||||
|
||||
if (this.props.poll.get('multiple')) {
|
||||
const tmp = { ...this.state.selected }
|
||||
if (tmp[value]) {
|
||||
delete tmp[value]
|
||||
} else {
|
||||
tmp[value] = true
|
||||
}
|
||||
this.setState({ selected: tmp })
|
||||
} else {
|
||||
const tmp = {}
|
||||
tmp[value] = true
|
||||
this.setState({ selected: tmp })
|
||||
}
|
||||
}
|
||||
|
||||
handleVote = () => {
|
||||
if (this.props.disabled) return
|
||||
|
||||
this.props.dispatch(vote(this.props.poll.get('id'), Object.keys(this.state.selected)))
|
||||
}
|
||||
|
||||
renderOption(option, optionIndex) {
|
||||
const { poll, disabled } = this.props
|
||||
const { selected } = this.state
|
||||
const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100
|
||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count'))
|
||||
const active = !!selected[`${optionIndex}`]
|
||||
const showResults = poll.get('voted') || poll.get('expired')
|
||||
const multiple = poll.get('multiple')
|
||||
|
||||
let titleEmojified = option.get('title_emojified')
|
||||
if (!titleEmojified) {
|
||||
const emojiMap = makeEmojiMap(poll)
|
||||
titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap)
|
||||
}
|
||||
|
||||
const chartClasses = cx({
|
||||
default: 1,
|
||||
positionAbsolute: 1,
|
||||
top0: 1,
|
||||
left0: 1,
|
||||
radiusSmall: 1,
|
||||
height100PC: 1,
|
||||
backgroundSubtle2: !leading,
|
||||
backgroundColorBrandLight: leading,
|
||||
})
|
||||
|
||||
const inputClasses = cx('poll__input', {
|
||||
'poll__input--checkbox': multiple,
|
||||
'poll__input--active': active,
|
||||
})
|
||||
|
||||
const listItemClasses = cx({
|
||||
default: 1,
|
||||
flexRow: 1,
|
||||
paddingVertical10PX: showResults,
|
||||
marginBottom10PX: 1,
|
||||
border1PX: !showResults,
|
||||
borderColorSecondary: !showResults,
|
||||
circle: !showResults,
|
||||
cursorPointer: !showResults,
|
||||
backgroundSubtle_onHover: !showResults,
|
||||
backgroundSubtle: !showResults && active,
|
||||
})
|
||||
|
||||
const textContainerClasses = cx({
|
||||
default: 1,
|
||||
width100PC: 1,
|
||||
paddingHorizontal15PX: 1,
|
||||
paddingVertical10PX: !showResults,
|
||||
cursorPointer: !showResults,
|
||||
alignItemsCenter: !showResults,
|
||||
})
|
||||
|
||||
return (
|
||||
<li className={listItemClasses} key={option.get('title')}>
|
||||
{
|
||||
showResults && (
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ width }) =>
|
||||
<span className={chartClasses} style={{ width: `${width}%` }} />
|
||||
}
|
||||
</Motion>
|
||||
)
|
||||
}
|
||||
|
||||
<label className={textContainerClasses}>
|
||||
<Text
|
||||
size='medium'
|
||||
color='primary'
|
||||
weight={leading ? 'bold' : 'normal'}
|
||||
className={[_s.displayFlex, _s.flexRow, _s.width100PC, _s.alignItemsCenter].join(' ')}
|
||||
>
|
||||
{
|
||||
!showResults &&
|
||||
<input
|
||||
name='vote-options'
|
||||
type={multiple ? 'checkbox' : 'radio'}
|
||||
value={optionIndex}
|
||||
checked={active}
|
||||
onChange={this.handleOptionChange}
|
||||
disabled={disabled}
|
||||
className={[_s.default, _s.marginRight10PX].join(' ')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!showResults && <span className={inputClasses} />
|
||||
}
|
||||
|
||||
<span
|
||||
className={_s.text}
|
||||
dangerouslySetInnerHTML={{ __html: titleEmojified }}
|
||||
/>
|
||||
|
||||
{
|
||||
showResults &&
|
||||
<span className={_s.marginLeftAuto}>
|
||||
{Math.round(percent)}%
|
||||
</span>
|
||||
}
|
||||
</Text>
|
||||
</label>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { poll, intl } = this.props
|
||||
|
||||
if (!poll) return null
|
||||
|
||||
const timeRemaining = poll.get('expired') ?
|
||||
intl.formatMessage(messages.closed)
|
||||
: <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />
|
||||
const showResults = poll.get('voted') || poll.get('expired')
|
||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item)
|
||||
|
||||
return (
|
||||
<div className={[_s.default, _s.paddingHorizontal15PX, _s.paddingVertical10PX].join(' ')}>
|
||||
<ul className={[_s.default, _s.listStyleNone].join(' ')}>
|
||||
{
|
||||
poll.get('options').map((option, i) => this.renderOption(option, i))
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter].join(' ')}>
|
||||
{
|
||||
!showResults &&
|
||||
<Button
|
||||
narrow
|
||||
className={_s.marginRight10PX}
|
||||
disabled={disabled}
|
||||
onClick={this.handleVote}
|
||||
>
|
||||
<Text color='inherit' size='small' className={_s.paddingHorizontal10PX}>
|
||||
{intl.formatMessage(messages.vote)}
|
||||
</Text>
|
||||
</Button>
|
||||
}
|
||||
|
||||
<Text color='secondary'>
|
||||
<FormattedMessage
|
||||
id='poll.total_votes'
|
||||
defaultMessage='{count, plural, one {# vote} other {# votes}}'
|
||||
values={{
|
||||
count: poll.get('votes_count'),
|
||||
}}
|
||||
/>
|
||||
{
|
||||
poll.get('expires_at') &&
|
||||
<Fragment>
|
||||
<DotTextSeperator />
|
||||
<Text color='secondary' className={_s.marginLeft5PX}>
|
||||
{timeRemaining}
|
||||
</Text>
|
||||
</Fragment>
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user