diff --git a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx index 7fc3b4a..dfa1922 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/exercise/exercise.tsx @@ -1,111 +1,188 @@ -import { ChangeEvent, useState } from 'react'; -import { TextFieldProps, TextFieldComponent } from '../components'; +import { useState } from 'react'; +import { Button } from '@shared/components/Button/Button.component'; /* * Observations * 💅 The current implementation uses the Render Props Pattern - * Don't worry about the UI in the components file. + * We need to refactor this into a custom hook for Pokemon capture mechanics */ -interface IFieldProps { +interface IPokemonCaptureProps { + area: string; + children: (captureState: { + wildPokemon: Pokemon | null; + pokeballs: number; + capturing: boolean; + capturedPokemon: Pokemon[]; + attemptCapture: () => void; + encounterPokemon: () => void; + runAway: () => void; + restockPokeballs: (amount?: number) => void; + }) => React.ReactNode; +} + +interface Pokemon { + id: number; name: string; - validate?: (value: string) => boolean; - required?: boolean; - - // 1B 💣 - remove these four params and references in the function. - id: string; - label: string; - errorMessage?: string; - children: (props: TextFieldProps) => React.ReactNode; + type: string; + captureRate: number; } -const validateTextString = (value: string) => - value.trim().length === 0; - -// 1A 👨🏻‍💻 - We need to refactor this to be called useField -export const Field = ({ - name, - required, - validate, - // 1B 💣 - remove these four params and references in the function. - label, - id, - errorMessage, +const WILD_POKEMON = [ + { id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 }, + { id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 }, + { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 } +]; + +// 1A 👨🏻💻 - We need to refactor this to be called usePokemonCapture +export const PokemonCaptureSystem = ({ children -}: IFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required && validate) { - setHasError(validate(event.target.value)); - } +}: IPokemonCaptureProps) => { + const [wildPokemon, setWildPokemon] = useState( + null + ); + const [pokeballs, setPokeballs] = useState(10); + const [capturing, setCapturing] = useState(false); + const [capturedPokemon, setCapturedPokemon] = useState( + [] + ); - setValue(event.target.value!); + const encounterPokemon = () => { + const randomPokemon = + WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)]; + setWildPokemon(randomPokemon); }; - const onFocus = () => { - if (isTouched) { - setHasError(false); + const attemptCapture = async () => { + if (!wildPokemon || pokeballs <= 0) return; + + setCapturing(true); + setPokeballs((prev) => prev - 1); + + // Simulate capture attempt + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const success = Math.random() < wildPokemon.captureRate; + if (success) { + setCapturedPokemon((prev) => [...prev, wildPokemon]); + setWildPokemon(null); } - setIsTouched(true); + setCapturing(false); }; - const onBlur = () => { - if (value && validate && validate(value)) { - setHasError(true); - } + const runAway = () => { + setWildPokemon(null); + }; + + const restockPokeballs = (amount: number = 5) => { + setPokeballs(prev => prev + amount); }; - // 1C 👨🏻‍💻 - Just return the object instead of children. + // 1C 👨🏻💻 - Just return the object instead of children return children({ - // 1D 👨🏻‍💻 - move name into input - name, - input: { - required, - onBlur, - onFocus, - onChange - }, - hasError, - // 1B 💣 - remove these three params and references in the function. - label, - id, - errorMessage + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + attemptCapture, + encounterPokemon, + runAway, + restockPokeballs }); }; -// 2A 🤔 - What if we wanted to make multiple Fields? Our current solution would -// require us to call useField multiple times in the same component. Let's refactor -// what we have done into a field component which uses IFieldProps as params. +// 2A 🤔 - What if we wanted to use this capture logic in multiple components? +// Let's make a component which uses the usePokemonCapture hook and takes an area prop export const Exercise = () => { - // 1E 👨🏻‍💻 - call the useField and pass the { name: "input", validate: validateTextString, required: true } + // 1E 👨🏻💻 - call the usePokemonCapture hook here return ( -
- {/* 1F 💣 - Remove the Field component and pull the values required for TextFieldComponent to run */} - - {({ name, label, id, errorMessage, hasError, input }) => ( - +
+

+ 🌿 Pokemon Capture System +

+ + {/* 1F 💣 - Remove the PokemonCaptureSystem component and use the hook directly */} + + {({ + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + attemptCapture, + encounterPokemon, + runAway, + restockPokeballs + }) => ( +
+
+
+

Pokeballs: {pokeballs} 🔴

+

+ Captured: {capturedPokemon.length} Pokemon +

+
+ +
+ + {!wildPokemon ? ( +
+

No wild Pokemon in sight...

+ +
+ ) : ( +
+

+ A wild {wildPokemon.name} appeared! ⚡ +

+

+ Type: {wildPokemon.type} | Capture Rate:{' '} + {(wildPokemon.captureRate * 100).toFixed(0)}% +

+ + {capturing ? ( +
+

+ 🔴 Pokeball is shaking... +

+
+ ) : ( +
+ + +
+ )} +
+ )} + + {capturedPokemon.length > 0 && ( +
+

Captured Pokemon:

+
+ {capturedPokemon.map((pokemon, index) => ( + + {pokemon.name} + + ))} +
+
+ )} +
)} - - +
+
); }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx b/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx index ec340b2..af92980 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx +++ b/src/course/02-lessons/01-Bronze/Hooks/final/final.tsx @@ -1,85 +1,185 @@ -import { ChangeEvent, useState } from 'react'; -import { TextFieldComponent } from '../components'; +/* eslint-disable react-refresh/only-export-components */ +import { useState } from 'react'; +import { Button } from '@shared/components/Button/Button.component'; -interface IFieldProps { +interface Pokemon { + id: number; name: string; - required?: boolean; - validate?: (value: string) => boolean; + type: string; + captureRate: number; } -const validateTextString = (value: string) => - value.trim().length === 0; - -export const useField = ({ - name, - required, - validate -}: IFieldProps) => { - const [value, setValue] = useState(''); - const [hasError, setHasError] = useState(false); - const [isTouched, setIsTouched] = useState(false); - - const onChange = (event: ChangeEvent) => { - if (required && validate) { - setHasError(validate(event.target.value)); - } +interface UsePokemonCaptureProps { + area: string; + initialPokeballs?: number; +} + +const WILD_POKEMON = [ + { id: 1, name: 'Pidgey', type: 'Flying', captureRate: 0.8 }, + { id: 2, name: 'Rattata', type: 'Normal', captureRate: 0.9 }, + { id: 3, name: 'Pikachu', type: 'Electric', captureRate: 0.3 }, + { id: 4, name: 'Caterpie', type: 'Bug', captureRate: 0.95 }, + { id: 5, name: 'Weedle', type: 'Bug/Poison', captureRate: 0.95 } +]; - setValue(event.target.value!); +export const usePokemonCapture = ({ + initialPokeballs = 10 +}: UsePokemonCaptureProps) => { + const [wildPokemon, setWildPokemon] = useState( + null + ); + const [pokeballs, setPokeballs] = useState(initialPokeballs); + const [capturing, setCapturing] = useState(false); + const [capturedPokemon, setCapturedPokemon] = useState( + [] + ); + + const encounterPokemon = () => { + const randomPokemon = + WILD_POKEMON[Math.floor(Math.random() * WILD_POKEMON.length)]; + setWildPokemon(randomPokemon); }; - const onFocus = () => { - if (isTouched) { - setHasError(false); + const attemptCapture = async () => { + if (!wildPokemon || pokeballs <= 0) return; + + setCapturing(true); + setPokeballs((prev) => prev - 1); + + // Simulate capture attempt with animation delay + await new Promise((resolve) => setTimeout(resolve, 1500)); + + const success = Math.random() < wildPokemon.captureRate; + if (success) { + setCapturedPokemon((prev) => [...prev, wildPokemon]); + setWildPokemon(null); } - setIsTouched(true); + setCapturing(false); }; - const onBlur = () => { - if (value && validate && validate(value)) { - setHasError(true); - } + const runAway = () => { + setWildPokemon(null); + }; + + const restockPokeballs = (amount: number = 5) => { + setPokeballs((prev) => prev + amount); }; return { - hasError, - input: { - name, - required, - onBlur, - onFocus, - onChange - } + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + encounterPokemon, + attemptCapture, + runAway, + restockPokeballs }; }; -const Field = ({ required, validate, name }: IFieldProps) => { - const { hasError, input } = useField({ - name: 'input', - validate, - required - }); +const PokemonCaptureInterface = ({ area }: { area: string }) => { + const { + wildPokemon, + pokeballs, + capturing, + capturedPokemon, + encounterPokemon, + attemptCapture, + runAway, + restockPokeballs + } = usePokemonCapture({ area }); return ( - +
+

🌿 {area} Area

+ +
+
+

Pokeballs: {pokeballs} 🔴

+

+ Captured: {capturedPokemon.length} Pokemon +

+
+ +
+ + {!wildPokemon ? ( +
+

No wild Pokemon in sight...

+ +
+ ) : ( +
+

+ A wild {wildPokemon.name} appeared! ⚡ +

+

+ Type: {wildPokemon.type} | Capture Rate:{' '} + {(wildPokemon.captureRate * 100).toFixed(0)}% +

+ + {capturing ? ( +
+

+ 🔴 Pokeball is shaking... +

+
+ ) : ( +
+ + +
+ )} +
+ )} + + {capturedPokemon.length > 0 && ( +
+

Captured Pokemon:

+
+ {capturedPokemon.map((pokemon, index) => ( + + {pokemon.name} ({pokemon.type}) + + ))} +
+
+ )} +
); }; export const Final = () => { return ( -
- - +
+

+ 🎯 Pokemon Capture System +

+

+ Using the usePokemonCapture() hook to manage capture mechanics + across different areas. +

+ +
+ + +
+
); }; diff --git a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx index aba50c0..8a760f4 100644 --- a/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx +++ b/src/course/02-lessons/01-Bronze/Hooks/lesson.mdx @@ -10,49 +10,51 @@ Additionally, Hooks enhance reusability and reduce prop drilling. Custom Hooks a ## Before Hooks -Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that seperation of concerns. +Before hooks we used to have to make class components which enforced a lot of prop drilling into components and a lot more complicated setup for state management. The Presentational & Container pattern was used a lot more back when we were using class components and that was purely for that separation of concerns. ```jsx import React, { Component } from 'react'; -class Profile extends Component { +class PokemonCapture extends Component { constructor(props) { super(props); this.state = { - loading: false, - user: {} + capturing: false, + wildPokemon: null, + pokeballs: 10, + capturedPokemon: [] }; } componentDidMount() { - this.updateProfile(this.props.id); + this.encounterWildPokemon(this.props.area); } componentDidUpdate(prevProps) { - if (prevProps.id !== this.props.id) { - this.updateProfile(this.props.id); + if (prevProps.area !== this.props.area) { + this.encounterWildPokemon(this.props.area); } } componentWillUnmount() { - // do some unmounting actions + // cleanup capture animations } - fetchUser(id) { - // fetch users logic here + encounterWildPokemon(area) { + // find random pokemon in area } - async updateProfile(id) { - this.setState({ loading: true }); - // fetch users data - await this.fetchUser(id); - this.setState({ loading: false }); + async attemptCapture(pokemon) { + this.setState({ capturing: true }); + // capture logic with success rate + await this.throwPokeball(pokemon); + this.setState({ capturing: false }); } render() { - // ... some jsx + // ... pokemon capture jsx } } -export default Profile; +export default PokemonCapture; ``` ## With Hooks @@ -60,36 +62,40 @@ export default Profile; With Hooks, functional components can handle both the presentational aspects and the business logic. This means you no longer need to separate your components strictly into presentational and container types. ```jsx -import React from 'react'; +import React, { useState, useEffect } from 'react'; -const Profile = ({ id }) => { - const [isLoading, setIsLoading] = useState(false); - const [user, setUser] = useState({}); +const PokemonCapture = ({ area }) => { + const [capturing, setCapturing] = useState(false); + const [wildPokemon, setWildPokemon] = useState(null); + const [pokeballs, setPokeballs] = useState(10); + const [capturedPokemon, setCapturedPokemon] = useState([]); useEffect(() => { - updateProfile(id); - }, [id]); + encounterWildPokemon(area); + }, [area]); - const fetchUser = (id) => { - // fetch users logic here + const encounterWildPokemon = (area) => { + // find random pokemon in area }; - const updateProfile = async (id) => { - setIsLoading(true); - // fetch users data - await fetchUser(id); - setIsLoading(false); + const attemptCapture = async (pokemon) => { + setCapturing(true); + // capture logic with success rate + await throwPokeball(pokemon); + setCapturing(false); }; - return { - // Some jsx code - }; + return ( + // Pokemon capture interface jsx + ); }; ``` ## Exercise -In this lesson we are going to refactor the Field component that we made into a react hook instead of following the render props pattern. +In this lesson we are going to refactor the Pokemon capture component into a custom React hook called **usePokemonCapture()** instead of following the render props pattern. + +You'll create a hook that manages capture attempts, pokeball inventory, success rates, and wild Pokemon encounters. Head over to the exercise and let's get started. diff --git a/src/shared/components/Button/Button.component.tsx b/src/shared/components/Button/Button.component.tsx index 04f4b45..4b84895 100644 --- a/src/shared/components/Button/Button.component.tsx +++ b/src/shared/components/Button/Button.component.tsx @@ -4,6 +4,7 @@ import { HTMLAttributes } from 'react'; interface IButton extends HTMLAttributes { className?: string; children: React.ReactNode | React.ReactNode[]; + disabled?: boolean; } const buttonClasses =