React Dashboard for Financial Advisor API
Build a React frontend that connects to your Spring Boot financial advisor API.
Introduction
30 Second Summary
Managing financial portfolios through terminal commands works, but it is not how real users interact with software. A visual interface lets advisors click through clients and portfolios instead of memorizing API endpoints.
In this project, you will build a React dashboard that connects to your existing Spring Boot API. You will navigate advisors, clients, and portfolios through the browser and manage securities with add and delete functionality.
What You'll Build
A React single-page application that serves as the frontend dashboard for your Financial Advisor system, letting you drill down from advisors to clients to portfolios to securities.
By the end of this project, you'll have:
- A Vite-scaffolded React application running on localhost:5173 that communicates with your Spring Boot backend.
- A CORS configuration class on your backend that allows cross-origin requests from the React frontend.
- An interactive dashboard with advisor/client/portfolio selection panels, a securities table, and an add-security form with full create and delete operations.
- Secret Mission: Add an edit form that lets you update existing securities in-place using PUT requests.
Are there any prerequisites?
Yes. You need to have completed the Financial Advisor API with Spring Boot project with Portfolio and Security entities added. Your Spring Boot API should be running on localhost:8080 with endpoints for advisors, clients, portfolios, and securities.
You should also have IntelliJ IDEA and OpenJDK 21 installed from that previous project.
Install Node.js, VS Code, and Scaffold the React App
Your Spring Boot API is running and ready to serve data. Now you need a frontend application that can display that data in a browser.
In this step, you will scaffold a React project using Vite, a modern build tool that gives you instant dev server startup and fast hot-reloading. Then you will clean out the default template code so you have a blank canvas to build on.
In this step, get ready to:
- Scaffold a new React project with Vite.
- Start the Vite dev server and confirm it loads in the browser.
- Clean up the default template files to create a blank starting point.
Scaffold the React project with Vite
Vite scaffolds a ready-to-run React project in seconds. The project will live in a folder called advisor-dashboard in your home directory.
- Open a new terminal window. Your first terminal is still running the Spring Boot backend, so you need a fresh one for frontend commands.
- Navigate to your home directory by running this command:
cd ~
- Scaffold the React project by running this command:
npm create vite@latest advisor-dashboard -- --template react
What does this command do?
- npm create vite@latest runs the Vite project scaffolder using the latest version.
- advisor-dashboard is the name of the folder it creates for your project.
- -- --template react tells Vite to use the React JavaScript template so you skip any interactive prompts.
- Move into the new project folder and install its dependencies by running these commands:
cd advisor-dashboard
npm install
- You should see a progress bar as packages download. Once it finishes, you will see a summary of installed packages.
- Start the Vite dev server by running:
npm run dev
- Your terminal should display a local URL. Open your browser and go to http://localhost:5173.
- You should see the default Vite + React welcome page with a spinning logo and a counter button.
Don't see the page?
- Check that your terminal shows a local URL in its output. If the port is different from 5173, use the URL shown in your terminal instead.
- Make sure you ran npm install before npm run dev. Without installed dependencies, the dev server cannot start.
Need more help?
Open the project in VS Code and clean up the template
The default Vite template comes with demo content you do not need. You will replace the main files with clean starting points so you can build the dashboard from scratch.
- Keep the dev server running in your current terminal. It will auto-refresh the browser as you save changes.
- Press the Super key (the Windows key on your keyboard) to open the Activities overview.
- Type Terminal and open a new terminal window.
- Open the project in VS Code by running this command:
code ~/advisor-dashboard
Why a second terminal?
Your first terminal is occupied running the Vite dev server. Opening a second terminal lets you launch VS Code without stopping the server.
From here on, you can also use VS Code's built-in terminal (Ctrl+`) for any future commands.
- In VS Code's Explorer sidebar on the left, expand the src folder.
- Click on App.jsx to open it.
- Select all the content (Ctrl+A) and replace it with the following code:
import { useState, useEffect } from 'react';
import './App.css';
function App() {
return (
<div>
</div>
);
}
export default App;
What does this code do?
- useState and useEffect are React hooks you will use in later steps to manage data and fetch from the API.
- The App function returns an empty <div> for now. This is your blank canvas.
- export default App makes this component available to main.jsx, which renders it into the page.
- Save App.jsx (Ctrl+S).
- Now open App.css in the src folder.
- Select all the content (Ctrl+A) and delete it so the file is completely empty.
- Save App.css.
- Open main.jsx in the src folder.
- Select all the content and replace it with the following code:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
What changed in main.jsx?
The original template imported a global CSS file you do not need. This clean version only imports your App component and renders it into the page.
- Save main.jsx.
- Open index.html in the project root (it sits at the top level of the Explorer sidebar, not inside src).
- Select all the content and replace it with the following code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Financial Advisor Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
What changed in index.html?
The original template included a favicon link and a generic title. This version removes the favicon reference and sets the browser tab title to Financial Advisor Dashboard.
- Save index.html.
- Switch to your browser. The page should now be completely blank (white background, no text, no logos).
- Check the browser tab title. It should read Financial Advisor Dashboard.
Still seeing the old Vite page?
- Make sure you saved all four files: App.jsx, App.css, main.jsx, and index.html. Unsaved changes will not trigger a refresh.
- If the browser still shows old content, try a hard refresh with Ctrl+Shift+R.
- Check the terminal running your dev server for any red error messages. A typo in main.jsx or App.jsx will show there.
Need more help?
✔️ Awesome, I've got everything!
Great. Make sure all four files are saved before moving on.
ⓧ I'd like to double check the full code
Here are all four files as they should look at this point.
import { useState, useEffect } from 'react';
import './App.css';
function App() {
return (
<div>
</div>
);
}
export default App;
src/App.css should be completely empty with no content at all.
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Financial Advisor Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Your React project is scaffolded, cleaned up, and running. Next up, you will configure your Spring Boot backend to accept requests from this frontend.
Configure CORS on the Spring Boot Backend
Your React app and Spring Boot API run on different ports. The browser blocks requests between different origins by default, so your frontend on localhost:5173 cannot talk to your backend on localhost:8080 without explicit permission.
In this step, you will add a CORS configuration class to the Spring Boot backend. This tells the backend to accept requests from your React dev server, unlocking the connection between your two apps.
In this step, get ready to:
- Create a CORS configuration class in the Spring Boot project.
- Verify the React app can fetch data from the API without errors.
Create the WebConfig.java CORS configuration
The Spring Boot backend needs a configuration class that explicitly allows cross-origin requests from the React dev server. Without this, every fetch call from your frontend will be blocked by the browser.
- Switch back to IntelliJ IDEA where your Spring Boot project is open.
- In the Project tool window on the left, expand src/main/java/com/example/advisorapi.
- Right-click the advisorapi package and select New, then Package.
- Type config and press Enter.
- Right-click the new config package and select New, then Java Class.
- Type WebConfig and press Enter.
- Replace the entire contents of WebConfig.java with this code:
package com.example.advisorapi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// Marks this class as a Spring configuration bean
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// Allow the React dev server to call all API endpoints
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
What does this code do?
This class tells Spring Boot which external origins are allowed to make requests to the API.
- @Configuration registers this class as a Spring bean that runs at startup.
- addMapping("/api/**") applies the CORS rules to every endpoint under /api/.
- allowedOrigins("http://localhost:5173") permits requests only from the Vite dev server.
- allowedMethods lists the HTTP verbs the frontend can use. You need all four for viewing, creating, updating, and deleting securities.
- Save WebConfig.java with Cmd+S (macOS) or Ctrl+S (Windows).
Why a global config instead of @CrossOrigin?
Spring offers a @CrossOrigin annotation you can put on individual controllers. Your project has multiple controllers (advisors, clients, portfolios, securities), so a single global config class is cleaner than repeating the annotation on every controller.
Seeing a red underline on imports?
Make sure the package declaration at the top is package com.example.advisorapi.config; and that the file is inside the config folder under advisorapi. If IntelliJ shows unresolved imports, try File then Invalidate Caches / Restart.
help me fix red underlines on my Spring Boot WebConfig imports
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before moving on.
ⓧ I'd like to double check the full code
package com.example.advisorapi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:5173")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*");
}
}
Restart the backend and verify CORS
Spring Boot only loads configuration classes at startup. Since you added a new class while the app was already running, you need to restart it for the CORS rules to take effect.
- In IntelliJ, find the terminal or Run panel where your Spring Boot app is running.
- Press Ctrl+C to stop the running server.
- Restart the application by running this command:
./mvnw spring-boot:run
Wait for the Spring Boot startup log to appear. You should see a line similar to Started AdvisorApiApplication in the console output.
- Switch to your browser and navigate to http://localhost:5173 (your React app).
- Open the browser developer console with Cmd+Option+J (macOS) or Ctrl+Shift+J (Windows).
- Paste this fetch command into the console and press Enter:
fetch('http://localhost:8080/api/advisors').then(r => r.json()).then(d => console.log(d))
You should see a JSON array of advisor objects logged in the console. This confirms CORS is working and your React app can communicate with the Spring Boot backend.
Seeing a CORS error in the console?
Check that you restarted the Spring Boot app after saving WebConfig.java. The CORS config only loads at startup.
Verify the origin in your config matches exactly: http://localhost:5173 with no trailing slash. Also confirm the Spring Boot app is running on port 8080 and not another port.
help me debug a CORS error between my React and Spring Boot apps
Your backend is now ready to accept requests from the React frontend. Next up, you will build the dashboard UI that lets advisors browse clients and portfolios.
Build the Advisor, Client, and Portfolio Selection View
Your Spring Boot API is running and your React app is connected. Now it is time to build the main interface that lets advisors navigate through their data.
The dashboard follows a drill-down pattern: pick an advisor, see their clients, pick a client, see their portfolios. Each selection triggers a new API call and updates the view. You will use React hooks to manage this cascading state.
In this step, get ready to:
- Fetch and display a list of advisors when the page loads.
- Wire up cascading selection so clicking an advisor shows clients, clicking a client shows portfolios.
- Style the dashboard as a three-panel layout with visual highlighting.
Fetch the advisors list on page load
React components use useState to hold data that changes over time and useEffect to run code when the component first appears on screen. You will combine both to load the advisors from your API as soon as the page opens.
- In VS Code, open src/App.jsx and replace the entire file contents with the following code:
import { useState, useEffect } from 'react';
import './App.css';
// Base URL for all API calls to the Spring Boot backend
const API_BASE = 'http://localhost:8080/api';
function App() {
// State for the list of advisors and which one is selected
const [advisors, setAdvisors] = useState([]);
const [selectedAdvisor, setSelectedAdvisor] = useState(null);
const [clients, setClients] = useState([]);
const [selectedClient, setSelectedClient] = useState(null);
const [portfolios, setPortfolios] = useState([]);
const [selectedPortfolio, setSelectedPortfolio] = useState(null);
const [securities, setSecurities] = useState([]);
// Form data state for adding securities (used in the next step)
const [formData, setFormData] = useState({
name: '',
category: '',
purchaseDate: '',
purchasePrice: '',
quantity: ''
});
// Fetch all advisors when the component first mounts
useEffect(() => {
fetch(`${API_BASE}/advisors`)
.then(res => res.json())
.then(data => setAdvisors(data))
.catch(err => console.error('Failed to fetch advisors:', err));
}, []);
return (
<div className="app">
<h1>Financial Advisor Dashboard</h1>
</div>
);
}
export default App;
What does this code do?
This sets up the foundation for your entire dashboard.
- API_BASE stores the backend URL so you do not repeat it in every fetch call.
- Each useState pair holds one piece of the drill-down chain: advisors, clients, portfolios, and the currently selected item at each level.
- The useEffect with an empty dependency array ([]) runs exactly once when the page loads, fetching the advisors list from your Spring Boot API.
- Save src/App.jsx.
- Switch to your browser at http://localhost:5173. You should see the heading "Financial Advisor Dashboard" on a blank page. Open the browser console (F12 then the Console tab) and confirm there are no errors.
Seeing a blank page or console errors?
- If you see a CORS error in the console, make sure your Spring Boot app is still running on port 8080 with the WebConfig class from the previous step.
- If the page is completely blank with no heading, check that you saved src/App.jsx and that the Vite dev server is still running in your terminal.
Help me debug my React app - I see errors in the console after adding useState and useEffect.
Add cascading selection handlers
Each level of the hierarchy needs a click handler that fetches the next level's data. When an advisor is clicked, you fetch their clients. When a client is clicked, you fetch their portfolios. Each handler also resets the selections below it so stale data does not persist.
- In src/App.jsx, find the closing }, []); of the useEffect block. Add the following handler functions directly below it, above the return statement:
function handleAdvisorClick(advisor) {
setSelectedAdvisor(advisor);
setSelectedClient(null);
setPortfolios([]);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${advisor.id}/clients`)
.then(res => res.json())
.then(data => setClients(data))
.catch(err => console.error('Failed to fetch clients:', err));
}
function handleClientClick(client) {
setSelectedClient(client);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${client.id}/portfolios`)
.then(res => res.json())
.then(data => setPortfolios(data))
.catch(err => console.error('Failed to fetch portfolios:', err));
}
function handlePortfolioClick(portfolio) {
setSelectedPortfolio(portfolio);
fetchSecurities(selectedAdvisor.id, selectedClient.id, portfolio.id);
}
function fetchSecurities(advisorId, clientId, portfolioId) {
fetch(`${API_BASE}/advisors/${advisorId}/clients/${clientId}/portfolios/${portfolioId}/securities`)
.then(res => res.json())
.then(data => setSecurities(data))
.catch(err => console.error('Failed to fetch securities:', err));
}
function handleFormChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
What do these handlers do?
- handleAdvisorClick stores the selected advisor, clears everything below it (clients, portfolios, securities), then fetches that advisor's clients.
- handleClientClick stores the selected client, clears portfolios and securities, then fetches portfolios for that client under the currently selected advisor.
- handlePortfolioClick stores the selected portfolio and calls fetchSecurities to load the securities data (you will display these in the next step).
- handleFormChange is a reusable function that updates form fields by name. You will use it in the next step for the add-security form.
Now you need the JSX that renders the three panels and wires up these handlers.
- In the same file, replace the entire return block (from return ( to the matching );) with the following:
return (
<div className="app">
<h1>Financial Advisor Dashboard</h1>
<div className="layout">
<div className="panel">
<h2>Advisors</h2>
<ul className="list">
{advisors.map(advisor => (
<li
key={advisor.id}
className={selectedAdvisor?.id === advisor.id ? 'selected' : ''}
onClick={() => handleAdvisorClick(advisor)}
>
{advisor.firstName} {advisor.lastName}
</li>
))}
</ul>
</div>
<div className="panel">
<h2>Clients</h2>
{selectedAdvisor ? (
<ul className="list">
{clients.map(client => (
<li
key={client.id}
className={selectedClient?.id === client.id ? 'selected' : ''}
onClick={() => handleClientClick(client)}
>
{client.firstName} {client.lastName}
</li>
))}
</ul>
) : (
<p className="placeholder">Select an advisor to view clients</p>
)}
</div>
<div className="panel">
<h2>Portfolios</h2>
{selectedClient ? (
portfolios.length > 0 ? (
<ul className="list">
{portfolios.map(portfolio => (
<li
key={portfolio.id}
className={selectedPortfolio?.id === portfolio.id ? 'selected' : ''}
onClick={() => handlePortfolioClick(portfolio)}
>
Portfolio #{portfolio.id}
<span className="portfolio-date">Created: {portfolio.creationDate}</span>
</li>
))}
</ul>
) : (
<p className="placeholder">No portfolios found for this client</p>
)
) : (
<p className="placeholder">Select a client to view portfolios</p>
)}
</div>
</div>
</div>
);
What is conditional rendering?
The ternary operators (condition ? showThis : showThat) control what appears in each panel based on the current state.
- The Clients panel shows placeholder text until an advisor is selected. Once selected, it maps through the clients array and renders each as a clickable list item.
- The Portfolios panel has a nested conditional: first it checks if a client is selected, then it checks if any portfolios exist for that client.
- The className={selectedAdvisor?.id === advisor.id ? 'selected' : ''} pattern adds a CSS class to highlight the currently selected item.
- Save src/App.jsx.
- Refresh your browser. You should see "Financial Advisor Dashboard" as a heading, with your advisors listed below under an "Advisors" label. Click one of your advisors. The Clients panel should populate with that advisor's clients.
- Click a client. The Portfolios panel should show their portfolios with a creation date beneath each one.
Clicking an advisor does nothing?
- Open the browser console and look for fetch errors. If you see a 404, make sure your Spring Boot API has the /api/advisors/{id}/clients endpoint implemented from the previous project.
- If you see "handleAdvisorClick is not defined", make sure you placed the handler functions between the useEffect closing and the return statement (not outside the App function).
- If the Portfolios panel stays empty after clicking a client, check that your API returns an array (even if there is only one portfolio).
Help me debug my cascading selection - clicking items doesn't load the next level.
Style the three-panel layout
The data is flowing, but the page does not look like a dashboard yet. A flex layout will arrange the three panels side by side, and hover and selected states will give the learner clear visual feedback on which item is active.
- Open src/App.css and replace its contents with the following styles:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
color: #333;
padding: 20px;
}
.app {
max-width: 1100px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a1a2e;
}
h2 {
margin-bottom: 12px;
color: #16213e;
}
h3 {
margin-top: 20px;
margin-bottom: 12px;
}
.layout {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.panel {
flex: 1;
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 200px;
}
.list {
list-style: none;
}
.list li {
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
}
.list li:hover {
background: #e8f4fd;
}
.list li.selected {
background: #0f4c75;
color: white;
}
.list li.selected .portfolio-date {
color: #b8d4e3;
}
.portfolio-date {
display: block;
font-size: 0.8rem;
color: #666;
margin-top: 2px;
}
.placeholder {
color: #888;
font-style: italic;
}
What do these styles do?
- .layout uses display: flex to place the three panels in a horizontal row with equal spacing.
- .panel gives each column a white card appearance with rounded corners and a subtle shadow.
- .list li.selected turns the background dark blue with white text when an item is active, matching the selected class your click handlers apply.
- .placeholder displays italic gray text in panels that are waiting for a prior selection.
- Save src/App.css and refresh your browser.
- You should see three white panel cards arranged side by side. The Advisors panel lists your advisors. The Clients panel shows italic placeholder text until you click an advisor. The Portfolios panel shows placeholder text until you click a client.
- Click through the hierarchy: select an advisor, then a client, then a portfolio. Each selected item should highlight in dark blue with white text.
Panels stacking vertically instead of side by side?
- Make sure the outer wrapper in your JSX uses className="layout" (not class). React uses className instead of the HTML class attribute.
- Check that your CSS file is saved and that import './App.css' appears at the top of App.jsx.
- If the selected highlight is not appearing, verify that your .list li.selected selector has a dot before selected.
Help me fix my CSS layout - the panels are not appearing correctly.
✔️ Awesome, I've got everything!
Great. Make sure both src/App.jsx and src/App.css are saved before moving on.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
import { useState, useEffect } from 'react';
import './App.css';
const API_BASE = 'http://localhost:8080/api';
function App() {
const [advisors, setAdvisors] = useState([]);
const [selectedAdvisor, setSelectedAdvisor] = useState(null);
const [clients, setClients] = useState([]);
const [selectedClient, setSelectedClient] = useState(null);
const [portfolios, setPortfolios] = useState([]);
const [selectedPortfolio, setSelectedPortfolio] = useState(null);
const [securities, setSecurities] = useState([]);
const [formData, setFormData] = useState({
name: '',
category: '',
purchaseDate: '',
purchasePrice: '',
quantity: ''
});
useEffect(() => {
fetch(`${API_BASE}/advisors`)
.then(res => res.json())
.then(data => setAdvisors(data))
.catch(err => console.error('Failed to fetch advisors:', err));
}, []);
function handleAdvisorClick(advisor) {
setSelectedAdvisor(advisor);
setSelectedClient(null);
setPortfolios([]);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${advisor.id}/clients`)
.then(res => res.json())
.then(data => setClients(data))
.catch(err => console.error('Failed to fetch clients:', err));
}
function handleClientClick(client) {
setSelectedClient(client);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${client.id}/portfolios`)
.then(res => res.json())
.then(data => setPortfolios(data))
.catch(err => console.error('Failed to fetch portfolios:', err));
}
function handlePortfolioClick(portfolio) {
setSelectedPortfolio(portfolio);
fetchSecurities(selectedAdvisor.id, selectedClient.id, portfolio.id);
}
function fetchSecurities(advisorId, clientId, portfolioId) {
fetch(`${API_BASE}/advisors/${advisorId}/clients/${clientId}/portfolios/${portfolioId}/securities`)
.then(res => res.json())
.then(data => setSecurities(data))
.catch(err => console.error('Failed to fetch securities:', err));
}
function handleFormChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
return (
<div className="app">
<h1>Financial Advisor Dashboard</h1>
<div className="layout">
<div className="panel">
<h2>Advisors</h2>
<ul className="list">
{advisors.map(advisor => (
<li
key={advisor.id}
className={selectedAdvisor?.id === advisor.id ? 'selected' : ''}
onClick={() => handleAdvisorClick(advisor)}
>
{advisor.firstName} {advisor.lastName}
</li>
))}
</ul>
</div>
<div className="panel">
<h2>Clients</h2>
{selectedAdvisor ? (
<ul className="list">
{clients.map(client => (
<li
key={client.id}
className={selectedClient?.id === client.id ? 'selected' : ''}
onClick={() => handleClientClick(client)}
>
{client.firstName} {client.lastName}
</li>
))}
</ul>
) : (
<p className="placeholder">Select an advisor to view clients</p>
)}
</div>
<div className="panel">
<h2>Portfolios</h2>
{selectedClient ? (
portfolios.length > 0 ? (
<ul className="list">
{portfolios.map(portfolio => (
<li
key={portfolio.id}
className={selectedPortfolio?.id === portfolio.id ? 'selected' : ''}
onClick={() => handlePortfolioClick(portfolio)}
>
Portfolio #{portfolio.id}
<span className="portfolio-date">Created: {portfolio.creationDate}</span>
</li>
))}
</ul>
) : (
<p className="placeholder">No portfolios found for this client</p>
)
) : (
<p className="placeholder">Select a client to view portfolios</p>
)}
</div>
</div>
</div>
);
}
export default App;
And here is the complete src/App.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
color: #333;
padding: 20px;
}
.app {
max-width: 1100px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a1a2e;
}
h2 {
margin-bottom: 12px;
color: #16213e;
}
h3 {
margin-top: 20px;
margin-bottom: 12px;
}
.layout {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.panel {
flex: 1;
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 200px;
}
.list {
list-style: none;
}
.list li {
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
}
.list li:hover {
background: #e8f4fd;
}
.list li.selected {
background: #0f4c75;
color: white;
}
.list li.selected .portfolio-date {
color: #b8d4e3;
}
.portfolio-date {
display: block;
font-size: 0.8rem;
color: #666;
margin-top: 2px;
}
.placeholder {
color: #888;
font-style: italic;
}
Your selection view is working. Next up, you will build the securities table and form so advisors can view, add, and delete securities within a portfolio.
Display and Manage Securities
Your dashboard can now drill down from advisors to clients to portfolios. The final piece is displaying what each portfolio actually holds: securities.
In this step, you will render a securities table below the selection panels, build a form for adding new securities, and wire up delete functionality. This completes the full create-read-delete cycle through the browser.
In this step, get ready to:
- Render a securities table that appears when a portfolio is selected.
- Build an Add Security form that sends a POST request to the API.
- Wire up a Delete button to remove securities from the portfolio.
Render the securities table
When a portfolio is selected, the dashboard should reveal a table of securities below the three selection panels. You already have a fetchSecurities function that populates the securities state when a portfolio is clicked. Now you need to render that data.
- In src/App.jsx, find the closing </div> tag for the layout div (the one right after the three panel divs).
- Add the following securities table block directly below that closing </div>, but still inside the outer .app div:
{selectedPortfolio && (
<div className="securities-section">
<h2>Securities for Portfolio #{selectedPortfolio.id}</h2>
<table className="securities-table">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Purchase Date</th>
<th>Purchase Price</th>
<th>Quantity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{securities.map(security => (
<tr key={security.id}>
<td>{security.name}</td>
<td>{security.category}</td>
<td>{security.purchaseDate}</td>
<td>${security.purchasePrice}</td>
<td>{security.quantity}</td>
<td>
<button
className="delete-btn"
onClick={() => handleDeleteSecurity(security.id)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
What does this code do?
The {selectedPortfolio && (...)} pattern is conditional rendering. The entire securities section only appears in the DOM when a portfolio has been selected.
- securities.map() iterates over the array and renders one table row per security.
- Each row displays the security's name, category, purchase date, price (with a dollar sign), and quantity.
- The Delete button references handleDeleteSecurity which you will add in the final substep.
- Save src/App.jsx.
- In your browser, click through an advisor, then a client, then a portfolio. You should see a "Securities for Portfolio #" heading appear below the panels. If that portfolio has data, the table rows appear. If not, the table headers show with no rows.
Not seeing the securities section?
- Make sure the new JSX block is inside the outer .app div but after the .layout div. If it is outside the return() parentheses, React will not render it.
- Check the browser console for syntax errors. A missing curly brace or parenthesis in JSX will stop the entire component from rendering.
- Confirm your Spring Boot API is still running on port 8080. The securities fetch silently fails if the backend is down.
Why isn't my securities table showing up after selecting a portfolio?
Create the Add Security form
Viewing securities is useful, but advisors also need to add new ones. You will build a form that collects security details and sends them to the API as a POST request.
- In src/App.jsx, find the handleFormChange function. Add the following handleAddSecurity function directly below it:
function handleAddSecurity(e) {
e.preventDefault();
// Build the payload with correct types for the API
const payload = {
name: formData.name,
category: formData.category,
purchaseDate: formData.purchaseDate,
purchasePrice: parseFloat(formData.purchasePrice),
quantity: parseInt(formData.quantity, 10)
};
// POST the new security to the portfolio's endpoint
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${selectedClient.id}/portfolios/${selectedPortfolio.id}/securities`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(() => {
// Refresh the list and clear the form
fetchSecurities(selectedAdvisor.id, selectedClient.id, selectedPortfolio.id);
setFormData({ name: '', category: '', purchaseDate: '', purchasePrice: '', quantity: '' });
})
.catch(err => console.error('Failed to add security:', err));
}
What does this function do?
- e.preventDefault() stops the browser from doing a full page reload when the form submits.
- parseFloat() and parseInt() convert the string form values to numbers, since the Spring Boot API expects numeric types for price and quantity.
- After the POST succeeds, fetchSecurities re-fetches the full list so the table updates. Then the form fields are cleared back to empty strings.
Now add the form JSX that lets users enter security details.
- In the return statement, find the closing </table> tag you added in the previous substep. Add the following form code between that </table> and the closing </div> of the securities-section:
<h3>Add Security</h3>
<form className="security-form" onSubmit={handleAddSecurity}>
<input
type="text"
name="name"
placeholder="Name"
value={formData.name}
onChange={handleFormChange}
required
/>
<input
type="text"
name="category"
placeholder="Category"
value={formData.category}
onChange={handleFormChange}
required
/>
<input
type="date"
name="purchaseDate"
value={formData.purchaseDate}
onChange={handleFormChange}
required
/>
<input
type="number"
name="purchasePrice"
placeholder="Purchase Price"
step="0.01"
value={formData.purchasePrice}
onChange={handleFormChange}
required
/>
<input
type="number"
name="quantity"
placeholder="Quantity"
value={formData.quantity}
onChange={handleFormChange}
required
/>
<button type="submit">Add Security</button>
</form>
What is a controlled form in React?
Each input's value is tied to a piece of React state (formData). When the user types, handleFormChange updates state, and React re-renders the input with the new value. This is called a controlled component.
The name attribute on each input matches the key in formData (e.g. name="purchasePrice" maps to formData.purchasePrice). This lets one handler function manage all five fields.
- Save src/App.jsx.
- In the browser, select an advisor, client, and portfolio. Below the table you should see an "Add Security" heading with five input fields and an "Add Security" button.
- Fill in all five fields and click Add Security. The new security should appear in the table above, and the form fields should clear.
Form submits but no security appears?
- Open the browser DevTools Network tab and check the POST request. If you see a 400 status, your JSON field names may not match what the Spring Boot API expects (name, category, purchaseDate, purchasePrice, quantity).
- Make sure the Spring Boot backend is running. A network error in the console means the API is unreachable.
- Verify that the date field uses the browser date picker (type date). If you manually typed a date in the wrong format, the API may reject it.
Help me debug why my Add Security POST request is failing.
Wire up the Delete button and add styles
The Delete button in the table already references handleDeleteSecurity, but that function does not exist yet. Let's add it so advisors can remove securities from a portfolio.
- In src/App.jsx, find the handleAddSecurity function you just added. Add the following handleDeleteSecurity function directly below it:
function handleDeleteSecurity(securityId) {
// Send DELETE request to remove the specific security
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${selectedClient.id}/portfolios/${selectedPortfolio.id}/securities/${securityId}`, {
method: 'DELETE'
})
.then(() => fetchSecurities(selectedAdvisor.id, selectedClient.id, selectedPortfolio.id))
.catch(err => console.error('Failed to delete security:', err));
}
What does this function do?
The function sends a DELETE request to the specific security's URL (identified by securityId). After deletion succeeds, it calls fetchSecurities to refresh the table. The deleted row disappears without a full page reload.
- Save src/App.jsx.
Now add styles so the securities table and form look polished.
- Open src/App.css and add the following styles at the bottom of the file, after the existing .placeholder rule:
.securities-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.securities-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.securities-table th,
.securities-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.securities-table th {
background: #f8f9fa;
font-weight: 600;
}
.delete-btn {
background: #e74c3c;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #c0392b;
}
.security-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: flex-end;
}
.security-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.security-form button {
padding: 8px 16px;
background: #0f4c75;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.security-form button:hover {
background: #1b262c;
}
What do these styles do?
- .securities-section gives the table area a white card appearance matching the panels above.
- .securities-table makes the table full width with subtle row separators and a grey header row.
- .delete-btn is a red button that darkens on hover, signaling a destructive action.
- .security-form uses flexbox with wrapping so the inputs flow naturally across the available width.
- Save src/App.css and return to your browser.
- Test the full flow: click an advisor, click a client, click a portfolio. The securities table should appear styled with a white card, header row, and data rows.
- Add a new security using the form. Confirm it appears in the table.
- Click the red Delete button on that security. Confirm it disappears from the table.
Delete button not working?
- Check the browser console. If you see "handleDeleteSecurity is not defined", make sure the function is declared inside the App component (before the return statement).
- If the console shows a 404 or 500 error, the security ID in the URL might not match an actual record. Try adding a fresh security first, then deleting it.
- If styles are not applying, confirm there are no typos in your class names. The JSX uses className="delete-btn" which must match .delete-btn in your CSS exactly.
Help me fix my Delete button in the securities table.
Time to double check your code is complete.
✔️ Awesome, I've got everything!
Great. Make sure both src/App.jsx and src/App.css are saved.
ⓧ I'd like to double check the full code
Here is the complete src/App.jsx at this point:
import { useState, useEffect } from 'react';
import './App.css';
const API_BASE = 'http://localhost:8080/api';
function App() {
const [advisors, setAdvisors] = useState([]);
const [selectedAdvisor, setSelectedAdvisor] = useState(null);
const [clients, setClients] = useState([]);
const [selectedClient, setSelectedClient] = useState(null);
const [portfolios, setPortfolios] = useState([]);
const [selectedPortfolio, setSelectedPortfolio] = useState(null);
const [securities, setSecurities] = useState([]);
const [formData, setFormData] = useState({
name: '',
category: '',
purchaseDate: '',
purchasePrice: '',
quantity: ''
});
useEffect(() => {
fetch(`${API_BASE}/advisors`)
.then(res => res.json())
.then(data => setAdvisors(data))
.catch(err => console.error('Failed to fetch advisors:', err));
}, []);
function handleAdvisorClick(advisor) {
setSelectedAdvisor(advisor);
setSelectedClient(null);
setPortfolios([]);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${advisor.id}/clients`)
.then(res => res.json())
.then(data => setClients(data))
.catch(err => console.error('Failed to fetch clients:', err));
}
function handleClientClick(client) {
setSelectedClient(client);
setSelectedPortfolio(null);
setSecurities([]);
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${client.id}/portfolios`)
.then(res => res.json())
.then(data => setPortfolios(data))
.catch(err => console.error('Failed to fetch portfolios:', err));
}
function handlePortfolioClick(portfolio) {
setSelectedPortfolio(portfolio);
fetchSecurities(selectedAdvisor.id, selectedClient.id, portfolio.id);
}
function fetchSecurities(advisorId, clientId, portfolioId) {
fetch(`${API_BASE}/advisors/${advisorId}/clients/${clientId}/portfolios/${portfolioId}/securities`)
.then(res => res.json())
.then(data => setSecurities(data))
.catch(err => console.error('Failed to fetch securities:', err));
}
function handleFormChange(e) {
setFormData({ ...formData, [e.target.name]: e.target.value });
}
function handleAddSecurity(e) {
e.preventDefault();
const payload = {
name: formData.name,
category: formData.category,
purchaseDate: formData.purchaseDate,
purchasePrice: parseFloat(formData.purchasePrice),
quantity: parseInt(formData.quantity, 10)
};
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${selectedClient.id}/portfolios/${selectedPortfolio.id}/securities`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(() => {
fetchSecurities(selectedAdvisor.id, selectedClient.id, selectedPortfolio.id);
setFormData({ name: '', category: '', purchaseDate: '', purchasePrice: '', quantity: '' });
})
.catch(err => console.error('Failed to add security:', err));
}
function handleDeleteSecurity(securityId) {
fetch(`${API_BASE}/advisors/${selectedAdvisor.id}/clients/${selectedClient.id}/portfolios/${selectedPortfolio.id}/securities/${securityId}`, {
method: 'DELETE'
})
.then(() => fetchSecurities(selectedAdvisor.id, selectedClient.id, selectedPortfolio.id))
.catch(err => console.error('Failed to delete security:', err));
}
return (
<div className="app">
<h1>Financial Advisor Dashboard</h1>
<div className="layout">
<div className="panel">
<h2>Advisors</h2>
<ul className="list">
{advisors.map(advisor => (
<li
key={advisor.id}
className={selectedAdvisor?.id === advisor.id ? 'selected' : ''}
onClick={() => handleAdvisorClick(advisor)}
>
{advisor.firstName} {advisor.lastName}
</li>
))}
</ul>
</div>
<div className="panel">
<h2>Clients</h2>
{selectedAdvisor ? (
<ul className="list">
{clients.map(client => (
<li
key={client.id}
className={selectedClient?.id === client.id ? 'selected' : ''}
onClick={() => handleClientClick(client)}
>
{client.firstName} {client.lastName}
</li>
))}
</ul>
) : (
<p className="placeholder">Select an advisor to view clients</p>
)}
</div>
<div className="panel">
<h2>Portfolios</h2>
{selectedClient ? (
portfolios.length > 0 ? (
<ul className="list">
{portfolios.map(portfolio => (
<li
key={portfolio.id}
className={selectedPortfolio?.id === portfolio.id ? 'selected' : ''}
onClick={() => handlePortfolioClick(portfolio)}
>
Portfolio #{portfolio.id}
<span className="portfolio-date">Created: {portfolio.creationDate}</span>
</li>
))}
</ul>
) : (
<p className="placeholder">No portfolios found for this client</p>
)
) : (
<p className="placeholder">Select a client to view portfolios</p>
)}
</div>
</div>
{selectedPortfolio && (
<div className="securities-section">
<h2>Securities for Portfolio #{selectedPortfolio.id}</h2>
<table className="securities-table">
<thead>
<tr>
<th>Name</th>
<th>Category</th>
<th>Purchase Date</th>
<th>Purchase Price</th>
<th>Quantity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{securities.map(security => (
<tr key={security.id}>
<td>{security.name}</td>
<td>{security.category}</td>
<td>{security.purchaseDate}</td>
<td>${security.purchasePrice}</td>
<td>{security.quantity}</td>
<td>
<button
className="delete-btn"
onClick={() => handleDeleteSecurity(security.id)}
>
Delete
</button>
</td>
</tr>
))}
</tbody>
</table>
<h3>Add Security</h3>
<form className="security-form" onSubmit={handleAddSecurity}>
<input
type="text"
name="name"
placeholder="Name"
value={formData.name}
onChange={handleFormChange}
required
/>
<input
type="text"
name="category"
placeholder="Category"
value={formData.category}
onChange={handleFormChange}
required
/>
<input
type="date"
name="purchaseDate"
value={formData.purchaseDate}
onChange={handleFormChange}
required
/>
<input
type="number"
name="purchasePrice"
placeholder="Purchase Price"
step="0.01"
value={formData.purchasePrice}
onChange={handleFormChange}
required
/>
<input
type="number"
name="quantity"
placeholder="Quantity"
value={formData.quantity}
onChange={handleFormChange}
required
/>
<button type="submit">Add Security</button>
</form>
</div>
)}
</div>
);
}
export default App;
And here is the complete src/App.css:
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #f5f5f5;
color: #333;
padding: 20px;
}
.app {
max-width: 1100px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 24px;
color: #1a1a2e;
}
h2 {
margin-bottom: 12px;
color: #16213e;
}
h3 {
margin-top: 20px;
margin-bottom: 12px;
}
.layout {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.panel {
flex: 1;
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
min-height: 200px;
}
.list {
list-style: none;
}
.list li {
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 4px;
transition: background 0.2s;
}
.list li:hover {
background: #e8f4fd;
}
.list li.selected {
background: #0f4c75;
color: white;
}
.list li.selected .portfolio-date {
color: #b8d4e3;
}
.portfolio-date {
display: block;
font-size: 0.8rem;
color: #666;
margin-top: 2px;
}
.placeholder {
color: #888;
font-style: italic;
}
.securities-section {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.securities-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.securities-table th,
.securities-table td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.securities-table th {
background: #f8f9fa;
font-weight: 600;
}
.delete-btn {
background: #e74c3c;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #c0392b;
}
.security-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: flex-end;
}
.security-form input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.security-form button {
padding: 8px 16px;
background: #0f4c75;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.security-form button:hover {
background: #1b262c;
}
Your React dashboard is fully functional. You can navigate the entire entity hierarchy and manage securities through the browser. Next up, the Secret Mission challenges you to add edit functionality so advisors can update existing securities in-place.
Secret mission
Add Edit Functionality for Securities
Your dashboard can add and delete securities, but what about updating existing ones? Real financial systems need the ability to correct mistakes and adjust holdings without deleting and re-creating records. In this secret mission, you'll add in-place editing that toggles your form between add and edit mode.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The advisor-dashboard React project folder on your Desktop (no cost).
- Node.js v24 LTS installed on your system (no cost).
- VS Code installed on your system via snap (no cost).
- The WebConfig.java CORS configuration added to your Spring Boot project (no cost).
Keep everything running
No action needed. Choose this if you are still actively building on the dashboard or plan to add more features soon.
- Leave the Spring Boot API running on port 8080 in its terminal.
- Leave the Vite dev server running on port 5173 in its terminal.
- Node.js and VS Code remain installed for future projects.
- The WebConfig.java file stays in your Spring Boot project, keeping CORS enabled for the React frontend.
Pause - I'll come back to this later
Shut down running processes to free up system resources. Your project files remain on disk and you can restart everything later.
- In the terminal running the Vite dev server, press Ctrl+C to stop it.
- In the terminal running the Spring Boot API, press Ctrl+C to stop it.
When you are ready to resume, restart the backend and frontend:
- Start the Spring Boot API by running this in the backend project directory:
./mvnw spring-boot:run
- Start the React dev server by running this in the advisor-dashboard directory:
npm run dev
Delete - I don't want to use this again
Remove all project resources and start fresh. Your Spring Boot project remains unchanged except for the CORS config file.
- Stop the Vite dev server by pressing Ctrl+C in its terminal.
- Stop the Spring Boot API by pressing Ctrl+C in its terminal.
- Remove the React project folder by running:
rm -rf ~/Desktop/advisor-dashboard
- If you want to remove the CORS configuration from your Spring Boot project, delete the file src/main/java/com/example/advisorapi/config/WebConfig.java in IntelliJ.
- To uninstall VS Code, run:
sudo snap remove code
- To uninstall Node.js, run:
sudo apt-get purge nodejs
Keep Node.js and VS Code?
Node.js and VS Code are useful for many other projects. Only remove them if you are certain you will not need them again.
Nice Work!
Nice Work!
Nice work! You just built a full-stack financial advisor system with a React frontend consuming your Spring Boot REST API.
You've proven you can connect a modern JavaScript UI to a Java backend.
You've learned how to:
- Build a React single-page application from scratch using Vite and connect it to a REST API using the browser Fetch API.
- Configure CORS on a Spring Boot backend to allow cross-origin requests from a frontend running on a different port.
- Implement full create and delete operations on portfolio securities through an interactive browser UI with drill-down navigation across advisors, clients, and portfolios.
- Secret Mission: Added in-place editing of securities with PUT requests and controlled form state management.
Ready to quiz yourself?