Level 1 · Lesson 5
Lay out the card
Level 1 · Lesson 5Make the cook report problems instead of failing silently, then give the card a waiting state, an empty state, an error state and the full recipe.
Behind, or starting here? Download the code as it should be before this lesson.
Right now the card has one shape: the recipe, or Loading… forever if anything went wrong. A real card has four: waiting, problem, nothing found, and the dish. The cook changes first, so the card has something to show for each.
Use the tunnel for this lesson if you like: none of these steps touch the manifest. Start it with the key exported, as in lesson 3.
Step 1: Let the cook report problems
Locked until the step before it is done.
When findDish throws, the resolver call fails and the card never hears why. Catch the error, write it to the log for you, and send a short message back as data for the person looking at the card:
src/resolvers/index.jsCatch supplier errors and send them back as data.
|
resolver.define('getDish', async () => { |
const query = 'pad thai'; |
Removed: const dish = await findDish(query, process.env.API_NINJAS_KEY); |
Removed: return { query, dish }; |
Added: try { |
Added: const dish = await findDish(query, process.env.API_NINJAS_KEY); |
Added: return { query, dish }; |
Added: } catch (error) { |
Added: console.error(`Supplier lookup failed for "${query}":`, error); |
Added: return { query, dish: null, error: error.message }; |
Added: } |
}); |
|
export const handler = resolver.getDefinitions(); |
Why: Resolver
console.error goes to the app’s logs, which lesson 7 reads. error.message is one of the plain sentences findDish throws, such as the one about a missing key, so it is safe to show.
Stuck? I changed code in src/
Next step: next
Step 2: Fetch more furniture
Locked until the step before it is done.
Each state needs a UI Kit component: Spinner while waiting, SectionMessage for problems and empty results, List and ListItem for the ingredients.
src/frontend/index.jsxAdd the list, spinner and message components.
import React, { useEffect, useState } from 'react'; |
Removed: import ForgeReconciler, { Heading, Inline, Lozenge, Stack, Text } from '@forge/react'; |
Added: import ForgeReconciler, { Heading, Inline, List, ListItem, Lozenge, SectionMessage, Spinner, Stack, Text } from '@forge/react'; |
import { invoke } from '@forge/bridge'; |
|
const App = () => { |
Why: UI Kit
Next step: next
Step 3: Handle waiting, problems and nothing found
Locked until the step before it is done.
Return early for each state before the full card. The order matters: no answer yet, then a problem, then an empty result.
src/frontend/index.jsxShow a spinner, a problem or an empty card before the full card.
useEffect(() => { |
invoke('getDish').then(setResult); |
}, []); |
Removed: const dish = result?.dish; |
Added: if (!result) return <Spinner label="Asking the supplier" />; |
Added: if (result.error) { |
Added: return ( |
Added: <SectionMessage appearance="error" title="The kitchen could not get a recipe"> |
Added: <Text>{result.error}</Text> |
Added: </SectionMessage> |
Added: ); |
Added: } |
Added: if (!result.dish) { |
Added: return ( |
Added: <SectionMessage appearance="information" title={`No recipe for “${result.query}”`}> |
Added: <Text>Name a dish in the work item summary, such as Pad thai, then reload.</Text> |
Added: </SectionMessage> |
Added: ); |
Added: } |
Added: const { dish } = result; |
return ( |
<Stack space="space.200"> |
<Inline space="space.100" alignBlock="center"> |
Why: UI Kit
After these checks, dish always exists, so the full card below no longer needs dish ? everywhere.
Stuck? The card says there is no recipe for my work item
Next step: next
Step 4: Plate the full card
Locked until the step before it is done.
Now lay out the whole recipe: the heading and servings, then the ingredients as a list, then the method.
src/frontend/index.jsxTitle and servings, then ingredients as a list, then the method.
return ( |
<Stack space="space.200"> |
<Inline space="space.100" alignBlock="center"> |
Removed: <Heading size="medium">{dish ? dish.title : 'Loading...'}</Heading> |
Removed: {dish && <Lozenge>{dish.servings}</Lozenge>} |
Added: <Heading size="medium">{dish.title}</Heading> |
Added: <Lozenge>{dish.servings}</Lozenge> |
</Inline> |
Removed: <Text>{dish ? dish.method : ''}</Text> |
Added: <Heading size="small">Ingredients</Heading> |
Added: <List type="unordered"> |
Added: {dish.ingredients.map((item) => ( |
Added: <ListItem key={item}>{item}</ListItem> |
Added: ))} |
Added: </List> |
Added: <Heading size="small">Method</Heading> |
Added: <Text>{dish.method}</Text> |
</Stack> |
); |
}; |
Why: UI Kit
React needs a key on each list item; the ingredient’s own text works here.
Next step: next
Step 5: Taste it, then break it on purpose
Locked until the step before it is done.
Deploy if the tunnel is not running, then reload the Pad thai ticket:
forge deployRecipe card
Pad Thai 4 servings
Ingredients
- 8 oz rice noodles
- 2 tbsp tamarind paste
- 1 cup bean sprouts
Method
Soak the noodles in warm water, then stir-fry with the sauce…
Then see a state you wrote. With the tunnel running, change 'pad thai' in src/resolvers/index.js to a dish nobody sells, such as 'moon cheese', save, and reload. The card shows the information message instead of failing. Change it back to 'pad thai' when you have seen it.
Stuck? I changed code in src/ · The card says there is no recipe for my work item
Next step: next
Compare with yours: download the code after this lesson.