working version

This commit is contained in:
ak 2023-08-14 12:19:05 -07:00
parent 32288ffc7c
commit dc40a782f1
13 changed files with 4088 additions and 0 deletions

5
.eslintignore Normal file
View file

@ -0,0 +1,5 @@
node_modules/
dist/
.prettierrc.js
.eslintrc.cjs
vite.config.js

21
.eslintrc.cjs Normal file
View 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
View 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
View file

@ -0,0 +1,4 @@
node_modules/
dist/
vite.config.js
.eslintrc.cjs

12
index.html Normal file
View 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

File diff suppressed because it is too large Load diff

34
package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

50
src/components/App.jsx Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})