Skip to content

Commit

Permalink
feat: playing card atom
Browse files Browse the repository at this point in the history
  • Loading branch information
gvieiraschwade authored and tib-tib committed Aug 12, 2020
1 parent fd6c978 commit fc383fb
Show file tree
Hide file tree
Showing 14 changed files with 2,151 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"printWidth": 120,
"singleQuote": true,
"trailingComma": "all",
"trailingComma": "es5",
"tabWidth": 2,
"semi": true
}
31 changes: 31 additions & 0 deletions docs/playing-cards-svg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Playing cards svg

The SVG embedded in this project comes from [https://github.com/htdebeer/SVG-cards](https://github.com/htdebeer/SVG-cards), after a little optmization via [SVGOMG](https://jakearchibald.github.io/svgomg/)

In order to use them:

```jsx
import svgCards from '[RELATIVE_PATH_TO_ASSETS]/svg-cards-optimized.svg';

<svg viewBox="0 0 171 251" className={classes.svgStyle}>
<use xlinkHref={`${svgCards}#joker_black`} />
</svg>
```

The various cards have the following names:

* Jokers: joker_black and joker_red
* Back card: back or alternative-back
* Picture cards: {club,diamond,heart,spade}_{king,queen,jack}
* Pip cards:{club,diamond,heart,spade}_{1,2,3,4,5,6,7,8,9,10}

Examples:

* The ace of club is `club_1`.
* The queen of diamonds is `diamond_queen`.

The cards have the following natural positions:

* width: `169.075`
* height: `244.64`
* center: `(+98.0375, +122.320)`
14 changes: 9 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-jss": "^10.3.0",
Expand All @@ -17,7 +18,9 @@
}
},
"lint-staged": {
"*.{ts,tsx}": ["yarn lint"]
"*.{ts,tsx}": [
"yarn lint"
]
},
"scripts": {
"start": "react-scripts start",
Expand All @@ -40,15 +43,16 @@
]
},
"devDependencies": {
"@commitlint/cli": "^9.1.1",
"@commitlint/config-conventional": "^9.1.1",
"@testing-library/jest-dom": "^5.11.2",
"@testing-library/react": "^10.4.8",
"@testing-library/user-event": "^12.1.1",
"@types/classnames": "^2.2.10",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@commitlint/cli": "^9.1.1",
"@commitlint/config-conventional": "^9.1.1",
"@types/react-router-dom": "^5.1.5",
"@typescript-eslint/eslint-plugin": "^3.8.0",
"@typescript-eslint/parser": "^3.8.0",
Expand All @@ -62,8 +66,8 @@
"eslint-plugin-react": "^7.20.5",
"eslint-plugin-react-hooks": "^4.0.8",
"husky": "^4.2.5",
"jest-environment-jsdom-sixteen": "^1.0.3",
"lint-staged": "^10.2.11",
"prettier": "^2.0.5",
"jest-environment-jsdom-sixteen": "^1.0.3"
"prettier": "^2.0.5"
}
}
1,849 changes: 1,849 additions & 0 deletions src/assets/svg-cards-optimized.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/atoms/BaseCard/BaseCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('BaseCard', () => {
<BaseCard
frontFace={<div data-testid="front-face">MY FRONT</div>}
backFace={<div data-testid="back-face">MY BACK</div>}
/>,
/>
);
cardContainer = getByRole('button');
frontFace = getByTestId('front-face');
Expand Down
46 changes: 19 additions & 27 deletions src/components/atoms/BaseCard/BaseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useState, KeyboardEvent } from 'react';
import { createUseStyles } from 'react-jss';
import classnames from 'classnames';

/**
* Properties for BaseCard component
Expand All @@ -22,29 +23,20 @@ const useStyles = createUseStyles({
position: 'absolute',
top: 0,
left: 0,
color: 'cornSilk',
textAlign: 'center',
font: `3em/240px 'Helvetica Neue', Helvetica, sans-serif`,
boxShadow: '-5px 5px 5px #aaa',
transition: '0.6s',
},
// Modal classes
backVisible: {},
frontVisible: {},
// Style classes
card: ({ width, height }) => ({
width: width || 600,
height: height || 400,
perspective: 1000,
cursor: 'pointer',
'&$backVisible $cardFlipper': { transform: 'rotateY(180deg)' },
'&$backVisible $cardFront': { opacity: 0 },
'&$backVisible $cardBack': { opacity: 1 },
'&$frontVisible $cardFront': { opacity: 1 },
'&$frontVisible $cardBack': { opacity: 0 },
outline: 'none',
overflow: 'hidden',
}),
faceVisible: { opacity: 1 },
faceHidden: { opacity: 0 },
flipped: { transform: 'rotateY(180deg)' },
cardFlipper: {
transition: '0.6s',
transformStyle: 'preserve-3d',
Expand All @@ -62,7 +54,7 @@ const useStyles = createUseStyles({
});

/**
* Base card molecule
* Base card
* @param props Card props
*/
export const BaseCard: React.FC<BaseCardProps> = ({
Expand All @@ -71,24 +63,24 @@ export const BaseCard: React.FC<BaseCardProps> = ({
width = undefined,
height = undefined,
} = {}) => {
const { backVisible, frontVisible, card, cardFlipper, cardFront, cardBack } = useStyles({ width, height });
const [visibleFace, setVisibleFace] = useState<'front' | 'back'>('front');
const { card, faceVisible, faceHidden, flipped, cardFlipper, cardFront, cardBack } = useStyles({
width,
height,
});
const [isBackVisible, setIsBackVisible] = useState<boolean>(false);

const turnCard = (): void => setVisibleFace(visibleFace === 'front' ? 'back' : 'front');
const turnCard = (): void => setIsBackVisible(!isBackVisible);
const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => event.keyCode === 32 && turnCard();

return (
<div
data-testid="BaseCard"
role="button"
onClick={turnCard}
onKeyDown={onKeyDown}
className={`${card} ${visibleFace === 'back' ? backVisible : frontVisible}`}
tabIndex={0}
>
<div className={cardFlipper}>
<div className={cardFront}>{frontFace}</div>
<div className={cardBack}>{backFace}</div>
<div data-testid="BaseCard" role="button" onClick={turnCard} onKeyDown={onKeyDown} className={card} tabIndex={0}>
<div className={classnames(cardFlipper, { [flipped]: isBackVisible })}>
<div className={classnames(cardFront, { [faceHidden]: isBackVisible, [faceVisible]: !isBackVisible })}>
{frontFace}
</div>
<div className={classnames(cardBack, { [faceHidden]: !isBackVisible, [faceVisible]: isBackVisible })}>
{backFace}
</div>
</div>
</div>
);
Expand Down
61 changes: 61 additions & 0 deletions src/components/atoms/PlayingCard/PlayingCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { render } from '@testing-library/react';
import { PlayingCard } from './PlayingCard';

const testCardSvg = (el: HTMLElement, pattern: string | RegExp) =>
expect(el).toHaveAttribute('xlink:href', expect.stringMatching(pattern));

describe('PlayingCard', () => {
it('should render correctly', () => {
const { getByTestId } = render(<PlayingCard />);
expect(getByTestId('BaseCard')).toBeInTheDocument();
});

describe('on default rendering', () => {
let frontFace: HTMLElement;
let backFace: HTMLElement;
beforeEach(() => {
const { getByTestId } = render(<PlayingCard />);
frontFace = getByTestId('PlayingCard_front_use');
backFace = getByTestId('PlayingCard_back_use');
});

it('should default front to joker_black', () => {
testCardSvg(frontFace, /#joker_black$/i);
});

it('should default front to no fill', () => {
expect(frontFace).toHaveAttribute('fill', '');
});

it('should default back to back', () => {
testCardSvg(backFace, /#back$/i);
});

it('should default back to black color', () => {
expect(backFace).toHaveAttribute('fill', 'black');
});
});

describe('on rendering', () => {
let frontFace: HTMLElement;
let backFace: HTMLElement;
beforeEach(() => {
const { getByTestId } = render(<PlayingCard card="spade_4" backColor="red" />);
frontFace = getByTestId('PlayingCard_front_use');
backFace = getByTestId('PlayingCard_back_use');
});

it('should render the correct card', () => {
testCardSvg(frontFace, /#spade_4$/i);
});

it('should always show back as back', () => {
testCardSvg(backFace, /#back$/i);
});

it('should render back as backColor property', () => {
expect(backFace).toHaveAttribute('fill', 'red');
});
});
});
40 changes: 40 additions & 0 deletions src/components/atoms/PlayingCard/PlayingCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { createUseStyles } from 'react-jss';
import svgCards from '../../../assets/svg-cards-optimized.svg';
import { BaseCard } from '../BaseCard/BaseCard';
import { PlayingCardType } from '../../../models/PlayingCardType';

/**
* Properties for PlayingCard
*/
export interface PlayingCardProps {
/** Card to be rendered */
card?: PlayingCardType;
/** Color to be applied to back face */
backColor?: string;
}

const useStyles = createUseStyles({
svgStyle: {
width: '100%',
},
});

export const PlayingCard: React.FC<PlayingCardProps> = ({ card = 'joker_black', backColor = 'black' }) => {
const classes = useStyles();

const generateSvg = (isFront: boolean) => {
const testId = `PlayingCard_${isFront ? 'front' : 'back'}`;
return (
<svg data-testid={testId} viewBox="0 0 171 251" className={classes.svgStyle}>
<use
data-testid={`${testId}_use`}
xlinkHref={`${svgCards}#${isFront ? card : 'back'}`}
fill={isFront ? '' : backColor}
/>
</svg>
);
};

return <BaseCard height={251} width={171} frontFace={generateSvg(true)} backFace={generateSvg(false)} />;
};
2 changes: 1 addition & 1 deletion src/components/pages/HomePage/HomePage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import HomePage from './HomePage';
describe('HomePage', () => {
it('should render home page', () => {
const { getByText } = render(<HomePage />);
const divElement = getByText(/I am on homepage/i);
const divElement = getByText(/Card UI/i);
expect(divElement).toBeInTheDocument();
});
});
72 changes: 69 additions & 3 deletions src/components/pages/HomePage/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,88 @@
import React from 'react';
import { createUseStyles } from 'react-jss';
import { BaseCard } from '../../atoms/BaseCard/BaseCard';
import { PlayingCard } from '../../atoms/PlayingCard/PlayingCard';
import { PlayingCardType } from '../../../models/PlayingCardType';

const useStyles = createUseStyles({
homePage: {
background: '#cccccc',
height: '100%',
color: 'white',
overflow: 'auto',
display: 'grid',
gridTemplateColumns: 'repeat(5, 1fr)',
},
});

const HomePage: React.FC = () => {
const classes = useStyles();

const allCards: PlayingCardType[] = [
'club_1',
'club_2',
'club_3',
'club_4',
'club_5',
'club_6',
'club_7',
'club_8',
'club_9',
'club_10',
'club_jack',
'club_queen',
'club_king',
'diamond_1',
'diamond_2',
'diamond_3',
'diamond_4',
'diamond_5',
'diamond_6',
'diamond_7',
'diamond_8',
'diamond_9',
'diamond_10',
'diamond_jack',
'diamond_queen',
'diamond_king',
'heart_1',
'heart_2',
'heart_3',
'heart_4',
'heart_5',
'heart_6',
'heart_7',
'heart_8',
'heart_9',
'heart_10',
'heart_jack',
'heart_queen',
'heart_king',
'spade_1',
'spade_2',
'spade_3',
'spade_4',
'spade_5',
'spade_6',
'spade_7',
'spade_8',
'spade_9',
'spade_10',
'spade_jack',
'spade_queen',
'spade_king',
'joker_black',
'joker_red',
];
const backColors = ['black', 'red', 'blue', '#048b9a'];

return (
<div className={classes.homePage}>
I am on homepage
<BaseCard frontFace={<div>MY FRONT</div>} backFace={<div>MY BACK</div>} />
<h1>Card UI</h1>
{allCards.map((card, index) => (
<div key={card}>
<PlayingCard card={card} backColor={backColors[index % backColors.length]} />
</div>
))}
</div>
);
};
Expand Down
Loading

0 comments on commit fc383fb

Please sign in to comment.