Course menuForge Kitchen

Level 1 · Lesson 5

Lay out the card

Make 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.

About 25 min5 steps

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

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:

Editsrc/resolvers/index.js

Catch 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/

Step 2: Fetch more furniture

Each state needs a UI Kit component: Spinner while waiting, SectionMessage for problems and empty results, List and ListItem for the ingredients.

Editsrc/frontend/index.jsx

Add 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

Step 3: Handle waiting, problems and nothing found

Return early for each state before the full card. The order matters: no answer yet, then a problem, then an empty result.

Editsrc/frontend/index.jsx

Show 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

Step 4: Plate the full card

Now lay out the whole recipe: the heading and servings, then the ingredients as a list, then the method.

Editsrc/frontend/index.jsx

Title 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.

Step 5: Taste it, then break it on purpose

Deploy if the tunnel is not running, then reload the Pad thai ticket:

RunTerminal
forge deploy
You should see, on the work itemExampleAn example: the supplier’s recipe will differ.

Recipe 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

Compare with yours: download the code after this lesson.

Type at least two letters.