Skip to content

Commit 9556cf6

Browse files
feat: implement the render children pattern
1 parent 058349f commit 9556cf6

File tree

5 files changed

+462
-0
lines changed

5 files changed

+462
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Exercise } from './exercise';
3+
4+
const meta: Meta<typeof Exercise> = {
5+
title: 'Lessons/🥈 Silver/🔄 Render Children Pattern/Exercise',
6+
component: Exercise,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { useState, useEffect } from 'react';
2+
3+
interface IPokemon {
4+
id: number;
5+
name: string;
6+
type: string;
7+
hp: number;
8+
}
9+
10+
/*
11+
* Observations
12+
* 💅 Search component has hardcoded display for all states
13+
* Loading, success, and error displays are tightly coupled
14+
15+
* Tasks
16+
* 1A 💻 - Add render props to IPokemonSearchProps:
17+
* renderLoading: () => React.ReactNode;
18+
* renderSuccess: (pokemon: IPokemon[]) => React.ReactNode;
19+
* renderError: (error: string) => React.ReactNode;
20+
*
21+
* 1B 💻 - Replace hardcoded JSX with render function calls
22+
* 1C 💻 - In Exercise component, provide render functions for each state
23+
*/
24+
25+
interface IPokemonSearchProps {
26+
searchTerm: string;
27+
}
28+
29+
const mockPokemon: IPokemon[] = [
30+
{ id: 1, name: 'Pikachu', type: 'Electric', hp: 35 },
31+
{ id: 4, name: 'Charmander', type: 'Fire', hp: 39 },
32+
{ id: 7, name: 'Squirtle', type: 'Water', hp: 44 },
33+
{ id: 25, name: 'Pichu', type: 'Electric', hp: 20 }
34+
];
35+
36+
export const PokemonSearch = ({
37+
searchTerm
38+
}: IPokemonSearchProps) => {
39+
const [loading, setLoading] = useState(false);
40+
const [pokemon, setPokemon] = useState<IPokemon[]>([]);
41+
const [error, setError] = useState<string | null>(null);
42+
43+
// Simulate API call
44+
const searchPokemon = async (term: string) => {
45+
setLoading(true);
46+
setError(null);
47+
48+
try {
49+
await new Promise((resolve) => setTimeout(resolve, 1000));
50+
51+
if (term === 'error') {
52+
throw new Error('Pokemon not found!');
53+
}
54+
55+
const results = mockPokemon.filter((p) =>
56+
p.name.toLowerCase().includes(term.toLowerCase())
57+
);
58+
setPokemon(results);
59+
} catch (err) {
60+
setError(err instanceof Error ? err.message : 'Unknown error');
61+
} finally {
62+
setLoading(false);
63+
}
64+
};
65+
66+
// Auto-search when searchTerm changes
67+
useEffect(() => {
68+
if (searchTerm) {
69+
searchPokemon(searchTerm);
70+
}
71+
}, [searchTerm]);
72+
73+
if (loading) {
74+
return (
75+
<div
76+
className="bg-blue-50 p-6 rounded-lg"
77+
role="status"
78+
aria-label="Loading Pokemon search results"
79+
>
80+
<div className="animate-pulse">
81+
<div className="h-4 bg-blue-200 rounded w-1/4 mb-4"></div>
82+
<div className="space-y-2">
83+
<div className="h-3 bg-blue-200 rounded"></div>
84+
<div className="h-3 bg-blue-200 rounded w-5/6"></div>
85+
</div>
86+
</div>
87+
<span className="sr-only">Searching for Pokemon...</span>
88+
</div>
89+
);
90+
}
91+
92+
if (error) {
93+
return (
94+
<div
95+
className="bg-red-50 p-6 rounded-lg border border-red-200"
96+
role="alert"
97+
aria-labelledby="error-title"
98+
>
99+
<div className="flex items-center">
100+
<span
101+
className="text-red-500 text-xl mr-2"
102+
aria-hidden="true"
103+
>
104+
⚠️
105+
</span>
106+
<div>
107+
<h3 id="error-title" className="font-bold text-red-800">
108+
Search Error
109+
</h3>
110+
<p className="text-red-600">{error}</p>
111+
</div>
112+
</div>
113+
</div>
114+
);
115+
}
116+
117+
return (
118+
<div
119+
className="bg-green-50 p-6 rounded-lg"
120+
role="region"
121+
aria-labelledby="results-title"
122+
>
123+
<h3 id="results-title" className="font-bold mb-4">
124+
Found {pokemon.length} Pokemon
125+
</h3>
126+
<ul className="grid gap-3" role="list">
127+
{pokemon.map((p) => (
128+
<li
129+
key={p.id}
130+
className="bg-white p-3 rounded flex justify-between"
131+
role="listitem"
132+
>
133+
<div>
134+
<span className="font-bold">{p.name}</span>
135+
<span className="text-gray-600 ml-2">({p.type})</span>
136+
</div>
137+
<span className="text-sm">HP: {p.hp}</span>
138+
</li>
139+
))}
140+
</ul>
141+
</div>
142+
);
143+
};
144+
145+
export const Exercise = () => {
146+
const [searchTerm, setSearchTerm] = useState('pika');
147+
148+
return (
149+
<div className="space-y-4">
150+
<label
151+
htmlFor="pokemon-search"
152+
className="block text-sm font-medium text-gray-700 mb-1"
153+
>
154+
Search Pokemon
155+
</label>
156+
<input
157+
id="pokemon-search"
158+
type="text"
159+
value={searchTerm}
160+
onChange={(e) => setSearchTerm(e.target.value)}
161+
placeholder="Search Pokemon..."
162+
className="w-full p-2 border rounded"
163+
aria-describedby="search-instructions"
164+
/>
165+
<div id="search-instructions" className="sr-only">
166+
Type to search for Pokemon by name
167+
</div>
168+
169+
<div aria-live="polite" aria-label="Search results">
170+
<PokemonSearch searchTerm={searchTerm} />
171+
</div>
172+
</div>
173+
);
174+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
import { Final } from './final';
3+
4+
const meta: Meta<typeof Final> = {
5+
title: 'Lessons/🥈 Silver/🔄 Render Children Pattern/Final',
6+
component: Final,
7+
parameters: {
8+
layout: 'centered',
9+
},
10+
};
11+
12+
export default meta;
13+
type Story = StoryObj<typeof meta>;
14+
15+
export const Default: Story = {};

0 commit comments

Comments
 (0)