Skip to content

Commit 8fe3bda

Browse files
feat: implement react hooks rebrand (#46)
1 parent 5badabf commit 8fe3bda

File tree

4 files changed

+359
-175
lines changed

4 files changed

+359
-175
lines changed
Lines changed: 159 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,188 @@
1-
import { ChangeEvent, useState } from 'react';
2-
import { TextFieldProps, TextFieldComponent } from '../components';
1+
import { useState } from 'react';
2+
import { Button } from '@shared/components/Button/Button.component';
33

44
/*
55
* Observations
66
* 💅 The current implementation uses the Render Props Pattern
7-
* Don't worry about the UI in the components file.
7+
* We need to refactor this into a custom hook for Pokemon capture mechanics
88
*/
99

10-
interface IFieldProps {
10+
interface IPokemonCaptureProps {
11+
area: string;
12+
children: (captureState: {
13+
wildPokemon: Pokemon | null;
14+
pokeballs: number;
15+
capturing: boolean;
16+
capturedPokemon: Pokemon[];
17+
attemptCapture: () => void;
18+
encounterPokemon: () => void;
19+
runAway: () => void;
20+
restockPokeballs: (amount?: number) => void;
21+
}) => React.ReactNode;
22+
}
23+
24+
interface Pokemon {
25+
id: number;
1126
name: string;
12-
validate?: (value: string) => boolean;
13-
required?: boolean;
14-
15-
// 1B 💣 - remove these four params and references in the function.
16-
id: string;
17-
label: string;
18-
errorMessage?: string;
19-
children: (props: TextFieldProps) => React.ReactNode;
27+
type: string;
28+
captureRate: number;
2029
}
2130

22-
const validateTextString = (value: string) =>
23-
value.trim().length === 0;
24-
25-
// 1A 👨🏻‍💻 - We need to refactor this to be called useField
26-
export const Field = ({
27-
name,
28-
required,
29-
validate,
30-
// 1B 💣 - remove these four params and references in the function.
31-
label,
32-
id,
33-
errorMessage,
31+
const WILD_POKEMON = [
32+
{ id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 },
33+
{ id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 },
34+
{ id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 }
35+
];
36+
37+
// 1A 👨🏻💻 - We need to refactor this to be called usePokemonCapture
38+
export const PokemonCaptureSystem = ({
3439
children
35-
}: IFieldProps) => {
36-
const [value, setValue] = useState('');
37-
const [hasError, setHasError] = useState(false);
38-
const [isTouched, setIsTouched] = useState(false);
39-
40-
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
41-
if (required && validate) {
42-
setHasError(validate(event.target.value));
43-
}
40+
}: IPokemonCaptureProps) => {
41+
const [wildPokemon, setWildPokemon] = useState<Pokemon | null>(
42+
null
43+
);
44+
const [pokeballs, setPokeballs] = useState(10);
45+
const [capturing, setCapturing] = useState(false);
46+
const [capturedPokemon, setCapturedPokemon] = useState<Pokemon[]>(
47+
[]
48+
);
4449

45-
setValue(event.target.value!);
50+
const encounterPokemon = () => {
51+
const randomPokemon =
52+
WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)];
53+
setWildPokemon(randomPokemon);
4654
};
4755

48-
const onFocus = () => {
49-
if (isTouched) {
50-
setHasError(false);
56+
const attemptCapture = async () => {
57+
if (!wildPokemon || pokeballs <= 0) return;
58+
59+
setCapturing(true);
60+
setPokeballs((prev) => prev - 1);
61+
62+
// Simulate capture attempt
63+
await new Promise((resolve) => setTimeout(resolve, 1500));
64+
65+
const success = Math.random() < wildPokemon.captureRate;
66+
if (success) {
67+
setCapturedPokemon((prev) => [...prev, wildPokemon]);
68+
setWildPokemon(null);
5169
}
5270

53-
setIsTouched(true);
71+
setCapturing(false);
5472
};
5573

56-
const onBlur = () => {
57-
if (value && validate && validate(value)) {
58-
setHasError(true);
59-
}
74+
const runAway = () => {
75+
setWildPokemon(null);
76+
};
77+
78+
const restockPokeballs = (amount: number = 5) => {
79+
setPokeballs(prev => prev + amount);
6080
};
6181

62-
// 1C 👨🏻💻 - Just return the object instead of children.
82+
// 1C 👨🏻💻 - Just return the object instead of children
6383
return children({
64-
// 1D 👨🏻‍💻 - move name into input
65-
name,
66-
input: {
67-
required,
68-
onBlur,
69-
onFocus,
70-
onChange
71-
},
72-
hasError,
73-
// 1B 💣 - remove these three params and references in the function.
74-
label,
75-
id,
76-
errorMessage
84+
wildPokemon,
85+
pokeballs,
86+
capturing,
87+
capturedPokemon,
88+
attemptCapture,
89+
encounterPokemon,
90+
runAway,
91+
restockPokeballs
7792
});
7893
};
7994

80-
// 2A 🤔 - What if we wanted to make multiple Fields? Our current solution would
81-
// require us to call useField multiple times in the same component. Let's refactor
82-
// what we have done into a field component which uses IFieldProps as params.
95+
// 2A 🤔 - What if we wanted to use this capture logic in multiple components?
96+
// Let's make a component which uses the usePokemonCapture hook and takes an area prop
8397

8498
export const Exercise = () => {
85-
// 1E 👨🏻💻 - call the useField and pass the { name: "input", validate: validateTextString, required: true }
99+
// 1E 👨🏻💻 - call the usePokemonCapture hook here
86100
return (
87-
<form noValidate name="form">
88-
{/* 1F 💣 - Remove the Field component and pull the values required for TextFieldComponent to run */}
89-
<Field
90-
name="input"
91-
id="input"
92-
label="Enter your name"
93-
required
94-
errorMessage="Please enter your name"
95-
validate={validateTextString}
96-
>
97-
{({ name, label, id, errorMessage, hasError, input }) => (
98-
<TextFieldComponent
99-
// This will be input.name now.
100-
name={name}
101-
label={label}
102-
id={id}
103-
errorMessage={errorMessage}
104-
hasError={hasError}
105-
input={input}
106-
/>
101+
<div className="p-6 bg-green-50 rounded-lg">
102+
<h2 className="text-2xl font-bold mb-4">
103+
🌿 Pokemon Capture System
104+
</h2>
105+
106+
{/* 1F 💣 - Remove the PokemonCaptureSystem component and use the hook directly */}
107+
<PokemonCaptureSystem area="tall-grass">
108+
{({
109+
wildPokemon,
110+
pokeballs,
111+
capturing,
112+
capturedPokemon,
113+
attemptCapture,
114+
encounterPokemon,
115+
runAway,
116+
restockPokeballs
117+
}) => (
118+
<div>
119+
<div className="mb-4 flex justify-between items-center">
120+
<div>
121+
<p className="text-lg">Pokeballs: {pokeballs} 🔴</p>
122+
<p className="text-sm text-gray-600">
123+
Captured: {capturedPokemon.length} Pokemon
124+
</p>
125+
</div>
126+
<Button onClick={() => restockPokeballs()} disabled={pokeballs >= 20}>
127+
🛍️ Buy Pokeballs (+5)
128+
</Button>
129+
</div>
130+
131+
{!wildPokemon ? (
132+
<div className="text-center">
133+
<p className="mb-4">No wild Pokemon in sight...</p>
134+
<Button onClick={encounterPokemon}>
135+
🔍 Search for Pokemon
136+
</Button>
137+
</div>
138+
) : (
139+
<div className="bg-white p-4 rounded-lg border-2 border-dashed border-green-300">
140+
<h3 className="text-xl font-bold text-green-700">
141+
A wild {wildPokemon.name} appeared! ⚡
142+
</h3>
143+
<p className="text-gray-600 mb-4">
144+
Type: {wildPokemon.type} | Capture Rate:{' '}
145+
{(wildPokemon.captureRate * 100).toFixed(0)}%
146+
</p>
147+
148+
{capturing ? (
149+
<div className="text-center">
150+
<p className="text-lg animate-pulse">
151+
🔴 Pokeball is shaking...
152+
</p>
153+
</div>
154+
) : (
155+
<div className="flex gap-2">
156+
<Button
157+
onClick={attemptCapture}
158+
disabled={pokeballs <= 0}
159+
>
160+
🔴 Throw Pokeball ({pokeballs} left)
161+
</Button>
162+
<Button onClick={runAway}>🏃 Run Away</Button>
163+
</div>
164+
)}
165+
</div>
166+
)}
167+
168+
{capturedPokemon.length > 0 && (
169+
<div className="mt-4">
170+
<h4 className="font-bold mb-2">Captured Pokemon:</h4>
171+
<div className="flex gap-2 flex-wrap">
172+
{capturedPokemon.map((pokemon, index) => (
173+
<span
174+
key={index}
175+
className="bg-blue-100 px-2 py-1 rounded text-sm"
176+
>
177+
{pokemon.name}
178+
</span>
179+
))}
180+
</div>
181+
</div>
182+
)}
183+
</div>
107184
)}
108-
</Field>
109-
</form>
185+
</PokemonCaptureSystem>
186+
</div>
110187
);
111188
};

0 commit comments

Comments
 (0)