|
1 | 1 | "use client"; |
2 | 2 |
|
3 | | -import React, { useEffect, useState } from "react"; |
| 3 | +import React from "react"; |
4 | 4 | import { CodeSpace } from "./components/CodeSpace"; |
5 | | -import { Challenge, CodeBlock, GameState } from "./types"; |
6 | | -import { checkIsWin, generateHtml, loadRandomChallenge } from "./challenge"; |
7 | | -import { formatTime, shuffleArray } from "./utils"; |
| 5 | +import { formatTime } from "./utils"; |
8 | 6 | import { GameOverModal } from "./components/GameOverModal"; |
9 | 7 | import { PreviewPane } from "./components/PreviewPane"; |
| 8 | +import { SparkRushProvider, useSparkRush } from "./SparkRushContext"; |
10 | 9 |
|
11 | | -export const SparkRush = () => { |
12 | | - const [gameState, setGameState] = useState<GameState>({ |
13 | | - timeRemaining: 180, |
14 | | - score: 0, |
15 | | - gameActive: true, |
16 | | - gameOver: false, |
17 | | - }); |
18 | | - |
19 | | - const [challenge, setChallenge] = useState<Challenge>(loadRandomChallenge()); |
20 | | - const [codeBlocks, setCodeBlocks] = useState<CodeBlock[]>([]); |
21 | | - |
22 | | - const [showSuccessFlash, setShowSuccessFlash] = useState(false); |
23 | | - const [showTargetFlash, setShowTargetFlash] = useState(true); |
24 | | - |
25 | | - // initiate code blocks based on target |
26 | | - useEffect(() => { |
27 | | - // shuffle challenge code blocks and make sure that it is not in win condition |
28 | | - let blocks = shuffleArray(challenge.codeBlocks); |
29 | | - while (checkIsWin(blocks, challenge.codeBlocks)) { |
30 | | - blocks = shuffleArray(challenge.codeBlocks); |
31 | | - } |
32 | | - setCodeBlocks(blocks); |
33 | | - }, [challenge]); |
34 | | - |
35 | | - // Flash ONLY the target when a new challenge loads |
36 | | - useEffect(() => { |
37 | | - setShowTargetFlash(true); |
38 | | - setTimeout(() => { |
39 | | - setShowTargetFlash(false); |
40 | | - }, 1000); |
41 | | - }, [challenge]); |
42 | | - |
43 | | - // Timer |
44 | | - useEffect(() => { |
45 | | - if (!gameState.gameActive) return; |
46 | | - |
47 | | - const interval = setInterval(() => { |
48 | | - setGameState((prev) => { |
49 | | - const newTime = prev.timeRemaining - 1; |
50 | | - if (newTime <= 0) { |
51 | | - return { |
52 | | - ...prev, |
53 | | - timeRemaining: 0, |
54 | | - gameActive: false, |
55 | | - gameOver: true, |
56 | | - }; |
57 | | - } |
58 | | - return { ...prev, timeRemaining: newTime }; |
59 | | - }); |
60 | | - }, 1000); |
61 | | - |
62 | | - return () => clearInterval(interval); |
63 | | - }, [gameState.gameActive]); |
64 | | - |
65 | | - |
66 | | - |
67 | | - // Check for win condition |
68 | | - useEffect(() => { |
69 | | - if (codeBlocks.length === 0) return; |
70 | | - |
71 | | - if (checkIsWin(codeBlocks, challenge.codeBlocks)) { |
72 | | - setGameState((prev) => ({ ...prev, score: prev.score + 1 })); |
73 | | - setShowSuccessFlash(true); // turn green |
74 | | - |
75 | | - setTimeout(() => { |
76 | | - const newChallenge = loadRandomChallenge(); |
77 | | - while (newChallenge.id === challenge.id) { |
78 | | - newChallenge.id = loadRandomChallenge().id; |
79 | | - } |
80 | | - |
81 | | - setChallenge(newChallenge); |
82 | | - setShowSuccessFlash(false); |
83 | | - }, 1000); |
84 | | - } |
85 | | - }, [codeBlocks]); |
86 | | - |
87 | | - const handleReset = () => { |
88 | | - const newChallenge = loadRandomChallenge(); |
89 | | - setChallenge(newChallenge); |
90 | | - setGameState({ |
91 | | - timeRemaining: 180, |
92 | | - score: 0, |
93 | | - gameActive: true, |
94 | | - gameOver: false, |
95 | | - }); |
96 | | - }; |
| 10 | +const SparkRushGame = () => { |
| 11 | + const { gameState, challenge, showTargetFlash, showSuccessOverlay } = |
| 12 | + useSparkRush(); |
97 | 13 |
|
98 | 14 | return ( |
99 | | - <> |
100 | | - <div className="w-full h-screen bg-slate-950 font-sans overflow-hidden"> |
101 | | - {gameState.gameOver && ( |
102 | | - <GameOverModal score={gameState.score} onReset={handleReset} /> |
103 | | - )} |
| 15 | + <div |
| 16 | + className={`w-full h-screen font-sans overflow-hidden flex flex-col transition-all duration-200 ${ |
| 17 | + showSuccessOverlay ? "bg-yellow-100" : "bg-white" |
| 18 | + }`} |
| 19 | + > |
| 20 | + {/* score overlay */} |
| 21 | + <div |
| 22 | + className={`absolute inset-0 bg-opacity-50 flex items-center justify-center z-50 transition-all duration-500 pointer-events-none ${ |
| 23 | + showSuccessOverlay ? "visible opacity-100" : "invisible opacity-0" |
| 24 | + } flex flex-col gap-8`} |
| 25 | + > |
| 26 | + <div className="text-yellow-300 drop-shadow-sm drop-shadow-black text-9xl font-bold "> |
| 27 | + {gameState.score} |
| 28 | + </div> |
| 29 | + <div className="text-yellow-300 drop-shadow-sm drop-shadow-black text-3xl font-bold "> |
| 30 | + {`Time : ${formatTime(gameState.timeRemaining)}`} |
| 31 | + </div> |
| 32 | + </div> |
| 33 | + |
| 34 | + {/* countdown overlay */} |
| 35 | + <div |
| 36 | + className={`absolute inset-0 bg-opacity-50 flex items-center justify-center z-50 transition-all duration-500 pointer-events-none ${ |
| 37 | + gameState.timeRemaining <= 5 && !showSuccessOverlay? "visible opacity-100" : "invisible opacity-0" |
| 38 | + } flex flex-col gap-8`} |
| 39 | + > |
| 40 | + <div className="text-yellow-300 drop-shadow-sm drop-shadow-black text-9xl font-bold "> |
| 41 | + {gameState.timeRemaining} |
| 42 | + </div> |
| 43 | + </div> |
104 | 44 |
|
105 | | - <div className="grid grid-cols-2 h-full p-6 gap-6"> |
| 45 | + {gameState.gameOver && <GameOverModal />} |
| 46 | + |
| 47 | + {/* Header */} |
| 48 | + <header className="flex items-center justify-between p-4 border-b border-gray-200"> |
| 49 | + <h1 className="text-2xl font-bold"> |
| 50 | + <span style={{ color: "var(--google-blue)" }}>S</span> |
| 51 | + <span style={{ color: "var(--google-red)" }}>p</span> |
| 52 | + <span style={{ color: "var(--google-yellow)" }}>a</span> |
| 53 | + <span style={{ color: "var(--google-green)" }}>r</span> |
| 54 | + <span style={{ color: "var(--google-blue)" }}>k</span> |
| 55 | + <span style={{ color: "var(--foreground)" }}> </span> |
| 56 | + <span style={{ color: "var(--google-red)" }}>R</span> |
| 57 | + <span style={{ color: "var(--google-yellow)" }}>u</span> |
| 58 | + <span style={{ color: "var(--google-green)" }}>s</span> |
| 59 | + <span style={{ color: "var(--google-blue)" }}>h</span> |
| 60 | + </h1> |
| 61 | + <div className="flex items-center gap-6"> |
106 | 62 | <div |
107 | | - className={`flex flex-col ${ |
108 | | - showSuccessFlash ? "bg-green-400/20" : "bg-slate-900/40" |
109 | | - } w-full p-6 rounded-lg border-slate-800 border `} |
| 63 | + className={`text-2xl font-semibold transition-colors flex items-center gap-2 ${ |
| 64 | + gameState.timeRemaining <= 10 |
| 65 | + ? "text-red-500 animate-pulse" |
| 66 | + : "text-gray-600" |
| 67 | + }`} |
110 | 68 | > |
111 | | - <div className="mb-6"> |
112 | | - <h1 className="text-4xl font-bold text-slate-200 mb-2"> |
113 | | - Spark Rush |
114 | | - </h1> |
115 | | - <div className="flex gap-8 items-center"> |
116 | | - <div |
117 | | - className={`text-xl font-semibold ${ |
118 | | - gameState.timeRemaining <= 10 |
119 | | - ? "text-orange-400 animate-pulse" |
120 | | - : "text-slate-300" |
121 | | - }`} |
122 | | - > |
123 | | - ⏱ {formatTime(gameState.timeRemaining)} |
124 | | - </div> |
125 | | - <div className="text-xl font-semibold text-slate-300"> |
126 | | - ✓ {gameState.score} |
127 | | - </div> |
128 | | - </div> |
129 | | - </div> |
| 69 | + <span className="text-2xl">⏱</span>{" "} |
| 70 | + {formatTime(gameState.timeRemaining)} |
| 71 | + </div> |
| 72 | + <div className="text-2xl font-semibold text-green-500 flex items-center gap-2"> |
| 73 | + <span className="text-2xl">✓</span> {gameState.score} |
| 74 | + </div> |
| 75 | + </div> |
| 76 | + </header> |
| 77 | + |
| 78 | + {/* Main Content */} |
| 79 | + <main className="flex-1 grid grid-cols-2 gap-8 p-8 overflow-hidden"> |
| 80 | + {/* Left Column */} |
| 81 | + <div className="flex flex-col gap-8"> |
| 82 | + {/* Challenge Info */} |
| 83 | + <div className="flex-shrink-0 p-6 rounded-xl border-2 border-gray-200 bg-white shadow-sm"> |
| 84 | + <h2 |
| 85 | + className="text-3xl font-bold mb-2" |
| 86 | + style={{ color: "var(--google-blue)" }} |
| 87 | + > |
| 88 | + {challenge.title} |
| 89 | + </h2> |
| 90 | + <p className="text-gray-600 text-lg">{challenge.description}</p> |
| 91 | + </div> |
130 | 92 |
|
131 | | - <CodeSpace codeBlocks={codeBlocks} setCodeBlocks={setCodeBlocks} /> |
| 93 | + {/* Code Space */} |
| 94 | + <div |
| 95 | + className={`flex-1 flex flex-col transition-all duration-300 rounded-xl border-2 ${"bg-gray-50 border-gray-200"}`} |
| 96 | + > |
| 97 | + <CodeSpace /> |
132 | 98 | </div> |
| 99 | + </div> |
133 | 100 |
|
134 | | - <PreviewPane |
135 | | - codeBlocks={codeBlocks} |
136 | | - targetHtml={generateHtml(challenge.codeBlocks)} |
137 | | - showTargetFlash={showTargetFlash} |
138 | | - /> |
| 101 | + {/* Right Column (Preview) */} |
| 102 | + <div className="flex-1 flex flex-col"> |
| 103 | + <PreviewPane showTargetFlash={showTargetFlash} /> |
139 | 104 | </div> |
140 | | - </div> |
141 | | - </> |
| 105 | + </main> |
| 106 | + </div> |
| 107 | + ); |
| 108 | +}; |
| 109 | + |
| 110 | +export const SparkRush = () => { |
| 111 | + return ( |
| 112 | + <SparkRushProvider> |
| 113 | + <SparkRushGame /> |
| 114 | + </SparkRushProvider> |
142 | 115 | ); |
143 | 116 | }; |
0 commit comments