From 10962ef79c0a3c01b22bba0495f382a077ca18d7 Mon Sep 17 00:00:00 2001 From: Drikus Roor Date: Mon, 18 Dec 2023 13:24:18 +0100 Subject: [PATCH] Added: Add 2x3 grid support for Matching Pairs component if it has equal or less than 6 sections (#657) * refactor: Update scss-watch command to be scss:watch * feat: Add classNames utility function This commit adds a utility function called `classNames` to the `util` directory of the frontend code. The `classNames` function takes in multiple string arguments and returns a single string with the classes joined together, separated by a space. It also filters out any falsy values before joining the classes. The commit also includes a test suite for the `classNames` function, covering scenarios where multiple classes are joined, falsy values are filtered, and empty strings and no arguments are provided. * feat: Update MatchingPairs column amount behavior - Add condition to determine column count based on the number of sections in the code. - Update CSS styles for the playing board based on column count. - Remove unnecessary white spaces and newlines in the code. * story: Add MatchingPairs story with default props and two columns support This commit adds the MatchingPairs component with its default props and support for displaying two columns of sections. The component automatically adjusts the number of columns based on the number of sections provided. If there are six or less sections, two columns are displayed. If there are more than six sections, four columns are displayed. The component is also displayed in fullscreen mode. * test: Add tests for the MatchingPairs component - Added tests for the MatchingPairs component to ensure it properly displays two columns when the sections length is less than or equal to 6, and four columns when the sections length is greater than 6. The tests use a mock PlayCard component and check for the presence or absence of specific CSS classes on the playing board. * refactor: MatchingPairs displays 3 columns instead of 2 when sections <= 6 * config: Add storybook script and update permissions on test-front-ci * fix: Fix classes & story by actually showing 3 columns instead of 2 --- frontend/package.json | 2 +- .../src/components/Playback/MatchingPairs.js | 71 ++++---- .../components/Playback/MatchingPairs.scss | 20 ++- .../components/Playback/MatchingPairs.test.js | 30 ++++ frontend/src/stories/MatchingPairs.stories.js | 168 ++++++++++++++++++ frontend/src/util/classNames.js | 10 ++ frontend/src/util/classNames.test.js | 19 ++ scripts/test-front-ci | 0 8 files changed, 278 insertions(+), 42 deletions(-) create mode 100644 frontend/src/components/Playback/MatchingPairs.test.js create mode 100644 frontend/src/stories/MatchingPairs.stories.js create mode 100644 frontend/src/util/classNames.js create mode 100644 frontend/src/util/classNames.test.js mode change 100644 => 100755 scripts/test-front-ci diff --git a/frontend/package.json b/frontend/package.json index d54eea678..8f05561b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,7 +31,7 @@ "test:ci": "react-scripts test --coverage --watchAll=false", "eject": "react-scripts eject", "scss": "sass src/index.scss src/index.css", - "scss-watch": "sass src/index.scss src/index.css; sass --watch src/index.scss src/index.css", + "scss:watch": "sass src/index.scss src/index.css; sass --watch src/index.scss src/index.css", "storybook": "REACT_APP_API_ROOT=http://localhost:8000 && storybook dev -p 6006", "storybook:build": "storybook build", "lint:ts": "eslint . --ext .js,.jsx,.ts,.tsx", diff --git a/frontend/src/components/Playback/MatchingPairs.js b/frontend/src/components/Playback/MatchingPairs.js index 632bdf031..f38431d21 100644 --- a/frontend/src/components/Playback/MatchingPairs.js +++ b/frontend/src/components/Playback/MatchingPairs.js @@ -10,7 +10,7 @@ const MatchingPairs = ({ finishedPlaying, stopAudioAfter, submitResult, -}) => { +}) => { const xPosition = useRef(-1); const yPosition = useRef(-1); const score = useRef(undefined); @@ -19,13 +19,14 @@ const MatchingPairs = ({ const [total, setTotal] = useState(100); const [message, setMessage] = useState('Pick a card') const [end, setEnd] = useState(false); + const columnCount = sections.length > 6 ? 4 : 3; const resultBuffer = useRef([]); const startTime = useRef(Date.now()); - + const setScoreMessage = (score) => { - switch (score) { + switch (score) { case -10: return '-10
Misremembered'; case 0: return '0
No match'; case 10: return '+10
Lucky match'; @@ -44,23 +45,23 @@ const MatchingPairs = ({ } // Show (animated) feedback after second click on second card or finished playing - const showFeedback = () => { - + const showFeedback = () => { + const turnedCards = sections.filter(s => s.turned); // Check if this turn has finished - if (turnedCards.length === 2) { + if (turnedCards.length === 2) { // update total score & display current score setTotal(total+score.current); - setMessage(setScoreMessage(score.current)); + setMessage(setScoreMessage(score.current)); // show end of turn animations - switch (score.current) { + switch (score.current) { case 10: turnedCards[0].lucky = true; - turnedCards[1].lucky = true; + turnedCards[1].lucky = true; break; case 20: turnedCards[0].memory = true; - turnedCards[1].memory = true; + turnedCards[1].memory = true; break; default: turnedCards[0].nomatch = true; @@ -68,10 +69,10 @@ const MatchingPairs = ({ // reset nomatch cards for coming turns setTimeout(() => { turnedCards[0].nomatch = false; - turnedCards[1].nomatch = false; + turnedCards[1].nomatch = false; }, 700); - break; - } + break; + } // add third click event to finish the turn document.getElementById('root').addEventListener('click', finishTurn); @@ -79,27 +80,27 @@ const MatchingPairs = ({ } } - const checkMatchingPairs = (index) => { + const checkMatchingPairs = (index) => { const currentCard = sections[index]; const turnedCards = sections.filter(s => s.turned); if (turnedCards.length < 2) { if (turnedCards.length === 1) { // We have two turned cards currentCard.turned = true; - secondCard.current = index; + secondCard.current = index; // set no mouse events for all but current - sections.forEach(section => section.noevents = true); + sections.forEach(section => section.noevents = true); currentCard.noevents = true; // check for match - const lastCard = sections[firstCard.current]; + const lastCard = sections[firstCard.current]; if (lastCard.group === currentCard.group) { - // match + // match if (currentCard.seen) { - score.current = 20; + score.current = 20; } else { - score.current = 10; + score.current = 10; } - } else { + } else { if (currentCard.seen) { score.current = -10; } else { score.current = 0; } }; @@ -114,8 +115,8 @@ const MatchingPairs = ({ currentCard.noevents = true; // clear message setMessage(''); - } - resultBuffer.current.push({ + } + resultBuffer.current.push({ selectedSection: currentCard.id, cardIndex: index, score: score.current, @@ -128,9 +129,9 @@ const MatchingPairs = ({ const finishTurn = () => { finishedPlaying(); // remove matched cards from the board - if (score.current === 10 || score.current === 20) { + if (score.current === 10 || score.current === 20) { sections[firstCard.current].inactive = true; - sections[secondCard.current].inactive = true; + sections[secondCard.current].inactive = true; } firstCard.current = -1; secondCard.current = -1; @@ -139,12 +140,12 @@ const MatchingPairs = ({ score.current = undefined; // Turn all cards back and enable events sections.forEach(section => section.turned = false); - sections.forEach(section => section.noevents = false); + sections.forEach(section => section.noevents = false); // Check if the board is empty if (sections.filter(s => s.inactive).length === sections.length) { // all cards have been turned - setEnd(true); - } else { setMessage(''); } + setEnd(true); + } else { setMessage(''); } } if (end) { @@ -157,17 +158,17 @@ const MatchingPairs = ({
-
Score:
{total}
+
Score:
{total}
-
+
{Object.keys(sections).map((index) => ( - { playSection(index); @@ -177,13 +178,13 @@ const MatchingPairs = ({ playing={playerIndex === index} section={sections[index]} onFinish={showFeedback} - stopAudioAfter={stopAudioAfter} + stopAudioAfter={stopAudioAfter} /> ) )}
-
+ ) } -export default MatchingPairs; \ No newline at end of file +export default MatchingPairs; diff --git a/frontend/src/components/Playback/MatchingPairs.scss b/frontend/src/components/Playback/MatchingPairs.scss index 5851ad68f..a27622f58 100644 --- a/frontend/src/components/Playback/MatchingPairs.scss +++ b/frontend/src/components/Playback/MatchingPairs.scss @@ -16,14 +16,18 @@ position: absolute; left: 50%; transform: translateX(-50%); - margin: 0px auto; + margin: 0px auto; } @media (min-aspect-ratio: 1/1) { -.playing-board { + .playing-board { grid-template-columns: 16vh 16vh 16vh 16vh; column-gap: 1.5vh; row-gap: 1.5vh; + + &.playing-board--three-columns { + grid-template-columns: 16vh 16vh 16vh; + } } } @@ -32,12 +36,16 @@ grid-template-columns: 18vw 18vw 18vw 18vw; column-gap: 2vw; row-gap: 2vw; + + &.playing-board--three-columns { + grid-template-columns: 18vw 18vw 18vw; + } } } .playing-board { display: inline-grid; - max-width: 100%; + max-width: 100%; } .matching-pairs__feedback, .matching-pairs__score { @@ -57,7 +65,7 @@ .matching-pairs__feedback { float: left; color: white; - + &.fbnomatch { color: $gray; } @@ -70,8 +78,8 @@ &.fbmisremembered { color: $red; } -} +} .matching-pairs__score { float: right; -} \ No newline at end of file +} diff --git a/frontend/src/components/Playback/MatchingPairs.test.js b/frontend/src/components/Playback/MatchingPairs.test.js new file mode 100644 index 000000000..4ceac1208 --- /dev/null +++ b/frontend/src/components/Playback/MatchingPairs.test.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import MatchingPairs from './MatchingPairs'; + +jest.mock("../PlayButton/PlayCard", () => (props) => ( +
+)); + +describe('MatchingPairs Component', () => { + + const baseProps = { + playSection: jest.fn(), + playerIndex: 0, + finishedPlaying: jest.fn(), + stopAudioAfter: jest.fn(), + submitResult: jest.fn(), + }; + + it('displays three columns when sections length is less than or equal to 6', () => { + const sections = new Array(6).fill({}).map((_, index) => ({ id: index })); + const { container } = render(); + expect(container.querySelector('.playing-board--three-columns')).toBeInTheDocument(); + }); + + it('displays four columns when sections length is greater than 6', () => { + const sections = new Array(7).fill({}).map((_, index) => ({ id: index })); + const { container } = render(); + expect(container.querySelector('.playing-board--two-columns')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/stories/MatchingPairs.stories.js b/frontend/src/stories/MatchingPairs.stories.js new file mode 100644 index 000000000..092255888 --- /dev/null +++ b/frontend/src/stories/MatchingPairs.stories.js @@ -0,0 +1,168 @@ +import MatchingPairs from '../components/Playback/MatchingPairs'; + +import audio from './assets/audio.wav'; + +export default { + title: 'MatchingPairs', + component: MatchingPairs, + parameters: { + layout: 'fullscreen', + docs: { + description: { + component: 'This story shows the component with the default props.', + story: 'This story shows the component with the default props.', + }, + } + }, +}; + +const getDefaultArgs = (overrides = {}) => ({ + playSection: () => { }, + sections: [ + { + id: 1, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 2, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 3, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 4, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 5, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 6, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 7, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 8, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + ], + playerIndex: 0, + stopAudio: () => { }, + submitResult: () => { }, + finishedPlaying: () => { }, + ...overrides, +}) + +export const Default = { + args: { + ...getDefaultArgs(), + }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + docs: { + description: { + component: 'This story shows the component with the default props.', + }, + }, + }, +}; + +export const WithThreeColumns = { + args: getDefaultArgs({ + sections: [ + { + id: 1, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 2, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 3, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 4, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 5, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + { + id: 6, + url: audio, + turned: false, + lucky: false, + memory: false, + }, + ], + }, + ), + decorators: [ + (Story) => ( +
+ +
+ ), + ], + parameters: { + docs: { + description: { + component: 'This story shows the component with three columns. The component automatically adjusts the number of columns based on the number of sections. Six or less sections will result in three columns, more than six sections will result in four columns.', + }, + }, + }, +}; diff --git a/frontend/src/util/classNames.js b/frontend/src/util/classNames.js new file mode 100644 index 000000000..6f9d1fe77 --- /dev/null +++ b/frontend/src/util/classNames.js @@ -0,0 +1,10 @@ +/** + * + * @param {...string} classes + * @returns string + */ +export function classNames(...classes) { + return classes.filter(Boolean).join(' ') +} + +export default classNames diff --git a/frontend/src/util/classNames.test.js b/frontend/src/util/classNames.test.js new file mode 100644 index 000000000..4adf41cf2 --- /dev/null +++ b/frontend/src/util/classNames.test.js @@ -0,0 +1,19 @@ +import classNames from './classNames'; + +describe('classNames function', () => { + it('joins multiple string arguments', () => { + expect(classNames('class1', 'class2', 'class3')).toBe('class1 class2 class3'); + }); + + it('filters out falsy values', () => { + expect(classNames('class1', '', 'class2', null, 'class3', undefined, false)).toBe('class1 class2 class3'); + }); + + it('returns an empty string for only falsy values', () => { + expect(classNames('', null, undefined, false)).toBe(''); + }); + + it('returns an empty string for no arguments', () => { + expect(classNames()).toBe(''); + }); +}); diff --git a/scripts/test-front-ci b/scripts/test-front-ci old mode 100644 new mode 100755