diff --git a/package.json b/package.json index 5b56dd344a..201d08c824 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "1.0.0", "private": true, "dependencies": { + "@giscus/react": "^2.0.3", "@types/react": "^18.0.6", "@types/react-dom": "^18.0.2", - "@giscus/react": "^2.0.3", + "node-sass": "^7.0.1", "plop": "^3.0.5", "react": "^18.0.0", "react-dom": "^18.0.0", @@ -50,7 +51,7 @@ ] }, "devDependencies": { - "typescript": "^4.6.4", - "react-snap": "^1.23.0" + "react-snap": "^1.23.0", + "typescript": "^4.6.4" } } diff --git a/src/meta/play-meta.js b/src/meta/play-meta.js index 94370b664d..71025d7b9d 100644 --- a/src/meta/play-meta.js +++ b/src/meta/play-meta.js @@ -14,6 +14,7 @@ import { AnalogClock, PasswordGenerator, WhyTypescript, +NetlifyCardGame, //import play here } from "plays"; @@ -212,5 +213,18 @@ export const plays = [ blog: '', video: '', language: 'ts' + }, { + id: 'pl-memory-game', + name: 'Memory Game', + description: 'simple memory game or memory testing game build with ReactJS', + component: () => {return }, + path: '/plays/memory-game', + level: 'Advanced', + tags: 'MemoryGame, CardGame, ReactJS', + github: 'Angryman18', + cover: 'https://cdn.pixabay.com/photo/2017/01/03/16/42/klee-1949946_960_720.jpg', + blog: '', + video: '', + language: 'js' }, //replace new play item here ]; diff --git a/src/plays/index.js b/src/plays/index.js index 754a500644..aaf98bc912 100644 --- a/src/plays/index.js +++ b/src/plays/index.js @@ -16,4 +16,5 @@ export { default as AnalogClock } from 'plays/analog-clock/AnalogClock'; export { default as PasswordGenerator } from 'plays/password-generator/PasswordGenerator'; export { default as WhyTypescript } from 'plays/why-typescript/WhyTypescript'; +export { default as NetlifyCardGame } from 'plays/netlify-card-game/NetlifyCardGame'; //add export here diff --git a/src/plays/netlify-card-game/NetlifyCardGame.jsx b/src/plays/netlify-card-game/NetlifyCardGame.jsx new file mode 100644 index 0000000000..7cd08d2e50 --- /dev/null +++ b/src/plays/netlify-card-game/NetlifyCardGame.jsx @@ -0,0 +1,254 @@ +import { getPlayById } from "meta/play-meta-util"; + +import PlayHeader from "common/playlists/PlayHeader"; +import { useState, useEffect, useRef } from "react"; + +// css +import "./NetlifyCardGame.scss"; + +// components +import Modal from "./modal"; + +import q1 from "./icons/q1.png"; +import q2 from "./icons/q2.png"; +import q3 from "./icons/q3.png"; +import q4 from "./icons/q4.png"; +import q5 from "./icons/q5.png"; +import q6 from "./icons/q6.png"; +import q7 from "./icons/q7.png"; +import q8 from "./icons/q8.png"; + +const imgArr = [q1, q2, q3, q4, q5, q6, q7, q8]; + +const initialState = { + moves: 0, + time: 0, + elapsedTime: 0, +}; + +function NetlifyCardGame(props) { + // Do not remove the below lines. + // The following code is to fetch the current play from the URL + const { id } = props; + const play = getPlayById(id); + + // Your Code Start below. + // static 16 images with 2 duplicate image + const imgArray = useRef(imgArr.concat(imgArr)); + + // dynamic 16 images after shuffling + const [newImgArray, setNewImgArray] = useState([]); + + // board staticstics + const [boardStats, setBoardStats] = useState({ ...initialState }); + + // duplicate array without matched images + const duplicateImgArray = useRef([]); + + // disabled click while updating state if taking time + const disableClick = useRef(false); + + // show guidence modal + const [showModal, setShowModal] = useState(false); + + const toggle = (e) => { + setShowModal(!showModal); + }; + + // timer ref + const timer = useRef(null); + + // matched objects on the board + const [matchedItems, setMatchedItems] = useState([]); + + // shuffling actuall work is done by this function + const shufflingArray = () => { + const shuffledArray = imgArray.current.sort(() => Math.random() - 0.5); + return shuffledArray.map((item, index) => ({ + img: item, + id: index, + show: false, + })); + }; + + // shuffling function exteranally to manage "Play Again Button Click" + const shuffle = () => { + const shuffledArray = shufflingArray(); + duplicateImgArray.current = shuffledArray; + setNewImgArray(shuffledArray); + }; + + const shuffling = useRef(shuffle); + + useEffect(() => { + shuffling.current(); // shuffle at first reload + }, [shuffling]); + + useEffect(() => { + if (boardStats.time !== 0 && matchedItems.length < 8) { + // starts timer when user clicks + const secondCalculations = setInterval(() => { + const calcSeconds = new Date().getTime() - boardStats.time.getTime(); + setBoardStats((pre) => ({ ...pre, elapsedTime: calcSeconds / 1000 })); + }, 1000); + + return () => clearInterval(secondCalculations); // after upateing every second we have to clean the event listener + } + }, [boardStats.time, matchedItems.length]); + + const boxClickHandler = + ({ id, img }) => + () => { + const currentlyShownItem = duplicateImgArray.current.find( + (i) => i.show === true + ); // if false means no active item + if (currentlyShownItem?.id === id) return; // if same item clicked again + if (matchedItems.some((i) => i.img === img)) return; + + // count the moves and set current time of the first move is done + setBoardStats({ + ...boardStats, + time: boardStats?.moves === 0 ? new Date() : boardStats.time, + moves: boardStats.moves + 1, + }); + + // clicked item image pair + const imageItems = newImgArray.filter((i) => i.img === img); + if (currentlyShownItem) { + // checking if clicked item img is same as active item img + const findPair = imageItems.find( + (i) => i.img === currentlyShownItem.img + ); + if (!findPair) { + // if not same, then find the item from the main array + const findClickedItem = imageItems.find((i) => i.id === id); + findClickedItem.show = true; // need to display it for sometime + const updatedItemList = newImgArray.map((i) => + i.id === findClickedItem.id ? findClickedItem : i + ); + setNewImgArray(updatedItemList); // updating the main array coz we need to show a glimpse of the clicked item + disableClick.current = true; // while rage clicking state update taking time so we need to disable click + return timeOutCall(findClickedItem, currentlyShownItem); // timeout will hide the item after 1 second + } else { + // if user clicks the same imgage of the active image then we find which one is clicked specefically by id this time + disableClick.current = true; // disable the click + const otherPair = imageItems.find( + (i) => i.id !== currentlyShownItem.id + ); // searching for same image other object + otherPair.show = true; // have to render it + const updatedItemList = newImgArray.map((i) => + i.id === otherPair.id ? otherPair : i + ); // updated array maintaining the index + duplicateImgArray.current = updatedItemList.filter( + (i) => i.show === false + ); // updating the duplicate array + setNewImgArray(updatedItemList); // updating the main object + setMatchedItems([...matchedItems, findPair]); + disableClick.current = false; + } + } else { + const findClickedItem = newImgArray.find((i) => i.id === id); + findClickedItem.show = true; + const updatedArray = newImgArray.map((i) => + i.id === findClickedItem.id ? findClickedItem : i + ); + setNewImgArray(updatedArray); + } + }; + + const timeOutCall = (clickedItem, activeItem) => { + return (timer.current = setTimeout(() => { + clickedItem.show = false; + if (activeItem) { + activeItem.show = false; // for the first time when user clicks the current object can be undefined so have to check it + } + const updatedArray = newImgArray.map((i) => { + if (i.id === clickedItem.id) { + return clickedItem; + } else if (activeItem && i.id === activeItem?.id) { + return activeItem; + } else { + return i; + } + }); + disableClick.current = false; + setNewImgArray(updatedArray); + clearTimeout(timer.current); + }, 1500)); + }; + + const resetHandler = () => { + // resetting the entire game + setBoardStats({ ...initialState }); + setMatchedItems([]); + shuffle(); + }; + + const calculateMerit = () => { + const time = Math.floor(boardStats?.elapsedTime); + if (time <= 40) { + return "Execelent"; + } else if (time > 40 && time <= 60) { + return "Good"; + } else { + return "Average"; + } + }; + + return ( + <> +
+ +
+ {/* Your Code Starts Here */} +

+

How to Play?

+

+
+
+ {newImgArray.map((item, idx) => { + return ( +
+
+ img +
+
+ ); + })} +
+

Moves: {boardStats.moves}

+

+ Elapsed Time: {Math.floor(boardStats.elapsedTime)} Seconds +

+ {matchedItems.length === 8 && ( +

+ Congrats! {calculateMerit()} Performance +

+ )} + +
+
+
+ {/* Your Code Ends Here */} +
+
+ + + ); +} + +export default NetlifyCardGame; diff --git a/src/plays/netlify-card-game/NetlifyCardGame.scss b/src/plays/netlify-card-game/NetlifyCardGame.scss new file mode 100644 index 0000000000..823fb39fdd --- /dev/null +++ b/src/plays/netlify-card-game/NetlifyCardGame.scss @@ -0,0 +1,136 @@ +$memory-game-box-width: 50%; + +$memory-game-single-item-img: url("./question.png"); +$memory-game-single-item-bg: rgb(241, 239, 239); + +$memory-game-single-item-hover: rgba(59, 154, 156, 1); +$memory-game-single-item-hover-bg: url("./question1.png"); + +.memory-game .container { + display: flex; + justify-content: center; + align-items: center; +} + +.memory-game .guide { + text-align: center; + padding: 0; + margin: 0; + color: #1f6ed4; + + p { + cursor: pointer; + display: inline; + } +} + +.memory-game .container .App { + margin: 5px; + width: $memory-game-box-width; + height: calc(90vh - 25px); + display: grid; + padding: 25px; + grid-template-columns: 1fr 1fr 1fr 1fr; + grid-template-rows: 1fr 1fr 1fr 1fr; + gap: 20px; + + .item { + position: relative; + width: 100%; + background: $memory-game-single-item-bg; + border-radius: 10px; + background-image: $memory-game-single-item-img; + background-size: 50%; + background-repeat: no-repeat; + background-position: center; + transition: all 100ms ease-out; + + .img-container { + width: 100%; + height: 100%; + background-color: rgb(240, 240, 240); + border-radius: 10px; + display: none; + } + + .shown { + display: flex; + justify-content: center; + align-items: center; + + .item-img { + width: 60%; + animation: picAppear 0.5s ease-out forwards; + } + + @keyframes picAppear { + 0% { + width: 0%; + } + 100% { + width: 60%; + } + } + } + + .hidden { + display: none; + } + + &:hover { + background-color: $memory-game-single-item-hover; + background-image: $memory-game-single-item-hover-bg; + cursor: pointer; + } + } + .footer { + grid-column: 1/5; + .reset { + padding: 5px 10px; + border: 1px solid #ccc; + background-color: white; + color: black; + border-radius: 5px; + outline: none; + font-size: 1rem; + cursor: pointer; + &:hover { + color: white; + background-color: black; + transition: all 100ms ease-out; + } + } + } +} + +@media screen and (max-width: 1366px) { + .memory-game .container .App { + width: 80vh; + margin: 5px; + padding: 10px; + gap: 10px; + } +} + +@media screen and (min-width: 1600px) { + .memory-game .container .App { + width: 80vh; + height: 70vh; + margin: 5px; + padding: 10px; + gap: 10px; + } +} + +@media screen and (max-width: 768px) { + .memory-game .container .App { + margin: 5px; + width: calc(100vh - 35px); + height: auto; + + .item { + height: 90px; + border-radius: 10px; + } + } +} diff --git a/src/plays/netlify-card-game/Readme.md b/src/plays/netlify-card-game/Readme.md new file mode 100644 index 0000000000..31b4c65d1e --- /dev/null +++ b/src/plays/netlify-card-game/Readme.md @@ -0,0 +1,15 @@ +# Memory Game + +The `Memory Game` is a very fun play build with ReactJS & moreover its a memory test game which is actually to uncover cards and memorize them for next move. + + +There is some key things we can also learn from projects are + +- Handling complex logics in ReactJS. +- Advanced use of useEffect and handling asynchronous function and cleanup function inside it without memory leakage. +- Managing Complex and Nested States. +- use of scss in ReactJS. + +# File Contents + +- main file `NetlifyCardGame.jsx` contents all the functions and logics. the `icons` folder contains all the required icons and `NetlifyCardGame.scss` contents the sass code for designing. diff --git a/src/plays/netlify-card-game/close.png b/src/plays/netlify-card-game/close.png new file mode 100644 index 0000000000..61330d6b1b Binary files /dev/null and b/src/plays/netlify-card-game/close.png differ diff --git a/src/plays/netlify-card-game/guideimages/s1.png b/src/plays/netlify-card-game/guideimages/s1.png new file mode 100644 index 0000000000..fb61a43be6 Binary files /dev/null and b/src/plays/netlify-card-game/guideimages/s1.png differ diff --git a/src/plays/netlify-card-game/guideimages/s2.png b/src/plays/netlify-card-game/guideimages/s2.png new file mode 100644 index 0000000000..2d11747a38 Binary files /dev/null and b/src/plays/netlify-card-game/guideimages/s2.png differ diff --git a/src/plays/netlify-card-game/guideimages/s3.png b/src/plays/netlify-card-game/guideimages/s3.png new file mode 100644 index 0000000000..7bfdfc28c1 Binary files /dev/null and b/src/plays/netlify-card-game/guideimages/s3.png differ diff --git a/src/plays/netlify-card-game/guideimages/s4.png b/src/plays/netlify-card-game/guideimages/s4.png new file mode 100644 index 0000000000..76eacb0d51 Binary files /dev/null and b/src/plays/netlify-card-game/guideimages/s4.png differ diff --git a/src/plays/netlify-card-game/icons/q1.png b/src/plays/netlify-card-game/icons/q1.png new file mode 100644 index 0000000000..c8dca579d8 Binary files /dev/null and b/src/plays/netlify-card-game/icons/q1.png differ diff --git a/src/plays/netlify-card-game/icons/q2.png b/src/plays/netlify-card-game/icons/q2.png new file mode 100644 index 0000000000..b4e1fa0dee Binary files /dev/null and b/src/plays/netlify-card-game/icons/q2.png differ diff --git a/src/plays/netlify-card-game/icons/q3.png b/src/plays/netlify-card-game/icons/q3.png new file mode 100644 index 0000000000..55ec9bbae4 Binary files /dev/null and b/src/plays/netlify-card-game/icons/q3.png differ diff --git a/src/plays/netlify-card-game/icons/q4.png b/src/plays/netlify-card-game/icons/q4.png new file mode 100644 index 0000000000..854c44e53d Binary files /dev/null and b/src/plays/netlify-card-game/icons/q4.png differ diff --git a/src/plays/netlify-card-game/icons/q5.png b/src/plays/netlify-card-game/icons/q5.png new file mode 100644 index 0000000000..3a5e9d981c Binary files /dev/null and b/src/plays/netlify-card-game/icons/q5.png differ diff --git a/src/plays/netlify-card-game/icons/q6.png b/src/plays/netlify-card-game/icons/q6.png new file mode 100644 index 0000000000..00dccf4b35 Binary files /dev/null and b/src/plays/netlify-card-game/icons/q6.png differ diff --git a/src/plays/netlify-card-game/icons/q7.png b/src/plays/netlify-card-game/icons/q7.png new file mode 100644 index 0000000000..3d8dcc3e68 Binary files /dev/null and b/src/plays/netlify-card-game/icons/q7.png differ diff --git a/src/plays/netlify-card-game/icons/q8.png b/src/plays/netlify-card-game/icons/q8.png new file mode 100644 index 0000000000..28da5f6bf5 Binary files /dev/null and b/src/plays/netlify-card-game/icons/q8.png differ diff --git a/src/plays/netlify-card-game/modal.js b/src/plays/netlify-card-game/modal.js new file mode 100644 index 0000000000..87f991dec6 --- /dev/null +++ b/src/plays/netlify-card-game/modal.js @@ -0,0 +1,70 @@ +import { Fragment, useState, useEffect } from "react"; + +// css +import "./modal.scss"; + +// assets +import s1 from "./guideimages/s1.png"; +import s2 from "./guideimages/s2.png"; +import s3 from "./guideimages/s3.png"; +import s4 from "./guideimages/s4.png"; +import close from './close.png' + +const Modal = ({ showModal, toggle }) => { + const [currState, setCurrentState] = useState(0); + + const structuringData = [ + { + info: "Click any of the box to unvail the the hidden image.", + image: s1, + }, + { + info: "Now Click another box to unvail another image. if its not the same the both of them will be hidden again.", + image: s2, + }, + { + info: "If the both images are matched they will stay visible and you want to uncover other images and also find their pair", + image: s3, + }, + { + info: "Lastly images position will be different on every 'reset' button click any you can keep continue memorise the glimpse of the images and find their pair. ", + image: s4, + }, + ]; + + useEffect(() => { + setCurrentState(0); + }, [showModal]); + + const buttonHandler = (val) => (e) => { + if ((currState === 0 && val < 0) || (currState === 3 && val > 0)) return; + setCurrentState(currState + val); + }; + + if (!showModal) return false; + return ( + +
+ clonse +
+

How to Play!

+

{structuringData[currState].info}

+ pic +
+ {currState > 0 ? :

} + +
+
+
+
+ + ); +}; + +export default Modal; diff --git a/src/plays/netlify-card-game/modal.scss b/src/plays/netlify-card-game/modal.scss new file mode 100644 index 0000000000..24010f7a7c --- /dev/null +++ b/src/plays/netlify-card-game/modal.scss @@ -0,0 +1,99 @@ +.memory-game-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +.memory-game-modal { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: #fff; + width: 50vw; + max-width: 800px; + z-index: 11; + border-radius: 10px; + animation: modalApear 0.3s ease-in-out; + + .close-icon { + position: absolute; + right: 1.5rem; + top: 1.5rem; + width: 30px; + padding: 5px; + filter: brightness(0.1); + cursor: pointer; + + @media screen and (max-width: 768px) { + right: 1rem; + top: 1rem; + width: 30px; + } + } + + .content { + margin: 0 2rem; + padding-bottom: 1rem; + .text { + text-align: center; + } + p { + text-align: center; + } + .guide-image { + width: 70%; + display: block; + aspect-ratio: 16/9; + height: 100%; + margin: 0 auto; + } + + .button-section { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 32px; + + button { + padding: 0.5rem 2rem; + font-size: 1rem; + border-radius: 20px; + border: none; + background-color: rgba(81, 91, 212, 1); + outline: none; + color: #fff; + font-weight: bold; + &:disabled { + background-color: rgba(81, 91, 212, 0.5); + pointer-events: none; + } + } + @media screen and (max-width: 768px) { + padding: 0; + margin-top: 10px; + } + } + } +} + +@keyframes modalApear { + 0% { + opacity: 0; + top: 48%; + } + 100% { + opacity: 1; + top: 50%; + } +} + +@media screen and (max-width: 768px) { + .memory-game-modal { + width: calc(100vw - 20px); + } +} diff --git a/src/plays/netlify-card-game/question.png b/src/plays/netlify-card-game/question.png new file mode 100644 index 0000000000..83479f531d Binary files /dev/null and b/src/plays/netlify-card-game/question.png differ diff --git a/src/plays/netlify-card-game/question1.png b/src/plays/netlify-card-game/question1.png new file mode 100644 index 0000000000..6f013b4ded Binary files /dev/null and b/src/plays/netlify-card-game/question1.png differ