Skip to content

Commit 058349f

Browse files
feat: implement render props pokemon pattern
1 parent 8f1e580 commit 058349f

File tree

4 files changed

+230
-202
lines changed

4 files changed

+230
-202
lines changed

src/course/01-introduction/01-Welcome.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ Each lesson is broken down in an exercise file and a final file. The exercise fi
5050

5151
- [Compound components pattern](?path=/docs/lessons-🥈-Silver-compound-components-pattern-01-lesson--docs)
5252
- [Controlled component pattern](?path=/docs/lessons-🥈-Silver-controlled-components-pattern-01-lesson--docs)
53+
- [FACC pattern](?path=/docs/lessons-🥈-silver-facc-pattern-01-lesson--docs)
54+
- [Render children pattern](?path=/docs/lessons-🥈-silver-render-children-pattern-01-lesson--docs)
5355
- [Render props pattern](?path=/docs/lessons-🥈-Silver-render-props-pattern-01-lesson--docs)
5456
- [The Provider pattern](?path=/docs/lessons-🥈-Silver-provider-pattern-01-lesson--docs)
5557
- [The State Reducer pattern](?path=/docs/lessons-🥈-Silver-state-reducer-pattern-01-lesson--docs)
Lines changed: 83 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,106 @@
1-
import { ChangeEvent, useState } from 'react';
2-
import { Input } from '@shared/components/Input/Input.component';
3-
import { Label } from '@shared/components/Label/Label.component';
4-
import { ErrorMessage } from '@shared/components/ErrorMessage/ErrorMessage.component';
5-
6-
export interface ITextInputFieldProps {
7-
name: string;
8-
id: string;
9-
label: string;
10-
required?: boolean;
11-
errorMessage?: string;
1+
interface ITypeEffectiveness {
2+
attacking: string;
3+
defending: string;
4+
effectiveness: number;
5+
description: string;
126
}
137

148
/*
159
* Observations
16-
* 💅 The current implementation uses the Controlled Component Pattern
17-
* The UI is already split into small components
10+
* 💅 Type effectiveness calculator is tightly coupled with table display
11+
* Logic and presentation are mixed together
1812
1913
* Tasks
20-
* 1A 👨🏻‍💻 - Refactor the UI layer into its own component and setup the interface for its types to be:
21-
* hasError: boolean;
22-
* errorMessage?: string;
23-
* id: string;
24-
* name: string;
25-
* label: string;
26-
* input: HTMLAttributes<HTMLInputElement> & { required?: boolean };
27-
*
28-
* 1B 👨🏻‍💻 - Add these new types to the TextInputField
29-
* validate?: (value: string) => boolean;
30-
* children: (props: ITextFieldProps) => React.ReactNode;
31-
*
32-
* 1C 👨🏻‍💻 - Replace the return of TextInput field with the children prop we have defined.
33-
* 💅 You need to call children and pass down the props you need (the types above are the hint)
14+
* 1A 💻 - Add render prop to IPokemonTypeCalculatorProps:
15+
* render: (effectiveness: ITypeEffectiveness[]) => React.ReactNode;
3416
*
35-
* 1D 👨🏻‍💻 - In the Exercise component you want to add the UI component in as children.
36-
* 💅 - The children should look like this {(props) => <UIComponent {...props} />}
17+
* 1B 💻 - Replace the hardcoded table JSX with render prop call
18+
* 1C 💻 - In Exercise component, use render prop to display results
3719
*/
3820

39-
const validateTextString = (value: string) =>
40-
value.trim().length === 0;
41-
42-
export const TextInputField = ({
43-
name,
44-
label,
45-
id,
46-
required,
47-
errorMessage
48-
}: ITextInputFieldProps) => {
49-
const [value, setValue] = useState('');
50-
const [hasError, setHasError] = useState(false);
51-
const [isTouched, setIsTouched] = useState(false);
52-
53-
const onChange = (event: ChangeEvent<HTMLInputElement>) => {
54-
if (required) {
55-
setHasError(validateTextString(event.target.value));
56-
}
21+
interface IPokemonTypeCalculatorProps {
22+
attackingType: string;
23+
}
5724

58-
setValue(event.target.value);
59-
};
25+
const typeChart: Record<string, Record<string, number>> = {
26+
Fire: { Grass: 2, Water: 0.5, Fire: 0.5, Electric: 1, Ice: 2 },
27+
Water: { Fire: 2, Grass: 0.5, Water: 0.5, Electric: 1, Ice: 1 },
28+
Grass: { Water: 2, Fire: 0.5, Grass: 0.5, Electric: 1, Ice: 1 },
29+
Electric: { Water: 2, Fire: 1, Grass: 0.5, Electric: 0.5, Ice: 1 },
30+
Ice: { Grass: 2, Fire: 0.5, Water: 0.5, Electric: 1, Ice: 0.5 }
31+
};
6032

61-
const onFocus = () => {
62-
if (isTouched) {
63-
setHasError(false);
64-
}
33+
const getEffectivenessDescription = (value: number): string => {
34+
if (value === 2) return 'Super Effective';
35+
if (value === 0.5) return 'Not Very Effective';
36+
return 'Normal Damage';
37+
};
6538

66-
setIsTouched(true);
67-
};
39+
export const PokemonTypeCalculator = ({
40+
attackingType
41+
}: IPokemonTypeCalculatorProps) => {
42+
const defendingTypes = Object.keys(typeChart);
6843

69-
const onBlur = () => {
70-
if (value && validateTextString(value)) {
71-
setHasError(true);
72-
}
73-
};
44+
const effectiveness: ITypeEffectiveness[] = defendingTypes.map(
45+
(defendingType) => ({
46+
attacking: attackingType,
47+
defending: defendingType,
48+
effectiveness: typeChart[attackingType]?.[defendingType] ?? 1,
49+
description: getEffectivenessDescription(
50+
typeChart[attackingType]?.[defendingType] ?? 1
51+
)
52+
})
53+
);
7454

7555
return (
76-
<div className="flex flex-col gap-2">
77-
<Label htmlFor={id}>{label}</Label>
78-
<Input
79-
name={name}
80-
id={id}
81-
required={required}
82-
onChange={onChange}
83-
onFocus={onFocus}
84-
onBlur={onBlur}
85-
hasError={hasError}
86-
/>
87-
{errorMessage && hasError && (
88-
<ErrorMessage message={errorMessage} />
89-
)}
56+
<div className="bg-blue-50 p-6 rounded-lg">
57+
<h3 className="text-xl font-bold mb-4">
58+
{attackingType} Type Effectiveness
59+
</h3>
60+
<div className="bg-white rounded-lg overflow-hidden">
61+
<table className="w-full">
62+
<thead className="bg-gray-50">
63+
<tr>
64+
<th className="px-4 py-2 text-left">Defending Type</th>
65+
<th className="px-4 py-2 text-left">Multiplier</th>
66+
<th className="px-4 py-2 text-left">Effectiveness</th>
67+
</tr>
68+
</thead>
69+
<tbody>
70+
{effectiveness.map((item, index) => (
71+
<tr key={index} className="border-t">
72+
<td className="px-4 py-2 font-medium">
73+
{item.defending}
74+
</td>
75+
<td className="px-4 py-2">
76+
<span
77+
className={`font-bold ${
78+
item.effectiveness === 2
79+
? 'text-green-600'
80+
: item.effectiveness === 0.5
81+
? 'text-red-600'
82+
: 'text-gray-600'
83+
}`}
84+
>
85+
{item.effectiveness}x
86+
</span>
87+
</td>
88+
<td className="px-4 py-2 text-sm text-gray-600">
89+
{item.description}
90+
</td>
91+
</tr>
92+
))}
93+
</tbody>
94+
</table>
95+
</div>
9096
</div>
9197
);
9298
};
9399

94100
export const Exercise = () => {
95101
return (
96-
<form noValidate name="form">
97-
<TextInputField
98-
name="input"
99-
id="input"
100-
label="Enter your name"
101-
required
102-
errorMessage="Please enter your name"
103-
/>
104-
</form>
102+
<div className="space-y-6">
103+
<PokemonTypeCalculator attackingType="Fire" />
104+
</div>
105105
);
106106
};

0 commit comments

Comments
 (0)