Skip to content

Commit d3b3fe5

Browse files
feat: implement the FACC
1 parent 9556cf6 commit d3b3fe5

File tree

6 files changed

+453
-2
lines changed

6 files changed

+453
-2
lines changed

src/course/02-lessons/02-Silver/Controlled/exercise/exercise.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ const EvolutionModal = ({
9595
{/* ♿️ Another requirement is to return focus to the actioner, but FocusLock does that for us when this component unmounts! 🦸🏻♀️ */}
9696
<FocusLock returnFocus={true}>
9797
<div>
98-
{/* 2f - 👨🏻💻♿️ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */}
98+
{/* 2f - 💻 ♿️ Add id={`evolution_title_${id}`} - this creates the relationship between the title and modal */}
9999
<h2 className="text-2xl font-bold text-center mb-4 text-blue-800">
100100
✨ Evolution Time! ✨
101101
</h2>
@@ -141,7 +141,7 @@ const EvolutionModal = ({
141141
</Button>
142142
</div>
143143
</div>
144-
{/* 2i - 👨🏻💻♿️ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */}
144+
{/* 2i - 💻 ♿️ Add id={`evolution_body_${id}`} - this creates the relationship between the content and modal */}
145145
<div className="mt-4 text-center text-sm text-gray-600">
146146
Your {pokemon.name} is ready to evolve into{' '}
147147
{evolution.name}!
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Exercise } from './exercise';
3+
4+
const meta: Meta<typeof Exercise> = {
5+
title: 'Lessons/🥈 Silver/🎯 FACC Pattern/Exercise',
6+
component: Exercise,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { useState } from 'react';
2+
3+
interface IPokemon {
4+
name: string;
5+
hp: number;
6+
maxHp: number;
7+
attack: number;
8+
}
9+
10+
interface IBattleState {
11+
playerPokemon: IPokemon;
12+
enemyPokemon: IPokemon;
13+
turn: 'player' | 'enemy';
14+
battleLog: string[];
15+
winner: string | null;
16+
}
17+
18+
/*
19+
* Observations
20+
* 💅 Battle logic is tightly coupled with specific battle display
21+
* The UI is hardcoded within the battle simulator
22+
23+
* Tasks
24+
* 1A 💻 - Refactor to use FACC pattern by adding children prop:
25+
* children: (battleState: IBattleState, actions: IBattleActions) => React.ReactNode;
26+
*
27+
* 1B 💻 - Create IBattleActions interface with:
28+
* attack: () => void;
29+
* resetBattle: () => void;
30+
*
31+
* 1C 💻 - Replace the JSX return with children function call
32+
* 1D 💻 - In Exercise component, use FACC to render battle display
33+
*/
34+
35+
export const PokemonBattleSimulator = () => {
36+
const [battleState, setBattleState] = useState<IBattleState>({
37+
playerPokemon: {
38+
name: 'Charizard',
39+
hp: 100,
40+
maxHp: 100,
41+
attack: 25
42+
},
43+
enemyPokemon: {
44+
name: 'Blastoise',
45+
hp: 100,
46+
maxHp: 100,
47+
attack: 20
48+
},
49+
turn: 'player',
50+
battleLog: [],
51+
winner: null
52+
});
53+
54+
const attack = () => {
55+
if (battleState.winner) return;
56+
57+
setBattleState((prev) => {
58+
const newState = { ...prev };
59+
60+
if (prev.turn === 'player') {
61+
const damage = prev.playerPokemon.attack;
62+
newState.enemyPokemon.hp = Math.max(
63+
0,
64+
prev.enemyPokemon.hp - damage
65+
);
66+
newState.battleLog = [
67+
...prev.battleLog,
68+
`${prev.playerPokemon.name} attacks for ${damage} damage!`
69+
];
70+
71+
if (newState.enemyPokemon.hp === 0) {
72+
newState.winner = prev.playerPokemon.name;
73+
} else {
74+
newState.turn = 'enemy';
75+
}
76+
} else {
77+
const damage = prev.enemyPokemon.attack;
78+
newState.playerPokemon.hp = Math.max(
79+
0,
80+
prev.playerPokemon.hp - damage
81+
);
82+
newState.battleLog = [
83+
...prev.battleLog,
84+
`${prev.enemyPokemon.name} attacks for ${damage} damage!`
85+
];
86+
87+
if (newState.playerPokemon.hp === 0) {
88+
newState.winner = prev.enemyPokemon.name;
89+
} else {
90+
newState.turn = 'player';
91+
}
92+
}
93+
94+
return newState;
95+
});
96+
};
97+
98+
const resetBattle = () => {
99+
setBattleState({
100+
playerPokemon: {
101+
name: 'Charizard',
102+
hp: 100,
103+
maxHp: 100,
104+
attack: 25
105+
},
106+
enemyPokemon: {
107+
name: 'Blastoise',
108+
hp: 100,
109+
maxHp: 100,
110+
attack: 20
111+
},
112+
turn: 'player',
113+
battleLog: [],
114+
winner: null
115+
});
116+
};
117+
118+
return (
119+
<div className="bg-blue-50 p-6 rounded-lg max-w-2xl">
120+
<h2 className="text-2xl font-bold mb-4">Pokemon Battle</h2>
121+
122+
<div className="grid grid-cols-2 gap-4 mb-4">
123+
<div className="bg-green-100 p-4 rounded">
124+
<h3 className="font-bold">
125+
{battleState.playerPokemon.name}
126+
</h3>
127+
<div className="w-full bg-gray-200 rounded-full h-2">
128+
<div
129+
className="bg-green-500 h-2 rounded-full"
130+
style={{
131+
width: `${(battleState.playerPokemon.hp / battleState.playerPokemon.maxHp) * 100}%`
132+
}}
133+
/>
134+
</div>
135+
<p>
136+
{battleState.playerPokemon.hp}/
137+
{battleState.playerPokemon.maxHp} HP
138+
</p>
139+
</div>
140+
141+
<div className="bg-red-100 p-4 rounded">
142+
<h3 className="font-bold">
143+
{battleState.enemyPokemon.name}
144+
</h3>
145+
<div className="w-full bg-gray-200 rounded-full h-2">
146+
<div
147+
className="bg-red-500 h-2 rounded-full"
148+
style={{
149+
width: `${(battleState.enemyPokemon.hp / battleState.enemyPokemon.maxHp) * 100}%`
150+
}}
151+
/>
152+
</div>
153+
<p>
154+
{battleState.enemyPokemon.hp}/
155+
{battleState.enemyPokemon.maxHp} HP
156+
</p>
157+
</div>
158+
</div>
159+
160+
<div className="mb-4">
161+
<button
162+
onClick={attack}
163+
disabled={!!battleState.winner}
164+
className="bg-blue-500 text-white px-4 py-2 rounded mr-2 disabled:opacity-50"
165+
>
166+
Attack
167+
</button>
168+
<button
169+
onClick={resetBattle}
170+
className="bg-gray-500 text-white px-4 py-2 rounded"
171+
>
172+
Reset
173+
</button>
174+
</div>
175+
176+
{battleState.winner && (
177+
<div className="bg-yellow-100 p-4 rounded mb-4">
178+
<h3 className="font-bold">{battleState.winner} wins!</h3>
179+
</div>
180+
)}
181+
182+
<div className="bg-white p-4 rounded max-h-32 overflow-y-auto">
183+
{battleState.battleLog.map((log, index) => (
184+
<p key={index} className="text-sm">
185+
{log}
186+
</p>
187+
))}
188+
</div>
189+
</div>
190+
);
191+
};
192+
193+
export const Exercise = () => {
194+
return <PokemonBattleSimulator />;
195+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Final } from './final';
3+
4+
const meta: Meta<typeof Final> = {
5+
title: 'Lessons/🥈 Silver/🎯 FACC Pattern/Final',
6+
component: Final,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};

0 commit comments

Comments
 (0)