import { useState } from 'preact/hooks'; import shortenNumber from '../utils/shorten-number'; import EmojiText from './emoji-text'; import Icon from './icon'; import RelativeTime from './relative-time'; export default function Poll({ poll, lang, readOnly, refresh = () => {}, votePoll = () => {}, }) { const [uiState, setUIState] = useState('default'); const { expired, expiresAt, id, multiple, options, ownVotes, voted, votersCount, votesCount, emojis, } = poll; const expiresAtDate = !!expiresAt && new Date(expiresAt); // Update poll at point of expiry // NOTE: Disable this because setTimeout runs immediately if delay is too large // https://stackoverflow.com/a/56718027/20838 // useEffect(() => { // let timeout; // if (!expired && expiresAtDate) { // const ms = expiresAtDate.getTime() - Date.now() + 1; // +1 to give it a little buffer // if (ms > 0) { // timeout = setTimeout(() => { // setUIState('loading'); // (async () => { // // await refresh(); // setUIState('default'); // })(); // }, ms); // } // } // return () => { // clearTimeout(timeout); // }; // }, [expired, expiresAtDate]); const pollVotesCount = votersCount || votesCount; let roundPrecision = 0; if (pollVotesCount <= 1000) { roundPrecision = 0; } else if (pollVotesCount <= 10000) { roundPrecision = 1; } else if (pollVotesCount <= 100000) { roundPrecision = 2; } const [showResults, setShowResults] = useState(false); const optionsHaveVoteCounts = options.every((o) => o.votesCount !== null); return ( <div lang={lang} dir="auto" class={`poll ${readOnly ? 'read-only' : ''} ${ uiState === 'loading' ? 'loading' : '' }`} > {(showResults && optionsHaveVoteCounts) || voted || expired ? ( <> <div class="poll-options"> {options.map((option, i) => { const { title, votesCount: optionVotesCount } = option; const percentage = pollVotesCount ? ((optionVotesCount / pollVotesCount) * 100).toFixed( roundPrecision, ) : 0; // check if current poll choice is the leading one const isLeading = optionVotesCount > 0 && optionVotesCount === Math.max(...options.map((o) => o.votesCount)); return ( <div key={`${i}-${title}-${optionVotesCount}`} class={`poll-option poll-result ${ isLeading ? 'poll-option-leading' : '' }`} style={{ '--percentage': `${percentage}%`, }} > <div class="poll-option-title"> <span> <EmojiText text={title} emojis={emojis} /> </span> {voted && ownVotes.includes(i) && ( <> {' '} <Icon icon="check-circle" /> </> )} </div> <div class="poll-option-votes" title={`${optionVotesCount} vote${ optionVotesCount === 1 ? '' : 's' }`} > {percentage}% </div> </div> ); })} </div> {!expired && !voted && ( <button class="poll-vote-button plain2" disabled={uiState === 'loading'} onClick={(e) => { e.preventDefault(); setShowResults(false); }} > <Icon icon="arrow-left" size="s" /> Hide results </button> )} </> ) : ( <form onSubmit={async (e) => { e.preventDefault(); const form = e.target; const formData = new FormData(form); const choices = []; formData.forEach((value, key) => { if (key === 'poll') { choices.push(value); } }); if (!choices.length) return; setUIState('loading'); await votePoll(choices); setUIState('default'); }} > <div class="poll-options"> {options.map((option, i) => { const { title } = option; return ( <div class="poll-option"> <label class="poll-label"> <input type={multiple ? 'checkbox' : 'radio'} name="poll" value={i} disabled={uiState === 'loading'} readOnly={readOnly} /> <span class="poll-option-title"> <EmojiText text={title} emojis={emojis} /> </span> </label> </div> ); })} </div> {!readOnly && ( <button class="poll-vote-button" type="submit" disabled={uiState === 'loading'} > Vote </button> )} </form> )} <p class="poll-meta"> {!expired && !readOnly && ( <button type="button" class="plain small" disabled={uiState === 'loading'} style={{ marginLeft: -8, }} onClick={(e) => { e.preventDefault(); setUIState('loading'); (async () => { await refresh(); setUIState('default'); })(); }} title="Refresh" > <Icon icon="refresh" alt="Refresh" /> </button> )} {!voted && !expired && !readOnly && optionsHaveVoteCounts && ( <button type="button" class="plain small" disabled={uiState === 'loading'} onClick={(e) => { e.preventDefault(); setShowResults(!showResults); }} title={showResults ? 'Hide results' : 'Show results'} > <Icon icon={showResults ? 'eye-open' : 'eye-close'} alt={showResults ? 'Hide results' : 'Show results'} />{' '} </button> )} {!expired && !readOnly && ' '} <span title={votesCount}>{shortenNumber(votesCount)}</span> vote {votesCount === 1 ? '' : 's'} {!!votersCount && votersCount !== votesCount && ( <> {' '} • <span title={votersCount}> {shortenNumber(votersCount)} </span>{' '} voter {votersCount === 1 ? '' : 's'} </> )}{' '} • {expired ? 'Ended' : 'Ending'}{' '} {!!expiresAtDate && <RelativeTime datetime={expiresAtDate} />} </p>{' '} </div> ); }