We can dynamically adjust the grid layout based on the number of cards on the board. Here's how we can modify the component to achieve this:
function getGridClass(cardCount: number) {
if (cardCount <= 12) {
return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4";
} else {
return "grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5";
}
}return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<h1 className="text-4xl font-bold mb-4">Set Game</h1>
<div className="mb-4">
Score: {score} | Time: {time}s
</div>
<motion.div
className={`grid ${getGridClass(board.length)} gap-4 mb-4`}
animate={controls}
>
{board.map((card) => renderCard(card))}
</motion.div>
{/* ... rest of the JSX ... */}
</div>
);addThreeCards function to ensure it doesn't add cards if there are already more than 12 on the board:function addThreeCards() {
if (deck.length >= 3 && board.length <= 12) {
setBoard(prevBoard => [...prevBoard, ...deck.slice(0, 3)]);
setDeck(prevDeck => prevDeck.slice(3));
}
}handleValidSet function to maintain at least 12 cards on the board if possible:function handleValidSet(validSet: GameCard[]) {
setScore(score + 1);
// Remove the valid set from the board
let newBoard = board.filter(card => !validSet.includes(card));
// If there are less than 12 cards on the board after removing the set, add new cards
let newDeck = [...deck];
while (newBoard.length < 12 && newDeck.length > 0) {
newBoard.push(newDeck[0]);
newDeck = newDeck.slice(1);
}
setBoard(newBoard);
setDeck(newDeck);
setSelected([]);
if (newBoard.length === 0 && newDeck.length === 0) {
setGameOver(true);
setIsRunning(false);
}
}These changes will:
This approach should give you the desired behavior: the board will only expand to 5 columns when necessary, and it will try to maintain at least 12 cards on the board at all times.
While your current implementation works well as a single file, splitting it up can improve maintainability and readability, especially as the project grows. Here's how you could structure it according to Next.js and App Router best practices:
Components (components/):
Here's a suggested structure:
app/
page.tsx
components/
Card.tsx
GameBoard.tsx
ScoreBoard.tsx
hooks/
useSetGame.ts
lib/
game-logic.ts
types/
index.tsHere's how you might implement this:
"use client";
import { useSetGame } from "@/hooks/useSetGame";
import GameBoard from "@/components/GameBoard";
import ScoreBoard from "@/components/ScoreBoard";
import { Button } from "@/components/ui/button";
import { AnimatePresence, motion } from "framer-motion";
export default function SetGame() {
const {
board,
score,
time,
gameOver,
showSet,
showNoMatches,
addThreeCards,
resetGame,
renderCard,
} = useSetGame();
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-4">
<h1 className="text-4xl font-bold mb-4">Set Game</h1>
<ScoreBoard score={score} time={time} />
<GameBoard board={board} renderCard={renderCard} />
<div className="flex gap-4">
<Button onClick={addThreeCards}>Add 3 Cards</Button>
<Button onClick={resetGame}>Reset Game</Button>
</div>
{gameOver && (
<div className="mt-4 text-2xl font-bold text-green-600">
Congratulations! You completed the game in {time} seconds!
</div>
)}
<AnimatePresence>
{showSet && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
>
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
className="flex gap-4"
>
{showSet.map((card) => renderCard(card, true))}
</motion.div>
</motion.div>
)}
{showNoMatches && (
<motion.div
initial={{ opacity: 0, y: -50 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -50 }}
className="fixed top-10 left-1/2 transform -translate-x-1/2 bg-red-500 text-white px-4 py-2 rounded-md z-50"
>
No Matches Available
</motion.div>
)}
</AnimatePresence>
</div>
);
}The other files (hooks/useSetGame.ts, lib/game-logic.ts, etc.) would contain the respective logic that's currently in your single file.
This structure separates concerns, making the code more maintainable and easier to test. However, for a relatively simple game like this, keeping it in a single file is not necessarily bad practice, especially if it's not expected to grow significantly in complexity.
The decision to split the code should be based on your specific needs, team preferences, and the expected future complexity of the project. If you find the current structure manageable and don't anticipate significant additions, keeping it as a single file is perfectly acceptable.