|
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'; |
3 | 3 |
|
4 | 4 | /* |
5 | 5 | * Observations |
6 | 6 | * 💅 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 |
8 | 8 | */ |
9 | 9 |
|
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; |
11 | 26 | 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; |
20 | 29 | } |
21 | 30 |
|
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 = ({ |
34 | 39 | 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 | + ); |
44 | 49 |
|
45 | | - setValue(event.target.value!); |
| 50 | + const encounterPokemon = () => { |
| 51 | + const randomPokemon = |
| 52 | + WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)]; |
| 53 | + setWildPokemon(randomPokemon); |
46 | 54 | }; |
47 | 55 |
|
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); |
51 | 69 | } |
52 | 70 |
|
53 | | - setIsTouched(true); |
| 71 | + setCapturing(false); |
54 | 72 | }; |
55 | 73 |
|
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); |
60 | 80 | }; |
61 | 81 |
|
62 | | - // 1C 👨🏻💻 - Just return the object instead of children. |
| 82 | + // 1C 👨🏻💻 - Just return the object instead of children |
63 | 83 | 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 |
77 | 92 | }); |
78 | 93 | }; |
79 | 94 |
|
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 |
83 | 97 |
|
84 | 98 | export const Exercise = () => { |
85 | | - // 1E 👨🏻💻 - call the useField and pass the { name: "input", validate: validateTextString, required: true } |
| 99 | + // 1E 👨🏻💻 - call the usePokemonCapture hook here |
86 | 100 | 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> |
107 | 184 | )} |
108 | | - </Field> |
109 | | - </form> |
| 185 | + </PokemonCaptureSystem> |
| 186 | + </div> |
110 | 187 | ); |
111 | 188 | }; |
0 commit comments