From cf1b441aaa3927d6982b3f0a0253262650c97093 Mon Sep 17 00:00:00 2001 From: PeterJFB Date: Tue, 3 May 2022 19:57:03 +0200 Subject: [PATCH] Redesign poll component --- app/components/Poll/Poll.css | 242 +++++------- app/components/Poll/__tests__/Poll.spec.js | 11 +- app/components/Poll/index.js | 416 ++++++++++----------- app/reducers/polls.js | 1 + app/routes/overview/components/Overview.js | 1 - app/routes/polls/components/PollDetail.js | 1 + app/styles/variables.css | 2 +- cypress/integration/poll_spec.js | 3 +- 8 files changed, 313 insertions(+), 364 deletions(-) diff --git a/app/components/Poll/Poll.css b/app/components/Poll/Poll.css index 5e955a4d6c..76fb155c89 100644 --- a/app/components/Poll/Poll.css +++ b/app/components/Poll/Poll.css @@ -1,185 +1,149 @@ -@import '~app/styles/variables.css'; - -.optionWrapper { - justify-content: center; - min-height: 120px; - cursor: pointer; - position: relative; -} - -.pollTable { +.poll { + background-color: var(--lego-card-color); width: 100%; - font-size: 14px; -} - -.pollTable td { - border: 0; - padding: 5px; + min-width: 250px; + max-width: 500px; + margin: 0 auto; + overflow-y: hidden; + border-radius: 5px; } -.pollTable .textColumn { - border-right: 1px solid #c5c5c5; - text-align: right; - padding-right: 13px; - line-height: 16px; -} - -.pollTable .graphColumn { - width: auto; - min-width: 200px; - padding-left: 13px; -} - -.poll { - composes: withShadow from '~app/styles/utilities.css'; - background: var(--lego-card-color); - padding: 15px 20px 8px; +.topBar { + background-color: var(--lego-red); + position: relative; + width: 100%; + height: 70px; + padding-bottom: 25px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 20%); } -.pollLight { - background: var(--lego-card-color); +.pollIcon, +.arrowIcon { + color: var(--lego-card-color); } -.noVotes { - font-style: italic; +.headerBar { + color: var(--lego-font-color); + background-color: var(--lego-card-color); + position: absolute; + bottom: 0; + left: 50%; + width: 80%; + height: 50px; + transform: translate(-50%, 50%); + font-weight: 900; + text-align: center; + border-radius: 10px; + box-shadow: 0 3px 3px rgba(0, 0, 0, 20%); } -.pollGraph { - animation: graph 1.2s cubic-bezier(41%, 80%, 40%, 94%); - background-color: var(--lego-red-color); - padding-left: 8px; - border-radius: 0 2px 2px 0; - font-style: italic; - font-weight: 300; - color: var(--color-white); - height: 30px; +.contentWrapper { + overflow-y: hidden; + width: 100%; + transition: 0.5s height; } -.fullGraph { - background-color: #e7e7e7; - width: 100%; +.voteOptionsWrapper { display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: fit-content; } -html[data-theme='dark'] .fullGraph { +.voteButton, +.voteButton + .voteButton { color: var(--color-white); - background-color: var(--color-mono-gray-5); -} - -html[data-theme='dark'] .pollGraph { - color: var(--color-black); -} - -.pollGraph span { - vertical-align: middle; -} - -@keyframes graph { - from { - width: 1px; - } - - to { - width: 100%; - } -} - -.pollHeader { - border-radius: 8px; - margin-bottom: 20px; - margin-left: 20px; - font-size: 16px; - color: var(--lego-font-color); + background: var(--lego-red); + font-weight: 500; + width: 90%; + height: 2rem; + padding: 5px; + margin: 2px; } -.voteButton { - background: var(--lego-red-color); - color: var(--lego-color-gray-light); - border: 1px solid var(--border-gray); +.voteOptions { width: 100%; - margin: 0 !important; - font-size: 15px; - max-width: 400px; + padding-top: 35px; } -.voteButton:hover { - opacity: 0.8; +.resultsHiddenInfo { + font-style: italic; } -html[data-theme='dark'] .voteButton { - color: var(--color-dark-gray-3); +.pollTable { + width: 100%; + font-size: 14px; } -.moreOptionsLink { - justify-content: space-between; +.pollTable tr { + width: 100%; } -.arrow { - margin-top: 9px; - cursor: pointer; - - &:hover { - transform: scale(1.5); - color: var(--color-red-3); - transition: transform 0.2s; - } +.pollTable td { + border: 0; + padding: 5px; } -.blurContainer { - display: none; - position: absolute; - justify-content: center; - width: 100%; - height: 100%; +.pollTable .textColumn { + text-align: right; + line-height: 16px; + word-wrap: break-word; + width: fit-content; + max-width: 200px; + padding-right: 13px; + border-right: 3px solid var(--color-mono-gray-3); } -.blurOverlay { - position: absolute; - z-index: 2; - color: var(--color-black); - margin-top: 25px; +.pollTable .graphColumn { + width: auto; + min-width: 150px; + padding-left: 13px; + padding-right: 15px; } -.optionWrapper:hover .blurContainer { +.fullGraph { display: flex; + color: var(--color-black); + background-color: var(--color-mono-gray-5); + word-wrap: break-word; + width: 100%; } -.optionWrapper:hover .blurEffect { - filter: blur(3px); - pointer-events: none; -} - -.blurArrow { - margin-top: 40px; +html[data-theme='dark'] .fullGraph { + color: var(--color-white); } -.alignItems { - display: flex; - justify-content: center; +.pollGraph { + color: var(--color-white); + background-color: var(--lego-red); + height: 1.9rem; + padding-left: 8px; + font-style: italic; + font-weight: 300; + animation: graph 1.2s cubic-bezier(0.41, 0.8, 0.4, 0.94); + border-radius: 0 2px 2px 0; + box-shadow: 0 2px 2px rgba(0, 0, 0, 20%); } -.answered { - margin: 15px 0; - text-align: center; - font-weight: bold; +.outerGraphText { + margin-left: 3px; } -.bottomInfo { - display: flex; - justify-content: space-between; +.bottomBar, +.bottomBarExpanded { + background-color: var(--lego-red); + width: 100%; + height: 70px; + padding-top: 25px; + transition: 0.5s height, 0.5s padding; } -.resultsHidden { - font-style: italic; +.bottomBarExpanded { + height: 45px; + padding-top: 0; } -@media (--mobile-device) { - .blurContainer { - display: flex; - } - - .blurEffect { - filter: blur(3px); - pointer-events: none; - } +.arrowIcon { + text-shadow: 0 2px 2px rgba(0, 0, 0, 20%); } diff --git a/app/components/Poll/__tests__/Poll.spec.js b/app/components/Poll/__tests__/Poll.spec.js index 88712b5c17..d1c33cb32b 100644 --- a/app/components/Poll/__tests__/Poll.spec.js +++ b/app/components/Poll/__tests__/Poll.spec.js @@ -1,12 +1,5 @@ -import Poll from '../.'; +import { perfectRatios } from '../.'; -const props = { - poll: { - options: [{ votes: 1 }], - }, -}; - -const poll = new Poll(props); const perfectRatiosTests = [ { input: [{ ratio: 33.33 }, { ratio: 33.33 }, { ratio: 33.33 }], @@ -52,7 +45,7 @@ const perfectRatiosTests = [ describe('poll options', () => { it('should add up to 100%', () => { perfectRatiosTests.forEach(({ input, output }) => { - expect(poll.perfectRatios(input, input)).toEqual(output); + expect(perfectRatios(input, input)).toEqual(output); }); }); }); diff --git a/app/components/Poll/index.js b/app/components/Poll/index.js index e176624232..873cf31858 100644 --- a/app/components/Poll/index.js +++ b/app/components/Poll/index.js @@ -1,6 +1,6 @@ // @flow -import { Component } from 'react'; +import { useEffect, useRef, useState } from 'react'; import Button from 'app/components/Button'; import styles from './Poll.css'; import type { PollEntity, OptionEntity } from 'app/reducers/polls'; @@ -9,247 +9,237 @@ import { Link } from 'react-router-dom'; import Icon from 'app/components/Icon'; import { Flex } from 'app/components/Layout'; import Tooltip from 'app/components/Tooltip'; -import cx from 'classnames'; +import moment from 'moment-timezone'; type Props = { poll: PollEntity, handleVote: (pollId: number, optionId: number) => Promise<*>, allowedToViewHiddenResults?: boolean, - backgroundLight?: boolean, - truncate?: number, details?: boolean, + expanded?: boolean, + alwaysOpen?: boolean, }; type OptionEntityRatio = OptionEntity & { ratio: number, }; -type State = { - truncateOptions: boolean, - shuffledOptions: Array, - expanded: boolean, +// As described in: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100 +export const perfectRatios = ( + options: $ReadOnlyArray +): OptionEntityRatio[] => { + const off = + 100 - options.reduce((a, option) => a + Math.floor(option.ratio), 0); + return sortBy( + options, + (o: OptionEntityRatio) => Math.floor(o.ratio) - o.ratio + ) + .map((option: OptionEntityRatio, index: number) => { + return { + ...option, + ratio: Math.floor(option.ratio) + (index < off ? 1 : 0), + }; + }) + .sort((a, b) => b.ratio - a.ratio); }; -class Poll extends Component { - constructor(props: Props) { - super(props); - const options = this.optionsWithPerfectRatios(props.poll.options); - const shuffledOptions = this.shuffle(options); - if (props.truncate && options.length > props.truncate) { - this.state = { - truncateOptions: true, - shuffledOptions: shuffledOptions, - expanded: false, - }; - } else { - this.state = { - truncateOptions: false, - shuffledOptions: shuffledOptions, - expanded: true, - }; - } +const optionsWithPerfectRatios = (options: Array) => { + const totalVotes = options.reduce((a, option) => a + option.votes, 0); + const ratios = options.map((option) => { + return { ...option, ratio: (option.votes / totalVotes) * 100 }; + }); + return perfectRatios(ratios); +}; + +const shuffle = (array: Array) => { + const oldArray = array.slice(0); + const newArray = []; + for (let i = 0; i < array.length; i++) { + const randIndex = Math.floor(Math.random() * oldArray.length); + newArray[i] = oldArray[randIndex]; + oldArray.splice(randIndex, 1); } - toggleTruncate = () => { - this.setState({ - expanded: !this.state.expanded, - }); - }; + return newArray; +}; - optionsWithPerfectRatios = (options: Array) => { - const totalVotes = options.reduce((a, option) => a + option.votes, 0); - const ratios = options.map((option) => { - return { ...option, ratio: (option.votes / totalVotes) * 100 }; - }); - return this.perfectRatios(ratios); - }; +const Poll = ({ + poll, + handleVote, + details, + allowedToViewHiddenResults, + alwaysOpen = false, + expanded = false, +}: Props) => { + const { id, title, description, hasAnswered, totalVotes, resultsHidden } = + poll; + const optionRef = useRef(null); + const [isExpanded, setIsExpanded] = useState(expanded || alwaysOpen); + const [expandedHeight, setExpandedHeight] = useState(0); + const [optionsToShow, setOptionsToShow] = useState([]); - // As described in: https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100 - perfectRatios = ( - options: $ReadOnlyArray - ): OptionEntityRatio[] => { - const off = - 100 - options.reduce((a, option) => a + Math.floor(option.ratio), 0); - return sortBy( - options, - (o: OptionEntityRatio) => Math.floor(o.ratio) - o.ratio - ) - .map((option: OptionEntityRatio, index: number) => { - return { - ...option, - ratio: Math.floor(option.ratio) + (index < off ? 1 : 0), - }; - }) - .sort((a, b) => b.ratio - a.ratio); - }; + useEffect(() => { + if (optionRef.current !== null) + setExpandedHeight(optionRef.current.clientHeight); - shuffle = (array: Array) => { - const oldArray = array.slice(0); - const newArray = []; - for (let i = 0; i < array.length; i++) { - const randIndex = Math.floor(Math.random() * oldArray.length); - newArray[i] = oldArray[randIndex]; - oldArray.splice(randIndex, 1); - } + const options = optionsWithPerfectRatios(poll.options); + setOptionsToShow(hasAnswered ? options : shuffle(options)); + }, [expandedHeight, hasAnswered, optionRef, poll.options]); - return newArray; + const toggleTruncate = () => { + setIsExpanded(!isExpanded); }; - render() { - const { - poll, - handleVote, - backgroundLight, - details, - truncate, - allowedToViewHiddenResults, - } = this.props; - const { truncateOptions, expanded, shuffledOptions } = this.state; - const { id, title, description, hasAnswered, totalVotes, resultsHidden } = - poll; - const options = this.optionsWithPerfectRatios(this.props.poll.options); - const orderedOptions = hasAnswered ? options : shuffledOptions; - const optionsToShow = expanded - ? orderedOptions - : orderedOptions.slice(0, truncate); - const showResults = !resultsHidden || allowedToViewHiddenResults; - - return ( -
- - - - {title} - - - - - - {details && ( -
-

{description}

-
- )} - {hasAnswered && !showResults && ( -
- Du har svart - -
- )} - {hasAnswered && showResults && ( - - - - {optionsToShow.map(({ id, name, votes, ratio }) => { - return ( - - - - - ); - })} - -
{name} - {votes === 0 ? ( - Ingen stemmer - ) : ( -
-
-
- {ratio >= 18 && {`${ratio}%`}} -
-
- {ratio < 18 && ( - - {`${ratio}%`} - - )} -
- )} -
- {resultsHidden && ( -

- Resultatet er skjult for vanlige brukere. -

+ const now = moment(); + const isValid = moment(poll.validUntil).isAfter(now); + + const canAnswer = !hasAnswered && isValid; + const hideResults = resultsHidden && !allowedToViewHiddenResults; + + return ( + + + + + + + {!details && description.length !== 0 ? ( + + {title} + + ) : ( + <> {title} )} - )} - {!hasAnswered && ( - - {!expanded && ( - + + +
+ {canAnswer && ( + + {details && description} + {optionsToShow.map((option) => ( + + ))} + + )} + {!canAnswer && hideResults && ( + + {details && description} +
+ Resultatet er skjult +
+
+ )} + {!canAnswer && !hideResults && ( + + {details && description} + + + {optionsToShow.map(({ id, name, votes, ratio }) => { + return ( + + + + + ); + })} + +
{name} + {votes === 0 ? ( + + Ingen stemmer + + ) : ( +
+
+
+ {ratio >= 18 && {`${ratio}%`}} +
+
+ {ratio < 18 && ( + + {`${ratio}%`} + + )} +
+ )} +
+
+ )} + + + -

- Klikk her for å se alle alternativene. -

-
+ + Stemmer: {totalVotes} +
+ {resultsHidden && ( +
+ Resultatet er skjult for vanlige brukere. +
)} - {options && - optionsToShow.map((option) => ( - - - - ))} - )} -
-
- {truncateOptions && - (!hasAnswered || - !resultsHidden || - allowedToViewHiddenResults) && ( -
- -
- )} -
-
- {`Stemmer: ${totalVotes}`} - {hasAnswered && !showResults && ( - - Resultatet er skjult. - - )} -
-
- ); - } -} +
+ + {!alwaysOpen && ( + + )} + +
+ ); +}; export default Poll; diff --git a/app/reducers/polls.js b/app/reducers/polls.js index 0baf0c9646..d322f91453 100644 --- a/app/reducers/polls.js +++ b/app/reducers/polls.js @@ -16,6 +16,7 @@ export type PollEntity = { title: string, description: string, resultsHidden: boolean, + validUntil: string, pinned: boolean, tags: Tags, hasAnswered: boolean, diff --git a/app/routes/overview/components/Overview.js b/app/routes/overview/components/Overview.js index 67bd03792c..b98b10e828 100644 --- a/app/routes/overview/components/Overview.js +++ b/app/routes/overview/components/Overview.js @@ -171,7 +171,6 @@ class Overview extends Component { diff --git a/app/routes/polls/components/PollDetail.js b/app/routes/polls/components/PollDetail.js index 168f518f3a..1f310f4ab0 100644 --- a/app/routes/polls/components/PollDetail.js +++ b/app/routes/polls/components/PollDetail.js @@ -52,6 +52,7 @@ class PollDetail extends Component { handleVote={this.props.votePoll} allowedToViewHiddenResults={this.props.actionGrant.includes('edit')} details + alwaysOpen /> )} {this.state.editing && ( diff --git a/app/styles/variables.css b/app/styles/variables.css index 65a1539c86..cbe6cef125 100644 --- a/app/styles/variables.css +++ b/app/styles/variables.css @@ -113,7 +113,7 @@ --lego-red: #c0392b; --lego-max-width: 1100px; --lego-default-padding: 2rem; - --lego-card-color: #262626; + --lego-card-color: #262626; /* TODO: use placard-color instead of card-color on Poll.css */ --lego-footer-color: #e21617; --lego-dark-red-color: #b21c17; --lego-red-color: #c0392b; diff --git a/cypress/integration/poll_spec.js b/cypress/integration/poll_spec.js index 2989c9bb54..af608ff21d 100644 --- a/cypress/integration/poll_spec.js +++ b/cypress/integration/poll_spec.js @@ -124,9 +124,10 @@ describe('Polls', () => { cy.contains('Avstemning'); cy.contains(poll_form.title); cy.contains('Stemmer: 0'); + cy.get(c('Poll__bottomBar')).first().click(); cy.contains(poll_form.choice_1).click(); cy.contains('Stemmer: 1'); - cy.contains('a', poll_form.title).click(); + cy.contains('div', poll_form.title).click(); cy.url().should('include', '/polls'); });