Skip to content

Commit 76077ac

Browse files
committed
new(apps): card game
1 parent 125c7c6 commit 76077ac

File tree

5 files changed

+350
-1
lines changed

5 files changed

+350
-1
lines changed

public/apps/apps.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@
5353
"title": "Magic 8-Ball",
5454
"description": "Ask a yes/no question and let the Magic 8-Ball reveal your fate!",
5555
"icon": "QuestionIcon"
56+
},
57+
{
58+
"slug": "card-game",
59+
"to": "/apps/card-game",
60+
"title": "Higher or Lower Card Game",
61+
"description": "Guess if the next card will be higher or lower than the current one.",
62+
"icon": "CardsThreeIcon"
5663
}
5764
]
5865
},

src/components/AnimatedRoutes.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ import CronJobGeneratorPage from '../pages/apps/CronJobGeneratorPage';
4444
import ExcuseGeneratorPage from '../pages/apps/ExcuseGeneratorPage';
4545
import MagicEightBallPage from '../pages/apps/MagicEightBallPage';
4646
import JSONGeneratorPage from '../pages/apps/JSONGeneratorPage';
47-
import SettingsPage from '../pages/SettingsPage'; // Import SettingsPage
47+
import CardGamePage from '../pages/apps/CardGamePage';
48+
import SettingsPage from '../pages/SettingsPage';
4849

4950
import UsefulLinksPage from '../pages/UsefulLinksPage';
5051
import NotebooksPage from "../pages/notebooks/NotebooksPage";
@@ -399,6 +400,7 @@ function AnimatedRoutes() {
399400
<Route path="/apps::cron" element={<Navigate to="/apps/cron-job-generator" replace />} />
400401
<Route path="/apps::excuse" element={<Navigate to="/apps/excuse-generator" replace />} />
401402
<Route path="/apps::8ball" element={<Navigate to="/apps/magic-8-ball" replace />} />
403+
<Route path="/apps::card" element={<Navigate to="/apps/card-game" replace />} />
402404
{/* End of hardcoded redirects */}
403405
<Route
404406
path="/apps/ip"
@@ -414,6 +416,20 @@ function AnimatedRoutes() {
414416
</motion.div>
415417
}
416418
/>
419+
<Route
420+
path="/apps/card-game"
421+
element={
422+
<motion.div
423+
initial="initial"
424+
animate="in"
425+
exit="out"
426+
variants={pageVariants}
427+
transition={pageTransition}
428+
>
429+
<CardGamePage />
430+
</motion.div>
431+
}
432+
/>
417433
<Route
418434
path="/apps/cron-job-generator"
419435
element={

src/pages/apps/CardGamePage.js

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
import { Link } from 'react-router-dom';
3+
import { ArrowLeftIcon, CardsThreeIcon } from '@phosphor-icons/react';
4+
import colors from '../../config/colors';
5+
import { useToast } from '../../hooks/useToast';
6+
import useSeo from '../../hooks/useSeo';
7+
import '../../styles/CardGamePage.css';
8+
9+
const suits = ['♠', '♥', '♦', '♣'];
10+
const ranks = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
11+
12+
const getDeck = () => {
13+
const deck = [];
14+
for (const suit of suits) {
15+
for (const rank of ranks) {
16+
deck.push({ suit, rank, value: ranks.indexOf(rank) + 2 }); // Value 2-14 (A)
17+
}
18+
}
19+
return shuffleDeck(deck);
20+
};
21+
22+
const shuffleDeck = (deck) => {
23+
for (let i = deck.length - 1; i > 0; i--) {
24+
const j = Math.floor(Math.random() * (i + 1));
25+
[deck[i], deck[j]] = [deck[j], deck[i]];
26+
}
27+
return deck;
28+
};
29+
30+
const CardGamePage = () => {
31+
useSeo({
32+
title: 'Higher or Lower Card Game | Fezcodex',
33+
description: 'Play a simple Higher or Lower card game. Guess if the next card will be higher or lower than the current one.',
34+
keywords: ['Fezcodex', 'card game', 'higher or lower', 'game', 'react game'],
35+
ogTitle: 'Higher or Lower Card Game | Fezcodex',
36+
ogDescription: 'Play a simple Higher or Lower card game. Guess if the next card will be higher or lower than the current one.',
37+
ogImage: 'https://fezcode.github.io/logo512.png',
38+
twitterCard: 'summary_large_image',
39+
twitterTitle: 'Higher or Lower Card Game | Fezcodex',
40+
twitterDescription: 'Play a simple Higher or Lower card game. Guess if the next card will be higher or lower than the current one.',
41+
twitterImage: 'https://fezcode.github.io/logo512.png'
42+
});
43+
44+
const { addToast } = useToast();
45+
const [deck, setDeck] = useState([]);
46+
const [currentCard, setCurrentCard] = useState(null);
47+
const [nextCard, setNextCard] = useState(null);
48+
const [score, setScore] = useState(0);
49+
const [gameOver, setGameOver] = useState(false);
50+
const [gameStarted, setGameStarted] = useState(false);
51+
52+
const startGame = useCallback(() => {
53+
const newDeck = getDeck();
54+
const firstCard = newDeck.pop();
55+
setDeck(newDeck);
56+
setCurrentCard(firstCard);
57+
setNextCard(null);
58+
setScore(0);
59+
setGameOver(false);
60+
setGameStarted(true);
61+
}, [addToast]);
62+
63+
useEffect(() => {
64+
if (!gameStarted) {
65+
startGame();
66+
}
67+
}, [gameStarted, startGame]);
68+
69+
const drawNextCard = () => {
70+
if (deck.length === 0) {
71+
startGame();
72+
return null;
73+
}
74+
const drawnCard = deck.pop();
75+
setDeck([...deck]); // Trigger re-render for deck length
76+
return drawnCard;
77+
};
78+
79+
const handleGuess = (guess) => {
80+
if (gameOver || !currentCard) return;
81+
82+
const drawnNextCard = drawNextCard();
83+
if (!drawnNextCard) return; // If deck was empty and reset
84+
85+
setNextCard(drawnNextCard);
86+
87+
const isHigher = drawnNextCard.value > currentCard.value;
88+
const isLower = drawnNextCard.value < currentCard.value;
89+
const isSame = drawnNextCard.value === currentCard.value;
90+
91+
let correct = false;
92+
if (guess === 'higher' && isHigher) {
93+
correct = true;
94+
} else if (guess === 'lower' && isLower) {
95+
correct = true;
96+
} else if (isSame) {
97+
// If cards are the same, it's a draw, player doesn't lose, but doesn't win either.
98+
setCurrentCard(drawnNextCard);
99+
setNextCard(null);
100+
return;
101+
}
102+
103+
if (correct) {
104+
setScore(score + 1);
105+
setCurrentCard(drawnNextCard);
106+
setNextCard(null);
107+
} else {
108+
setGameOver(true);
109+
addToast({ title: 'Game Over!', message: `It was a ${drawnNextCard.rank} of ${drawnNextCard.suit}. Final score: ${score}`, duration: 7500, type: 'error' });
110+
}
111+
};
112+
113+
const cardStyle = {
114+
backgroundColor: colors['app-alpha-10'],
115+
borderColor: colors['app-alpha-50'],
116+
color: colors.app,
117+
};
118+
119+
const renderCard = (card, isNext = false) => {
120+
if (!card) return <div className={`card-placeholder ${isNext ? 'next-card-placeholder' : ''}`}>?</div>;
121+
122+
const isRed = card.suit === '♥' || card.suit === '♦';
123+
return (
124+
<div className={`card ${isRed ? 'red' : 'black'} ${isNext ? 'next-card' : ''}`}>
125+
<div className="card-corner top-left">
126+
<span className="rank">{card.rank}</span>
127+
<span className="suit">{card.suit}</span>
128+
</div>
129+
<div className="card-suit-center">{card.suit}</div>
130+
<div className="card-corner bottom-right">
131+
<span className="rank">{card.rank}</span>
132+
<span className="suit">{card.suit}</span>
133+
</div>
134+
</div>
135+
);
136+
};
137+
138+
return (
139+
<div className="py-16 sm:py-24">
140+
<div className="mx-auto max-w-7xl px-6 lg:px-8 text-gray-300">
141+
<Link
142+
to="/apps"
143+
className="text-article hover:underline flex items-center justify-center gap-2 text-lg mb-4"
144+
>
145+
<ArrowLeftIcon size={24} /> Back to Apps
146+
</Link>
147+
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl mb-4 flex items-center">
148+
<span className="codex-color">fc</span>
149+
<span className="separator-color">::</span>
150+
<span className="apps-color">apps</span>
151+
<span className="separator-color">::</span>
152+
<span className="single-app-color">card</span>
153+
</h1>
154+
<hr className="border-gray-700" />
155+
<div className="flex justify-center items-center mt-16">
156+
<div
157+
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"
158+
style={cardStyle}
159+
>
160+
<div
161+
className="absolute top-0 left-0 w-full h-full opacity-10"
162+
style={{
163+
backgroundImage:
164+
'radial-gradient(circle, white 1px, transparent 1px)',
165+
backgroundSize: '10px 10px',
166+
}}
167+
></div>
168+
<div className="relative z-10 p-1">
169+
<h1 className="text-3xl font-arvo font-normal mb-4 text-app flex items-center gap-2">
170+
<CardsThreeIcon size={32} /> Higher or Lower
171+
</h1>
172+
<hr className="border-gray-700 mb-4" />
173+
174+
<div className="flex flex-col items-center justify-center gap-4 mb-6">
175+
<p className="text-xl">Score: {score}</p>
176+
<div className="flex gap-8 items-center">
177+
{renderCard(currentCard)}
178+
<span className="text-4xl font-bold text-gray-400">VS</span>
179+
{renderCard(nextCard, true)}
180+
</div>
181+
<p className="text-sm text-gray-400">Cards left: {deck.length}</p>
182+
</div>
183+
184+
{!gameOver && currentCard && (
185+
<div className="flex justify-center gap-4 mt-4">
186+
<button
187+
onClick={() => handleGuess('higher')}
188+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
189+
style={{
190+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
191+
color: cardStyle.color,
192+
borderColor: cardStyle.borderColor,
193+
border: '1px solid',
194+
}}
195+
>
196+
Higher
197+
</button>
198+
<button
199+
onClick={() => handleGuess('lower')}
200+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
201+
style={{
202+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
203+
color: cardStyle.color,
204+
borderColor: cardStyle.borderColor,
205+
border: '1px solid',
206+
}}
207+
>
208+
Lower
209+
</button>
210+
</div>
211+
)}
212+
213+
{gameOver && (
214+
<div className="text-center mt-6">
215+
<p className="text-2xl font-bold text-red-500 mb-4">Game Over! Final Score: {score}</p>
216+
<button
217+
onClick={startGame}
218+
className="px-6 py-2 rounded-md text-lg font-arvo font-normal transition-colors duration-300 ease-in-out"
219+
style={{
220+
backgroundColor: 'rgba(0, 0, 0, 0.2)',
221+
color: cardStyle.color,
222+
borderColor: cardStyle.borderColor,
223+
border: '1px solid',
224+
}}
225+
>
226+
Play Again
227+
</button>
228+
</div>
229+
)}
230+
</div>
231+
</div>
232+
</div>
233+
</div>
234+
</div>
235+
);
236+
};
237+
238+
export default CardGamePage;

src/styles/CardGamePage.css

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* src/styles/CardGamePage.css */
2+
3+
.card-placeholder {
4+
width: 100px;
5+
height: 140px;
6+
border: 2px dashed #4a5568; /* gray-600 */
7+
border-radius: 8px;
8+
display: flex;
9+
align-items: center;
10+
justify-content: center;
11+
font-size: 2rem;
12+
color: #a0aec0; /* gray-400 */
13+
background-color: #2d3748; /* gray-800 */
14+
}
15+
16+
.card {
17+
width: 100px;
18+
height: 140px;
19+
background-color: #fff;
20+
border-radius: 8px;
21+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
22+
position: relative;
23+
display: flex;
24+
flex-direction: column;
25+
justify-content: space-between;
26+
align-items: center;
27+
padding: 8px;
28+
font-family: 'Arial', sans-serif;
29+
font-weight: bold;
30+
transition: transform 0.3s ease-in-out;
31+
}
32+
33+
.card.red {
34+
color: #ef4444; /* red-500 */
35+
}
36+
37+
.card.black {
38+
color: #1a202c; /* gray-900 */
39+
}
40+
41+
.card-corner {
42+
position: absolute;
43+
font-size: 1.2rem;
44+
line-height: 1;
45+
}
46+
47+
.card-corner.top-left {
48+
top: 5px;
49+
left: 5px;
50+
}
51+
52+
.card-corner.bottom-right {
53+
bottom: 5px;
54+
right: 5px;
55+
transform: rotate(180deg);
56+
}
57+
58+
.card-suit-center {
59+
font-size: 3rem;
60+
line-height: 1;
61+
position: absolute;
62+
top: 50%;
63+
left: 50%;
64+
transform: translate(-50%, -50%);
65+
}
66+
67+
/* Animation for next card */
68+
.next-card-placeholder {
69+
animation: pulse-border 1.5s infinite ease-in-out;
70+
}
71+
72+
@keyframes pulse-border {
73+
0% {
74+
border-color: #4a5568; /* gray-600 */
75+
}
76+
50% {
77+
border-color: #63b3ed; /* blue-300 */
78+
}
79+
100% {
80+
border-color: #4a5568; /* gray-600 */
81+
}
82+
}
83+
84+
.next-card {
85+
transform: translateY(-10px); /* Slight lift for emphasis */
86+
}

src/utils/appIcons.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
ClockIcon,
2424
SmileyWinkIcon,
2525
QuestionIcon,
26+
CardsThreeIcon,
2627
} from '@phosphor-icons/react';
2728

2829
export const appIcons = {
@@ -50,4 +51,5 @@ export const appIcons = {
5051
ClockIcon,
5152
SmileyWinkIcon,
5253
QuestionIcon,
54+
CardsThreeIcon,
5355
};

0 commit comments

Comments
 (0)