diff --git a/src/App.tsx b/src/App.tsx index 4f68cc8..e73d8c5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react' -import React from 'react'; // Added for useEffect +import React, { useState, useCallback, useMemo } from 'react'; import Card from './components/Card'; +import TierCard from './components/TierCard'; +import HistoryList from './components/HistoryList'; // --- Scratch Card Game Types and State --- export type Tier = { @@ -24,23 +25,16 @@ const TIERS: Tier[] = [ { name: 'Supreme', unlockPrice: 2000, buyPrice: 3000, gubblePointChance: 0.4 }, ]; -// --- Card Component --- - function App() { // --- Game State --- const [money, setMoney] = useState(20); // User starts with $20 const [unlockedTiers, setUnlockedTiers] = useState([]); // Unlocked tier names const [gubblePoints, setGubblePoints] = useState(0); - // Remove selectedTier state - - // --- Card State --- const [card, setCard] = useState<{ winningNumbers: number[]; fields: { value: number; scratched: boolean; won: number | null }[]; tier: Tier | null; } | null>(null); - - // --- Play History State --- const [history, setHistory] = useState<{ tier: string; winnings: number; @@ -49,47 +43,44 @@ function App() { }[]>([]); // --- Tier Unlock Logic --- - const handleUnlockTier = (tier: Tier) => { - if (money >= tier.unlockPrice && !unlockedTiers.includes(tier.name)) { - setMoney(money - tier.unlockPrice); - setUnlockedTiers([...unlockedTiers, tier.name]); - } - }; + const handleUnlockTier = useCallback((tier: Tier) => { + setMoney(m => { + if (m < tier.unlockPrice) return m; + setUnlockedTiers(prev => prev.includes(tier.name) ? prev : [...prev, tier.name]); + return m - tier.unlockPrice; + }); + }, []); - // --- Tier Select Logic --- - // Remove handleSelectTier - // const handleSelectTier = (tier: Tier) => { - // if (unlockedTiers.includes(tier.name)) { - // setSelectedTier(tier.name); - // } - // }; - - // Update handleBuyCard to accept tier as argument - const handleBuyCard = (tier: Tier) => { - if (!unlockedTiers.includes(tier.name) || money < tier.buyPrice) return; - // Generate 3 unique winning numbers (1-9) - const winningNumbers: number[] = []; - while (winningNumbers.length < 3) { - const n = Math.floor(Math.random() * 9) + 1; - if (!winningNumbers.includes(n)) winningNumbers.push(n); - } - // Generate 6 random fields (1-9) - const fields = Array.from({ length: 6 }, () => ({ value: Math.floor(Math.random() * 9) + 1, scratched: false, won: null })); - setMoney(money - tier.buyPrice); - setCard({ winningNumbers, fields, tier }); - // Add to history (winnings will be updated after all fields scratched) - setHistory((h) => [ - { tier: tier.name, winnings: 0, winningNumbers, fields: fields.map(f => f.value) }, - ...h.slice(0, 9) - ]); - }; + // --- Buy Card Logic --- + const handleBuyCard = useCallback((tier: Tier) => { + setMoney(m => { + if (m < tier.buyPrice) return m; + if (!unlockedTiers.includes(tier.name)) return m; + // Generate 3 unique winning numbers (1-9) + const winningNumbers: number[] = []; + while (winningNumbers.length < 3) { + const n = Math.floor(Math.random() * 9) + 1; + if (!winningNumbers.includes(n)) winningNumbers.push(n); + } + // Generate 6 random fields (1-9) + const fields = Array.from({ length: 6 }, () => ({ value: Math.floor(Math.random() * 9) + 1, scratched: false, won: null })); + setCard({ winningNumbers, fields, tier }); + setHistory(h => [ + { tier: tier.name, winnings: 0, winningNumbers, fields: fields.map(f => f.value) }, + ...h.slice(0, 9) + ]); + return m - tier.buyPrice; + }); + }, [unlockedTiers]); // --- Update history winnings after all fields scratched --- - React.useEffect(() => { + // This effect only runs when card changes + // Use useCallback for setHistory logic + const updateHistoryWinnings = useCallback(() => { if (!card) return; const allScratched = card.fields.every(f => f.scratched); if (allScratched) { - setHistory((h) => { + setHistory(h => { const [latest, ...rest] = h; if (!latest || latest.winningNumbers !== card.winningNumbers) return h; return [ @@ -100,6 +91,12 @@ function App() { } }, [card]); + // Only run when card changes + // eslint-disable-next-line react-hooks/exhaustive-deps + React.useEffect(updateHistoryWinnings, [card]); + + // --- Memoized winnings for current card --- + const currentWinnings = useMemo(() => card ? card.fields.reduce((sum, f) => sum + (f.won || 0), 0) : 0, [card]); // --- UI --- return ( @@ -108,73 +105,29 @@ function App() {
Money: ${money}
Gubble Points: {gubblePoints}
- {TIERS.map((tier) => { - const unlocked = unlockedTiers.includes(tier.name); - return ( -
-
{tier.name}
-
Unlock: ${tier.unlockPrice}
-
Buy: ${tier.buyPrice}
- {!unlocked ? ( - - ) : ( - - )} -
- ); - })} + {TIERS.map((tier) => ( + + ))}
- {/* Tier Info and Card Summary */} - {/* Remove all selectedTier-dependent UI (selected tier info, buy button outside tier cards, etc.) */} {card && ( <>
Winnings this card: - ${card.fields.reduce((sum, f) => sum + (f.won || 0), 0)} + ${currentWinnings}
)} - {/* Play History */} - {history.length > 0 && ( -
-
Play History
-
    - {history.map((h, i) => ( -
  • -
    - {h.tier} - Win: ${h.winnings} -
    -
    - {h.winningNumbers.map((n, j) => ( - {n} - ))} -
    -
    - {h.fields.map((n, j) => ( - {n} - ))} -
    -
  • - ))} -
-
- )} + - ) + ); } -export default App +export default App; diff --git a/src/components/Card.tsx b/src/components/Card.tsx index 8ff522e..8b1d92c 100644 --- a/src/components/Card.tsx +++ b/src/components/Card.tsx @@ -69,4 +69,4 @@ const Card: React.FC = ({ card, setCard, setMoney, setGubblePoints }) }; export type { CardProps }; -export default Card; \ No newline at end of file +export default React.memo(Card); \ No newline at end of file diff --git a/src/components/HistoryList.tsx b/src/components/HistoryList.tsx new file mode 100644 index 0000000..c09932a --- /dev/null +++ b/src/components/HistoryList.tsx @@ -0,0 +1,43 @@ +import React from 'react'; + +type HistoryItem = { + tier: string; + winnings: number; + winningNumbers: number[]; + fields: number[]; +}; + +type HistoryListProps = { + history: HistoryItem[]; +}; + +const HistoryList: React.FC = React.memo(({ history }) => { + if (history.length === 0) return null; + return ( +
+
Play History
+
    + {history.map((h, i) => ( +
  • +
    + {h.tier} + Win: ${h.winnings} +
    +
    + {h.winningNumbers.map((n, j) => ( + {n} + ))} +
    +
    + {h.fields.map((n, j) => ( + {n} + ))} +
    +
  • + ))} +
+
+ ); +}); + +export default HistoryList; \ No newline at end of file diff --git a/src/components/TierCard.tsx b/src/components/TierCard.tsx new file mode 100644 index 0000000..cdd9dcd --- /dev/null +++ b/src/components/TierCard.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import type { Tier } from '../App'; + +type TierCardProps = { + tier: Tier; + unlocked: boolean; + money: number; + onUnlock: (tier: Tier) => void; + onBuy: (tier: Tier) => void; +}; + +const TierCard: React.FC = React.memo(({ tier, unlocked, money, onUnlock, onBuy }) => { + return ( +
+
{tier.name}
+
Unlock: ${tier.unlockPrice}
+
Buy: ${tier.buyPrice}
+ {!unlocked ? ( + + ) : ( + + )} +
+ ); +}); + +export default TierCard; \ No newline at end of file