mirror of https://github.com/jitsi/jitsi-meet
feat(polls) Redesign (#12838)
Convert files to TS Move styles from SCSS to JSS Implement redesignpull/12843/head jitsi-meet_8258
parent
921f3ee8cd
commit
4f34a576d0
@ -1,22 +1,22 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { useSelector } from 'react-redux'; |
||||
|
||||
import { PollAnswer, PollResults } from '..'; |
||||
import { shouldShowResults } from '../../functions'; |
||||
|
||||
import PollAnswer from './PollAnswer'; |
||||
import PollResults from './PollResults'; |
||||
|
||||
|
||||
type Props = { |
||||
interface IProps { |
||||
|
||||
/** |
||||
* Id of the poll. |
||||
*/ |
||||
pollId: string, |
||||
pollId: string; |
||||
|
||||
} |
||||
|
||||
const PollItem = React.forwardRef<Props, HTMLElement>(({ pollId }: Props, ref) => { |
||||
const PollItem = React.forwardRef<HTMLDivElement, IProps>(({ pollId }: IProps, ref) => { |
||||
const showResults = useSelector(shouldShowResults(pollId)); |
||||
|
||||
return ( |
@ -1,84 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import AbstractPollResults from '../AbstractPollResults'; |
||||
import type { AbstractProps } from '../AbstractPollResults'; |
||||
|
||||
|
||||
/** |
||||
* Component that renders the poll results. |
||||
* |
||||
* @param {Props} props - The passed props. |
||||
* @returns {React.Node} |
||||
*/ |
||||
const PollResults = (props: AbstractProps) => { |
||||
const { |
||||
answers, |
||||
changeVote, |
||||
creatorName, |
||||
haveVoted, |
||||
showDetails, |
||||
question, |
||||
t, |
||||
toggleIsDetailed |
||||
} = props; |
||||
|
||||
return ( |
||||
<div className = 'poll-results'> |
||||
<div className = 'poll-header'> |
||||
<div className = 'poll-question'> |
||||
<strong>{ question }</strong> |
||||
</div> |
||||
<div className = 'poll-creator'> |
||||
{ t('polls.by', { name: creatorName }) } |
||||
</div> |
||||
</div> |
||||
<ol className = 'poll-result-list'> |
||||
{answers.map(({ name, percentage, voters, voterCount }, index) => |
||||
(<li key = { index }> |
||||
<div className = 'poll-answer-header'> |
||||
<span className = 'poll-answer-vote-name' >{name}</span> |
||||
</div> |
||||
<div className = 'poll-answer-short-results'> |
||||
<span className = 'poll-bar-container'> |
||||
<div |
||||
className = 'poll-bar' |
||||
style = {{ width: `${percentage}%` }} /> |
||||
</span> |
||||
<div className = 'poll-answer-vote-count-container'> |
||||
<span className = 'poll-answer-vote-count'>({voterCount}) {percentage}%</span> |
||||
</div> |
||||
</div> |
||||
{ showDetails && voters && voterCount > 0 |
||||
&& <ul className = 'poll-answer-voters'> |
||||
{voters.map(voter => |
||||
<li key = { voter.id }>{voter.name}</li> |
||||
)} |
||||
</ul>} |
||||
</li>) |
||||
)} |
||||
</ol> |
||||
<div className = { 'poll-result-links' }> |
||||
<a |
||||
className = { 'poll-detail-link' } |
||||
onClick = { toggleIsDetailed }> |
||||
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')} |
||||
</a> |
||||
<a |
||||
className = { 'poll-change-vote-link' } |
||||
onClick = { changeVote }> |
||||
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
); |
||||
|
||||
}; |
||||
|
||||
/* |
||||
* We apply AbstractPollResults to fill in the AbstractProps common |
||||
* to both the web and native implementations. |
||||
*/ |
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults); |
@ -0,0 +1,177 @@ |
||||
import React from 'react'; |
||||
import { makeStyles } from 'tss-react/mui'; |
||||
|
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web'; |
||||
import AbstractPollResults, { AbstractProps } from '../AbstractPollResults'; |
||||
|
||||
const useStyles = makeStyles()(theme => { |
||||
return { |
||||
container: { |
||||
margin: '24px', |
||||
padding: '16px', |
||||
backgroundColor: theme.palette.ui02, |
||||
borderRadius: '8px' |
||||
}, |
||||
header: { |
||||
marginBottom: '16px' |
||||
}, |
||||
question: { |
||||
...withPixelLineHeight(theme.typography.heading6), |
||||
color: theme.palette.text01, |
||||
marginBottom: '8px' |
||||
}, |
||||
creator: { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.text02 |
||||
}, |
||||
resultList: { |
||||
listStyleType: 'none', |
||||
margin: 0, |
||||
padding: 0, |
||||
|
||||
'& li': { |
||||
marginBottom: '16px' |
||||
} |
||||
}, |
||||
answerName: { |
||||
display: 'flex', |
||||
flexShrink: 1, |
||||
overflowWrap: 'anywhere', |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.text01, |
||||
marginBottom: '4px' |
||||
}, |
||||
answerResultContainer: { |
||||
display: 'flex', |
||||
justifyContent: 'space-between', |
||||
alignItems: 'center', |
||||
minWidth: '10em' |
||||
}, |
||||
barContainer: { |
||||
backgroundColor: theme.palette.ui03, |
||||
borderRadius: '4px', |
||||
height: '6px', |
||||
maxWidth: '160px', |
||||
width: '158px', |
||||
flexGrow: 1, |
||||
marginTop: '2px' |
||||
}, |
||||
bar: { |
||||
height: '6px', |
||||
borderRadius: '4px', |
||||
backgroundColor: theme.palette.action01 |
||||
}, |
||||
voteCount: { |
||||
flex: 1, |
||||
textAlign: 'right', |
||||
...withPixelLineHeight(theme.typography.bodyShortBold), |
||||
color: theme.palette.text01 |
||||
}, |
||||
voters: { |
||||
margin: 0, |
||||
marginTop: '4px', |
||||
listStyleType: 'none', |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
backgroundColor: theme.palette.ui03, |
||||
borderRadius: theme.shape.borderRadius, |
||||
padding: '8px 16px', |
||||
|
||||
'& li': { |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.text01, |
||||
margin: 0, |
||||
marginBottom: '2px', |
||||
|
||||
'&:last-of-type': { |
||||
marginBottom: 0 |
||||
} |
||||
} |
||||
}, |
||||
buttonsContainer: { |
||||
display: 'flex', |
||||
justifyContent: 'space-between', |
||||
|
||||
'& button': { |
||||
border: 0, |
||||
backgroundColor: 'transparent', |
||||
...withPixelLineHeight(theme.typography.bodyShortRegular), |
||||
color: theme.palette.link01 |
||||
} |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
/** |
||||
* Component that renders the poll results. |
||||
* |
||||
* @param {Props} props - The passed props. |
||||
* @returns {React.Node} |
||||
*/ |
||||
const PollResults = ({ |
||||
answers, |
||||
changeVote, |
||||
creatorName, |
||||
haveVoted, |
||||
showDetails, |
||||
question, |
||||
t, |
||||
toggleIsDetailed |
||||
}: AbstractProps) => { |
||||
const { classes } = useStyles(); |
||||
|
||||
return ( |
||||
<div className = { classes.container }> |
||||
<div className = { classes.header }> |
||||
<div className = { classes.question }> |
||||
{question} |
||||
</div> |
||||
<div className = { classes.creator }> |
||||
{t('polls.by', { name: creatorName })} |
||||
</div> |
||||
</div> |
||||
<ul className = { classes.resultList }> |
||||
{answers.map(({ name, percentage, voters, voterCount }, index) => |
||||
(<li key = { index }> |
||||
<div className = { classes.answerName }> |
||||
{name} |
||||
</div> |
||||
<div className = { classes.answerResultContainer }> |
||||
<span className = { classes.barContainer }> |
||||
<div |
||||
className = { classes.bar } |
||||
style = {{ width: `${percentage}%` }} /> |
||||
</span> |
||||
<div className = { classes.voteCount }> |
||||
{voterCount}({percentage}%) |
||||
</div> |
||||
</div> |
||||
{showDetails && voters && voterCount > 0 |
||||
&& <ul className = { classes.voters }> |
||||
{voters.map(voter => |
||||
<li key = { voter?.id }>{voter?.name}</li> |
||||
)} |
||||
</ul>} |
||||
</li>) |
||||
)} |
||||
</ul> |
||||
<div className = { classes.buttonsContainer }> |
||||
<button |
||||
onClick = { toggleIsDetailed }> |
||||
{showDetails ? t('polls.results.hideDetailedResults') : t('polls.results.showDetailedResults')} |
||||
</button> |
||||
<button |
||||
onClick = { changeVote }> |
||||
{haveVoted ? t('polls.results.changeVote') : t('polls.results.vote')} |
||||
</button> |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
/* |
||||
* We apply AbstractPollResults to fill in the AbstractProps common |
||||
* to both the web and native implementations. |
||||
*/ |
||||
// eslint-disable-next-line new-cap
|
||||
export default AbstractPollResults(PollResults); |
@ -1,58 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { useCallback, useEffect, useRef } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useSelector } from 'react-redux'; |
||||
|
||||
|
||||
import { Icon, IconMessage } from '../../../base/icons'; |
||||
import { browser } from '../../../base/lib-jitsi-meet'; |
||||
|
||||
import PollItem from './PollItem'; |
||||
|
||||
const PollsList = () => { |
||||
const { t } = useTranslation(); |
||||
|
||||
const polls = useSelector(state => state['features/polls'].polls); |
||||
const pollListEndRef = useRef(null); |
||||
|
||||
const scrollToBottom = useCallback(() => { |
||||
if (pollListEndRef.current) { |
||||
// Safari does not support options
|
||||
const param = browser.isSafari() |
||||
? false : { |
||||
behavior: 'smooth', |
||||
block: 'end', |
||||
inline: 'nearest' |
||||
}; |
||||
|
||||
pollListEndRef.current.scrollIntoView(param); |
||||
} |
||||
}, [ pollListEndRef.current ]); |
||||
|
||||
useEffect(() => { |
||||
scrollToBottom(); |
||||
}, [ polls ]); |
||||
|
||||
const listPolls = Object.keys(polls); |
||||
|
||||
return ( |
||||
<> |
||||
{listPolls.length === 0 |
||||
? <div className = 'pane-content'> |
||||
<Icon |
||||
className = 'empty-pane-icon' |
||||
src = { IconMessage } /> |
||||
<span className = 'empty-pane-message'>{t('polls.results.empty')}</span> |
||||
</div> |
||||
: listPolls.map((id, index) => ( |
||||
<PollItem |
||||
key = { id } |
||||
pollId = { id } |
||||
ref = { listPolls.length - 1 === index ? pollListEndRef : null } /> |
||||
))} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default PollsList; |
@ -0,0 +1,89 @@ |
||||
import React, { useCallback, useEffect, useRef } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useSelector } from 'react-redux'; |
||||
import { makeStyles } from 'tss-react/mui'; |
||||
|
||||
import { IReduxState } from '../../../app/types'; |
||||
import Icon from '../../../base/icons/components/Icon'; |
||||
import { IconMessage } from '../../../base/icons/svg'; |
||||
import { browser } from '../../../base/lib-jitsi-meet'; |
||||
import { withPixelLineHeight } from '../../../base/styles/functions.web'; |
||||
|
||||
import PollItem from './PollItem'; |
||||
|
||||
const useStyles = makeStyles()(theme => { |
||||
return { |
||||
container: { |
||||
height: '100%', |
||||
width: '100%', |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
flexDirection: 'column' |
||||
}, |
||||
emptyIcon: { |
||||
width: '100px', |
||||
padding: '16px', |
||||
|
||||
'& svg': { |
||||
width: '100%', |
||||
height: 'auto' |
||||
} |
||||
}, |
||||
emptyMessage: { |
||||
...withPixelLineHeight(theme.typography.bodyLongBold), |
||||
color: theme.palette.text02, |
||||
padding: '0 24px', |
||||
textAlign: 'center' |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
const PollsList = () => { |
||||
const { t } = useTranslation(); |
||||
const { classes, theme } = useStyles(); |
||||
|
||||
const polls = useSelector((state: IReduxState) => state['features/polls'].polls); |
||||
const pollListEndRef = useRef<HTMLDivElement>(null); |
||||
|
||||
const scrollToBottom = useCallback(() => { |
||||
if (pollListEndRef.current) { |
||||
// Safari does not support options
|
||||
const param = browser.isSafari() |
||||
? false : { |
||||
behavior: 'smooth' as const, |
||||
block: 'end' as const, |
||||
inline: 'nearest' as const |
||||
}; |
||||
|
||||
pollListEndRef.current.scrollIntoView(param); |
||||
} |
||||
}, [ pollListEndRef.current ]); |
||||
|
||||
useEffect(() => { |
||||
scrollToBottom(); |
||||
}, [ polls ]); |
||||
|
||||
const listPolls = Object.keys(polls); |
||||
|
||||
return ( |
||||
<> |
||||
{listPolls.length === 0 |
||||
? <div className = { classes.container }> |
||||
<Icon |
||||
className = { classes.emptyIcon } |
||||
color = { theme.palette.icon03 } |
||||
src = { IconMessage } /> |
||||
<span className = { classes.emptyMessage }>{t('polls.results.empty')}</span> |
||||
</div> |
||||
: listPolls.map((id, index) => ( |
||||
<PollItem |
||||
key = { id } |
||||
pollId = { id } |
||||
ref = { listPolls.length - 1 === index ? pollListEndRef : null } /> |
||||
))} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default PollsList; |
Loading…
Reference in new issue