|
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; |
12 | 6 | } |
13 | 7 |
|
14 | 8 | /* |
15 | 9 | * 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 |
18 | 12 |
|
19 | 13 | * 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; |
34 | 16 | * |
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 |
37 | 19 | */ |
38 | 20 |
|
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 | +} |
57 | 24 |
|
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 | +}; |
60 | 32 |
|
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 | +}; |
65 | 38 |
|
66 | | - setIsTouched(true); |
67 | | - }; |
| 39 | +export const PokemonTypeCalculator = ({ |
| 40 | + attackingType |
| 41 | +}: IPokemonTypeCalculatorProps) => { |
| 42 | + const defendingTypes = Object.keys(typeChart); |
68 | 43 |
|
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 | + ); |
74 | 54 |
|
75 | 55 | 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> |
90 | 96 | </div> |
91 | 97 | ); |
92 | 98 | }; |
93 | 99 |
|
94 | 100 | export const Exercise = () => { |
95 | 101 | 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> |
105 | 105 | ); |
106 | 106 | }; |
0 commit comments