working version
This commit is contained in:
parent
32288ffc7c
commit
dc40a782f1
13 changed files with 4088 additions and 0 deletions
5
.eslintignore
Normal file
5
.eslintignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.prettierrc.js
|
||||||
|
.eslintrc.cjs
|
||||||
|
vite.config.js
|
||||||
21
.eslintrc.cjs
Normal file
21
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'eslint-config-prettier'
|
||||||
|
],
|
||||||
|
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
vite.config.js
|
||||||
|
.eslintrc.cjs
|
||||||
12
index.html
Normal file
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" data-bs-theme="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Memory Card Game</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3725
package-lock.json
generated
Normal file
3725
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
package.json
Normal file
34
package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "react-memory-card-game",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^6.4.2",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"prettier": "^3.0.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.2.15",
|
||||||
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
"bootstrap": "^5.3.1",
|
||||||
|
"eslint": "^8.45.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.3",
|
||||||
|
"sass": "^1.65.1",
|
||||||
|
"uuid": "^9.0.0",
|
||||||
|
"vite": "^4.4.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
src/25.png
Normal file
BIN
src/25.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
50
src/components/App.jsx
Normal file
50
src/components/App.jsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import Cards from "./Cards.jsx";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [API, setAPI] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
// saves API
|
||||||
|
fetch("https://pokeapi.co/api/v2/generation/1/")
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((value) => setAPI(value));
|
||||||
|
}, []);
|
||||||
|
return API ? (
|
||||||
|
<>
|
||||||
|
<header className="d-flex flex-wrap justify-content-between align-items-center py-2 px-5 border-bottom">
|
||||||
|
<div className="d-flex flex-column align-items-start gap-2">
|
||||||
|
<h1 className="fs-3 float-md-start mb-0">Pokémon Memory Game</h1>
|
||||||
|
<h2 className="fs-6 mb-0 fst-italic">
|
||||||
|
Click on Pokémon to increase your score, but don't click any Pokémon
|
||||||
|
twice!
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex">
|
||||||
|
<ul className="list-group list-group-flush mb-0 w-100">
|
||||||
|
<li className="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Current Score:
|
||||||
|
<span
|
||||||
|
className="badge bg-light text-dark rounded-pill"
|
||||||
|
id="score"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
<li className="list-group-item d-flex justify-content-between align-items-center">
|
||||||
|
Best Score:
|
||||||
|
<span
|
||||||
|
className="badge bg-light text-dark rounded-pill"
|
||||||
|
id="best"
|
||||||
|
></span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<Cards api={API} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="h-100 w-100 d-flex justify-content-center align-items-center">
|
||||||
|
<FontAwesomeIcon icon={faSpinner} size="2xl" spinPulse />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/components/Cards.jsx
Normal file
155
src/components/Cards.jsx
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
export default function Cards({ api }) {
|
||||||
|
const [pokemon, setPokemon] = useState([]);
|
||||||
|
const [score, setScore] = useState(0);
|
||||||
|
const [bestScore, setBestScore] = useState(0);
|
||||||
|
|
||||||
|
function handleClickCB(index, clickBool) {
|
||||||
|
let temp = [...pokemon]; // get pokemon array
|
||||||
|
|
||||||
|
if (clickBool === true) {
|
||||||
|
if (score > bestScore) {
|
||||||
|
setBestScore(score);
|
||||||
|
}
|
||||||
|
setScore(0);
|
||||||
|
// unclick ALL the things!
|
||||||
|
for (let i = 0; i < temp.length; i++) {
|
||||||
|
const oldCard = temp[i]; // get card at index
|
||||||
|
if (oldCard.clicked === true) {
|
||||||
|
const newCard = {
|
||||||
|
number: oldCard.number,
|
||||||
|
clicked: false,
|
||||||
|
}; // make new card and change in array
|
||||||
|
temp[i] = newCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if unclicked
|
||||||
|
if (score === 11) {
|
||||||
|
// at score limit
|
||||||
|
alert("Congratulations! You got all the cards!");
|
||||||
|
window.location.reload(false);
|
||||||
|
}
|
||||||
|
// otherwise
|
||||||
|
setScore(score + 1); // append to score
|
||||||
|
|
||||||
|
// set clicked state
|
||||||
|
const mapped = temp.map(({ number, clicked }) => number); // extract just the pokedex number
|
||||||
|
const currentIndex = mapped.indexOf(
|
||||||
|
mapped.find((number) => number === index)
|
||||||
|
); // find the index of the current pokedex number in number-only array
|
||||||
|
let current = temp[currentIndex]; // since it's a mapped copy of the original, same index applies
|
||||||
|
current.clicked = true; // now set the clicked prop of the current card in index
|
||||||
|
temp[currentIndex] = current; // plug back into temp array
|
||||||
|
}
|
||||||
|
|
||||||
|
// rng new values into output array
|
||||||
|
const output = [];
|
||||||
|
while (temp.length) {
|
||||||
|
const index = Math.floor(Math.random() * temp.length); // get random index
|
||||||
|
output.push(temp.splice(index, 1)[0]); // pushes the random index value to array while removing it from temp array
|
||||||
|
}
|
||||||
|
setPokemon(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Card = ({ pokedexNumber, clicked, callback }) => {
|
||||||
|
const [cardClass, setCardClass] = useState(
|
||||||
|
"d-flex flex-column bg-light text-dark card rounded-3 justify-content-around"
|
||||||
|
);
|
||||||
|
function handleChange(event) {
|
||||||
|
if (event.type === "mousedown") {
|
||||||
|
setCardClass(
|
||||||
|
"d-flex flex-column bg-light text-dark card rounded-3 justify-content-around border border-primary border-5"
|
||||||
|
);
|
||||||
|
} else if (event.type === "mouseup") {
|
||||||
|
setCardClass(
|
||||||
|
"d-flex flex-column bg-light text-dark card rounded-3 justify-content-around"
|
||||||
|
);
|
||||||
|
callback(pokedexNumber, clicked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const temp = api.pokemon_species[pokedexNumber].name;
|
||||||
|
const species = temp.replace(temp.charAt(0), temp.charAt(0).toUpperCase());
|
||||||
|
const imgNumber = api.pokemon_species[pokedexNumber].url
|
||||||
|
.replace(
|
||||||
|
new RegExp("https://pokeapi.co/api/v2/pokemon-species/", "gi"),
|
||||||
|
""
|
||||||
|
)
|
||||||
|
.replace("/", "");
|
||||||
|
const imgSrc = `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${imgNumber}.png`;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={cardClass}
|
||||||
|
onMouseDown={handleChange}
|
||||||
|
onMouseUp={handleChange}
|
||||||
|
>
|
||||||
|
<img src={imgSrc} className="card-img-top" alt={species}></img>
|
||||||
|
<h3 className="card-title fs-5">
|
||||||
|
#{pokedexNumber} {species}
|
||||||
|
</h3>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// runs on first load
|
||||||
|
useEffect(() => {
|
||||||
|
// immediately loads user scores into DOM
|
||||||
|
document.getElementById("score").textContent = score;
|
||||||
|
document.getElementById("best").textContent = bestScore;
|
||||||
|
// makes cards
|
||||||
|
let indexes = [];
|
||||||
|
let batched = [];
|
||||||
|
for (let i = 0; i < 12; i++) {
|
||||||
|
let index;
|
||||||
|
function roll() {
|
||||||
|
index = parseInt(Math.random() * 151); // get random number between 0 and 151
|
||||||
|
}
|
||||||
|
roll(); // immediately roll to initialize index
|
||||||
|
if (indexes.length > 0) {
|
||||||
|
for (let z = 0; z < indexes.length; z++) {
|
||||||
|
while (indexes[z] === index) {
|
||||||
|
roll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if made it to here, roll is valid
|
||||||
|
indexes.push(index);
|
||||||
|
batched.push({
|
||||||
|
number: index,
|
||||||
|
clicked: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setPokemon(batched);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// runs on change in score or bestScore
|
||||||
|
useEffect(() => {
|
||||||
|
// loads user scores into DOM
|
||||||
|
document.getElementById("score").textContent = score;
|
||||||
|
document.getElementById("best").textContent = bestScore;
|
||||||
|
}, [score, bestScore]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-column p-4 flex-1-0-auto">
|
||||||
|
<div
|
||||||
|
className="d-flex flex-row flex-wrap gap-5 justify-content-around"
|
||||||
|
id="cardholder"
|
||||||
|
>
|
||||||
|
{pokemon.map((object) => {
|
||||||
|
const index = object.number;
|
||||||
|
const wasClicked = object.clicked;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
pokedexNumber={index}
|
||||||
|
key={uuidv4()}
|
||||||
|
clicked={wasClicked}
|
||||||
|
callback={handleClickCB}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
src/main.jsx
Normal file
10
src/main.jsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import "./styles/index.scss";
|
||||||
|
import App from "./components/App.jsx";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
41
src/styles/index.scss
Normal file
41
src/styles/index.scss
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
$input-focus-border-color: white;
|
||||||
|
$input-btn-focus-box-shadow: 0 0 0 0.25rem rgba(255, 255, 255, 0.25);
|
||||||
|
$form-select-indicator-color: $input-focus-border-color;
|
||||||
|
$form-select-focus-box-shadow: $input-btn-focus-box-shadow;
|
||||||
|
$enable-grid-classes: false;
|
||||||
|
$enable-cssgrid: true;
|
||||||
|
|
||||||
|
@import "../node_modules/bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
|
#root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
header > .d-flex:nth-child(2) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-1-0-auto {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 16rem;
|
||||||
|
height: 22rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 0 1rem 0.4rem rgba($primary, 0.75);
|
||||||
|
}
|
||||||
7
vite.config.js
Normal file
7
vite.config.js
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Loading…
Add table
Reference in a new issue