Skip to content

Commit ad9e0fa

Browse files
committed
new(apps): memory game
1 parent e9b40e3 commit ad9e0fa

File tree

5 files changed

+360
-1
lines changed

5 files changed

+360
-1
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,13 @@
6767
"title": "Soccer Pong",
6868
"description": "A Pong-style game with a soccer twist. Player vs. AI.",
6969
"icon": "SoccerBallIcon"
70+
},
71+
{
72+
"slug": "memory-game",
73+
"to": "/apps/memory-game",
74+
"title": "Memory Game",
75+
"description": "Test your memory by matching pairs of cards.",
76+
"icon": "BrainIcon"
7077
}
7178
]
7279
},

src/components/AnimatedRoutes.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import ExcuseGeneratorPage from '../pages/apps/ExcuseGeneratorPage';
4545
import MagicEightBallPage from '../pages/apps/MagicEightBallPage';
4646
import JSONGeneratorPage from '../pages/apps/JSONGeneratorPage';
4747
import CardGamePage from '../pages/apps/CardGamePage';
48-
import SoccerPongPage from '../pages/apps/SoccerPongPage'; // Import SoccerPongPage
48+
import SoccerPongPage from '../pages/apps/SoccerPongPage';
49+
import MemoryGamePage from '../pages/apps/MemoryGamePage'; // Import MemoryGamePage
4950
import SettingsPage from '../pages/SettingsPage';
5051

5152
import UsefulLinksPage from '../pages/UsefulLinksPage';
@@ -403,6 +404,7 @@ function AnimatedRoutes() {
403404
<Route path="/apps::8ball" element={<Navigate to="/apps/magic-8-ball" replace />} />
404405
<Route path="/apps::card" element={<Navigate to="/apps/card-game" replace />} />
405406
<Route path="/apps::sp" element={<Navigate to="/apps/soccer-pong" replace />} />
407+
<Route path="/apps::mg" element={<Navigate to="/apps/memory-game" replace />} />
406408
{/* End of hardcoded redirects */}
407409
<Route
408410
path="/apps/ip"
@@ -446,6 +448,20 @@ function AnimatedRoutes() {
446448
</motion.div>
447449
}
448450
/>
451+
<Route
452+
path="/apps/memory-game"
453+
element={
454+
<motion.div
455+
initial="initial"
456+
animate="in"
457+
exit="out"
458+
variants={pageVariants}
459+
transition={pageTransition}
460+
>
461+
<MemoryGamePage />
462+
</motion.div>
463+
}
464+
/>
449465
<Route
450466
path="/apps/cron-job-generator"
451467
element={

src/pages/apps/MemoryGamePage.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { ArrowLeftIcon, BrainIcon } from '@phosphor-icons/react'; // Using BrainIcon for memory game
4+
import colors from '../../config/colors';
5+
import useSeo from '../../hooks/useSeo';
6+
import '../../styles/MemoryGamePage.css';
7+
8+
const cardValues = ['🍎', '🍌', '🍒', '🍇', '🍋', '🍊', '🍓', '🍉']; // Example card values
9+
10+
const MemoryGamePage = () => {
11+
useSeo({
12+
title: 'Memory Game | Fezcodex',
13+
description: 'A classic memory game to test your concentration.',
14+
keywords: ['Fezcodex', 'memory game', 'match pairs', 'brain game', 'concentration'],
15+
ogTitle: 'Memory Game | Fezcodex',
16+
ogDescription: 'A classic memory game to test your concentration.',
17+
ogImage: 'https://fezcode.github.io/logo512.png',
18+
twitterCard: 'summary_large_image',
19+
twitterTitle: 'Memory Game | Fezcodex',
20+
twitterDescription: 'A classic memory game to test your concentration.',
21+
twitterImage: 'https://fezcode.github.io/logo512.png'
22+
});
23+
24+
const [cards, setCards] = useState([]);
25+
const [flippedCards, setFlippedCards] = useState([]);
26+
const [matchesFound, setMatchesFound] = useState(0);
27+
const [moves, setMoves] = useState(0);
28+
const [gameOver, setGameOver] = useState(false);
29+
const [gameStarted, setGameStarted] = useState(false);
30+
31+
const initializeGame = useCallback(() => {
32+
const shuffledCards = shuffleCards(cardValues);
33+
setCards(shuffledCards);
34+
setFlippedCards([]);
35+
setMatchesFound(0);
36+
setMoves(0);
37+
setGameOver(false);
38+
setGameStarted(false); // Game starts when "Play Game" is clicked
39+
}, []);
40+
41+
useEffect(() => {
42+
initializeGame();
43+
}, [initializeGame]);
44+
45+
const shuffleCards = (values) => {
46+
let id = 0;
47+
const initialCards = [...values, ...values].map(value => ({
48+
id: id++,
49+
value,
50+
isFlipped: false,
51+
isMatched: false,
52+
}));
53+
54+
// Fisher-Yates shuffle algorithm
55+
for (let i = initialCards.length - 1; i > 0; i--) {
56+
const j = Math.floor(Math.random() * (i + 1));
57+
[initialCards[i], initialCards[j]] = [initialCards[j], initialCards[i]];
58+
}
59+
return initialCards;
60+
};
61+
62+
const handleCardClick = (clickedCard) => {
63+
if (!gameStarted || gameOver || clickedCard.isFlipped || clickedCard.isMatched || flippedCards.length === 2) {
64+
return;
65+
}
66+
67+
const newCards = cards.map(card =>
68+
card.id === clickedCard.id ? { ...card, isFlipped: true } : card
69+
);
70+
setCards(newCards);
71+
setFlippedCards(prev => [...prev, clickedCard.id]);
72+
73+
if (flippedCards.length === 1) {
74+
setMoves(prev => prev + 1);
75+
const firstCard = cards.find(card => card.id === flippedCards[0]);
76+
const secondCard = clickedCard;
77+
78+
if (firstCard.value === secondCard.value) {
79+
// Match found
80+
setTimeout(() => {
81+
const matchedCards = newCards.map(card =>
82+
card.id === firstCard.id || card.id === secondCard.id ? { ...card, isMatched: true } : card
83+
);
84+
setCards(matchedCards);
85+
setMatchesFound(prev => prev + 1);
86+
setFlippedCards([]);
87+
}, 700);
88+
} else {
89+
// No match, flip back
90+
setTimeout(() => {
91+
const flippedBackCards = newCards.map(card =>
92+
card.id === firstCard.id || card.id === secondCard.id ? { ...card, isFlipped: false } : card
93+
);
94+
setCards(flippedBackCards);
95+
setFlippedCards([]);
96+
}, 1000);
97+
}
98+
}
99+
};
100+
101+
useEffect(() => {
102+
if (matchesFound === cardValues.length) {
103+
setGameOver(true);
104+
}
105+
}, [matchesFound]);
106+
107+
const cardStyle = {
108+
backgroundColor: colors['app-alpha-10'],
109+
borderColor: colors['app-alpha-50'],
110+
color: colors.app,
111+
};
112+
113+
return (
114+
<div className="py-16 sm:py-24">
115+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
116+
<Link
117+
to="/apps"
118+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
119+
>
120+
<ArrowLeftIcon size={24} /> Back to Apps
121+
</Link>
122+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
123+
<span className="codex-color">fc</span>
124+
<span className="separator-color">::</span>
125+
<span className="apps-color">apps</span>
126+
<span className="separator-color">::</span>
127+
<span className="single-app-color">mg</span>
128+
</h1>
129+
<hr className="border-gray-700" />
130+
<div className="flex justify-center items-center mt-16">
131+
<div
132+
className="group bg-transparent border rounded-lg shadow-2xl p-6 flex flex-col justify-between relative transform overflow-hidden h-full w-full max-w-4xl"
133+
style={cardStyle}
134+
>
135+
<div
136+
className="absolute top-0 left-0 w-full h-full opacity-10"
137+
style={{
138+
backgroundImage:
139+
'radial-gradient(circle, white 1px, transparent 1px)',
140+
backgroundSize: '10px 10px',
141+
}}
142+
></div>
143+
<div className="relative z-10 p-1">
144+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app flex items-center gap-2">
145+
<BrainIcon size={32} /> Memory Game
146+
</h1>
147+
<hr className="border-gray-700 mb-4" />
148+
149+
<div className="memory-game-area">
150+
<div className="game-info mb-4 text-xl font-bold">
151+
Moves: {moves} | Matches: {matchesFound}/{cardValues.length}
152+
</div>
153+
154+
{gameOver && (
155+
<div className="game-over-message text-center text-3xl font-bold mb-4">
156+
You Won!
157+
<button
158+
onClick={initializeGame}
159+
className="block mx-auto mt-4 px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
160+
style={{
161+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
162+
color: cardStyle.color,
163+
borderColor: cardStyle.borderColor,
164+
border: '1px solid',
165+
}}
166+
>
167+
Play Again
168+
</button>
169+
</div>
170+
)}
171+
172+
{!gameStarted && (
173+
<div className="start-game-message text-center text-3xl font-bold mb-4">
174+
Click "Play Game" to Start!
175+
<button
176+
onClick={() => setGameStarted(true)}
177+
className="block mx-auto mt-4 px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
178+
style={{
179+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
180+
color: cardStyle.color,
181+
borderColor: cardStyle.borderColor,
182+
border: '1px solid',
183+
}}
184+
>
185+
Play Game
186+
</button>
187+
</div>
188+
)}
189+
190+
<div className={`cards-grid ${!gameStarted || gameOver ? 'blurred' : ''}`}>
191+
{cards.map(card => (
192+
<div
193+
key={card.id}
194+
className={`card ${card.isFlipped || card.isMatched ? 'flipped' : ''} ${card.isMatched ? 'matched' : ''}`}
195+
onClick={() => handleCardClick(card)}
196+
>
197+
<div className="card-inner">
198+
<div className="card-face card-back"></div>
199+
<div className="card-face card-front">
200+
{card.value}
201+
</div>
202+
</div>
203+
</div>
204+
))}
205+
</div>
206+
</div>
207+
</div>
208+
</div>
209+
</div>
210+
</div>
211+
</div>
212+
);
213+
};
214+
215+
export default MemoryGamePage;

src/styles/MemoryGamePage.css

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* src/styles/MemoryGamePage.css */
2+
3+
.memory-game-area {
4+
display: flex;
5+
flex-direction: column;
6+
align-items: center;
7+
justify-content: center;
8+
width: 100%;
9+
max-width: 600px; /* Adjust as needed */
10+
margin: 0 auto;
11+
position: relative;
12+
}
13+
14+
.game-info {
15+
color: #cbd5e0; /* Light gray text */
16+
margin-bottom: 20px;
17+
}
18+
19+
.cards-grid {
20+
display: grid;
21+
grid-template-columns: repeat(4, 1fr); /* 4 columns */
22+
gap: 10px;
23+
perspective: 1000px; /* For 3D flip effect */
24+
}
25+
26+
.card {
27+
width: 100px; /* Adjust card size */
28+
height: 100px; /* Adjust card size */
29+
position: relative;
30+
transform-style: preserve-3d;
31+
transition: transform 0.6s;
32+
cursor: pointer;
33+
border-radius: 8px;
34+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
35+
}
36+
37+
.card.flipped {
38+
transform: rotateY(180deg);
39+
}
40+
41+
.card.matched {
42+
opacity: 0.5;
43+
cursor: default;
44+
}
45+
46+
.card-inner {
47+
position: relative;
48+
width: 100%;
49+
height: 100%;
50+
text-align: center;
51+
transition: transform 0.6s;
52+
transform-style: preserve-3d;
53+
border-radius: 8px;
54+
}
55+
56+
.card-face {
57+
position: absolute;
58+
width: 100%;
59+
height: 100%;
60+
backface-visibility: hidden;
61+
display: flex;
62+
align-items: center;
63+
justify-content: center;
64+
font-size: 2.5rem; /* Adjust emoji size */
65+
border-radius: 8px;
66+
border: 2px solid #4a5568; /* gray-600 */
67+
}
68+
69+
.card-back {
70+
background-color: #4299e1; /* Blue for card back */
71+
color: white;
72+
transform: rotateY(0deg);
73+
}
74+
75+
.card-front {
76+
background-color: #2d3748; /* Dark gray for card front */
77+
color: white;
78+
transform: rotateY(180deg);
79+
}
80+
81+
.game-over-message, .start-game-message {
82+
position: absolute;
83+
top: 50%;
84+
left: 50%;
85+
transform: translate(-50%, -50%);
86+
display: flex;
87+
flex-direction: column;
88+
align-items: center;
89+
justify-content: center;
90+
background-color: rgba(0, 0, 0, 0.8);
91+
z-index: 20;
92+
color: white;
93+
font-size: 2rem;
94+
border-radius: 8px;
95+
padding: 20px; /* Add padding to ensure content is not squished */
96+
min-width: 250px; /* Ensure a minimum width */
97+
text-align: center; /* Ensure text is centered within the message box */
98+
}
99+
100+
.game-over-message button, .start-game-message button {
101+
margin-top: 20px;
102+
padding: 10px 20px;
103+
font-size: 1.2rem;
104+
cursor: pointer;
105+
background-color: #4299e1;
106+
color: white;
107+
border: none;
108+
border-radius: 5px;
109+
transition: background-color 0.3s ease;
110+
}
111+
112+
.game-over-message button:hover, .start-game-message button:hover {
113+
background-color: #3182ce;
114+
}
115+
116+
.cards-grid.blurred {
117+
filter: blur(5px);
118+
pointer-events: none; /* Disable interaction when blurred */
119+
}

0 commit comments

Comments
 (0)