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