create-mcp-use-app 2.0.0-beta.2 → 2.0.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +10 -4
  3. package/dist/templates/blank/package.json +9 -5
  4. package/dist/templates/blank/tsconfig.json +13 -10
  5. package/dist/templates/mcp-apps/index.ts +119 -15
  6. package/dist/templates/mcp-apps/{tools.d.ts → mcp-env.d.ts} +3 -1
  7. package/dist/templates/mcp-apps/package.json +9 -10
  8. package/dist/templates/mcp-apps/public/fruits/apple.webp +0 -0
  9. package/dist/templates/mcp-apps/public/fruits/apricot.webp +0 -0
  10. package/dist/templates/mcp-apps/public/fruits/avocado.webp +0 -0
  11. package/dist/templates/mcp-apps/public/fruits/banana.webp +0 -0
  12. package/dist/templates/mcp-apps/public/fruits/blueberry.webp +0 -0
  13. package/dist/templates/mcp-apps/public/fruits/cherries.webp +0 -0
  14. package/dist/templates/mcp-apps/public/fruits/coconut.webp +0 -0
  15. package/dist/templates/mcp-apps/public/fruits/grapes.webp +0 -0
  16. package/dist/templates/mcp-apps/public/fruits/lemon.webp +0 -0
  17. package/dist/templates/mcp-apps/public/fruits/mango.webp +0 -0
  18. package/dist/templates/mcp-apps/public/fruits/orange.webp +0 -0
  19. package/dist/templates/mcp-apps/public/fruits/pear.webp +0 -0
  20. package/dist/templates/mcp-apps/public/fruits/pineapple.webp +0 -0
  21. package/dist/templates/mcp-apps/public/fruits/plum.webp +0 -0
  22. package/dist/templates/mcp-apps/public/fruits/strawberry.webp +0 -0
  23. package/dist/templates/mcp-apps/public/fruits/watermelon.webp +0 -0
  24. package/dist/templates/mcp-apps/tsconfig.json +9 -4
  25. package/dist/templates/mcp-apps/views/product-search-result/components/Accordion.tsx +35 -0
  26. package/dist/templates/mcp-apps/views/product-search-result/components/AccordionItem.tsx +35 -0
  27. package/dist/templates/mcp-apps/views/product-search-result/components/Carousel.tsx +52 -0
  28. package/dist/templates/mcp-apps/views/product-search-result/components/CarouselItem.tsx +91 -0
  29. package/dist/templates/mcp-apps/views/product-search-result/components/CarouselSkeleton.tsx +18 -0
  30. package/dist/templates/mcp-apps/views/product-search-result/hooks/useCarouselAnimation.ts +72 -0
  31. package/dist/templates/mcp-apps/views/product-search-result/view.css +81 -0
  32. package/dist/templates/mcp-apps/views/product-search-result/view.tsx +371 -0
  33. package/dist/templates/starter/package.json +9 -8
  34. package/dist/templates/starter/tsconfig.json +13 -10
  35. package/package.json +1 -1
  36. package/dist/templates/blank/public/favicon.ico +0 -0
  37. package/dist/templates/mcp-apps/public/favicon.ico +0 -0
  38. package/dist/templates/mcp-apps/public/fruits/apple.png +0 -0
  39. package/dist/templates/mcp-apps/public/fruits/apricot.png +0 -0
  40. package/dist/templates/mcp-apps/public/fruits/avocado.png +0 -0
  41. package/dist/templates/mcp-apps/public/fruits/banana.png +0 -0
  42. package/dist/templates/mcp-apps/public/fruits/blueberry.png +0 -0
  43. package/dist/templates/mcp-apps/public/fruits/cherries.png +0 -0
  44. package/dist/templates/mcp-apps/public/fruits/coconut.png +0 -0
  45. package/dist/templates/mcp-apps/public/fruits/grapes.png +0 -0
  46. package/dist/templates/mcp-apps/public/fruits/lemon.png +0 -0
  47. package/dist/templates/mcp-apps/public/fruits/mango.png +0 -0
  48. package/dist/templates/mcp-apps/public/fruits/orange.png +0 -0
  49. package/dist/templates/mcp-apps/public/fruits/pear.png +0 -0
  50. package/dist/templates/mcp-apps/public/fruits/pineapple.png +0 -0
  51. package/dist/templates/mcp-apps/public/fruits/plum.png +0 -0
  52. package/dist/templates/mcp-apps/public/fruits/strawberry.png +0 -0
  53. package/dist/templates/mcp-apps/public/fruits/watermelon.png +0 -0
  54. package/dist/templates/mcp-apps/resources/product-search-result/view.tsx +0 -25
  55. package/dist/templates/starter/public/favicon.ico +0 -0
@@ -0,0 +1,81 @@
1
+ /* Carousel scroll container with padding for blur effect */
2
+ .carousel-scroll-container {
3
+ padding-bottom: 2rem;
4
+ }
5
+
6
+ /* Hover effect styles for carousel items */
7
+ .carousel-item {
8
+ position: relative;
9
+ container-type: size;
10
+ cursor: pointer;
11
+ transition-property: translate, scale;
12
+ transition-duration: 0.12s;
13
+ transition-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
14
+ -webkit-tap-highlight-color: transparent;
15
+ overflow: hidden;
16
+ border-radius: 12px;
17
+ }
18
+
19
+ .carousel-item:active {
20
+ translate: 0 1px;
21
+ scale: 0.99;
22
+ }
23
+
24
+ .carousel-item-content {
25
+ position: relative;
26
+ z-index: 2;
27
+ width: 100%;
28
+ height: 100%;
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ }
33
+
34
+ .carousel-item-bg {
35
+ position: absolute;
36
+ inset: 0;
37
+ display: grid;
38
+ place-items: center;
39
+ transform: translateZ(0);
40
+ filter: blur(28px) saturate(5) brightness(1.3);
41
+ translate: calc(var(--pointer-x, -10) * 50cqi)
42
+ calc(var(--pointer-y, -10) * 50cqh);
43
+ scale: var(--icon-scale, 3.4);
44
+ opacity: var(--icon-opacity, 0.25);
45
+ will-change: transform, filter, opacity;
46
+ pointer-events: none;
47
+ z-index: 1;
48
+ transition:
49
+ opacity 0.26s ease-out,
50
+ scale 0.26s ease-out;
51
+ border-radius: inherit;
52
+ }
53
+
54
+ .carousel-item-bg img {
55
+ width: 100px;
56
+ -webkit-user-select: none;
57
+ user-select: none;
58
+ -webkit-user-drag: none;
59
+ }
60
+
61
+ .carousel-item::after {
62
+ content: "";
63
+ position: absolute;
64
+ pointer-events: none;
65
+ inset: 0px;
66
+ border-radius: inherit;
67
+ border: 3px solid transparent;
68
+ backdrop-filter: blur(0px) saturate(4.2) brightness(2.5) contrast(2.5);
69
+ -webkit-backdrop-filter: blur(0px) saturate(4.2) brightness(2.5) contrast(2.5);
70
+ mask:
71
+ linear-gradient(#fff 0 100%) border-box,
72
+ linear-gradient(#fff 0 100%) padding-box;
73
+ mask-composite: exclude;
74
+ z-index: 3;
75
+ transform: translateZ(0);
76
+ }
77
+
78
+ .carousel-item-highlighted {
79
+ outline: 2px solid rgb(37 99 235);
80
+ outline-offset: 2px;
81
+ }
@@ -0,0 +1,371 @@
1
+ import {
2
+ Image,
3
+ ModelContext,
4
+ ThemeProvider,
5
+ ViewControls,
6
+ useCallTool,
7
+ useDisplayMode,
8
+ useHostContext,
9
+ useOpenExternal,
10
+ useSendFollowUp,
11
+ useToolContext,
12
+ useViewTheme,
13
+ useViewTool,
14
+ } from "mcp-use/react";
15
+ import { useCallback, useState } from "react";
16
+ import { z } from "zod";
17
+
18
+ import { Accordion } from "./components/Accordion.js";
19
+ import { Carousel } from "./components/Carousel.js";
20
+ import { CarouselSkeleton } from "./components/CarouselSkeleton.js";
21
+ import "./view.css";
22
+
23
+ const shellClass =
24
+ "relative rounded-3xl border border-neutral-200 bg-white dark:border-neutral-700 dark:bg-neutral-900";
25
+
26
+ const iconButtonClass =
27
+ "rounded-full border border-neutral-300 p-2 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:hover:bg-neutral-800";
28
+
29
+ function HeartIcon({ filled }: { filled: boolean }) {
30
+ if (filled) {
31
+ return (
32
+ <svg
33
+ width="14"
34
+ height="14"
35
+ viewBox="0 0 24 24"
36
+ fill="currentColor"
37
+ aria-hidden
38
+ >
39
+ <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
40
+ </svg>
41
+ );
42
+ }
43
+ return (
44
+ <svg
45
+ width="14"
46
+ height="14"
47
+ viewBox="0 0 24 24"
48
+ fill="none"
49
+ stroke="currentColor"
50
+ strokeWidth="2"
51
+ aria-hidden
52
+ >
53
+ <path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z" />
54
+ </svg>
55
+ );
56
+ }
57
+
58
+ function ExpandIcon() {
59
+ return (
60
+ <svg
61
+ width="14"
62
+ height="14"
63
+ viewBox="0 0 24 24"
64
+ fill="none"
65
+ stroke="currentColor"
66
+ strokeWidth="2"
67
+ aria-hidden
68
+ >
69
+ <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
70
+ </svg>
71
+ );
72
+ }
73
+
74
+ function PipIcon() {
75
+ return (
76
+ <svg
77
+ width="14"
78
+ height="14"
79
+ viewBox="0 0 24 24"
80
+ fill="none"
81
+ stroke="currentColor"
82
+ strokeWidth="2"
83
+ aria-hidden
84
+ >
85
+ <rect x="2" y="4" width="20" height="14" rx="2" />
86
+ <rect x="13" y="11" width="7" height="5" rx="1" fill="currentColor" />
87
+ </svg>
88
+ );
89
+ }
90
+
91
+ function CloseIcon() {
92
+ return (
93
+ <svg
94
+ width="14"
95
+ height="14"
96
+ viewBox="0 0 24 24"
97
+ fill="none"
98
+ stroke="currentColor"
99
+ strokeWidth="2"
100
+ aria-hidden
101
+ >
102
+ <line x1="18" y1="6" x2="6" y2="18" />
103
+ <line x1="6" y1="6" x2="18" y2="18" />
104
+ </svg>
105
+ );
106
+ }
107
+
108
+ function ProductSearchResultContent() {
109
+ const view = useToolContext<"search-fruits">();
110
+ const theme = useViewTheme();
111
+ const { locale, hostCapabilities } = useHostContext();
112
+ const { displayMode, availableDisplayModes, requestDisplayMode } =
113
+ useDisplayMode();
114
+ const sendFollowUp = useSendFollowUp();
115
+ const openExternal = useOpenExternal();
116
+
117
+ const [favorites, setFavorites] = useState<string[]>([]);
118
+ const [highlighted, setHighlighted] = useState<string | null>(null);
119
+
120
+ const details = useCallTool("get-fruit-details");
121
+
122
+ useViewTool(
123
+ {
124
+ name: "highlight-fruit",
125
+ description: "Highlight a visible result in the carousel",
126
+ inputSchema: z.object({ id: z.string() }),
127
+ },
128
+ async ({ id }) => {
129
+ setHighlighted(id);
130
+ return { content: [{ type: "text", text: `Highlighted ${id}` }] };
131
+ }
132
+ );
133
+
134
+ const toggleFavorite = useCallback((fruit: string) => {
135
+ setFavorites((current) =>
136
+ current.includes(fruit)
137
+ ? current.filter((f) => f !== fruit)
138
+ : [...current, fruit]
139
+ );
140
+ }, []);
141
+
142
+ const accordionItems = [
143
+ {
144
+ question: "Demo of the autosize feature",
145
+ answer:
146
+ "This is a demo of the autosize feature. The view will automatically resize to fit the content, as supported by the MCP Apps specification.",
147
+ },
148
+ ];
149
+
150
+ const root = theme === "dark" ? "dark" : "";
151
+ const lang = locale?.split("-")[0] ?? "en";
152
+ const isFullscreen = displayMode === "fullscreen";
153
+ const isPip = displayMode === "pip";
154
+
155
+ if (view.status === "error") {
156
+ return (
157
+ <div className={`${root} ${shellClass} p-8`} role="alert">
158
+ <p className="m-0 font-medium text-neutral-900 dark:text-neutral-100">
159
+ Search failed
160
+ </p>
161
+ <p className="mt-2 mb-0 text-sm text-neutral-600 dark:text-neutral-400">
162
+ {view.error.message}
163
+ </p>
164
+ </div>
165
+ );
166
+ }
167
+
168
+ if (view.status === "pending") {
169
+ return (
170
+ <div className={`${root} ${shellClass}`}>
171
+ <div className="p-8 pb-4">
172
+ <h5 className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
173
+ MCP-Apps Template
174
+ </h5>
175
+ <h2 className="mb-3 text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
176
+ Lovely Little Fruit Shop
177
+ </h2>
178
+ <div className="h-5 w-48 animate-pulse rounded-md bg-neutral-100 dark:bg-neutral-800" />
179
+ </div>
180
+ <CarouselSkeleton />
181
+ </div>
182
+ );
183
+ }
184
+
185
+ const { query, results } = view.toolOutput;
186
+ const selectedFruit = details.data?.structuredContent;
187
+ const isLoadingDetails = details.isPending;
188
+
189
+ return (
190
+ <div className={`${root} ${shellClass}`}>
191
+ <ModelContext
192
+ content={`User is viewing results for "${query}"; favorites: ${favorites.join(", ") || "none"}`}
193
+ />
194
+
195
+ <div className="absolute top-4 right-4 z-10 flex items-center gap-2">
196
+ <span className="rounded-full px-2.5 py-1 text-xs font-medium tracking-wide text-neutral-500 uppercase dark:text-neutral-400">
197
+ {lang}
198
+ </span>
199
+
200
+ {favorites.length > 0 && (
201
+ <span className="flex items-center gap-1 rounded-full px-2.5 py-1 text-xs font-medium text-red-500">
202
+ <HeartIcon filled />
203
+ {favorites.length}
204
+ </span>
205
+ )}
206
+
207
+ {!isFullscreen && !isPip && (
208
+ <>
209
+ {availableDisplayModes.includes("pip") && (
210
+ <button
211
+ type="button"
212
+ className={iconButtonClass}
213
+ title="Picture-in-picture"
214
+ onClick={() => {
215
+ void requestDisplayMode({ mode: "pip" });
216
+ }}
217
+ >
218
+ <PipIcon />
219
+ </button>
220
+ )}
221
+ {availableDisplayModes.includes("fullscreen") && (
222
+ <button
223
+ type="button"
224
+ className={iconButtonClass}
225
+ title="Fullscreen"
226
+ onClick={() => {
227
+ void requestDisplayMode({ mode: "fullscreen" });
228
+ }}
229
+ >
230
+ <ExpandIcon />
231
+ </button>
232
+ )}
233
+ </>
234
+ )}
235
+
236
+ {(isFullscreen || isPip) && (
237
+ <button
238
+ type="button"
239
+ className={iconButtonClass}
240
+ title="Exit"
241
+ onClick={() => {
242
+ void requestDisplayMode({ mode: "inline" });
243
+ }}
244
+ >
245
+ <CloseIcon />
246
+ </button>
247
+ )}
248
+ </div>
249
+
250
+ <div className="p-8 pb-4">
251
+ <h5 className="mb-1 text-sm text-neutral-500 dark:text-neutral-400">
252
+ MCP-Apps Template
253
+ </h5>
254
+ <h2 className="mb-1 text-2xl font-semibold text-neutral-900 dark:text-neutral-100">
255
+ Lovely Little Fruit Shop
256
+ </h2>
257
+ <p className="text-base text-neutral-600 dark:text-neutral-400">
258
+ {query
259
+ ? `Showing results for "${query}"`
260
+ : "Tap a fruit to see details"}
261
+ </p>
262
+ </div>
263
+
264
+ <Carousel
265
+ results={results}
266
+ favorites={favorites}
267
+ highlighted={highlighted}
268
+ onSelectFruit={(fruit: string) => {
269
+ void details.callTool({ fruit });
270
+ }}
271
+ onToggleFavorite={toggleFavorite}
272
+ />
273
+
274
+ {selectedFruit && (
275
+ <div className="mx-8 my-6 flex items-center gap-6 rounded-2xl border border-neutral-200 bg-neutral-50 p-5 dark:border-neutral-700 dark:bg-neutral-950">
276
+ <div
277
+ className={`shrink-0 rounded-xl p-4 ${
278
+ results.find((r) => r.fruit === selectedFruit.fruit)?.color ?? ""
279
+ }`}
280
+ >
281
+ <Image
282
+ src={`/fruits/${selectedFruit.fruit}.webp`}
283
+ alt={selectedFruit.fruit}
284
+ className="h-24 w-24 object-contain"
285
+ />
286
+ </div>
287
+ <div className="flex-1">
288
+ {isLoadingDetails ? (
289
+ <div className="h-4 w-32 animate-pulse rounded bg-neutral-200 dark:bg-neutral-800" />
290
+ ) : (
291
+ <>
292
+ <div className="mb-2 flex items-center gap-2">
293
+ <h3 className="text-lg font-semibold capitalize">
294
+ {selectedFruit.fruit}
295
+ </h3>
296
+ <button
297
+ type="button"
298
+ onClick={() => toggleFavorite(selectedFruit.fruit)}
299
+ className={`rounded-full p-1 transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-800 ${
300
+ favorites.includes(selectedFruit.fruit)
301
+ ? "text-red-500"
302
+ : "text-neutral-500"
303
+ }`}
304
+ title={
305
+ favorites.includes(selectedFruit.fruit)
306
+ ? "Remove from favorites"
307
+ : "Add to favorites"
308
+ }
309
+ >
310
+ <HeartIcon
311
+ filled={favorites.includes(selectedFruit.fruit)}
312
+ />
313
+ </button>
314
+ </div>
315
+ <ul className="space-y-1">
316
+ {selectedFruit.facts.map((fact: string) => (
317
+ <li
318
+ key={fact}
319
+ className="flex items-start gap-2 text-sm text-neutral-600 dark:text-neutral-400"
320
+ >
321
+ <span className="mt-0.5 text-blue-600">•</span>
322
+ {fact}
323
+ </li>
324
+ ))}
325
+ </ul>
326
+ <div className="mt-3 flex flex-wrap gap-2">
327
+ {hostCapabilities?.message !== undefined && (
328
+ <button
329
+ type="button"
330
+ onClick={() => {
331
+ void sendFollowUp({
332
+ prompt: `Tell me more interesting facts about ${selectedFruit.fruit}`,
333
+ }).catch(() => {});
334
+ }}
335
+ className="cursor-pointer rounded-lg bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-100 dark:bg-blue-950 dark:text-blue-300 dark:hover:bg-blue-900"
336
+ >
337
+ Ask the AI for more about {selectedFruit.fruit}
338
+ </button>
339
+ )}
340
+ <button
341
+ type="button"
342
+ onClick={() => {
343
+ void openExternal({
344
+ url: `https://images.example.com/producers/${selectedFruit.fruit}`,
345
+ });
346
+ }}
347
+ className="cursor-pointer rounded-lg border border-neutral-300 px-3 py-1.5 text-xs font-medium text-neutral-700 transition-colors hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-300 dark:hover:bg-neutral-800"
348
+ >
349
+ View producer
350
+ </button>
351
+ </div>
352
+ </>
353
+ )}
354
+ </div>
355
+ </div>
356
+ )}
357
+
358
+ <Accordion items={accordionItems} />
359
+ </div>
360
+ );
361
+ }
362
+
363
+ export default function ProductSearchResult() {
364
+ return (
365
+ <ThemeProvider>
366
+ <ViewControls>
367
+ <ProductSearchResultContent />
368
+ </ViewControls>
369
+ </ThemeProvider>
370
+ );
371
+ }
@@ -5,17 +5,18 @@
5
5
  "node": ">=22.22.2"
6
6
  },
7
7
  "version": "1.0.0",
8
- "description": "MCP server with starter tools, resources, and prompts",
9
- "author": "",
8
+ "description": "an mcp-use server",
9
+ "author": "mcp-use",
10
10
  "license": "MIT",
11
+ "homepage": "https://github.com/mcp-use/mcp-use",
11
12
  "keywords": [
12
13
  "mcp",
13
- "server",
14
- "starter",
15
- "tools",
16
- "resources",
17
- "prompts",
18
- "mcp-use"
14
+ "mcp-server",
15
+ "chatgpt plugins",
16
+ "claude connector",
17
+ "mcp-app",
18
+ "mcp-use",
19
+ "manufact"
19
20
  ],
20
21
  "main": "dist/index.js",
21
22
  "scripts": {
@@ -1,20 +1,23 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2022",
3
+ "target": "ES2024",
4
4
  "jsx": "react-jsx",
5
- "module": "ESNext",
6
- "moduleResolution": "bundler",
7
- "allowJs": true,
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
8
8
  "strict": true,
9
- "outDir": "./dist",
10
- "sourceMap": true,
11
- "allowSyntheticDefaultImports": true,
12
9
  "esModuleInterop": true,
13
10
  "forceConsistentCasingInFileNames": true,
14
11
  "skipLibCheck": true,
15
- "incremental": true,
16
- "tsBuildInfoFile": "./dist/.tsbuildinfo"
12
+ "noEmit": true
17
13
  },
18
- "include": ["index.ts", "src/**/*", "resources/**/*", "server.ts", ".mcp-use/**/*.d.ts"],
14
+ "include": [
15
+ "index.ts",
16
+ "mcp-env.d.ts",
17
+ "src/**/*",
18
+ "views/**/*",
19
+ "server.ts",
20
+ ".mcp-use/**/*.d.ts"
21
+ ],
19
22
  "exclude": ["node_modules", "dist"]
20
23
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mcp-use-app",
3
- "version": "2.0.0-beta.2",
3
+ "version": "2.0.0-beta.3",
4
4
  "type": "module",
5
5
  "description": "Create MCP-Use apps with one command",
6
6
  "author": "mcp-use, Inc.",
@@ -1,25 +0,0 @@
1
- import { useToolContext } from "mcp-use/react";
2
-
3
- export default function ProductSearchResult() {
4
- const view = useToolContext<"search-fruits">();
5
-
6
- if (view.status === "pending") {
7
- return (
8
- <p>
9
- Searching{view.toolInput?.query ? ` for ${view.toolInput.query}` : ""}…
10
- </p>
11
- );
12
- }
13
- if (view.status === "error") return <p>{view.error.message}</p>;
14
-
15
- return (
16
- <main>
17
- <h1>Fruit results</h1>
18
- <ul>
19
- {view.toolOutput.items.map((item) => (
20
- <li key={item.id}>{item.name}</li>
21
- ))}
22
- </ul>
23
- </main>
24
- );
25
- }