Card Game UI/UX 🐺 Gwent: Witcher + Demo 🍿
Tags: webdev, react, javascript, css
Published: 2021-08-13 19:02:19

<h1>☢️ Warning !!! ☣️ The site is very heavily loaded, be careful. ☣️</h1>
<a href="https://kerthin.github.io/gwentReact-templateSait/" target="_blank"><h1>Full Portfolio Demo (!!! 15 SECONDS PRELOAD)</h1></a>
{% github Kerthin/gwentReact-templateSait %}

<strong>Recomend use 0.5x</strong>
{% codepen https://codepen.io/RodenKerthin/pen/oNbXNxo %}

<h1>Introduction</h1>
About 2 years ago, I decided to download a card game called Gwent on my iPad. I am a big fan of the Witcher series of games and it was interesting for me to try a separate project from the Witcher universe. Initially, I didn't have high hopes for the game, but after I played the game, I was extremely impressed. In addition to the interesting gameplay, I couldn't help but pay attention to the visual part of the game. It is executed perfectly.

And first of all, it is the way the playing cards are made that attracts attention. In addition to various bronze and gold borders with a beautiful font and icons, the map is surprised by its animated part.

![Alt Text](https://i.redd.it/jjb4d03ulo551.gif)
![Alt Text](http://img2.reactor.cc/pics/post/Witcher-Gif-The-Witcher-фэндомы-Гвинт-4348367.gif)
![Alt Text](https://media.tenor.com/images/21b1399c4cdaecc20ab794cb3fdc962d/tenor.gif)

<strong>Well, what can I say, and the gameplay itself in the game looks great</strong>
![Alt Text](https://media.giphy.com/media/iia4lHU6s8R6PRHv9q/giphy.gif)
![Alt Text](https://media.giphy.com/media/o61Uf7amb6N62zGnRA/giphy.gif)

<strong>By the way, the game used to look like this.</strong>
![Alt Text](https://media.giphy.com/media/kD4PcSk8BM3K0/giphy.gif)

And after all those pleasant emotions that I experienced, I decided to try to implement part of the interface of this game. Namely, the section with the selection of cards for creating a deck.

<h1>Concenpt</h1>
The site is a collection of cards, each of which belongs to a different faction (there are only 7 of them). Each of the cards has its own special characteristics, such as: `provisions`, `cost`, `card type`, `color`, `name` and `faction`. Based on this data, we need to create appropriate `filters` that will help us find the necessary maps.

All information about the maps will be stored in <a href="https://github.com/Kerthin/gwentReact-templateSait/blob/master/src/variables/Array/CARD_INFO_ARRAY.js" target="_blank">our array</a>.

<h1>Design</h1>
As for the interface, I decided to recreate it close to the original.

<strong>Game UI:</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zlcxrzh1vbyko6l7kc2g.png)

As for the cards themselves, I decided to find the sources of all the elements that are used in the styling of cards.

<strong>1. Each faction should have its own color style.</strong>

<strong>Neutral</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c0b4tu50527zaj2hi9gu.png)

<strong>Monsters</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e83f3f9cz5xqageqagl6.png)

<strong>Nilfgaard</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tun5j05zgiy9tfj5xiz2.png)

<strong>Northern</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2qntueykrq6qknstlkef.png)

<strong>Scoiatael</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4dnyxt60nhz36vvickos.png)

<strong>Skellige</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0nj2363ja5wpeqfvf7it.png)

<strong>Syndicate</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pumvmc3f6ha41ctauqyn.png)

<strong>2. You also need to indicate the cost of the card. To do this, we need special icons.</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/focuyb7d8s46yazb9xyk.png)

<strong>3. We also need to divide the cards into types.</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ev7bqt004zguncjhw5hc.png)

<strong>4. Font.</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q9k0e6r6ucfgyo1ikhoq.png)

<strong>5. And it remains only to choose bronze and gold borders for .</strong>
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1qqfd0r2x04wily20hxa.png)

<h1>Create Card</h1>
To create a correct card, we need to take into account all the data specified in the array. The `fraction` (specified as a `class`), `border` (specified as a `class`), the `path to the video` (specified in `src`), `cost` (specified as a `class`), the `name` of the card and its `description`.

The videos for the cards themselves are stored in the `build/video` folder, where they are all divided into sections with provisions.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wty2mzy4lfa7sxgo5h47.png)

```javascript
class Card extends React.Component {
	render() {
		const product = this.props.product;

		var provPath = String(product.prov.match(/\d+/));

		var factionClass = product.faction;
		var borderClass = product.border;
		var videoPath = `img/video/${product.type}/${provPath}/${product.src}`;

		var prov = <p className="provisionBg__number">{parseInt(product.prov.match(/\d+/))}</p>;

		var cost;
		if (product.usual) {
			cost = 'usual';
		} else if (product.rare) {
			cost = 'rare';
		} else if (product.epic) {
			cost = 'epic';
		} else {
			cost = 'legendary';
		}

		var strengthType;
		var provBlock = <div className="provisionBg">{prov}</div>;

		if (product.type === 'special') {
			strengthType = <div className="strengthBg__special"></div>;
		} else if (product.type === 'artifact') {
			strengthType = <div className="strengthBg__artifact"></div>;
		} else if (product.type === 'strategy') {
			videoPath = `img/video/${product.type}/${product.src}`;
			strengthType = <div className="strengthBg__strategy"></div>;
			provBlock = null;
		} else {
			strengthType = <p className="strengthBg__number">{product.strength}</p>;
		}

		var nameTooltip = <h1 className="tooltip__title">{product.name}</h1>;

		return (
			<div className={`card _${factionClass} _${borderClass}`}>
				<video src={videoPath} poster="img/poster/cursedScroll.mp4" preload="true" autoPlay loop muted></video>
				
				<div className="borderBg"></div>
				<div className={`strengthBg _${cost}`}>{strengthType}</div>
				{provBlock}

				<div className="tooltip">
					<div className="tooltip__titleWrap">{nameTooltip}</div>
					<div className="tooltip__infoWrap">
						<div className="tooltip__info">{product.tooltip}</div>
					</div>
				</div>
			</div>
		);
	}
}
```

<strong>SCSS</strong>
```javascript
.card {
	position: relative;
	float: left;
	width: 134px;
	height: 194px;
	margin-left: 30px;
	margin-top: 30px;
	cursor: pointer;
	transition: all .3s;

	video {
		position: absolute;
		width: 100%;
		height: 100%;
	}

	&._bronze .borderBg {
		background-image: url('../../../../img/cardElements/border/borderBronze.png');
	}
	&._gold .borderBg {
		background-image: url('../../../../img/cardElements/border/borderGold.png');
	}

	@mixin cardFactionElements($faction) {
		&._#{$faction} {
			.strengthBg {
				background-image: url('../../../../img/cardElements/#{$faction}/strength.png');
			}
			.provisionBg {
				background-image: url('../../../../img/cardElements/#{$faction}/provision.png');
			}
			.tooltip__titleWrap {
				background-image: url('../../../../img/cardElements/#{$faction}/tooltip.jpg');
			}
		}
	}

	@include cardFactionElements('neutral');
	@include cardFactionElements('monsters');
	@include cardFactionElements('nilfgaard');
	@include cardFactionElements('northern');
	@include cardFactionElements('scoiatael');
	@include cardFactionElements('skellige');
	@include cardFactionElements('syndicate');

	&:hover {
		transform: scale(1.1);
		z-index: 5;

		.tooltip {
			display: block;
		}
		.tooltip {
			&:hover {
				display: none;
			}
		}
	}

	&:last-child {
		margin-bottom: 20px;
	}

	&:nth-child(6n-1),
	&:nth-child(6n) {
		.tooltip {
			left: -177px;
		}
	}
}
.borderBg {
	position: absolute;
	width: 100%;
	height: 100%;
}
.strengthBg {
	position: absolute;
	width: 43px;
	height: 43px;
	@extend %containBg;

	&__special,
	&__artifact,
	&__strategy,
	&__number {
		@extend %containBg;
	}

	@mixin cardTypeIcon($type, $mTop, $mLeft, $width, $height) {
		&__#{$type} {
			margin-top: #{$mTop}px;
			margin-left: #{$mLeft}px;
			width: #{$width}px;
			height: #{$height}px;
			background-image: url('../../../../img/cardElements/type/#{$type}.png');
		}
	}

	@include cardTypeIcon('special', '5', '4', '32', '32');
	@include cardTypeIcon('artifact', '7', '6', '30', '30');
	@include cardTypeIcon('strategy', '8', '8', '25', '25');

	&__number {
		font-family: Gwent;
		font-size: 28px;
		color: $paletteStrength;
		text-align: center;
		line-height: 1.6;
		text-shadow: rgb(0, 0, 0) 0px 2px 5px;
	}

	@mixin cardGemIcon($cost) {
		&._#{$cost} {
			&:after {
				background-image: url('../../../../img/cardElements/gem/#{$cost}.png');
			}
		}
	}

	@include cardGemIcon('usual');
	@include cardGemIcon('rare');
	@include cardGemIcon('epic');
	@include cardGemIcon('legendary');

	&:after {
		content: '';
		position: absolute;
		top: 7px;
		left: 6px;
		width: 7px;
		height: 7px;
		@extend %containBg;
	}
}

.provisionBg {
	position: absolute;
	width: 26px;
	height: 54px;
	bottom: 4px;
	right: 4px;
	@extend %containBg;

	&__number {
		font-family: Gwent;
		font-size: 24px;
		color: $paletteSandPrimary;
		text-align: center;
		line-height: 3.3;
		text-shadow: rgb(0, 0, 0) 0px 2px 5px;
	}
}
```

<strong>Result</strong>
```javascript
{ name: "Draug",
	prov: "pp13",
	type: "unit",
	strength: "7",
	faction: "northern",
	border: "gold",
	legendary: true,
	src: "draug.mp4",
	tooltip: <p>{deploy}: Transform all allied Human units in this row into Kaedweni Revenants, without changing their power.</p>
}
```

![Alt Text](https://media.giphy.com/media/LrRDj190dDalWmer7A/giphy.gif)

<strong>Full View</strong>
![Alt Text](https://media.giphy.com/media/OzjfkNvMTpEf2ztPFF/giphy.gif)

<h1>Tooltip</h1>
It remains only to add the description output to each map when hovering.

```javascript
.tooltip {
	position: absolute;
	top: 2px;
	right: -177px;
	width: 177px;
	display: none;
	transition: all .3s;

	&__titleWrap {
		position: relative;
		padding-top: 3px;
		width: 100%;
		height: 56px;
		background-size: 100%;
		background-repeat: no-repeat;
	}
	&__title {
		margin-top: 10px;
		font-family: Gwent;
		font-size: 17px;
		text-transform: uppercase;
		color: $paletteParagraph;
		text-align: center;
		text-shadow: rgb(0, 0, 0) 0px 2px 5px;
	}

	&__infoWrap {
		background-image: url(../../../../img/cardElements/tooltipInfo/bottom.png), url(../../../../img/cardElements/tooltipInfo/center.jpg);
		background-size: 100%;
		border-bottom-left-radius: 8px;
		border-bottom-right-radius: 8px;
		padding-bottom: 10px;
		background-repeat: no-repeat, repeat-y;
		background-position: center bottom;
	}

	&__info {
		margin-bottom: 6px;
		position: relative;
		word-break: break-word;
		padding: 8px 12px 14px;

		p {
			font-size: 14px;
			color: $paletteTooltioParagraph;
			line-height: 16px;
			font-family: sans-serif;
		}
		span {
			font-weight: 600;
			color: $paletteTooltioKeyword;
		}
	}
}
```

<strong>Result</strong>
![Alt Text](https://media.giphy.com/media/s6B5F6KYdLblKFQJQ2/giphy.gif)

Since the hint is displayed to the right of the card, it is very important that the hint is displayed on the left of the rightmost cards.

```javascript
&:nth-child(6n-1),
&:nth-child(6n) {
	.tooltip {
	   left: -177px;
	}
}
```

<h1>Filters</h1>
The appearance of the filters is made in the form of a side menu with columns of sliding blocks.

![Alt Text](https://media.giphy.com/media/viPnRF3dHAU4qtQ3Fg/giphy.gif)

```javavscript
function BorderFilterWrap(props) {
	return(
		<label className="sideFilterWrap">
			<input type="checkbox" className="openFilter" />
			<FrontBlock frontFilterIcon={'color'} frontFilterName={'Color'} />
			<div className="popupFilter">
				<RadioButton value={""} radioName={"border"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"gold"} radioName={"border"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"bronze"} radioName={"border"} checked={props.checked} onChange={props.onChange} />
			</div>
		</label>
	);
}
```

```javascript
function CostFilterWrap(props) {
	return(
		<label className="sideFilterWrap">
			<input type="checkbox" className="openFilter" />
			<FrontBlock frontFilterIcon={'cost'} frontFilterName={'Cost'} />
			<div className="popupFilter">
				<CheckboxButton value={"usual"} checked={props.checkedUsual} onChange={props.onChangeUsual} />
				<CheckboxButton value={"rare"} checked={props.checkedRare} onChange={props.onChangeRare} />
				<CheckboxButton value={"epic"} checked={props.checkedEpic} onChange={props.onChangeEpic} />
				<CheckboxButton value={"legendary"} checked={props.checkedLegendary} onChange={props.onChangeLegendary} />
			</div>
		</label>
	);
}
```

```javascript
function FactionFilterWrap(props) {
	return(
		<label className="sideFilterWrap">
			<input type="checkbox" className="openFilter" />
			<FrontBlock frontFilterIcon={'faction'} frontFilterName={'Fraction'} />
			<div className="popupFilter">
				<RadioButton value={""} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"neutral"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"monsters"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"nilfgaard"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"northern"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"scoiatael"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"skellige"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"syndicate"} radioName={"faction"} checked={props.checked} onChange={props.onChange} />
			</div>
		</label>
	);
}
```

```javascript
function TypeFilterWrap(props) {
	return(
		<label className="sideFilterWrap">
			<input type="checkbox" className="openFilter" />
			<FrontBlock frontFilterIcon={'type'} frontFilterName={'Type'} />
			<div className="popupFilter">
				<RadioButton value={""} radioName={"type"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"unit"} radioName={"type"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"special"} radioName={"type"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"artifact"} radioName={"type"} checked={props.checked} onChange={props.onChange} />
				<RadioButton value={"strategy"} radioName={"type"} checked={props.checked} onChange={props.onChange} />
			</div>
		</label>
	);
}
```

<strong>SCSS</strong>
```javascript
@import "../../../../variables/StyleVariables/StyleVariables.scss";

.sideFilterWrap {
	position: relative;
	display: block;
	margin-bottom: 15px;
	width: 95%;
}
.openFilter {
	display: none;

	&:checked {
		+ .frontBlock {
			.frontBlock__toogle {
				transform: rotate(180deg);
			}
		}
	}
	&:checked {
		+ .frontBlock {
			+ .popupFilter {
				max-height: 1000px;
			}
		}
	}
}

.popupFilter {
	position: relative;
	display: block;
	width: 100%;
	max-height: 0;
	background-color: $palettePopup;
	overflow: hidden;
	transition: max-height .5s;
}

.filterBlock__buttonBg {
	@mixin sideFilterIcon($family, $nameIcon, $extension) {
		&._#{$nameIcon} {
			background-image: url('../../../../img/icons/#{$family}/#{$nameIcon}.#{$extension}');
		}
	}

	@include sideFilterIcon('faction', 'neutral', 'png');
	@include sideFilterIcon('faction', 'monsters', 'png');
	@include sideFilterIcon('faction', 'nilfgaard', 'png');
	@include sideFilterIcon('faction', 'northern', 'png');
	@include sideFilterIcon('faction', 'scoiatael', 'png');
	@include sideFilterIcon('faction', 'skellige', 'png');
	@include sideFilterIcon('faction', 'syndicate', 'png');
}
```

<strong>Working Filter</strong>
![Alt Text](https://media.giphy.com/media/KD5Vv5y43uUu8eRPIG/giphy.gif)

<h1>Provision Filter</h1>
Now we need to make a filter based on the provisions of the card.

Provisions is the number indicated at the bottom of the card, it indicates the amount of free space needed to put it in the deck.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5wxu3n5dhlj1tjwnewaw.png)

```javascript
function ProvisionFilter(props) {
	return(
		<div className="filterBlock__provWrap clearfix">
			<RadioButtonProv value={'p4'} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'0-4'} />
			<RadioButtonProv value={"p5"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'5'} />
			<RadioButtonProv value={"p6"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'6'} />
			<RadioButtonProv value={"p7"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'7'} />
			<RadioButtonProv value={"p8"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'8'} />
			<RadioButtonProv value={"p9"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'9'} />
			<RadioButtonProv value={"p10"} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'10'} />
			<RadioButtonProv value={'pp1'} radioName={"provision"} checked={props.checked} onChange={props.onChange} number={'11+'} />
		</div>
	);
}
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w67e32009j35kmsjffdc.png)

<strong>Result</strong>
![Alt Text](https://media.giphy.com/media/MzY41m5zKnPa8ynDNP/giphy.gif)

<h1>Text Filter</h1>
Well, the last thing left for us is to search by the name of the card.

```javascript
function TextFilter(props) {
	return(
		<div className="filterBlock__textFilterWrap">
			<input
					type="text"
					placeholder="SEARCH"
					value={props.value}
					onChange={props.onChange}
					className="filterBlock__textFilter"
				/>
		</div>
	);
}
```

![Alt Text](https://media.giphy.com/media/uvItCalYkrhYmfVwQG/giphy.gif)

<h1>Responsive</h1>
<strong>Resize Browser Window</strong>
![Alt Text](https://media.giphy.com/media/TSfMFZenvwMt4ZpkK2/giphy.gif)

<strong>Tablet</strong>
![Alt Text](https://media.giphy.com/media/WLnq09glYMKDnexotm/giphy.gif)

<h1>The End</h1>
Well, that's it. Thank you for your time. I want to note right away that I created this project 2 years ago. This is one of my first projects that I did with React. So take this article as a story about how I tried to copy the interface of the game and nothing more.

I can advise you to subscribe to my <a href="https://twitter.com/BelichenkoRoden"><strong>Twitter</strong></a>, I also post my works there.

<strong>Goodbye</strong>
![Alt Text](https://media.giphy.com/media/ZRMo8QM6M7lM4/giphy.gif)