Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: card atom #16

Merged
merged 2 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ yarn-error.log*

# VScode settings
/.vscode

# meld
*.orig
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)`
23 changes: 14 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,7 @@
"version": "0.0.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"classnames": "^2.2.6",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-jss": "^10.3.0",
Expand All @@ -24,12 +18,14 @@
}
},
"lint-staged": {
"*.{ts,tsx}": ["yarn lint"]
"*.{ts,tsx}": [
"yarn lint"
]
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"test": "react-scripts test --env=jest-environment-jsdom-sixteen",
"eject": "react-scripts eject",
"version": "conventional-changelog -p angular -i CHANGELOG.md -t v -s",
"lint": "eslint . --ext .ts --ext .tsx --fix"
Expand All @@ -49,6 +45,14 @@
"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",
"@types/react-router-dom": "^5.1.5",
"@typescript-eslint/eslint-plugin": "^3.8.0",
"@typescript-eslint/parser": "^3.8.0",
Expand All @@ -62,6 +66,7 @@
"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"
}
Expand Down
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.
49 changes: 49 additions & 0 deletions src/components/atoms/BaseCard/BaseCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { BaseCard } from './BaseCard';

describe('BaseCard', () => {
it('should render the card', () => {
const { getByTestId, getByRole } = render(<BaseCard />);
expect(getByRole('button')).toBeInTheDocument();
expect(getByTestId('BaseCard')).toBeInTheDocument();
});

describe('when rendered', () => {
let cardContainer: HTMLElement;
let frontFace: HTMLElement;
let backFace: HTMLElement;
beforeEach(() => {
const { getByTestId, getByRole } = render(
<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');
backFace = getByTestId('back-face');
});

it('should display the front face by default', () => {
expect(frontFace).toBeVisible();
});

it('should hide the back face by default', () => {
expect(backFace).not.toBeVisible();
});

it('should flip when clicked', async () => {
fireEvent.click(cardContainer);
expect(backFace).toBeVisible();
expect(frontFace).not.toBeVisible();
});

it('should flip again when clicked again', async () => {
fireEvent.click(cardContainer);
fireEvent.click(cardContainer);
expect(backFace).not.toBeVisible();
expect(frontFace).toBeVisible();
});
});
});
87 changes: 87 additions & 0 deletions src/components/atoms/BaseCard/BaseCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React, { useState, KeyboardEvent } from 'react';
import { createUseStyles } from 'react-jss';
import classnames from 'classnames';

/**
* Properties for BaseCard component
*/
export interface BaseCardProps {
/** Element to show at card front face */
frontFace?: JSX.Element;
/** Element to show at card back face */
backFace?: JSX.Element;
/** Defines the card height */
height?: number;
/** Defines the card width */
width?: number;
}

const useStyles = createUseStyles({
// Generic classes
cardFace: {
backfaceVisibility: 'hidden',
position: 'absolute',
top: 0,
left: 0,
transition: '0.6s',
},
guilhermevrs marked this conversation as resolved.
Show resolved Hide resolved
// Style classes
card: ({ width, height }) => ({
width: width || 600,
height: height || 400,
perspective: 1000,
cursor: 'pointer',
outline: 'none',
overflow: 'hidden',
}),
faceVisible: { opacity: 1 },
faceHidden: { opacity: 0 },
flipped: { transform: 'rotateY(180deg)' },
cardFlipper: {
transition: '0.6s',
transformStyle: 'preserve-3d',
position: 'relative',
},
cardFront: {
extend: 'cardFace',
zIndex: 2,
transform: 'rotateY(0deg)',
},
cardBack: {
extend: 'cardFace',
transform: 'rotateY(180deg)',
},
});

/**
* Base card
* @param props Card props
*/
export const BaseCard: React.FC<BaseCardProps> = ({
frontFace,
backFace,
width = undefined,
height = undefined,
} = {}) => {
const { card, faceVisible, faceHidden, flipped, cardFlipper, cardFront, cardBack } = useStyles({
width,
height,
});
const [isBackVisible, setIsBackVisible] = useState<boolean>(false);

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} 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>
);
};
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();
});
});
Loading