1
0
Fork 0
mirror of https://github.com/cheeaun/phanpy.git synced 2025-03-31 19:11:35 +02:00
phanpy/src/components/poll.jsx

284 lines
8.3 KiB
React
Raw Normal View History

import { plural } from '@lingui/core/macro';
import { Plural, Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'preact/hooks';
2023-04-23 00:55:47 +08:00
import shortenNumber from '../utils/shorten-number';
import EmojiText from './emoji-text';
2023-04-23 11:27:18 +08:00
import Icon from './icon';
2023-04-23 00:55:47 +08:00
import RelativeTime from './relative-time';
export default function Poll({
poll,
lang,
readOnly,
refresh = () => {},
votePoll = () => {},
}) {
const { t } = useLingui();
2023-04-23 00:55:47 +08:00
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 = multiple ? votersCount : votesCount;
2023-04-23 00:55:47 +08:00
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);
2023-04-25 20:41:08 +08:00
2023-04-23 00:55:47 +08:00
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;
2024-08-13 15:26:23 +08:00
const ratio = pollVotesCount
? optionVotesCount / pollVotesCount
: 0;
const percentage = ratio
? ratio.toLocaleString(i18n.locale || undefined, {
style: 'percent',
maximumFractionDigits: roundPrecision,
})
: '0%';
2023-04-23 00:55:47 +08:00
const isLeading =
optionVotesCount > 0 &&
optionVotesCount ===
Math.max(...options.map((o) => o.votesCount));
return (
2023-04-23 00:55:47 +08:00
<div
key={`${i}-${title}-${optionVotesCount}`}
class={`poll-option poll-result ${
isLeading ? 'poll-option-leading' : ''
2023-04-23 00:55:47 +08:00
}`}
style={{
2024-08-13 15:26:23 +08:00
'--percentage': `${ratio * 100}%`,
}}
2023-04-23 00:55:47 +08:00
>
<div class="poll-option-title">
<span>
<EmojiText text={title} emojis={emojis} />
</span>
{voted && ownVotes.includes(i) && (
<>
{' '}
2024-08-13 15:26:23 +08:00
<Icon icon="check-circle" alt={t`Voted`} />
</>
)}
</div>
<div
class="poll-option-votes"
2024-08-31 22:55:23 +08:00
title={plural(optionVotesCount, {
one: `# vote`,
other: `# votes`,
})}
>
2024-08-13 15:26:23 +08:00
{percentage}
</div>
2023-04-23 00:55:47 +08:00
</div>
);
})}
</div>
{!expired && !voted && (
<button
class="poll-vote-button plain2"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setShowResults(false);
}}
>
2024-08-13 15:26:23 +08:00
<Icon icon="arrow-left" size="s" /> <Trans>Hide results</Trans>
</button>
)}
</>
2023-04-23 00:55:47 +08:00
) : (
<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>
2023-04-23 00:55:47 +08:00
</label>
</div>
);
})}
</div>
{!readOnly && (
<button
class="poll-vote-button"
type="submit"
disabled={uiState === 'loading'}
>
2024-08-13 15:26:23 +08:00
<Trans>Vote</Trans>
2023-04-23 00:55:47 +08:00
</button>
)}
</form>
)}
2023-07-23 16:57:20 +08:00
<p class="poll-meta">
{!expired && !readOnly && (
<button
type="button"
class="plain small"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setUIState('loading');
2023-04-23 00:55:47 +08:00
(async () => {
await refresh();
setUIState('default');
})();
}}
2024-08-13 15:26:23 +08:00
title={t`Refresh`}
>
2024-08-13 15:26:23 +08:00
<Icon icon="refresh" alt={t`Refresh`} />
</button>
)}
{!voted && !expired && !readOnly && optionsHaveVoteCounts && (
<button
type="button"
class="plain small"
disabled={uiState === 'loading'}
onClick={(e) => {
e.preventDefault();
setShowResults(!showResults);
}}
2024-08-13 15:26:23 +08:00
title={showResults ? t`Hide results` : t`Show results`}
>
<Icon
icon={showResults ? 'eye-open' : 'eye-close'}
2024-08-13 15:26:23 +08:00
alt={showResults ? t`Hide results` : t`Show results`}
/>{' '}
</button>
2023-07-23 16:57:20 +08:00
)}
{!expired && !readOnly && ' '}
2024-08-13 15:26:23 +08:00
<Plural
value={votesCount}
one={
<Trans>
<span title={votesCount}>{shortenNumber(votesCount)}</span> vote
</Trans>
}
other={
<Trans>
<span title={votesCount}>{shortenNumber(votesCount)}</span> votes
</Trans>
}
/>
2023-07-23 16:57:20 +08:00
{!!votersCount && votersCount !== votesCount && (
<>
{' '}
2024-08-13 15:26:23 +08:00
&bull;{' '}
<Plural
value={votersCount}
one={
<Trans>
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
voter
</Trans>
}
other={
<Trans>
<span title={votersCount}>{shortenNumber(votersCount)}</span>{' '}
voters
</Trans>
}
/>
2023-07-23 16:57:20 +08:00
</>
)}{' '}
2024-08-13 15:26:23 +08:00
&bull;{' '}
{expired ? (
!!expiresAtDate ? (
<Trans>
Ended <RelativeTime datetime={expiresAtDate} />
</Trans>
) : (
t`Ended`
)
) : !!expiresAtDate ? (
<Trans>
Ending <RelativeTime datetime={expiresAtDate} />
</Trans>
) : (
t`Ending`
)}
</p>
2023-04-23 00:55:47 +08:00
</div>
);
}