diff --git a/package-lock.json b/package-lock.json index f77eae7..640a922 100644 --- a/package-lock.json +++ b/package-lock.json @@ -624,8 +624,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.18.tgz", "integrity": "sha512-EF77RqROHL+4LhMGW5NTeKqfUd/e4OOv6EDFQ/UQQiFyWuqkEKyEz0NDILxOFxWUEVdjT2GQ2cC7t12B6pESwg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-dart": { "version": "2.3.1", @@ -765,16 +764,14 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.12.tgz", "integrity": "sha512-JFffQ1dDVEyJq6tCDWv0r/RqkdSnV43P2F/3jJ9rwLgdsOIXwQbXrz6QDlvQLVvNSnORH9KjDtenFTGDyzfCaA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-html-symbol-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@cspell/dict-html-symbol-entities/-/dict-html-symbol-entities-4.0.4.tgz", "integrity": "sha512-afea+0rGPDeOV9gdO06UW183Qg6wRhWVkgCFwiO3bDupAoyXRuvupbb5nUyqSTsLXIKL8u8uXQlJ9pkz07oVXw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-java": { "version": "5.0.12", @@ -972,8 +969,7 @@ "resolved": "https://registry.npmjs.org/@cspell/dict-typescript/-/dict-typescript-3.2.3.tgz", "integrity": "sha512-zXh1wYsNljQZfWWdSPYwQhpwiuW0KPW1dSd8idjMRvSD0aSvWWHoWlrMsmZeRl4qM4QCEAjua8+cjflm41cQBg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@cspell/dict-vue": { "version": "3.0.5", @@ -1114,7 +1110,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1161,7 +1156,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -4129,7 +4123,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/chai": { "version": "5.2.2", @@ -4191,7 +4186,6 @@ "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4202,7 +4196,6 @@ "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4418,7 +4411,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6225,8 +6217,7 @@ "version": "0.0.1467305", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1467305.tgz", "integrity": "sha512-LxwMLqBoPPGpMdRL4NkLFRNy3QLp6Uqa7GNp1v6JaBheop2QrB9Q7q0A/q/CYYP9sBfZdHOyszVx4gc9zyk7ow==", - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" }, "node_modules/dir-glob": { "version": "3.0.1", @@ -6259,7 +6250,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dot-prop": { "version": "5.3.0", @@ -6712,7 +6704,6 @@ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6774,7 +6765,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9109,7 +9099,6 @@ "integrity": "sha512-lIHeR1qlIRrIN5VMccd8tI2Sgw6ieYXSVktcSHaNe3Z5nE/tcPQYQWOq00wxMvYOsz+73eAkNenVvmPC6bba9A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@asamuzakjp/dom-selector": "^6.5.4", "cssstyle": "^5.3.0", @@ -9852,6 +9841,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -11175,7 +11165,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11259,7 +11248,6 @@ "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -11291,7 +11279,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11321,6 +11308,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -11336,6 +11324,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -11348,7 +11337,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/progress": { "version": "2.0.3", @@ -11531,7 +11521,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11541,7 +11530,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -12057,7 +12045,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.2.tgz", "integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -13295,7 +13282,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -14391,7 +14377,6 @@ "integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14490,7 +14475,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", diff --git a/src/api/message.js b/src/api/message.js index 953f24f..94ab904 100644 --- a/src/api/message.js +++ b/src/api/message.js @@ -9,12 +9,24 @@ * This file contains client-side API functions that call our express.js backend routes */ -export const getMessage = async () => { +export const getPost = async (postId) => { try { - const response = await fetch('/api/message'); - const data = await response.json(); - return data.message; + // TODO: handle production and development environments + const response = await fetch(`http://localhost:5173/api/post/${postId}`); + return await response.json(); } catch (error) { - throw new Error('Failed to load message: ', error); + throw new Error('Failed to load post: ', error); + } +}; + +export const getComments = async (postId) => { + try { + // TODO: handle production and development environments + const response = await fetch( + `http://localhost:5173/api/comments?postId=${postId}`, + ); + return await response.json(); + } catch (error) { + throw new Error('Failed to load comments: ', error); } }; diff --git a/src/pages/welcome/Welcome.jsx b/src/pages/welcome/Welcome.jsx index bec17a2..f59b51c 100644 --- a/src/pages/welcome/Welcome.jsx +++ b/src/pages/welcome/Welcome.jsx @@ -5,34 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import { CodeSnippet, Column, Grid, Tile } from '@carbon/react'; -import { useEffect, useState } from 'react'; +import { + CodeSnippet, + Column, + Grid, + Tile, + UnorderedList, + ListItem, + Stack, +} from '@carbon/react'; -import { getMessage } from '../../api/message.js'; import { Footer } from '../../components/footer/Footer'; import { WelcomeHeader } from './WelcomeHeader.jsx'; import { PageLayout } from '../../layouts/page-layout.jsx'; +import PostComponent from './post/PostComponent.jsx'; +import { Suspense } from 'react'; // The styles are imported into index.scss by default. // Do the same unless you have a good reason not to. // import './welcome.scss'; const Welcome = () => { - const [message, setMessage] = useState(''); - - useEffect(() => { - const loadMessage = async () => { - try { - const msg = await getMessage(); - setMessage(msg); - } catch { - setMessage('Failed to load message'); - } - }; - - loadMessage(); - }, []); - return ( { sm={4} > - +

↳ An example of data fetching

-

- Below is a dynamically fetched message from an external API - endpoint. This showcases how to perform data fetching while - keeping components clean and separating network logic. -

- - Message: {message || 'Loading...'} - + +

+ Below is a dynamically fetched message from an external API + endpoint. This showcases how to perform data fetching while + keeping components clean and separating network logic. Here is + how it works: +

+ + + UI Layer - PostComponent.jsx manages React + state and renders the data using Carbon Design components + + + API Layer - Client-side functions in{' '} + api/message.js handle HTTP requests to our + Express backend + + + Service Layer - Server-side handlers in{' '} + service/postHandlers.js proxy requests to + external APIs (JSONPlaceholder) + + +

+ This pattern keeps your components focused on presentation + while centralizing data fetching logic for reusability and + testability. +

+
+ + +
diff --git a/src/pages/welcome/post/PostComponent.jsx b/src/pages/welcome/post/PostComponent.jsx new file mode 100644 index 0000000..fee7b13 --- /dev/null +++ b/src/pages/welcome/post/PostComponent.jsx @@ -0,0 +1,67 @@ +import { getComments, getPost } from '../../../api/message.js'; +import { Heading, Section, Tile, Stack, Layer } from '@carbon/react'; +import { useEffect, useState } from 'react'; + +const PostComponent = () => { + const [post, setPost] = useState(); + const [comments, setComments] = useState([]); + + const loadPost = async () => { + try { + const post = await getPost(1); + setPost(post); + } catch { + setPost('Failed to load message'); + } + }; + + const loadComments = async () => { + try { + const comments = await getComments(1); + setComments(comments); + } catch { + setComments('Failed to load comments'); + } + }; + + useEffect(() => { + loadPost(); + loadComments(); + }, []); + + return ( +
+ Posts + + +
+
+ {post?.title ?? 'Loading...'} +

{post?.body}

+
+
+ +
+ + Comments +
+ + {comments?.map((comment) => ( + + + {`From ${comment.email}`} +

{comment.body}

+
+
+ ))} +
+
+
+
+
+
+
+ ); +}; + +export default PostComponent; diff --git a/src/pages/welcome/welcome.scss b/src/pages/welcome/welcome.scss index de7cf35..fa90312 100644 --- a/src/pages/welcome/welcome.scss +++ b/src/pages/welcome/welcome.scss @@ -22,10 +22,6 @@ .cs--welcome__dynamic-message { margin-block-start: $spacing-05; - - p { - margin-inline-start: $spacing-05; - } } .cs--welcome__tile { diff --git a/src/routes/routes.js b/src/routes/routes.js index 49043e5..2cfb7bf 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -5,11 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import { getMessage } from '../service/message.js'; - -export const routeHandlers = { - getMessage, -}; +// eslint-disable-next-line import/default, import/no-named-as-default, import/no-named-as-default-member +import postHandlers from '../service/postHandlers.js'; /** * Registers all API routes on the given Express app instance. @@ -19,6 +16,7 @@ export const routeHandlers = { * @param app - Express app instance OR msw router in case of unit testing * @param handlers - Route handlers (can be mocked for testing) */ -export const getRoutes = (app, handlers = routeHandlers) => { - app.get('/api/message', handlers.getMessage); +export const getRoutes = (app, handlers = postHandlers) => { + app.get('/api/post/:id', handlers.getPost); + app.get('/api/comments', handlers.getComments); }; diff --git a/src/service/message.js b/src/service/message.js deleted file mode 100644 index f611e0c..0000000 --- a/src/service/message.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Copyright IBM Corp. 2025 - * - * This source code is licensed under the Apache-2.0 license found in the - * LICENSE file in the root directory of this source tree. - */ - -/** - * This file contains the functions that do async network requests - */ - -export const getMessage = async (req, res) => { - try { - const response = await fetch( - // TODO: replace with actual endpoint URL - 'https://jsonplaceholder.typicode.com/posts/1', - ); - // The sample endpoint returns a blogpost - const blogpost = await response.json(); - - // Return the blogpost's title - res.json({ message: blogpost.title }); - } catch { - res.status(500).json({ message: 'Failed to fetch message' }); - } -}; diff --git a/src/service/postHandlers.js b/src/service/postHandlers.js new file mode 100644 index 0000000..c9fd02c --- /dev/null +++ b/src/service/postHandlers.js @@ -0,0 +1,52 @@ +/** + * Copyright IBM Corp. 2025 + * + * This source code is licensed under the Apache-2.0 license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * This file contains the functions that do async network requests + */ + +export const getPost = async ({ params: { id } }, res) => { + // Validate that id is a positive integer + if (!/^\d+$/.test(id)) { + return res.status(400).json({ message: 'Invalid post id' }); + } + + try { + const response = await fetch( + // TODO: replace with actual endpoint URL + `https://jsonplaceholder.typicode.com/posts/${id}`, + ); + // The sample endpoint returns a blogpost + const blogpost = await response.json(); + + // Return the blogpost's title + res.json(blogpost); + } catch { + res.status(500).json({ message: 'Failed to fetch post' }); + } +}; + +export const getComments = async ({ query: { postId } }, res) => { + try { + const response = await fetch( + // TODO: replace with actual endpoint URL + `https://jsonplaceholder.typicode.com/comments?postId=${postId}`, + ); + // The sample endpoint returns a blogpost + const comments = await response.json(); + + // Return the blogpost's title + res.json(comments); + } catch { + res.status(500).json({ message: 'Failed to fetch comments' }); + } +}; + +export default { + getComments, + getPost, +};