From f48e677685a9269a2eb4e9abab59733f7af59d39 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Thu, 20 Nov 2025 12:10:00 +0100 Subject: [PATCH 1/8] feat(api): implement external API mock handlers and update routing - Added externalHandlers.js to simulate external API responses for posts and comments. - Updated routes to include new external API mock routes. - Refactored postHandlers to call the new external API endpoints. - Adjusted server.js to export base URL for easier configuration. - Enhanced test setup to mock external API calls correctly. Resolves: no-ticket Signed-off-by: [Balint Lendvai] --- src/api/message.js | 18 +++- src/pages/welcome/Welcome.jsx | 62 +++++++----- src/pages/welcome/post/PostComponent.jsx | 71 ++++++++++++++ src/routes/routes.js | 26 +++-- src/server.js | 6 +- src/service/externalHandlers.js | 120 +++++++++++++++++++++++ src/service/postHandlers.js | 77 +++++++++++++++ src/test/server.js | 37 ++++++- 8 files changed, 375 insertions(+), 42 deletions(-) create mode 100644 src/pages/welcome/post/PostComponent.jsx create mode 100644 src/service/externalHandlers.js create mode 100644 src/service/postHandlers.js diff --git a/src/api/message.js b/src/api/message.js index 953f24f..21cec60 100644 --- a/src/api/message.js +++ b/src/api/message.js @@ -9,12 +9,20 @@ * 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; + const response = await fetch(`/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 { + const response = await fetch(`/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 389012c..80109ec 100644 --- a/src/pages/welcome/Welcome.jsx +++ b/src/pages/welcome/Welcome.jsx @@ -11,35 +11,23 @@ import { Grid, Heading, Tile, + UnorderedList, + ListItem, Section, + Stack, } from '@carbon/react'; -import { useEffect, useState } from 'react'; +import { Suspense } from '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'; // 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 ( { lg={12} className="cs--welcome__dynamic-message" > -

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

+ 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..a6dade8 --- /dev/null +++ b/src/pages/welcome/post/PostComponent.jsx @@ -0,0 +1,71 @@ +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([]); + } + }; + + useEffect(() => { + const loadData = async () => { + await loadPost(); + await loadComments(); + }; + loadData(); + }, []); + + return ( +
+ Posts + + +
+
+ {post?.title ?? 'Loading...'} +

{post?.body}

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

{comment.body}

+
+
+ ))} +
+
+
+
+
+
+
+ ); +}; + +export default PostComponent; diff --git a/src/routes/routes.js b/src/routes/routes.js index 49043e5..597166f 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -4,12 +4,11 @@ * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ - -import { getMessage } from '../service/message.js'; - -export const routeHandlers = { - getMessage, -}; +import { getPost, getComments } from '../service/postHandlers.js'; +import { + getExternalPost, + getExternalComments, +} from '../service/externalHandlers.js'; /** * Registers all API routes on the given Express app instance. @@ -18,7 +17,18 @@ export const routeHandlers = { * and allows swapping out route handlers for mocks if needed. * @param app - Express app instance OR msw router in case of unit testing * @param handlers - Route handlers (can be mocked for testing) + * @param externalHandlers - External API mock handlers (can be mocked for testing) */ -export const getRoutes = (app, handlers = routeHandlers) => { - app.get('/api/message', handlers.getMessage); +export const getRoutes = ( + app, + handlers = { getPost, getComments }, + externalHandlers = { getExternalPost, getExternalComments }, +) => { + // Client-facing API routes (these call the external routes below) + app.get('/api/post/:id', handlers.getPost); + app.get('/api/comments', handlers.getComments); + + // Mock "external" API routes (simulate external services) + app.get('/api/external/post/:id', externalHandlers.getExternalPost); + app.get('/api/external/comments', externalHandlers.getExternalComments); }; diff --git a/src/server.js b/src/server.js index 52d52ec..2d79cb7 100644 --- a/src/server.js +++ b/src/server.js @@ -12,8 +12,8 @@ import { getRoutes } from './routes/routes.js'; // Constants const isProduction = process.env.NODE_ENV === 'production'; -const port = process.env.PORT || 5173; -const base = process.env.BASE || '/'; +export const port = process.env.PORT || 5173; +export const base = process.env.BASE || 'http://localhost'; const ABORT_DELAY = 10000; // Create http server @@ -109,5 +109,5 @@ app.use('*all', async (req, res) => { // Start http server app.listen(port, () => { - console.log(`Server started at http://localhost:${port}`); + console.log(`Server started at: ${base}:${port}`); }); diff --git a/src/service/externalHandlers.js b/src/service/externalHandlers.js new file mode 100644 index 0000000..f1694cb --- /dev/null +++ b/src/service/externalHandlers.js @@ -0,0 +1,120 @@ +/** + * 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 mock "external" API handlers that simulate external services. + * These are called by the main API handlers to demonstrate API-to-API communication. + * Just for demonstration purposes. + * You can remove/replace these with actual external API handlers in your actual application. + */ + +// Mock database for posts +const mockPosts = { + 1: { + id: 1, + title: 'Introduction to Carbon Design System', + body: ` + This is just a sample post. Carbon is IBM's open source design system for products and digital experiences. + With the IBM Design Language as its foundation, the system consists of working code, design tools and resources, + human interface guidelines, and a vibrant community of contributors.`, + userId: 1, + }, + 2: { + id: 2, + title: 'Getting Started with React Router', + body: 'This is just a sample post. React Router is a standard library for routing in React applications.', + userId: 1, + }, + 3: { + id: 3, + title: 'Building Modern Web Applications', + body: 'This is just a sample post. Modern web development requires understanding of various tools and frameworks.', + userId: 2, + }, +}; + +// Mock database for comments +const mockComments = { + 1: [ + { + id: 1, + postId: 1, + name: 'Great introduction!', + email: 'user1@example.com', + body: 'This is a really helpful overview of Carbon.', + }, + { + id: 2, + postId: 1, + name: 'Thanks for sharing', + email: 'user2@example.com', + body: 'Looking forward to learning more about Carbon.', + }, + ], + 2: [ + { + id: 3, + postId: 2, + name: 'Router questions', + email: 'user3@example.com', + body: 'How does this compare to other routing libraries?', + }, + ], + 3: [ + { + id: 4, + postId: 3, + name: 'Excellent article', + email: 'user4@example.com', + body: 'Very comprehensive guide to modern web development.', + }, + ], +}; + +/** + * Mock external API handler for fetching a single post + * Simulates an external service response + */ +export const getExternalPost = async ({ params: { id } }, res) => { + // Validate that id is a positive integer + if (!/^\d+$/.test(id)) { + return res.status(400).json({ message: 'Invalid post id' }); + } + + // Simulated network delay + await new Promise((resolve) => setTimeout(resolve, 100)); + const post = mockPosts[id]; + + if (!post) { + return res.status(404).json({ message: 'Post not found' }); + } + + res.json(post); +}; + +/** + * Mock external API handler for fetching comments for a post + * Simulates an external service response + */ +export const getExternalComments = async ({ query: { postId } }, res) => { + // Validate that postId is provided and is a positive integer + if (!postId || !/^\d+$/.test(postId)) { + return res.status(400).json({ message: 'Invalid or missing postId' }); + } + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 100)); + + const comments = mockComments[postId] || []; + + res.json(comments); +}; + +export default { + getExternalComments, + getExternalPost, +}; diff --git a/src/service/postHandlers.js b/src/service/postHandlers.js new file mode 100644 index 0000000..c18151c --- /dev/null +++ b/src/service/postHandlers.js @@ -0,0 +1,77 @@ +/** + * 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 + * These handlers call our "external" API routes to simulate API-to-API communication + */ + +/** + * Get the base URL for the server + * In production, this would be configured via environment variables + */ +import { base, port } from '../server.js'; + +const getBaseUrl = () => { + return `${base}:${port}`; +}; + +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 { + // Call our mock "external" API endpoint + const response = await fetch(`${getBaseUrl()}/api/external/post/${id}`); + + if (!response.ok) { + const error = await response.json(); + return res.status(response.status).json(error); + } + + const blogpost = await response.json(); + + // Return the blogpost + res.json(blogpost); + } catch (error) { + console.error('Error fetching post:', error); + res.status(500).json({ message: 'Failed to fetch post' }); + } +}; + +export const getComments = async ({ query: { postId } }, res) => { + if (!postId) { + return res.status(400).json({ message: 'Missing postId parameter' }); + } + + try { + // Call our mock "external" API endpoint + const response = await fetch( + `${getBaseUrl()}/api/external/comments?postId=${postId}`, + ); + + if (!response.ok) { + const error = await response.json(); + return res.status(response.status).json(error); + } + + const comments = await response.json(); + + // Return the comments + return res.json(comments); + } catch (error) { + console.error('Error fetching comments:', error); + return res.status(500).json({ message: 'Failed to fetch comments' }); + } +}; + +export default { + getComments, + getPost, +}; diff --git a/src/test/server.js b/src/test/server.js index 6405567..81f9a0b 100644 --- a/src/test/server.js +++ b/src/test/server.js @@ -6,6 +6,7 @@ */ import { setupServer } from 'msw/node'; +import { http } from 'msw'; import { getNetworking } from './networking'; import { getRouter } from './router'; import { getRoutes } from '../routes/routes'; @@ -14,9 +15,43 @@ const _setupServer = (...args) => { const mocks = []; const networking = getNetworking(); + // Set up internal API routes (including external mock routes) getRoutes(getRouter(mocks, networking)); - const server = setupServer(...mocks, ...args); + // Mock external API calls from postHandlers to our local external endpoints + // These intercept the fetch calls made by postHandlers.js + const externalMocks = [ + http.get('http://localhost:5173/api/external/post/:id', ({ params }) => { + return Response.json({ + id: params.id, + title: 'Test Post Title', + body: 'Test post body content', + userId: 1, + }); + }), + http.get('http://localhost:5173/api/external/comments', ({ request }) => { + const url = new URL(request.url); + const postId = url.searchParams.get('postId'); + return Response.json([ + { + id: 1, + postId: parseInt(postId), + name: 'Test Comment 1', + email: 'test1@example.com', + body: 'Test comment 1 body', + }, + { + id: 2, + postId: parseInt(postId), + name: 'Test Comment 2', + email: 'test2@example.com', + body: 'Test comment 2 body', + }, + ]); + }), + ]; + + const server = setupServer(...mocks, ...externalMocks, ...args); server.networking = networking; return server; }; From 6df9feaef448e31105e0c872892bb0312bee96e7 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Thu, 20 Nov 2025 14:22:24 +0100 Subject: [PATCH 2/8] refactor(server): extract server configuration to a separate module - Moved server configuration constants (port and base) from server.js to a new config/server-config.js file for better organization and testability. - Updated imports in postHandlers.js and test files to reflect the new configuration module. - Adjusted response handling in tests to use HttpResponse from msw. Resolves: no-ticket --- src/config/server-config.js | 11 +++++++++++ src/server.js | 3 +-- src/service/postHandlers.js | 2 +- src/test/router.js | 4 ++-- src/test/server.js | 6 +++--- 5 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 src/config/server-config.js diff --git a/src/config/server-config.js b/src/config/server-config.js new file mode 100644 index 0000000..af20359 --- /dev/null +++ b/src/config/server-config.js @@ -0,0 +1,11 @@ +/** + * 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. + */ + +// Server configuration constants +// Extracted to avoid importing the full server during tests +export const port = process.env.PORT || 5173; +export const base = process.env.BASE || 'http://localhost'; diff --git a/src/server.js b/src/server.js index 2d79cb7..fff0164 100644 --- a/src/server.js +++ b/src/server.js @@ -9,11 +9,10 @@ import fs from 'node:fs/promises'; import express from 'express'; import { Transform } from 'node:stream'; import { getRoutes } from './routes/routes.js'; +import { port, base } from './config/server-config.js'; // Constants const isProduction = process.env.NODE_ENV === 'production'; -export const port = process.env.PORT || 5173; -export const base = process.env.BASE || 'http://localhost'; const ABORT_DELAY = 10000; // Create http server diff --git a/src/service/postHandlers.js b/src/service/postHandlers.js index c18151c..fca5d6c 100644 --- a/src/service/postHandlers.js +++ b/src/service/postHandlers.js @@ -14,7 +14,7 @@ * Get the base URL for the server * In production, this would be configured via environment variables */ -import { base, port } from '../server.js'; +import { base, port } from '../config/server-config.js'; const getBaseUrl = () => { return `${base}:${port}`; diff --git a/src/test/router.js b/src/test/router.js index 6b9f9a7..24672bd 100644 --- a/src/test/router.js +++ b/src/test/router.js @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { http } from 'msw'; +import { http, HttpResponse } from 'msw'; export const getRouter = (mocks, networking) => { const apiRoute = (verb, path, handler) => { @@ -18,7 +18,7 @@ export const getRouter = (mocks, networking) => { const res = { json: (data) => { networking.removeRequest(path); - return Response.json(data); + return HttpResponse.json(data); }, }; diff --git a/src/test/server.js b/src/test/server.js index 81f9a0b..6c2b243 100644 --- a/src/test/server.js +++ b/src/test/server.js @@ -5,8 +5,8 @@ * LICENSE file in the root directory of this source tree. */ +import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import { http } from 'msw'; import { getNetworking } from './networking'; import { getRouter } from './router'; import { getRoutes } from '../routes/routes'; @@ -22,7 +22,7 @@ const _setupServer = (...args) => { // These intercept the fetch calls made by postHandlers.js const externalMocks = [ http.get('http://localhost:5173/api/external/post/:id', ({ params }) => { - return Response.json({ + return HttpResponse.json({ id: params.id, title: 'Test Post Title', body: 'Test post body content', @@ -32,7 +32,7 @@ const _setupServer = (...args) => { http.get('http://localhost:5173/api/external/comments', ({ request }) => { const url = new URL(request.url); const postId = url.searchParams.get('postId'); - return Response.json([ + return HttpResponse.json([ { id: 1, postId: parseInt(postId), From 65bbac46657f23fa6df35646765a398d2c9d4bc0 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Thu, 20 Nov 2025 15:45:24 +0100 Subject: [PATCH 3/8] feat(nav, dashboard): enhance navigation and dashboard with dynamic routing and parameter handling - Added a utility function to determine active menu items based on current path, supporting dynamic routes. - Updated NavHeaderItems to utilize the new active path logic. - Enhanced Dashboard component to access and display URL path and query parameters. - Configured routing to support dynamic dashboard paths with parameters. Resolves: no-ticket --- src/components/nav/NavHeaderItems.jsx | 16 +++++++-- src/pages/dashboard/Dashboard.jsx | 52 ++++++++++++++++++++++++++- src/routes/config.js | 7 ++++ 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/components/nav/NavHeaderItems.jsx b/src/components/nav/NavHeaderItems.jsx index 90d4d9d..8138d1b 100644 --- a/src/components/nav/NavHeaderItems.jsx +++ b/src/components/nav/NavHeaderItems.jsx @@ -1,6 +1,18 @@ import { HeaderMenu, HeaderMenuItem } from '@carbon/react'; import { Link as RouterLink } from 'react-router'; +/** + * Check if a menu path should be active based on the current path + * Handles both exact matches and dynamic route segments + */ +const isPathActive = (menuPath, currentPath) => { + if (!menuPath || !currentPath) return false; + // Exact match + if (menuPath === currentPath) return true; + // Match dynamic routes: /dashboard should be active for /dashboard/123 + return currentPath.startsWith(`${menuPath}/`); +}; + export const NavHeaderItems = ({ routesInHeader, currentPath }) => ( <> {routesInHeader.map(({ path, carbon }) => @@ -16,7 +28,7 @@ export const NavHeaderItems = ({ routesInHeader, currentPath }) => ( as={RouterLink} to={subRoute.path} key={subRoute.path} - isActive={subRoute.path === currentPath} + isActive={isPathActive(subRoute.path, currentPath)} > {subRoute.carbon.label} @@ -27,7 +39,7 @@ export const NavHeaderItems = ({ routesInHeader, currentPath }) => ( as={RouterLink} key={path} to={path} - isActive={path === currentPath} + isActive={isPathActive(path, currentPath)} > {carbon?.label} diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 586bdd4..da7f98c 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -5,8 +5,9 @@ * LICENSE file in the root directory of this source tree. */ -import { Column, Grid, Tile } from '@carbon/react'; +import { Column, Grid, Link, Tile, Stack, Tag } from '@carbon/react'; import { useState } from 'react'; +import { useParams, useSearchParams } from 'react-router'; import { Footer } from '../../components/footer/Footer'; import { PageLayout } from '../../layouts/page-layout'; @@ -31,6 +32,15 @@ const NumberTile = () => { }; const Dashboard = () => { + // Access path parameters (e.g., /dashboard/1234 -> id = "1234") + const params = useParams(); + const { id } = params; + + // Access query parameters (e.g., /dashboard/1234?q=xxx&name=John -> q = "xxx", name = "John") + const [searchParams] = useSearchParams(); + const queryParam = searchParams.get('q'); + const nameParam = searchParams.get('name'); + return ( { + {/* Example: Display URL parameters when present */} + + + + URL Parameters Example + {nameParam && ( +

Hello {nameParam}! 👋

+ )} +

+ This demonstrates how to access both path parameters and query + parameters from the URL.
+ Try accessing:{' '} + + /dashboard/1234?q=xxx&name=Anne + +

+ + {id && ( +
+ Path Parameter (id):{' '} + {id} +
+ )} + {queryParam && ( +
+ Query Parameter (q):{' '} + {queryParam} +
+ )} + {nameParam && ( +
+ Query Parameter (name):{' '} + {nameParam} +
+ )} +
+
+
+
+ diff --git a/src/routes/config.js b/src/routes/config.js index 1a0b0c5..86b529e 100644 --- a/src/routes/config.js +++ b/src/routes/config.js @@ -44,6 +44,10 @@ export const routes = [ inHeader: true, }, }, + { + path: '/dashboard/:id', + element: Dashboard, + }, { path: '/link-1', element: Placeholder, @@ -154,6 +158,9 @@ const routesProcessed = routes.map((route) => { const path = route.path || route.carbon.virtualPath; const subMenu = routes.filter((subRoute) => { + // Only include routes with carbon config in navigation menus + if (!subRoute.carbon) return false; + const subPath = subRoute.path || subRoute.carbon.virtualPath; const childPath = new RegExp(`^${path}/[^/]+$`); // match direct parent only From 537d382b3db90ecdf236d14b60e3e5b900e64001 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Thu, 20 Nov 2025 17:28:15 +0100 Subject: [PATCH 4/8] fix: query param typo fix --- src/pages/dashboard/Dashboard.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index da7f98c..72bace1 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -62,8 +62,8 @@ const Dashboard = () => { This demonstrates how to access both path parameters and query parameters from the URL.
Try accessing:{' '} - - /dashboard/1234?q=xxx&name=Anne + + /dashboard/1234?q=xyz&name=Anne

From 927aa7508e26a5d12c61b51c455d22e3873eb5f9 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Fri, 21 Nov 2025 13:28:21 +0100 Subject: [PATCH 5/8] fix: sentence case fixes and some pollishing Resolves: no-ticket --- src/pages/dashboard/Dashboard.jsx | 32 +++++++++++++++---------------- src/pages/welcome/Welcome.jsx | 6 +++--- src/service/externalHandlers.js | 4 ++-- src/test/server.js | 11 ++++++----- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/pages/dashboard/Dashboard.jsx b/src/pages/dashboard/Dashboard.jsx index 72bace1..2c00aba 100644 --- a/src/pages/dashboard/Dashboard.jsx +++ b/src/pages/dashboard/Dashboard.jsx @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { Column, Grid, Link, Tile, Stack, Tag } from '@carbon/react'; +import { Column, Grid, Link, Tile, Stack } from '@carbon/react'; import { useState } from 'react'; import { useParams, useSearchParams } from 'react-router'; @@ -54,7 +54,7 @@ const Dashboard = () => { - URL Parameters Example + URL parameters example {nameParam && (

Hello {nameParam}! 👋

)} @@ -66,26 +66,26 @@ const Dashboard = () => { /dashboard/1234?q=xyz&name=Anne

- +
{id && ( -
- Path Parameter (id):{' '} - {id} -
+ <> +
Path parameter detected (id):
+
{id}
+ )} {queryParam && ( -
- Query Parameter (q):{' '} - {queryParam} -
+ <> +
Query parameter detected (q):
+
{queryParam}
+ )} {nameParam && ( -
- Query Parameter (name):{' '} - {nameParam} -
+ <> +
Query parameter detected (name):
+
{nameParam}
+ )} - +
diff --git a/src/pages/welcome/Welcome.jsx b/src/pages/welcome/Welcome.jsx index 80109ec..e907875 100644 --- a/src/pages/welcome/Welcome.jsx +++ b/src/pages/welcome/Welcome.jsx @@ -151,16 +151,16 @@ const Welcome = () => {

- UI Layer - PostComponent.jsx manages React + UI layer - PostComponent.jsx manages React state and renders the data using Carbon Design components - API Layer - Client-side functions in{' '} + API layer - Client-side functions in{' '} api/message.js handle HTTP requests to our Express backend - Service Layer - Server-side handlers in{' '} + Service layer - Server-side handlers in{' '} service/postHandlers.js proxy requests to external APIs. diff --git a/src/service/externalHandlers.js b/src/service/externalHandlers.js index f1694cb..263b210 100644 --- a/src/service/externalHandlers.js +++ b/src/service/externalHandlers.js @@ -25,13 +25,13 @@ const mockPosts = { }, 2: { id: 2, - title: 'Getting Started with React Router', + title: 'Getting started with React router', body: 'This is just a sample post. React Router is a standard library for routing in React applications.', userId: 1, }, 3: { id: 3, - title: 'Building Modern Web Applications', + title: 'Building modern web applications', body: 'This is just a sample post. Modern web development requires understanding of various tools and frameworks.', userId: 2, }, diff --git a/src/test/server.js b/src/test/server.js index 6c2b243..c94e480 100644 --- a/src/test/server.js +++ b/src/test/server.js @@ -10,6 +10,7 @@ import { setupServer } from 'msw/node'; import { getNetworking } from './networking'; import { getRouter } from './router'; import { getRoutes } from '../routes/routes'; +import { port, base } from '../config/server-config.js'; const _setupServer = (...args) => { const mocks = []; @@ -21,29 +22,29 @@ const _setupServer = (...args) => { // Mock external API calls from postHandlers to our local external endpoints // These intercept the fetch calls made by postHandlers.js const externalMocks = [ - http.get('http://localhost:5173/api/external/post/:id', ({ params }) => { + http.get(`${base}:${port}/api/external/post/:id`, ({ params }) => { return HttpResponse.json({ id: params.id, - title: 'Test Post Title', + title: 'Test post title', body: 'Test post body content', userId: 1, }); }), - http.get('http://localhost:5173/api/external/comments', ({ request }) => { + http.get(`${base}:${port}/api/external/comments`, ({ request }) => { const url = new URL(request.url); const postId = url.searchParams.get('postId'); return HttpResponse.json([ { id: 1, postId: parseInt(postId), - name: 'Test Comment 1', + name: 'Test comment 1', email: 'test1@example.com', body: 'Test comment 1 body', }, { id: 2, postId: parseInt(postId), - name: 'Test Comment 2', + name: 'Test comment 2', email: 'test2@example.com', body: 'Test comment 2 body', }, From fd132f630783bbeeea455bafaa18ae571a686f4a Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Fri, 21 Nov 2025 13:44:32 +0100 Subject: [PATCH 6/8] fix: update post title in externalHandlers --- src/service/externalHandlers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/externalHandlers.js b/src/service/externalHandlers.js index 263b210..d323003 100644 --- a/src/service/externalHandlers.js +++ b/src/service/externalHandlers.js @@ -16,7 +16,7 @@ const mockPosts = { 1: { id: 1, - title: 'Introduction to Carbon Design System', + title: 'What is Carbon?', body: ` This is just a sample post. Carbon is IBM's open source design system for products and digital experiences. With the IBM Design Language as its foundation, the system consists of working code, design tools and resources, From e037b454eb6cdf18cf5cc7c03a92dba026f2f11b Mon Sep 17 00:00:00 2001 From: Lee Chase Date: Fri, 21 Nov 2025 18:30:45 +0000 Subject: [PATCH 7/8] feat: fix preview:prod --- package-lock.json | 43 ++++----------------------------- src/config/server-config.js | 3 ++- src/routes/utils.js | 32 ++++++++++++++++++------ src/server.js | 4 +-- src/service/externalHandlers.js | 8 ++++-- src/service/postHandlers.js | 12 ++++++--- 6 files changed, 48 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93ec08e..dd62542 100644 --- a/package-lock.json +++ b/package-lock.json @@ -172,7 +172,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -551,7 +550,6 @@ "integrity": "sha512-J8hh5li0Q0RRS6IGg+MVPQVfjp1ePxzQsyMyjQOHLux8i8HwiWJmKLQ+4lKMzRXorLG2RtEoPVl00EaKlQEgKQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@carbon/layout": "^11.43.0", "@ibm/telemetry-js": "^1.5.0" @@ -649,7 +647,6 @@ "integrity": "sha512-aOUNqVv/5TGhNTn1HV+620ZlqhE7+Chs0TJoxwe/CCsLOdziCX9st3c5inyINPZDyvDK46mas1RmqvZwNYe/mA==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@ibm/telemetry-js": "^1.5.0" } @@ -660,7 +657,6 @@ "integrity": "sha512-bjFzY8Wy5Umj+g41ZGj3L3b/z2gDBDKfzfc9M3ZAHaj73PZ7Z/Z5jT0IFlihEv8wUwZfbhbtnb4jeNhckfLINA==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@ibm/telemetry-js": "^1.5.0" } @@ -671,7 +667,6 @@ "integrity": "sha512-BISe++dQ3wz0GRHvqM8l+gtQbqgDrDMpzcQ246qCp+X8BA0+XnH8nL/rLhAL8SpRpLOBqfk/JPiKPlWeYzLb6Q==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.3", "@carbon/feature-flags": "0.32.0", @@ -757,7 +752,6 @@ "integrity": "sha512-Kq4gan/qQCErgt46OpA2EU91g1CEsH+3duTBUZmoLwGqlPd3AwzYvoIxisUCrMSYut5J9CuxNxEBnCD8Iv9ghg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@carbon/colors": "^11.42.0", "@carbon/layout": "^11.43.0", @@ -772,7 +766,6 @@ "integrity": "sha512-5SwbuMj7VO7QAxB273MQVMoVLEvwcMVflhmriWA4I+YWMDwtoprPHiXseO6ueoRGJp+TOqnItvkPMPOdKz5sjQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@carbon/grid": "^11.45.0", "@carbon/layout": "^11.43.0", @@ -1006,8 +999,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", @@ -1147,16 +1139,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", @@ -1354,8 +1344,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", @@ -1503,7 +1492,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1550,7 +1538,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1619,7 +1606,6 @@ "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", "license": "MIT", - "peer": true, "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", @@ -4416,7 +4402,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -4567,7 +4552,6 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4578,7 +4562,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4788,7 +4771,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5355,7 +5337,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -6630,8 +6611,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", @@ -7117,7 +7097,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7178,7 +7157,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -9497,7 +9475,6 @@ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -11259,7 +11236,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -11343,7 +11319,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" @@ -11375,7 +11350,6 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -11615,7 +11589,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11625,7 +11598,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" }, @@ -12126,7 +12098,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.93.3.tgz", "integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -13216,7 +13187,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", @@ -14250,7 +14220,6 @@ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -14326,7 +14295,6 @@ "integrity": "sha512-2Fqty3MM9CDwOVet/jaQalYlbcjATZwPYGcqpiYQqgQ/dLC7GuHdISKgTYIVF/kaishKxLzleKWWfbSDklyIKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.10", "@vitest/mocker": "4.0.10", @@ -14988,7 +14956,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/config/server-config.js b/src/config/server-config.js index af20359..02c09c8 100644 --- a/src/config/server-config.js +++ b/src/config/server-config.js @@ -8,4 +8,5 @@ // Server configuration constants // Extracted to avoid importing the full server during tests export const port = process.env.PORT || 5173; -export const base = process.env.BASE || 'http://localhost'; +export const base = process.env.BASE || '/'; +export const baseUrl = `http://localhost:${port}`; diff --git a/src/routes/utils.js b/src/routes/utils.js index ef3b786..4e99354 100644 --- a/src/routes/utils.js +++ b/src/routes/utils.js @@ -7,6 +7,21 @@ import { routes } from './config.js'; +/** + * Converts a route pattern to a regex that matches dynamic parameters + * @param {string} pattern - Route pattern like '/dashboard/:id' + * @returns {RegExp} Regular expression to match the pattern + */ +function patternToRegex(pattern) { + // Escape special regex characters except for :param patterns + const regexPattern = pattern + .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars + .replace(/\*/g, '.*') // Convert * to match anything + .replace(/:([^/]+)/g, '([^/]+)'); // Convert :param to capture group + + return new RegExp(`^${regexPattern}$`); +} + /** * Finds the matching route for a given URL path * @param {string} pathname - The URL path to match against @@ -16,23 +31,26 @@ export function findMatchingRoute(pathname) { // Clean up the pathname const path = pathname.startsWith('/') ? pathname : `/${pathname}`; - // Try to find an exact match first + // Try to find a match for (const route of routes) { if (!route.path) continue; // Skip the wildcard route for now if (route.path === '*') continue; - // Simple path matching logic - if ( - route.path === path || - (route.path.endsWith('*') && path.startsWith(route.path.slice(0, -1))) - ) { + // Check for exact match first (most efficient) + if (route.path === path) { + return route; + } + + // Check for pattern match (handles :param and *) + const regex = patternToRegex(route.path); + if (regex.test(path)) { return route; } } - // If no exact match, find the wildcard route (404) + // If no match, find the wildcard route (404) return routes.find((route) => route.path === '*') || null; } diff --git a/src/server.js b/src/server.js index fff0164..01689db 100644 --- a/src/server.js +++ b/src/server.js @@ -9,7 +9,7 @@ import fs from 'node:fs/promises'; import express from 'express'; import { Transform } from 'node:stream'; import { getRoutes } from './routes/routes.js'; -import { port, base } from './config/server-config.js'; +import { port, base, baseUrl } from './config/server-config.js'; // Constants const isProduction = process.env.NODE_ENV === 'production'; @@ -108,5 +108,5 @@ app.use('*all', async (req, res) => { // Start http server app.listen(port, () => { - console.log(`Server started at: ${base}:${port}`); + console.log(`Server started at: ${baseUrl}`); }); diff --git a/src/service/externalHandlers.js b/src/service/externalHandlers.js index d323003..4d6f521 100644 --- a/src/service/externalHandlers.js +++ b/src/service/externalHandlers.js @@ -79,7 +79,9 @@ const mockComments = { * Mock external API handler for fetching a single post * Simulates an external service response */ -export const getExternalPost = async ({ params: { id } }, res) => { +export const getExternalPost = async (req, res) => { + const { id } = req.params; + // Validate that id is a positive integer if (!/^\d+$/.test(id)) { return res.status(400).json({ message: 'Invalid post id' }); @@ -100,7 +102,9 @@ export const getExternalPost = async ({ params: { id } }, res) => { * Mock external API handler for fetching comments for a post * Simulates an external service response */ -export const getExternalComments = async ({ query: { postId } }, res) => { +export const getExternalComments = async (req, res) => { + const { postId } = req.query; + // Validate that postId is provided and is a positive integer if (!postId || !/^\d+$/.test(postId)) { return res.status(400).json({ message: 'Invalid or missing postId' }); diff --git a/src/service/postHandlers.js b/src/service/postHandlers.js index fca5d6c..c4cd030 100644 --- a/src/service/postHandlers.js +++ b/src/service/postHandlers.js @@ -14,13 +14,15 @@ * Get the base URL for the server * In production, this would be configured via environment variables */ -import { base, port } from '../config/server-config.js'; +import { baseUrl } from '../config/server-config.js'; const getBaseUrl = () => { - return `${base}:${port}`; + return baseUrl; }; -export const getPost = async ({ params: { id } }, res) => { +export const getPost = async (req, res) => { + const { id } = req.params; + // Validate that id is a positive integer if (!/^\d+$/.test(id)) { return res.status(400).json({ message: 'Invalid post id' }); @@ -45,7 +47,9 @@ export const getPost = async ({ params: { id } }, res) => { } }; -export const getComments = async ({ query: { postId } }, res) => { +export const getComments = async (req, res) => { + const { postId } = req.query; + if (!postId) { return res.status(400).json({ message: 'Missing postId parameter' }); } From 9a3fb40f6f3ef90b63903190caa2c8cf93389954 Mon Sep 17 00:00:00 2001 From: Balint Lendvai Date: Mon, 24 Nov 2025 15:35:19 +0100 Subject: [PATCH 8/8] feat: enhance PostComponent to accept dynamic postId prop also added JSDoc for PostComponent Resolves: no-ticket --- src/pages/welcome/Welcome.jsx | 2 +- src/pages/welcome/post/PostComponent.jsx | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pages/welcome/Welcome.jsx b/src/pages/welcome/Welcome.jsx index e907875..7b6d34e 100644 --- a/src/pages/welcome/Welcome.jsx +++ b/src/pages/welcome/Welcome.jsx @@ -172,7 +172,7 @@ const Welcome = () => {

- +
diff --git a/src/pages/welcome/post/PostComponent.jsx b/src/pages/welcome/post/PostComponent.jsx index a6dade8..a15d034 100644 --- a/src/pages/welcome/post/PostComponent.jsx +++ b/src/pages/welcome/post/PostComponent.jsx @@ -2,22 +2,29 @@ import { getComments, getPost } from '../../../api/message.js'; import { Heading, Section, Tile, Stack, Layer } from '@carbon/react'; import { useEffect, useState } from 'react'; -const PostComponent = () => { +/** + * Renders a single blog post and its comments. + * + * @param {Object} props + * @param {number} [props.postId] - The ID of the post to display. + * If not provided, defaults to 1 and renders the first post. + */ +const PostComponent = ({ postId = 1 }) => { const [post, setPost] = useState(); const [comments, setComments] = useState([]); - const loadPost = async () => { + const loadPost = async (id) => { try { - const post = await getPost(1); + const post = await getPost(id); setPost(post); } catch { setPost('Failed to load message'); } }; - const loadComments = async () => { + const loadComments = async (id) => { try { - const comments = await getComments(1); + const comments = await getComments(id); setComments(comments); } catch { setComments([]); @@ -26,11 +33,11 @@ const PostComponent = () => { useEffect(() => { const loadData = async () => { - await loadPost(); - await loadComments(); + await loadPost(postId); + await loadComments(postId); }; loadData(); - }, []); + }, [postId]); return (