From 525af23881ba8d880c35ad1e16423abf609e1c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20PAILHAREY?= <rpailharey@grandlyon.com> Date: Thu, 6 Jul 2023 15:36:10 +0000 Subject: [PATCH] feat(challenge): reset duel --- .../Challenge/ChallengeCardDone.spec.tsx | 101 +++- .../Challenge/ChallengeCardDone.tsx | 63 +- .../Challenge/ChallengeCardUnlocked.spec.tsx | 22 + .../Challenge/ChallengeCardUnlocked.tsx | 8 +- src/components/Challenge/ChallengeView.tsx | 50 +- .../ChallengeCardDone.spec.tsx.snap | 537 ++++++++++++++++-- .../__snapshots__/ChallengeView.spec.tsx.snap | 26 +- src/components/Challenge/challengeCard.scss | 1 + .../Challenge/challengeCardDone.scss | 17 +- .../Challenge/challengeCardOnGoing.scss | 2 +- .../Challenge/challengeCardUnlocked.scss | 1 - src/components/Challenge/challengeView.scss | 3 + src/components/Duel/duelUnlocked.scss | 6 + src/components/Quiz/quizFinish.scss | 4 +- src/enum/userChallenge.enum.ts | 1 + src/locales/fr.json | 3 +- src/services/challenge.service.ts | 9 + src/services/duel.service.spec.ts | 14 + src/services/duel.service.ts | 15 + src/store/challenge/challenge.slice.ts | 5 +- src/styles/components/_buttons.scss | 13 +- 21 files changed, 766 insertions(+), 135 deletions(-) diff --git a/src/components/Challenge/ChallengeCardDone.spec.tsx b/src/components/Challenge/ChallengeCardDone.spec.tsx index 07371a118..433705ca2 100644 --- a/src/components/Challenge/ChallengeCardDone.spec.tsx +++ b/src/components/Challenge/ChallengeCardDone.spec.tsx @@ -1,6 +1,12 @@ +import { Button } from '@material-ui/core' import ChallengeCardDone from 'components/Challenge/ChallengeCardDone' -import { shallow } from 'enzyme' +import { mount } from 'enzyme' +import toJson from 'enzyme-to-json' import React from 'react' +import * as reactRedux from 'react-redux' +import { Provider } from 'react-redux' +import configureStore from 'redux-mock-store' +import { waitForComponentToPaint } from '../../../tests/__mocks__/testUtils' import { userChallengeData } from '../../../tests/__mocks__/userChallengeData.mock' jest.mock('cozy-ui/transpiled/react/I18n', () => ({ @@ -8,11 +14,10 @@ jest.mock('cozy-ui/transpiled/react/I18n', () => ({ t: (str: string) => str, })), })) -const mockImportIconById = jest.fn() const mockFormatNumberValues = jest.fn() jest.mock('utils/utils', () => { return { - importIconById: jest.fn(() => mockImportIconById), + importIconById: jest.fn(() => null), formatNumberValues: jest.fn(() => mockFormatNumberValues), getChallengeTitleWithLineReturn: jest.fn(() => 'Challenge 1'), } @@ -24,11 +29,91 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockedNavigate, })) +const mockUpdateUserChallenge = jest.fn() +jest.mock('services/challenge.service', () => { + return jest.fn(() => { + return { + updateUserChallenge: mockUpdateUserChallenge, + } + }) +}) + +const mockStore = configureStore([]) +const mockDispatch = jest.fn() +const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch') + describe('ChallengeCardDone component', () => { - it('should be rendered correctly', () => { - const component = shallow( - <ChallengeCardDone userChallenge={userChallengeData[0]} /> - ).getElement() - expect(component).toMatchSnapshot() + const storeNoCurrentChallenge = mockStore({ + ecolyo: { + challenge: { currentChallenge: null }, + }, + }) + it('should be rendered correctly', async () => { + const wrapper = mount( + <Provider store={storeNoCurrentChallenge}> + <ChallengeCardDone userChallenge={userChallengeData[0]} /> + </Provider> + ) + await waitForComponentToPaint(wrapper) + expect(toJson(wrapper)).toMatchSnapshot() + }) + + describe('Reset final challenge', () => { + beforeEach(() => { + mockUpdateUserChallenge.mockClear() + mockDispatch.mockClear() + }) + it('should reset challenge if no other challenge is on going', async () => { + useDispatchSpy.mockImplementationOnce(() => mockDispatch) + const wrapper = mount( + <Provider store={storeNoCurrentChallenge}> + <ChallengeCardDone userChallenge={userChallengeData[0]} /> + </Provider> + ) + wrapper.find(Button).last().simulate('click') + await waitForComponentToPaint(wrapper) + expect(mockDispatch).toBeCalledTimes(1) + expect(mockDispatch).toBeCalledWith({ + type: 'challenge/updateUserChallengeList', + }) + expect(mockUpdateUserChallenge).toBeCalledTimes(1) + }) + it('should not reset challenge if another challenge is on going', async () => { + useDispatchSpy.mockImplementationOnce(() => mockDispatch) + const store = mockStore({ + ecolyo: { + challenge: { currentChallenge: userChallengeData[1] }, + }, + }) + const wrapper = mount( + <Provider store={store}> + <ChallengeCardDone userChallenge={userChallengeData[0]} /> + </Provider> + ) + wrapper.find(Button).last().simulate('click') + await waitForComponentToPaint(wrapper) + expect(mockDispatch).toBeCalledTimes(0) + expect(mockUpdateUserChallenge).toBeCalledTimes(0) + }) + it('should be primary button is challenge is lost', async () => { + const wrapper = mount( + <Provider store={storeNoCurrentChallenge}> + <ChallengeCardDone userChallenge={userChallengeData[1]} /> + </Provider> + ) + await waitForComponentToPaint(wrapper) + const resetButton = wrapper.find('button').last() + expect(resetButton.hasClass('btn-primary-challenge')).toBe(true) + }) + it('should be secondary button is challenge is won', async () => { + const wrapper = mount( + <Provider store={storeNoCurrentChallenge}> + <ChallengeCardDone userChallenge={userChallengeData[0]} /> + </Provider> + ) + await waitForComponentToPaint(wrapper) + const resetButton = wrapper.find('button').last() + expect(resetButton.hasClass('btn-secondary-negative')).toBe(true) + }) }) }) diff --git a/src/components/Challenge/ChallengeCardDone.tsx b/src/components/Challenge/ChallengeCardDone.tsx index 9430da1fb..217f24cd8 100644 --- a/src/components/Challenge/ChallengeCardDone.tsx +++ b/src/components/Challenge/ChallengeCardDone.tsx @@ -2,11 +2,19 @@ import { Button } from '@material-ui/core' import defaultIcon from 'assets/icons/visu/duelResult/default.svg' import classNames from 'classnames' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' +import { useClient } from 'cozy-client' import { useI18n } from 'cozy-ui/transpiled/react/I18n' -import { UserChallengeSuccess } from 'enum/userChallenge.enum' +import { + UserChallengeSuccess, + UserChallengeUpdateFlag, +} from 'enum/userChallenge.enum' import { UserChallenge } from 'models' -import React, { useEffect, useState } from 'react' +import React, { Dispatch, useEffect, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' import { useNavigate } from 'react-router-dom' +import ChallengeService from 'services/challenge.service' +import { AppActionsTypes, AppStore } from 'store' +import { updateUserChallengeList } from 'store/challenge/challenge.slice' import { formatNumberValues, getChallengeTitleWithLineReturn, @@ -21,8 +29,14 @@ const ChallengeCardDone = ({ }) => { const { t } = useI18n() const navigate = useNavigate() + const client = useClient() + const dispatch = useDispatch<Dispatch<AppActionsTypes>>() + const [winIcon, setWinIcon] = useState<string>(defaultIcon) const [lossIcon, setLossIcon] = useState<string>(defaultIcon) + const { currentChallenge } = useSelector( + (state: AppStore) => state.ecolyo.challenge + ) const getUserSaving = (_userChallenge: UserChallenge) => { let label @@ -52,6 +66,15 @@ const ChallengeCardDone = ({ navigate('/challenges/duel?id=' + userChallenge.id) } + const handleChallengeReset = async () => { + const challengeService = new ChallengeService(client) + const updatedChallenge = await challengeService.updateUserChallenge( + userChallenge, + UserChallengeUpdateFlag.DUEL_RESET + ) + dispatch(updateUserChallengeList(updatedChallenge)) + } + useEffect(() => { async function handleEcogestureIcon() { const icon = await importIconById(userChallenge.id + '-1', 'duelResult') @@ -106,16 +129,32 @@ const ChallengeCardDone = ({ {t('challenge.card_done.final_defi')} </span> </div> - <Button - aria-label={t('challenge.card_done.final_defi_view')} - onClick={goDuel} - classes={{ - root: 'btn-secondary-negative review-btn', - label: 'text-15-bold', - }} - > - {t('challenge.card_done.final_defi_view')} - </Button> + <div className="buttons"> + <Button + aria-label={t('challenge.card_done.final_defi_view')} + onClick={goDuel} + classes={{ + root: 'btn-secondary-negative grey-border', + label: 'text-15-bold', + }} + > + {t('challenge.card_done.final_defi_view')} + </Button> + <Button + aria-label={t('challenge.card_done.reset_defi')} + onClick={handleChallengeReset} + classes={{ + root: + userChallenge.success === UserChallengeSuccess.WIN + ? 'btn-secondary-negative grey-border' + : 'btn-primary-challenge', + label: 'text-15-bold', + }} + disabled={currentChallenge !== null} + > + {t('challenge.card_done.reset_defi')} + </Button> + </div> </div> ) } diff --git a/src/components/Challenge/ChallengeCardUnlocked.spec.tsx b/src/components/Challenge/ChallengeCardUnlocked.spec.tsx index 13cb72235..a5f422cda 100644 --- a/src/components/Challenge/ChallengeCardUnlocked.spec.tsx +++ b/src/components/Challenge/ChallengeCardUnlocked.spec.tsx @@ -1,3 +1,4 @@ +import { Button } from '@material-ui/core' import defaultIcon from 'assets/icons/visu/challenge/challengeLocked.svg' import { FluidType } from 'enum/fluid.enum' import { mount } from 'enzyme' @@ -6,8 +7,10 @@ import { Provider } from 'react-redux' import UsageEventService from 'services/usageEvent.service' import { createMockEcolyoStore, + mockChallengeState, mockGlobalState, } from '../../../tests/__mocks__/store' +import { waitForComponentToPaint } from '../../../tests/__mocks__/testUtils' import { userChallengeData } from '../../../tests/__mocks__/userChallengeData.mock' import ChallengeCardUnlocked from './ChallengeCardUnlocked' import ChallengeNoFluidModal from './ChallengeNoFluidModal' @@ -70,6 +73,7 @@ describe('ChallengeCardUnlocked component', () => { fluidTypes: [FluidType.ELECTRICITY], fluidStatus: [{ ...mockGlobalState.fluidStatus[0], status: 200 }], }, + challenge: mockChallengeState, }) const wrapper = mount( <Provider store={store}> @@ -81,4 +85,22 @@ describe('ChallengeCardUnlocked component', () => { expect(wrapper.find(ChallengeNoFluidModal).prop('open')).toBeFalsy() expect(mockStartUserChallenge).toHaveBeenCalledWith(userChallengeData[0]) }) + + it('should not be able to launch challenge if another one is active', () => { + const store = createMockEcolyoStore({ + global: mockGlobalState, + challenge: { + ...mockChallengeState, + currentChallenge: userChallengeData[1], + }, + }) + const wrapper = mount( + <Provider store={store}> + <ChallengeCardUnlocked userChallenge={userChallengeData[0]} /> + </Provider> + ) + waitForComponentToPaint(wrapper) + const resetButton = wrapper.find(Button).last() + expect(resetButton.prop('disabled')).toBe(true) + }) }) diff --git a/src/components/Challenge/ChallengeCardUnlocked.tsx b/src/components/Challenge/ChallengeCardUnlocked.tsx index e0e70808e..dc7688006 100644 --- a/src/components/Challenge/ChallengeCardUnlocked.tsx +++ b/src/components/Challenge/ChallengeCardUnlocked.tsx @@ -24,10 +24,11 @@ const ChallengeCardUnlocked = ({ const { t } = useI18n() const client: Client = useClient() const dispatch = useDispatch<Dispatch<AppActionsTypes>>() - const { fluidTypes, fluidStatus } = useSelector( - (state: AppStore) => state.ecolyo.global - ) const [openNoFluidModal, setopenNoFluidModal] = useState(false) + const { + global: { fluidTypes, fluidStatus }, + challenge: { currentChallenge }, + } = useSelector((state: AppStore) => state.ecolyo) const [challengeIcon, setChallengeIcon] = useState(defaultIcon) let statusRequirementOk = false @@ -93,6 +94,7 @@ const ChallengeCardUnlocked = ({ root: 'btn-duel-active', label: 'text-16-bold', }} + disabled={currentChallenge !== null} > {t('challenge.card_unlocked.button_launch')} </Button> diff --git a/src/components/Challenge/ChallengeView.tsx b/src/components/Challenge/ChallengeView.tsx index 95f526de7..94673b495 100644 --- a/src/components/Challenge/ChallengeView.tsx +++ b/src/components/Challenge/ChallengeView.tsx @@ -6,7 +6,6 @@ import CozyBar from 'components/Header/CozyBar' import Header from 'components/Header/Header' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { UserChallengeState } from 'enum/userChallenge.enum' -import { UserChallenge } from 'models' import React, { useCallback, useEffect, useState } from 'react' import { useSelector } from 'react-redux' import { AppStore } from 'store' @@ -22,12 +21,11 @@ const ChallengeView = () => { const marginPx = 16 const cardWidth = window.outerWidth < 500 ? window.outerWidth - marginPx * 6 : 285 - const cardHeight = window.outerHeight * 0.6 + const cardHeight = window.outerHeight * 0.65 const [headerHeight, setHeaderHeight] = useState<number>(0) const [touchStart, setTouchStart] = useState<number>() 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) @@ -46,22 +44,14 @@ const ChallengeView = () => { index < userChallengeList.length - 1 || (isLastDuelDone && index < userChallengeList.length) ) { - if (index === 0) - setContainerTranslation( - (prev: number) => prev - cardWidth - marginPx * 1.2 - ) - else if (index >= 1) - setContainerTranslation((prev: number) => prev - cardWidth - marginPx) - else setContainerTranslation((prev: number) => prev - cardWidth) + setContainerTranslation(prev => prev - cardWidth - marginPx) setIndex(prev => prev + 1) } - }, [cardWidth, index, userChallengeList.length]) + }, [cardWidth, index, isLastDuelDone, userChallengeList.length]) const moveSliderLeft = useCallback(() => { if (index > 0) { - if (index >= 1) - setContainerTranslation((prev: number) => prev + cardWidth + marginPx) - else setContainerTranslation((prev: number) => prev + cardWidth) + setContainerTranslation(prev => prev + cardWidth + marginPx) setIndex(prev => prev - 1) } if (index <= 1) { @@ -95,28 +85,20 @@ const ChallengeView = () => { } useEffect(() => { - userChallengeList.forEach((challenge: UserChallenge, i: number) => { - if ( + let currentChallengeIndex = userChallengeList.findIndex( + challenge => challenge.state === UserChallengeState.UNLOCKED || challenge.state === UserChallengeState.ONGOING || challenge.state === UserChallengeState.DUEL - ) { - setLastChallengeIndex(i) - if (lastChallengeIndex === 0) return - else if (lastChallengeIndex === 1) { - setContainerTranslation(0 - cardWidth * lastChallengeIndex) - } else { - setContainerTranslation( - 0 - cardWidth * lastChallengeIndex - marginPx * 1.2 - ) - } - if (isLastDuelDone) { - setLastChallengeIndex(i + 1) - } - setIndex(i) - } - }) - }, [userChallengeList, lastChallengeIndex, cardWidth, isLastDuelDone]) + ) + if (currentChallengeIndex === -1) { + currentChallengeIndex = isLastDuelDone ? userChallengeList.length : 0 + } + setContainerTranslation( + -currentChallengeIndex * (cardWidth + marginPx) + marginPx + ) + setIndex(currentChallengeIndex) + }, [userChallengeList, cardWidth, isLastDuelDone]) useEffect(() => { if ( @@ -166,7 +148,7 @@ const ChallengeView = () => { {isLastDuelDone && ( <ChallengeCard indexSlider={index} - index={5} + index={userChallengeList.length} cardWidth={cardWidth} cardHeight={cardHeight} isChallengeCardLast={true} diff --git a/src/components/Challenge/__snapshots__/ChallengeCardDone.spec.tsx.snap b/src/components/Challenge/__snapshots__/ChallengeCardDone.spec.tsx.snap index 54a1e470a..4065b79a9 100644 --- a/src/components/Challenge/__snapshots__/ChallengeCardDone.spec.tsx.snap +++ b/src/components/Challenge/__snapshots__/ChallengeCardDone.spec.tsx.snap @@ -1,58 +1,501 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ChallengeCardDone component should be rendered correctly 1`] = ` -<div - className="cardContent cardDone" +<Provider + store={ + Object { + "clearActions": [Function], + "dispatch": [Function], + "getActions": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + } + } > - <div - className="challengeName text-22-bold" - > - Challenge 1 - </div> - <div - className="iconResult" - > - <StyledIcon - className="imgResult" - icon="test-file-stub" - size={180} - /> - </div> - <div - className="statsResult" + <ChallengeCardDone + userChallenge={ + Object { + "action": Object { + "ecogesture": null, + "startDate": null, + "state": 0, + }, + "description": "Description challenge 1", + "duel": Object { + "description": "Je parie un ours polaire que vous ne pouvez pas consommer moins que #CONSUMPTION € en 1 semaine", + "duration": "P30D", + "fluidTypes": Array [], + "id": "DUEL001", + "startDate": null, + "state": 0, + "threshold": 0, + "title": "Title DUEL001", + "userConsumption": 0, + }, + "endingDate": null, + "exploration": Object { + "complementary_description": "Refaire un tour dans son profil si déjà fait", + "date": null, + "description": "Avoir complété son profil", + "ecogesture_id": "", + "fluid_condition": Array [], + "id": "EXPLORATION001", + "message_success": "Vous avez complété votre profil ou refait un tour dans votre profil", + "progress": 0, + "state": 0, + "target": 1, + "type": 1, + }, + "id": "CHALLENGE0001", + "progress": Object { + "actionProgress": 0, + "explorationProgress": 0, + "quizProgress": 0, + }, + "quiz": Object { + "customQuestion": Object { + "interval": 20, + "period": Object {}, + "questionLabel": "Custom1", + "result": 0, + "singleFluid": false, + "timeStep": 20, + "type": 0, + }, + "id": "QUIZ001", + "questions": Array [ + Object { + "answers": Array [ + Object { + "answerLabel": "86 km", + "isTrue": true, + }, + Object { + "answerLabel": "78 km", + "isTrue": false, + }, + Object { + "answerLabel": "56 km", + "isTrue": false, + }, + ], + "explanation": "L’aqueduc du Gier est un des aqueducs antiques de Lyon desservant la ville antique de Lugdunum. Avec ses 86 km il est le plus long des quatre aqueducs ayant alimenté la ville en eau, et celui dont les structures sont le mieux conservées. Il doit son nom au fait qu'il puise aux sources du Gier, affluent du Rhône", + "questionLabel": "Quelle longueur faisait l’aqueduc du Gier pour acheminer l’eau sur Lyon à l’époque romaine ?", + "result": 0, + "source": "string", + }, + Object { + "answers": Array [ + Object { + "answerLabel": "1 point d’eau public pour 800 habitants.", + "isTrue": true, + }, + Object { + "answerLabel": "1 point d’eau public pour 400 habitants.", + "isTrue": false, + }, + Object { + "answerLabel": "1 point d’eau public pour 200 habitants.", + "isTrue": false, + }, + ], + "explanation": "string", + "questionLabel": "En 1800 à Lyon, combien de points d'eau y avait-il par habitants ?", + "result": 0, + "source": "string", + }, + Object { + "answers": Array [ + Object { + "answerLabel": "François Mitterrand", + "isTrue": false, + }, + Object { + "answerLabel": "Napoléon Ier", + "isTrue": true, + }, + Object { + "answerLabel": "Napoléon III", + "isTrue": false, + }, + ], + "explanation": "string", + "questionLabel": "Qui officialise la création de la Compagnie Générale des eaux ?", + "result": 0, + "source": "string", + }, + Object { + "answers": Array [ + Object { + "answerLabel": "string", + "isTrue": false, + }, + Object { + "answerLabel": "string", + "isTrue": false, + }, + Object { + "answerLabel": "Aristide Dumont", + "isTrue": true, + }, + ], + "explanation": "string", + "questionLabel": "Quel ingénieur est à l’origine du projet d’alimentation en eau en 1856 ?", + "result": 0, + "source": "string", + }, + ], + "result": 0, + "startDate": null, + "state": 0, + }, + "startDate": null, + "state": 4, + "success": 2, + "target": 15, + "title": "Challenge 1", + } + } > <div - className="labelResult win" - > - challenge.card_done.win - </div> - <span - className="text-18" + className="cardContent cardDone" > - challenge.card_done.saving - <span - className="text-18-bold" + <div + className="challengeName text-22-bold" > - function () { + Challenge 1 + </div> + <div + className="iconResult" + > + <StyledIcon + className="imgResult" + icon="test-file-stub" + size={180} + > + <Icon + aria-hidden={true} + className="imgResult" + icon="test-file-stub" + size={180} + spin={false} + > + <Component + aria-hidden={true} + className="imgResult styles__icon___23x3R" + height={180} + style={Object {}} + width={180} + > + <svg + aria-hidden={true} + className="imgResult styles__icon___23x3R" + height={180} + style={Object {}} + width={180} + > + <use + xlinkHref="#test-file-stub" + /> + </svg> + </Component> + </Icon> + </StyledIcon> + </div> + <div + className="statsResult" + > + <div + className="labelResult win" + > + challenge.card_done.win + </div> + <span + className="text-18" + > + challenge.card_done.saving + <span + className="text-18-bold" + > + function () { return fn.apply(this, arguments); } - € - </span> - <br /> - challenge.card_done.final_defi - </span> - </div> - <WithStyles(ForwardRef(Button)) - aria-label="challenge.card_done.final_defi_view" - classes={ - Object { - "label": "text-15-bold", - "root": "btn-secondary-negative review-btn", - } - } - onClick={[Function]} - > - challenge.card_done.final_defi_view - </WithStyles(ForwardRef(Button))> -</div> + € + </span> + <br /> + challenge.card_done.final_defi + </span> + </div> + <div + className="buttons" + > + <WithStyles(ForwardRef(Button)) + aria-label="challenge.card_done.final_defi_view" + classes={ + Object { + "label": "text-15-bold", + "root": "btn-secondary-negative grey-border", + } + } + onClick={[Function]} + > + <ForwardRef(Button) + aria-label="challenge.card_done.final_defi_view" + classes={ + Object { + "colorInherit": "MuiButton-colorInherit", + "contained": "MuiButton-contained", + "containedPrimary": "MuiButton-containedPrimary", + "containedSecondary": "MuiButton-containedSecondary", + "containedSizeLarge": "MuiButton-containedSizeLarge", + "containedSizeSmall": "MuiButton-containedSizeSmall", + "disableElevation": "MuiButton-disableElevation", + "disabled": "Mui-disabled", + "endIcon": "MuiButton-endIcon", + "focusVisible": "Mui-focusVisible", + "fullWidth": "MuiButton-fullWidth", + "iconSizeLarge": "MuiButton-iconSizeLarge", + "iconSizeMedium": "MuiButton-iconSizeMedium", + "iconSizeSmall": "MuiButton-iconSizeSmall", + "label": "MuiButton-label text-15-bold", + "outlined": "MuiButton-outlined", + "outlinedPrimary": "MuiButton-outlinedPrimary", + "outlinedSecondary": "MuiButton-outlinedSecondary", + "outlinedSizeLarge": "MuiButton-outlinedSizeLarge", + "outlinedSizeSmall": "MuiButton-outlinedSizeSmall", + "root": "MuiButton-root btn-secondary-negative grey-border", + "sizeLarge": "MuiButton-sizeLarge", + "sizeSmall": "MuiButton-sizeSmall", + "startIcon": "MuiButton-startIcon", + "text": "MuiButton-text", + "textPrimary": "MuiButton-textPrimary", + "textSecondary": "MuiButton-textSecondary", + "textSizeLarge": "MuiButton-textSizeLarge", + "textSizeSmall": "MuiButton-textSizeSmall", + } + } + onClick={[Function]} + > + <WithStyles(ForwardRef(ButtonBase)) + aria-label="challenge.card_done.final_defi_view" + className="MuiButton-root btn-secondary-negative grey-border MuiButton-text" + component="button" + disabled={false} + focusRipple={true} + focusVisibleClassName="Mui-focusVisible" + onClick={[Function]} + type="button" + > + <ForwardRef(ButtonBase) + aria-label="challenge.card_done.final_defi_view" + className="MuiButton-root btn-secondary-negative grey-border MuiButton-text" + classes={ + Object { + "disabled": "Mui-disabled", + "focusVisible": "Mui-focusVisible", + "root": "MuiButtonBase-root", + } + } + component="button" + disabled={false} + focusRipple={true} + focusVisibleClassName="Mui-focusVisible" + onClick={[Function]} + type="button" + > + <button + aria-label="challenge.card_done.final_defi_view" + className="MuiButtonBase-root MuiButton-root btn-secondary-negative grey-border MuiButton-text" + disabled={false} + onBlur={[Function]} + onClick={[Function]} + onDragLeave={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseDown={[Function]} + onMouseLeave={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchMove={[Function]} + onTouchStart={[Function]} + tabIndex={0} + type="button" + > + <span + className="MuiButton-label text-15-bold" + > + challenge.card_done.final_defi_view + </span> + <WithStyles(memo) + center={false} + > + <ForwardRef(TouchRipple) + center={false} + classes={ + Object { + "child": "MuiTouchRipple-child", + "childLeaving": "MuiTouchRipple-childLeaving", + "childPulsate": "MuiTouchRipple-childPulsate", + "ripple": "MuiTouchRipple-ripple", + "ripplePulsate": "MuiTouchRipple-ripplePulsate", + "rippleVisible": "MuiTouchRipple-rippleVisible", + "root": "MuiTouchRipple-root", + } + } + > + <span + className="MuiTouchRipple-root" + > + <TransitionGroup + childFactory={[Function]} + component={null} + exit={true} + /> + </span> + </ForwardRef(TouchRipple)> + </WithStyles(memo)> + </button> + </ForwardRef(ButtonBase)> + </WithStyles(ForwardRef(ButtonBase))> + </ForwardRef(Button)> + </WithStyles(ForwardRef(Button))> + <WithStyles(ForwardRef(Button)) + aria-label="challenge.card_done.reset_defi" + classes={ + Object { + "label": "text-15-bold", + "root": "btn-secondary-negative grey-border", + } + } + disabled={false} + onClick={[Function]} + > + <ForwardRef(Button) + aria-label="challenge.card_done.reset_defi" + classes={ + Object { + "colorInherit": "MuiButton-colorInherit", + "contained": "MuiButton-contained", + "containedPrimary": "MuiButton-containedPrimary", + "containedSecondary": "MuiButton-containedSecondary", + "containedSizeLarge": "MuiButton-containedSizeLarge", + "containedSizeSmall": "MuiButton-containedSizeSmall", + "disableElevation": "MuiButton-disableElevation", + "disabled": "Mui-disabled", + "endIcon": "MuiButton-endIcon", + "focusVisible": "Mui-focusVisible", + "fullWidth": "MuiButton-fullWidth", + "iconSizeLarge": "MuiButton-iconSizeLarge", + "iconSizeMedium": "MuiButton-iconSizeMedium", + "iconSizeSmall": "MuiButton-iconSizeSmall", + "label": "MuiButton-label text-15-bold", + "outlined": "MuiButton-outlined", + "outlinedPrimary": "MuiButton-outlinedPrimary", + "outlinedSecondary": "MuiButton-outlinedSecondary", + "outlinedSizeLarge": "MuiButton-outlinedSizeLarge", + "outlinedSizeSmall": "MuiButton-outlinedSizeSmall", + "root": "MuiButton-root btn-secondary-negative grey-border", + "sizeLarge": "MuiButton-sizeLarge", + "sizeSmall": "MuiButton-sizeSmall", + "startIcon": "MuiButton-startIcon", + "text": "MuiButton-text", + "textPrimary": "MuiButton-textPrimary", + "textSecondary": "MuiButton-textSecondary", + "textSizeLarge": "MuiButton-textSizeLarge", + "textSizeSmall": "MuiButton-textSizeSmall", + } + } + disabled={false} + onClick={[Function]} + > + <WithStyles(ForwardRef(ButtonBase)) + aria-label="challenge.card_done.reset_defi" + className="MuiButton-root btn-secondary-negative grey-border MuiButton-text" + component="button" + disabled={false} + focusRipple={true} + focusVisibleClassName="Mui-focusVisible" + onClick={[Function]} + type="button" + > + <ForwardRef(ButtonBase) + aria-label="challenge.card_done.reset_defi" + className="MuiButton-root btn-secondary-negative grey-border MuiButton-text" + classes={ + Object { + "disabled": "Mui-disabled", + "focusVisible": "Mui-focusVisible", + "root": "MuiButtonBase-root", + } + } + component="button" + disabled={false} + focusRipple={true} + focusVisibleClassName="Mui-focusVisible" + onClick={[Function]} + type="button" + > + <button + aria-label="challenge.card_done.reset_defi" + className="MuiButtonBase-root MuiButton-root btn-secondary-negative grey-border MuiButton-text" + disabled={false} + onBlur={[Function]} + onClick={[Function]} + onDragLeave={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + onKeyUp={[Function]} + onMouseDown={[Function]} + onMouseLeave={[Function]} + onMouseUp={[Function]} + onTouchEnd={[Function]} + onTouchMove={[Function]} + onTouchStart={[Function]} + tabIndex={0} + type="button" + > + <span + className="MuiButton-label text-15-bold" + > + challenge.card_done.reset_defi + </span> + <WithStyles(memo) + center={false} + > + <ForwardRef(TouchRipple) + center={false} + classes={ + Object { + "child": "MuiTouchRipple-child", + "childLeaving": "MuiTouchRipple-childLeaving", + "childPulsate": "MuiTouchRipple-childPulsate", + "ripple": "MuiTouchRipple-ripple", + "ripplePulsate": "MuiTouchRipple-ripplePulsate", + "rippleVisible": "MuiTouchRipple-rippleVisible", + "root": "MuiTouchRipple-root", + } + } + > + <span + className="MuiTouchRipple-root" + > + <TransitionGroup + childFactory={[Function]} + component={null} + exit={true} + /> + </span> + </ForwardRef(TouchRipple)> + </WithStyles(memo)> + </button> + </ForwardRef(ButtonBase)> + </WithStyles(ForwardRef(ButtonBase))> + </ForwardRef(Button)> + </WithStyles(ForwardRef(Button))> + </div> + </div> + </ChallengeCardDone> +</Provider> `; diff --git a/src/components/Challenge/__snapshots__/ChallengeView.spec.tsx.snap b/src/components/Challenge/__snapshots__/ChallengeView.spec.tsx.snap index 3ddca6ea0..5b7b889d7 100644 --- a/src/components/Challenge/__snapshots__/ChallengeView.spec.tsx.snap +++ b/src/components/Challenge/__snapshots__/ChallengeView.spec.tsx.snap @@ -38,15 +38,15 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` className="challenge-container" style={ Object { - "transform": "translateX(-874.2px)", + "transform": "translateX(-586px)", } } > <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={0} - indexSlider={3} + indexSlider={2} key="CHALLENGE0001" moveToSlide={[Function]} userChallenge={ @@ -194,10 +194,10 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` } /> <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={1} - indexSlider={3} + indexSlider={2} key="CHALLENGE0002" moveToSlide={[Function]} userChallenge={ @@ -345,10 +345,10 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` } /> <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={2} - indexSlider={3} + indexSlider={2} key="CHALLENGE0003" moveToSlide={[Function]} userChallenge={ @@ -496,10 +496,10 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` } /> <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={3} - indexSlider={3} + indexSlider={2} key="CHALLENGE0004" moveToSlide={[Function]} userChallenge={ @@ -647,10 +647,10 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` } /> <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={4} - indexSlider={3} + indexSlider={2} key="CHALLENGE0005" moveToSlide={[Function]} userChallenge={ @@ -798,10 +798,10 @@ exports[`ChallengeView component should be rendered correctly 1`] = ` } /> <mock-challengecard - cardHeight={460.79999999999995} + cardHeight={499.20000000000005} cardWidth={285} index={5} - indexSlider={3} + indexSlider={2} key="CHALLENGE0006" moveToSlide={[Function]} userChallenge={ diff --git a/src/components/Challenge/challengeCard.scss b/src/components/Challenge/challengeCard.scss index 8088fe285..8e07734cf 100644 --- a/src/components/Challenge/challengeCard.scss +++ b/src/components/Challenge/challengeCard.scss @@ -8,6 +8,7 @@ color: white; display: flex; flex-direction: column; + height: 100%; &.active { transform: scale(1); } diff --git a/src/components/Challenge/challengeCardDone.scss b/src/components/Challenge/challengeCardDone.scss index 1b7fd3ba8..c7eee6ea1 100644 --- a/src/components/Challenge/challengeCardDone.scss +++ b/src/components/Challenge/challengeCardDone.scss @@ -41,10 +41,19 @@ .statsResult { text-align: center; } - .review-btn { - padding: 0.625rem; - margin: 0; - border: 1px solid $grey-bright; + .buttons { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; + + button { + padding: 0.625rem; + margin: 0; + &.grey-border { + border: 1px solid $grey-bright; + } + } } } } diff --git a/src/components/Challenge/challengeCardOnGoing.scss b/src/components/Challenge/challengeCardOnGoing.scss index ee4236859..b992883ca 100644 --- a/src/components/Challenge/challengeCardOnGoing.scss +++ b/src/components/Challenge/challengeCardOnGoing.scss @@ -4,7 +4,7 @@ .cardContent { background: transparent; &.onGoing { - border: 1px solid #e0e0e0; + border: 1px solid $grey-bright; background: inherit !important; .challengeTitle { margin-top: 0; diff --git a/src/components/Challenge/challengeCardUnlocked.scss b/src/components/Challenge/challengeCardUnlocked.scss index 16e4bf2bb..5199a12c6 100644 --- a/src/components/Challenge/challengeCardUnlocked.scss +++ b/src/components/Challenge/challengeCardUnlocked.scss @@ -10,7 +10,6 @@ filter: drop-shadow(0px 4px 16px rgba(0, 0, 0, 0.55)); button.btn-duel-active { - margin: auto; padding: 1.2rem 1.5rem; } .challengeIcon { diff --git a/src/components/Challenge/challengeView.scss b/src/components/Challenge/challengeView.scss index b0cc2a8fa..debbf9355 100644 --- a/src/components/Challenge/challengeView.scss +++ b/src/components/Challenge/challengeView.scss @@ -23,6 +23,9 @@ .cardContent { margin: auto; cursor: pointer; + &.onGoing { + padding-top: 2.5rem; + } .title { font-weight: 400; text-align: center; diff --git a/src/components/Duel/duelUnlocked.scss b/src/components/Duel/duelUnlocked.scss index 077c1eed9..9f8d88995 100644 --- a/src/components/Duel/duelUnlocked.scss +++ b/src/components/Duel/duelUnlocked.scss @@ -25,4 +25,10 @@ } .button-start { margin-top: 1rem; + width: 100%; + max-width: 175px; +} +button.btn-secondary-negative { + margin: 0; + padding: 0.5rem; } diff --git a/src/components/Quiz/quizFinish.scss b/src/components/Quiz/quizFinish.scss index 79da8a18a..288da76b5 100644 --- a/src/components/Quiz/quizFinish.scss +++ b/src/components/Quiz/quizFinish.scss @@ -9,10 +9,12 @@ color: $white; background: $grey-linear-gradient-background; text-align: center; + display: flex; + flex-direction: column; + align-items: center; button.btn-secondary-negative { border-color: $grey-bright; - min-width: 15rem; } .button-start { margin-top: 3rem; diff --git a/src/enum/userChallenge.enum.ts b/src/enum/userChallenge.enum.ts index 70ac30322..193179877 100644 --- a/src/enum/userChallenge.enum.ts +++ b/src/enum/userChallenge.enum.ts @@ -6,6 +6,7 @@ export enum UserChallengeUpdateFlag { DUEL_CONSUMPTION = 13, DUEL_WIN = 14, DUEL_LOSS = 15, + DUEL_RESET = 16, QUIZ = 20, QUIZ_START = 21, QUIZ_DONE = 22, diff --git a/src/locales/fr.json b/src/locales/fr.json index 5d0f18ff7..33bb09489 100644 --- a/src/locales/fr.json +++ b/src/locales/fr.json @@ -269,7 +269,8 @@ "win": "Gagné", "lost": "Perdu", "final_defi": "sur le duel final", - "final_defi_view": "Revoir le duel final" + "final_defi_view": "Revoir le duel final", + "reset_defi": "Relancer le défi" }, "card_last": { "title": "Tous les défis sont terminés", diff --git a/src/services/challenge.service.ts b/src/services/challenge.service.ts index b9de549d9..4373e8a88 100644 --- a/src/services/challenge.service.ts +++ b/src/services/challenge.service.ts @@ -673,6 +673,15 @@ export default class ChallengeService { success: UserChallengeSuccess.LOST, } break + case UserChallengeUpdateFlag.DUEL_RESET: + updatedDuel = await duelService.resetUserDuel(userChallenge.duel) + updatedUserChallenge = { + ...userChallenge, + state: UserChallengeState.DUEL, + duel: updatedDuel, + success: UserChallengeSuccess.ONGOING, + } + break case UserChallengeUpdateFlag.QUIZ_START: updatedQuiz = await quizService.startUserQuiz(userChallenge.quiz) updatedUserChallenge = { diff --git a/src/services/duel.service.spec.ts b/src/services/duel.service.spec.ts index dd4ea30f5..0a6260c98 100644 --- a/src/services/duel.service.spec.ts +++ b/src/services/duel.service.spec.ts @@ -120,6 +120,20 @@ describe('Duel service', () => { }) }) + describe('resetUserDuel method', () => { + it('should return the userDuel with unlocked state', async () => { + const result = await duelService.resetUserDuel(duelData) + const mockUpdatedDuel: UserDuel = { + ...duelData, + startDate: null, + state: UserDuelState.UNLOCKED, + threshold: 0, + userConsumption: 0, + } + expect(result).toEqual(mockUpdatedDuel) + }) + }) + describe('parseDuelEntityToDuel method', () => { it('should return the userDuel from a duelEntity', () => { const mockUpdatedDuel: UserDuel = { diff --git a/src/services/duel.service.ts b/src/services/duel.service.ts index 63e308e4c..d3f010ac5 100644 --- a/src/services/duel.service.ts +++ b/src/services/duel.service.ts @@ -269,6 +269,21 @@ export default class DuelService { return updatedUserDuel } + /** + * Return duel with updated state to UserDuelState.UNLOCKED + * @param {UserDuel} userDuel - userDuel to reset + * @returns {UserDuel} + */ + public async resetUserDuel(userDuel: UserDuel): Promise<UserDuel> { + return { + ...userDuel, + startDate: null, + state: UserDuelState.UNLOCKED, + threshold: 0, + userConsumption: 0, + } + } + /** * Return duel created from duel entity * @param {DuelEntity} duel - userDuel to update diff --git a/src/store/challenge/challenge.slice.ts b/src/store/challenge/challenge.slice.ts index eae360e05..3699373ef 100644 --- a/src/store/challenge/challenge.slice.ts +++ b/src/store/challenge/challenge.slice.ts @@ -56,7 +56,10 @@ export const challengeSlice = createSlice({ const updatedList = [...state.userChallengeList] const findIndex = updatedList.findIndex(challenge => challenge.id === id) updatedList[findIndex] = action.payload - if (typeof updatedList[findIndex + 1] !== 'undefined') { + if ( + typeof updatedList[findIndex + 1] !== 'undefined' && + updatedList[findIndex + 1].state === UserChallengeState.LOCKED + ) { updatedList[findIndex + 1] = { ...updatedList[findIndex + 1], state: UserChallengeState.UNLOCKED, diff --git a/src/styles/components/_buttons.scss b/src/styles/components/_buttons.scss index 8735392e4..8c91fc207 100644 --- a/src/styles/components/_buttons.scss +++ b/src/styles/components/_buttons.scss @@ -20,16 +20,11 @@ button { } } } - &.btn-primary-negative { - @include button( - transparent, - $gold-shadow, - 1px solid $grey-dark, - transparent - ) { - background-color: rgba($grey-dark, 0.2); + &.btn-primary-challenge { + @include button($blue-light, black, 1px solid $blue-light, transparent) { + background-color: rgba($blue-light, 0.2); span:first-child { - color: rgba($gold-shadow, 0.7); + color: black; } } } -- GitLab