Commit 9b6b757f authored by Marlène SIMONDANT's avatar Marlène SIMONDANT
Browse files

feat : add management of the end of all available challenges (modal + card)

parent ed97d829
<svg width="62" height="62" viewBox="0 0 62 62" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30.9999 58.3539C57.4593 48.0072 55.4028 33.2974 54.0319 11.3573C45.532 10.9834 37.9918 8.36552 30.9999 4.00244C24.0081 8.36552 16.4679 10.9834 7.96801 11.3573C6.59706 33.2974 4.54064 48.0072 30.9999 58.3539Z" fill="#1B1C22"/>
<path d="M31 0L29.2075 1.11858C22.6524 5.20911 15.6717 7.61771 7.81931 7.96319L4.77457 8.09716L4.58414 11.1446C4.52759 12.0496 4.46881 12.95 4.41037 13.8451C3.78152 23.4778 3.1917 32.5124 5.83816 40.2978C8.88926 49.2736 15.9673 56.1216 29.7672 61.5179L31 62V58.3535C5.57809 48.4124 6.4797 34.4437 7.80541 13.9043C7.85951 13.0662 7.91431 12.2171 7.96806 11.3569C16.4679 10.9829 24.0082 8.36507 31 4.00199V0Z" fill="#FFC600"/>
<path d="M31 0L32.7925 1.11858C39.3476 5.20911 46.3283 7.61771 54.1807 7.96319L57.2254 8.09716L57.4159 11.1446C57.4724 12.0496 57.5312 12.95 57.5896 13.8451C58.2185 23.4778 58.8083 32.5124 56.1618 40.2978C53.1107 49.2736 46.0327 56.1216 32.2328 61.5179L31 62V58.3535C56.4219 48.4124 55.5203 34.4437 54.1946 13.9043C54.1405 13.0662 54.0857 12.2171 54.0319 11.3569C45.5321 10.9829 37.9918 8.36507 31 4.00199V0Z" fill="#DB8300"/>
<path d="M22.639 21.3672H23.8419C24.3222 21.3672 24.783 21.5561 25.1226 21.8925C25.4623 22.2289 25.6532 22.6851 25.6532 23.1608V41.3684H20.8277V23.1608C20.8277 22.6851 21.0185 22.2289 21.3582 21.8925C21.6979 21.5561 22.1586 21.3672 22.639 21.3672ZM30.9965 30.2331H32.1993C32.6797 30.2331 33.1404 30.4221 33.4801 30.7585C33.8198 31.0948 34.0107 31.5511 34.0107 32.0268V41.3684H29.1852V32.0268C29.1852 31.5511 29.376 31.0948 29.7157 30.7585C30.0554 30.4221 30.5161 30.2331 30.9965 30.2331ZM39.8521 26.8035H41.055C41.5353 26.8035 41.9961 26.9925 42.3358 27.3288C42.6754 27.6652 42.8663 28.1214 42.8663 28.5971V41.3684H38.0408V28.5971C38.0408 28.1214 38.2316 27.6652 38.5713 27.3288C38.911 26.9925 39.3717 26.8035 39.8521 26.8035Z" fill="#FFC600"/>
</svg>
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.4486 17.7627L32.0591 15.7204L25.6267 2.98519C25.451 2.6365 25.1619 2.35424 24.8049 2.18266C23.9094 1.75096 22.8213 2.11071 22.3736 2.98519L15.9411 15.7204L1.55164 17.7627C1.15492 17.818 0.792211 18.0007 0.514509 18.2774C0.178784 18.6144 -0.00621659 19.0678 0.000159514 19.5379C0.00653562 20.008 0.203766 20.4564 0.548513 20.7846L10.9595 30.6972L8.49984 44.6943C8.44216 45.0199 8.47906 45.3548 8.60634 45.6609C8.73363 45.9671 8.94621 46.2323 9.21999 46.4265C9.49376 46.6207 9.81777 46.7361 10.1553 46.7596C10.4928 46.7831 10.8303 46.7137 11.1295 46.5595L24.0001 39.9511L36.8707 46.5595C37.2221 46.7421 37.6302 46.803 38.0212 46.7366C39.0073 46.5705 39.6704 45.6573 39.5004 44.6943L37.0407 30.6972L47.4517 20.7846C47.7351 20.5134 47.9221 20.1592 47.9788 19.7718C48.1318 18.8032 47.4404 17.9066 46.4486 17.7627Z" fill="url(#paint0_linear_12656_82845)"/>
<defs>
<linearGradient id="paint0_linear_12656_82845" x1="23.175" y1="2" x2="23.175" y2="46.764" gradientUnits="userSpaceOnUse">
<stop stop-color="#61F0F2"/>
<stop offset="1" stop-color="#48C2C4"/>
</linearGradient>
</defs>
</svg>
......@@ -6,13 +6,15 @@ import ChallengeCardLocked from './ChallengeCardLocked'
import ChallengeCardOnGoing from './ChallengeCardOnGoing'
import ChallengeCardUnlocked from './ChallengeCardUnlocked'
import './challengeCard.scss'
import ChallengeCardLast from './ChallengeCardLast'
interface ChallengeCardProps {
userChallenge: UserChallenge
userChallenge?: UserChallenge
indexSlider: number
index: number
cardWidth: number
cardHeight: number
isChallengeCardLast: boolean
moveToSlide: (slideIndex: number) => void
}
......@@ -22,10 +24,12 @@ const ChallengeCard: React.FC<ChallengeCardProps> = ({
index,
cardWidth,
cardHeight,
isChallengeCardLast,
moveToSlide,
}: ChallengeCardProps) => {
const renderCard = (state: UserChallengeState) => {
switch (state) {
const renderCard = (userChallenge: UserChallenge | undefined) => {
if (!userChallenge || isChallengeCardLast) return <ChallengeCardLast />
switch (userChallenge.state) {
case UserChallengeState.LOCKED:
return <ChallengeCardLocked userChallenge={userChallenge} />
case UserChallengeState.UNLOCKED:
......@@ -50,7 +54,7 @@ const ChallengeCard: React.FC<ChallengeCardProps> = ({
height: `${cardHeight}px`,
}}
>
{renderCard(userChallenge.state)}
{renderCard(userChallenge)}
</div>
)
}
......
import React from 'react'
import { mount } from 'enzyme'
import ChallengeCardLast from './ChallengeCardLast'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
const mockStore = configureStore([])
describe('ChallengeCardLast component', () => {
it('should be rendered correctly', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
const wrapper = mount(
<Provider store={store}>
<ChallengeCardLast />
</Provider>
)
expect(wrapper).toMatchSnapshot()
})
})
import React from 'react'
import './challengeCardLast.scss'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import ecolyoIcon from 'assets/icons/visu/challenge/ecolyo.svg'
import StyledIcon from 'components/CommonKit/Icon/StyledIcon'
import { Button } from '@material-ui/core'
import { updateModalIsFeedbacksOpen } from 'store/modal/modal.actions'
import { useDispatch } from 'react-redux'
interface ChallengeCardLastProps {}
const ChallengeCardLast: React.FC = () => {
const { t } = useI18n()
const dispatch = useDispatch()
const handleClickFeedbacks = (): void => {
dispatch(updateModalIsFeedbacksOpen(true))
}
return (
<div className="cardLast">
<StyledIcon icon={ecolyoIcon} size={62} />
<div className="content">
<div className="text-22-bold title-last">
{t('challenge.card_last.title')}
</div>
<div className="text-18-normal message">
{t('challenge.card_last.message1')}
</div>
<div className="text-18-normal message">
{t('challenge.card_last.message2')}
</div>
</div>
<Button
aria-label={t('challenge.card_last.button')}
onClick={handleClickFeedbacks}
className="btn1"
classes={{
root: 'btn-secondary-negative btn_lastCard',
label: 'text-15-bold',
}}
>
{t('challenge.card_last.button')}
</Button>
</div>
)
}
export default ChallengeCardLast
......@@ -65,7 +65,7 @@ const ChallengeCardOnGoing: React.FC<ChallengeCardOnGoingProps> = ({
dispatch(updateUserChallengeList(updatedChallenge))
}
setIsLoading(false)
history.push('/challenges/duel')
history.push('/challenges/duel?id=' + userChallenge.id)
} else {
setIsLoading(false)
toggleNoFluidModal()
......
......@@ -31,6 +31,7 @@ const ChallengeView: React.FC = () => {
const [touchEnd, setTouchEnd] = useState<number>()
const [index, setindex] = useState<number>(0)
const [lastChallengeIndex, setlastChallengeIndex] = useState<number>(0)
const [isLastDuelDone, setIsLastDuelDone] = useState<boolean>(false)
const [containerTranslation, setcontainerTranslation] =
useState<number>(marginPx)
const defineHeaderHeight = (height: number) => {
......@@ -44,7 +45,10 @@ const ChallengeView: React.FC = () => {
}
const moveSliderRight = useCallback(() => {
if (index < userChallengeList.length - 1) {
if (
index < userChallengeList.length - 1 ||
(isLastDuelDone && index < userChallengeList.length)
) {
if (index === 0)
setcontainerTranslation(
(prev: number) => prev - cardWitdh - marginPx * 1.2
......@@ -123,10 +127,22 @@ const ChallengeView: React.FC = () => {
0 - cardWitdh * lastChallengeIndex - marginPx * 1.2
)
}
if (isLastDuelDone) {
setlastChallengeIndex(i + 1)
}
setindex(i)
}
})
}, [userChallengeList, lastChallengeIndex, cardWitdh])
}, [userChallengeList, lastChallengeIndex, cardWitdh, isLastDuelDone])
useEffect(() => {
if (
userChallengeList[userChallengeList.length - 1].state ==
UserChallengeState.DONE
) {
setIsLastDuelDone(true)
}
}, [userChallengeList])
return (
<>
......@@ -160,9 +176,21 @@ const ChallengeView: React.FC = () => {
index={i}
cardWidth={cardWitdh}
cardHeight={cardHeight}
isChallengeCardLast={false}
moveToSlide={moveToSlide}
/>
))}
{isLastDuelDone && (
<ChallengeCard
indexSlider={index}
index={5}
cardWidth={cardWitdh}
cardHeight={cardHeight}
isChallengeCardLast={true}
moveToSlide={moveToSlide}
/>
)}
</div>
</div>
<div className="sliderButtons">
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ChallengeCardLast component should be rendered correctly 1`] = `ReactWrapper {}`;
......@@ -47,6 +47,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={0}
indexSlider={3}
isChallengeCardLast={false}
key="0"
moveToSlide={[Function]}
userChallenge={
......@@ -198,6 +199,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={1}
indexSlider={3}
isChallengeCardLast={false}
key="1"
moveToSlide={[Function]}
userChallenge={
......@@ -349,6 +351,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={2}
indexSlider={3}
isChallengeCardLast={false}
key="2"
moveToSlide={[Function]}
userChallenge={
......@@ -500,6 +503,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={3}
indexSlider={3}
isChallengeCardLast={false}
key="3"
moveToSlide={[Function]}
userChallenge={
......@@ -651,6 +655,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={4}
indexSlider={3}
isChallengeCardLast={false}
key="4"
moveToSlide={[Function]}
userChallenge={
......@@ -802,6 +807,7 @@ exports[`ChallengeView component should be rendered correctly 1`] = `
cardWidth={285}
index={5}
indexSlider={3}
isChallengeCardLast={false}
key="5"
moveToSlide={[Function]}
userChallenge={
......
@import '../../styles/base/typo-variables';
@import '../../styles/base/color';
.cardLast {
width: 100%;
height: inherit;
box-sizing: border-box;
padding: 5%;
transition: all 300ms ease;
border: 1px solid $grey-bright;
border-radius: 4px;
align-items: center;
text-align: center;
display: flex;
flex-direction: column;
.content {
max-height: 350px;
flex-direction: column;
display: flex;
}
svg {
max-height: 150px;
}
.title-last {
padding: 1rem 2.3rem 1.5rem;
}
.message {
margin: 0 0 0.7rem;
}
.btn_lastCard {
padding: 8px;
max-height: 33px;
}
.content,
.title-last,
.message,
.btn_lastCard,
svg {
flex: 1;
}
}
......@@ -21,8 +21,10 @@ import CaptionIncomingIcon from 'assets/icons/visu/duel/captionIncoming.svg'
import ChallengeService from 'services/challenge.service'
import DuelChart from 'components/Duel/DuelChart'
import DuelResultModal from 'components/Duel/DuelResultModal'
import LastDuelModal from 'components/Duel/lastDuelModal'
import UsageEventService from 'services/usageEvent.service'
import { UsageEventType } from 'enum/usageEvent.enum'
import { UserDuelState } from 'enum/userDuel.enum'
interface DuelOngoingProps {
userChallenge: UserChallenge
......@@ -40,9 +42,13 @@ const DuelOngoing: React.FC<DuelOngoingProps> = ({
const history = useHistory()
const [resultModal, setResultModal] = useState<boolean>(false)
const [winChallenge, setWinChallenge] = useState<boolean>(false)
const [isLastDuel, setIsLastDuel] = useState<boolean>(false)
const [width, setWidth] = useState<number>(0)
const [height, setHeight] = useState<number>(0)
const chartContainer = useRef<HTMLDivElement>(null)
const { userChallengeList } = useSelector(
(state: AppStore) => state.ecolyo.challenge
)
const duel: UserDuel = userChallenge.duel
const title: string = duel.title
......@@ -78,8 +84,25 @@ const DuelOngoing: React.FC<DuelOngoingProps> = ({
dispatch(unlockNextUserChallenge(updatedChallenge))
dispatch(toggleChallengeDuelNotification(false))
if (
userChallenge.id == userChallengeList[userChallengeList.length - 1].id
) {
setIsLastDuel(true)
} else {
history.push('/challenges')
}
}, [
client,
userChallenge,
winChallenge,
dispatch,
userChallengeList,
history,
])
const setLastResult = useCallback(async () => {
history.push('/challenges')
}, [client, dispatch, userChallenge, history, winChallenge])
}, [history])
useEffect(() => {
function handleResize() {
......@@ -129,13 +152,13 @@ const DuelOngoing: React.FC<DuelOngoingProps> = ({
<div className="duel-goal text-18-normal">
{t('duel.goal1', {
durationInDays,
// eslint-disable-next-line @typescript-eslint/camelcase
smart_count: durationInDays,
})}
<span> </span>
{t('duel.goal2', {
title,
// eslint-disable-next-line @typescript-eslint/camelcase
smart_count: title,
})}
</div>
......@@ -183,6 +206,7 @@ const DuelOngoing: React.FC<DuelOngoingProps> = ({
win={winChallenge}
handleCloseClick={setResult}
/>
<LastDuelModal open={isLastDuel} handleCloseClick={setLastResult} />
</>
)
}
......
......@@ -16,6 +16,16 @@ jest.mock('components/Content/Content', () => 'mock-content')
const mockUseSelector = jest.spyOn(reactRedux, 'useSelector')
const mockHistoryGoBack = jest.fn()
jest.mock('react-router-dom', () => ({
useLocation: () => ({
search: '?id=CHALLENGE0002',
}),
useHistory: () => ({
goBack: mockHistoryGoBack,
}),
}))
describe('DuelView component', () => {
it('should be rendered with DuelError component when no current challenge', () => {
mockUseSelector.mockReturnValue(challengeStateData)
......@@ -23,7 +33,7 @@ describe('DuelView component', () => {
expect(wrapper.find(DuelError).exists()).toBeTruthy()
})
it('should be rendered with DuelError component when current challenge with state = duel and duel state = done', () => {
it('should be rendered with DuelOngoing component when current challenge with state = duel and duel state = done', () => {
const updatedUserChallenge = {
...userChallengeData[1],
state: UserChallengeState.DUEL,
......@@ -31,11 +41,13 @@ describe('DuelView component', () => {
}
const updatedChallengeState = {
...challengeStateData,
userChallengeList: userChallengeData,
currentChallenge: updatedUserChallenge,
}
updatedChallengeState.userChallengeList[1] = updatedUserChallenge
mockUseSelector.mockReturnValue(updatedChallengeState)
const wrapper = shallow(<DuelView />)
expect(wrapper.find(DuelError).exists()).toBeTruthy()
expect(wrapper.find(DuelOngoing).exists()).toBeTruthy()
})
it('should be rendered with DuelError component when current challenge with state = duel and duel state = locked', () => {
......@@ -46,8 +58,10 @@ describe('DuelView component', () => {
}
const updatedChallengeState = {
...challengeStateData,
userChallengeList: userChallengeData,
currentChallenge: updatedUserChallenge,
}
updatedChallengeState.userChallengeList[1] = updatedUserChallenge
mockUseSelector.mockReturnValue(updatedChallengeState)
const wrapper = shallow(<DuelView />)
expect(wrapper.find(DuelError).exists()).toBeTruthy()
......@@ -60,8 +74,10 @@ describe('DuelView component', () => {
}
const updatedChallengeState = {
...challengeStateData,
userChallengeList: userChallengeData,
currentChallenge: updatedUserChallenge,
}
updatedChallengeState.userChallengeList[1] = updatedUserChallenge
mockUseSelector.mockReturnValue(updatedChallengeState)
const wrapper = shallow(<DuelView />)
expect(wrapper.find(DuelError).exists()).toBeTruthy()
......@@ -75,8 +91,10 @@ describe('DuelView component', () => {
}
const updatedChallengeState = {
...challengeStateData,
userChallengeList: userChallengeData,
currentChallenge: updatedUserChallenge,
}
updatedChallengeState.userChallengeList[1] = updatedUserChallenge
mockUseSelector.mockReturnValue(updatedChallengeState)
const wrapper = shallow(<DuelView />)
expect(wrapper.find(DuelUnlocked).exists()).toBeTruthy()
......@@ -90,8 +108,10 @@ describe('DuelView component', () => {
}
const updatedChallengeState = {
...challengeStateData,
userChallengeList: userChallengeData,
currentChallenge: updatedUserChallenge,
}
updatedChallengeState.userChallengeList[1] = updatedUserChallenge
mockUseSelector.mockReturnValue(updatedChallengeState)
const wrapper = shallow(<DuelView />)
expect(wrapper.find(DuelOngoing).exists()).toBeTruthy()
......
......@@ -12,13 +12,18 @@ import { UserDuelState } from 'enum/userDuel.enum'
import { UserChallenge } from 'models'
import DuelOngoing from './DuelOngoing'
import DuelEmptyValueModal from './DuelEmptyValueModal'
import { useHistory } from 'react-router-dom'
import { useHistory, useLocation } from 'react-router-dom'
const DuelView: React.FC = () => {
const [headerHeight, setHeaderHeight] = useState<number>(0)
const { currentChallenge } = useSelector(
const { userChallengeList } = useSelector(
(state: AppStore) => state.ecolyo.challenge
)
const id = new URLSearchParams(useLocation().search).get('id')
const challengeToDisplay: UserChallenge | undefined = userChallengeList.find(
challenge => challenge.id === id
)
const history = useHistory()
const defineHeaderHeight = useCallback((height: number) => {
setHeaderHeight(height)
......@@ -32,6 +37,8 @@ const DuelView: React.FC = () => {
return <DuelUnlocked userChallenge={challenge} />
case UserDuelState.ONGOING:
return <DuelOngoing userChallenge={challenge} />
case UserDuelState.DONE:
return <DuelOngoing userChallenge={challenge} />
case UserDuelState.NO_REF_PERIOD_VALID:
return (
<DuelEmptyValueModal
......@@ -58,9 +65,10 @@ const DuelView: React.FC = () => {
></Header>
<Content height={headerHeight}>
<div>
{currentChallenge &&
currentChallenge.state === UserChallengeState.DUEL ? (
renderDuel(currentChallenge)
{challengeToDisplay &&
(challengeToDisplay.state === UserChallengeState.DUEL ||
challengeToDisplay.state === UserChallengeState.DONE) ? (
renderDuel(challengeToDisplay)
) : (
<DuelError />
)}
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`lastDuelModal component should render correctly 1`] = `ReactWrapper {}`;
@import '../../styles/base/color';
.duel-last-modal-root {
text-align: center;
.closeIcon {
float: right;
cursor: pointer;
}
.icon {
margin: 2rem 0 0;
}
.title {
margin: 1rem 0 1rem;
}
.subtitle {
color: $blue-light;
margin: 1rem 0 0.5rem;
}
.content {
margin: 0.5rem 0;
}
button.btn-secondary-negative {
margin: 2rem 0.25rem 0.5rem;
}
}
#accessibility-title {
display: none;
}
import React from 'react'
import { mount } from 'enzyme'
import { act } from 'react-dom/test-utils'
import LastDuelModal from './lastDuelModal'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
describe('lastDuelModal component', () => {
it('should render correctly', async () => {
const wrapper = mount(
<LastDuelModal open={true} handleCloseClick={jest.fn()} />
)
await act(async () => {
await new Promise(resolve => setTimeout(resolve))
wrapper.update()
})
expect(wrapper).toMatchSnapshot()
})
})
import React, { useEffect, useState } from 'react'
import './lastDuelModal.scss'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import Dialog from '@material-ui/core/Dialog'
import star from 'assets/icons/visu/duel/star.svg'
import CloseIcon from 'assets/icons/ico/close.svg'
import StyledIcon from 'components/CommonKit/Icon/StyledIcon'
interface LastDuelModalProps {
open: boolean
handleCloseClick: () => void
}
const LastDuelModal: React.FC<LastDuelModalProps> = ({
open,
handleCloseClick,
}: LastDuelModalProps) => {
const { t } = useI18n()
return (
<Dialog
open={open}
onClose={handleCloseClick}
aria-labelledby={'accessibility-title'}