FundedNext Challenge Dashboard
Build a React dashboard that tracks trading risk limits for FundedNext challenges.
Introduction
30 Second Summary
Prop firm challenges give you access to funded accounts worth tens of thousands of dollars, but one bad trading day can end everything. Most traders blow their accounts because they lose track of how close they are to breaching a risk limit.
In this project, you will build a React dashboard that tracks your FundedNext challenge progress in real time. You will log trades, watch risk gauges update instantly, and embed affiliate links that earn you commissions on sign-ups.
What You'll Build
Picture selecting your challenge type, logging a losing trade, and instantly seeing a color-coded bar gauge jump from green to yellow, warning you to stop before you blow your account.
By the end of this project, you'll have:
- A live risk dashboard where logging a single trade instantly updates three color-coded gauges showing your daily loss, max drawdown, and profit target progress.
- A persistent trade logger that saves every entry to your browser's localStorage, so you can close the tab and reopen it without losing a single trade.
- Affiliate CTA buttons wired to your FundedNext referral link, turning every dashboard visitor into a potential commission.
- Secret Mission: Build a position size calculator that computes the maximum safe lot size based on your remaining daily loss budget and stop-loss distance.
Are there any prerequisites?
You'll need Node.js version 20.19 or higher and npm installed on your machine. A code editor like VS Code or Cursor is also required.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Scaffold the React Project
Your dashboard needs a running development environment before you can build anything visible. Vite gives you instant hot-reload, so every code change appears in the browser the moment you save.
In this step you will scaffold a React project with Vite, install Recharts for the risk gauges, and clean the starter files so the project is ready for dashboard code.
In this step, get ready to:
- Scaffold a React project with Vite and verify the starter page.
- Install Recharts for data visualization.
- Clean the starter files and prepare the project structure.
Scaffold the project with Vite's React template
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project by running this command:
npm create vite@latest fundednext-dashboard -- --template react
What will this command ask you?
Even though you specified the template, Vite will ask two interactive questions.
- "Which linter to use?" Select Oxlint (the default) and press Enter.
- "Install with npm and start now?" Select No and press Enter.
- Confirm the folder was created by running:
ls
You should see fundednext-dashboard listed in the output.
- Move into the project folder and install its dependencies by running these commands:
cd fundednext-dashboard
npm install
Before you start the dev server, what do you think you will see in the browser?
- Start the development server by running:
npm run dev
Open http://localhost:5173 in your browser. You should see the Vite + React starter page with spinning logos and a counter button.
Don't see the starter page?
- Make sure you ran npm install before npm run dev. Without installing dependencies first, the server cannot start.
- Check that nothing else is already running on port 5173. If it is, Vite will pick a different port and print the URL in the terminal.
- Help me debug my Vite dev server
- Stop the dev server by pressing Ctrl+C in the terminal.
Install Recharts for data visualization
Recharts is a React-specific charting library. It provides chart components that plug directly into React state, so your risk gauges will update the instant a trade is added.
- Install Recharts and its peer dependency by running:
npm install recharts react-is
What did you just install?
- recharts provides the BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Cell, and ResponsiveContainer components you will use for the risk gauges.
- react-is is a peer dependency that Recharts needs to identify React element types at runtime.
Install failed?
- Make sure you are inside the fundednext-dashboard folder. Run pwd to check your current directory.
- Help me fix my npm install error
Clean the starter files and set up the project structure
The Vite scaffold includes demo code and default styles you do not need. You will replace them with a clean starting point for the dashboard.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code (or Cursor) and press Enter to open your code editor.
- Click File in the top menu bar.
- Click Open Folder.
- Select the fundednext-dashboard folder on your Desktop, then click Open.
- In the Explorer sidebar, click on src/App.jsx to open it.
- Select all the existing code and replace it with the following:
import './App.css'
function App() {
return (
<div className="app">
<h1>FundedNext Challenge Dashboard</h1>
</div>
)
}
export default App
What does this code do?
- It imports the App.css stylesheet so any styles you define there apply to the dashboard.
- The App function returns a single <div> with the class app and the dashboard heading. Every component you build will slot into this shell.
- Now open src/main.jsx in the sidebar.
- Select all the existing code and replace it with the following:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
Why replace main.jsx?
The scaffold's version imports index.css, which adds Vite's default purple styling. Removing that import gives you a clean slate so only your own App.css styles apply.
The rest of the file is the same React entry point: mount the App component inside StrictMode and render it into the root div in index.html.
- Save both src/App.jsx and src/main.jsx.
- In your terminal, start the dev server by running:
npm run dev
Open http://localhost:5173 in your browser. You should see a plain "FundedNext Challenge Dashboard" heading on a white background with no Vite logos.
Still see the Vite demo page?
- Make sure you saved both App.jsx and main.jsx. Unsaved changes will not trigger a reload.
- Check that you removed the import './index.css' line from main.jsx. If it is still there, the Vite default styles override your layout.
- Help me fix my dashboard heading not appearing
- Now open src/App.css in the sidebar.
- Select all the existing code and replace it with the dashboard base styles:
:root {
--bg-primary: #0f1117;
--bg-secondary: #1a1d28;
--bg-tertiary: #252836;
--text-primary: #e4e6eb;
--text-secondary: #9ca3af;
--accent-green: #22c55e;
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
--accent-blue: #3b82f6;
--cta-gradient: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
What does this CSS do?
- The :root block defines CSS custom properties for the dark trading-terminal theme. Every component you build will reference these variables for consistent colors.
- The universal reset (*) removes browser default margins and padding so your layout starts from a clean slate.
- The .app class centers the dashboard at a max width of 1200px with even padding.
- Save src/App.css.
Your browser auto-refreshes. You should now see the "FundedNext Challenge Dashboard" heading in light text on a dark background.
Page still has a white background?
- Make sure you replaced the entire contents of App.css, not just appended to the bottom. The old Vite styles can override your dark theme.
- Confirm your App.jsx has import './App.css' at the top. Without this import, the styles will not load.
- Help me debug my dark theme not loading
- Now update the page title. Open index.html in the sidebar (it is in the project root, not inside src/).
- Find this line:
<title>Vite + React</title>
- Replace it with:
<title>FundedNext Challenge Dashboard</title>
- Save index.html. The browser tab title now reads "FundedNext Challenge Dashboard".
- Finally, clean up unused scaffold files and create the empty component files. Open a new terminal tab (or use your code editor's integrated terminal) and make sure you are inside the fundednext-dashboard folder.
- Delete the unused React SVG and create the project structure by running these commands:
macOS
rm src/assets/react.svg
mkdir src/components
touch src/challengeConfig.js src/components/ChallengeSelector.jsx src/components/TradeLogger.jsx src/components/RiskGauges.jsx
Windows
Remove-Item src\assets\react.svg
mkdir src\components
New-Item src\challengeConfig.js, src\components\ChallengeSelector.jsx, src\components\TradeLogger.jsx, src\components\RiskGauges.jsx
- Confirm the files exist by running:
ls src/components
You should see ChallengeSelector.jsx, RiskGauges.jsx, and TradeLogger.jsx listed.
Command not found or file not created?
- On Windows, if mkdir or New-Item fails, make sure you are using PowerShell, not Command Prompt.
- If touch is not recognized, you are likely on Windows without Git Bash. Use the PowerShell commands from the Windows tab instead.
- Help me create the component files
✔️ Awesome, I've got everything!
Great. Make sure you have saved all your files.
ⓧ I'd like to double check the full code
Here are the three files you edited. Compare each one against your code.
src/App.jsx:
import './App.css'
function App() {
return (
<div className="app">
<h1>FundedNext Challenge Dashboard</h1>
</div>
)
}
export default App
src/main.jsx:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>
);
src/App.css:
:root {
--bg-primary: #0f1117;
--bg-secondary: #1a1d28;
--bg-tertiary: #252836;
--text-primary: #e4e6eb;
--text-secondary: #9ca3af;
--accent-green: #22c55e;
--accent-red: #ef4444;
--accent-yellow: #f59e0b;
--accent-blue: #3b82f6;
--cta-gradient: linear-gradient(135deg, #3b82f6, #8b5cf6);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: var(--bg-primary);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
Your dev environment is ready and the dark theme is live. Next up, you will build the challenge selector and trade logger so the dashboard can accept real data.
Build the Challenge Selector and Trade Logger
Your dev server is running and the app renders a heading. Right now, though, the dashboard has no idea which FundedNext challenge the trader is on or what their account size is.
Without challenge rules loaded, the dashboard cannot calculate risk. Without a way to log trades, there is nothing to visualize. In this step, you will build the first functional layer: a selector that loads the correct rules and a form that accepts and stores trades.
In this step, get ready to:
- Define FundedNext challenge rules in a configuration file that converts percentage limits to dollar amounts.
- Build a challenge selector component with dropdowns for challenge type and account size.
- Build a trade logger with form inputs, a trade table, and localStorage persistence.
Create the challenge configuration data
FundedNext offers three challenge types, each with different daily loss limits, max loss limits, profit targets, and minimum trading days. Your dashboard needs a central place to store these rules so every component can look them up.
- Open src/challengeConfig.js in your editor and paste the following challenge rules:
// Maps each FundedNext challenge type to its official rules
export const CHALLENGE_CONFIGS = {
'stellar-1-step': {
name: 'Stellar 1-Step',
dailyLossPercent: 3,
maxLossPercent: 6,
profitTargetPercent: 10,
minTradingDays: 2,
accountSizes: [6000, 15000, 25000, 50000, 100000, 200000],
},
'stellar-2-step': {
name: 'Stellar 2-Step',
dailyLossPercent: 5,
maxLossPercent: 10,
profitTargetPercent: 8,
minTradingDays: 5,
accountSizes: [6000, 15000, 25000, 50000, 100000, 200000],
},
'stellar-lite': {
name: 'Stellar Lite',
dailyLossPercent: 4,
maxLossPercent: 8,
profitTargetPercent: 8,
minTradingDays: 5,
accountSizes: [5000, 10000, 25000, 50000, 100000],
},
};
What does this code do?
CHALLENGE_CONFIGS is an object keyed by challenge type string. Each entry holds the percentage-based rules straight from FundedNext's official trading objectives.
The accountSizes array lists every account size that challenge type offers. The selector dropdown will be populated from this array.
- Right below the closing }; of CHALLENGE_CONFIGS, add the helper function that converts percentages into dollar amounts:
// Converts percentage rules into dollar amounts for a given account size
export const getAccountLimits = (configKey, accountSize) => {
const config = CHALLENGE_CONFIGS[configKey];
if (!config) return null;
return {
...config,
accountSize,
dailyLossLimit: (config.dailyLossPercent / 100) * accountSize,
maxLossLimit: (config.maxLossPercent / 100) * accountSize,
profitTarget: (config.profitTargetPercent / 100) * accountSize,
};
};
What does getAccountLimits do?
getAccountLimits takes a challenge key (like 'stellar-1-step') and an account size (like 100000) and returns an object with three dollar amounts: dailyLossLimit, maxLossLimit, and profitTarget. For example, a $100,000 Stellar 1-Step account has a daily loss limit of $3,000 (3%).
- Save src/challengeConfig.js.
Build the challenge selector and wire it up
The selector component gives traders two dropdowns: one for challenge type and one for account size. When either changes, the component reports the selection up to App so the rest of the dashboard can react.
- Open src/components/ChallengeSelector.jsx and paste the challenge type dropdown:
import { CHALLENGE_CONFIGS } from '../challengeConfig';
function ChallengeSelector({ selectedChallenge, selectedSize, onChallengeChange, onSizeChange }) {
const challengeKeys = Object.keys(CHALLENGE_CONFIGS);
const currentConfig = CHALLENGE_CONFIGS[selectedChallenge];
return (
<div className="challenge-selector">
{/* Challenge type dropdown */}
<h2>Challenge Type</h2>
<select value={selectedChallenge} onChange={(e) => onChallengeChange(e.target.value)}>
{challengeKeys.map((key) => (
<option key={key} value={key}>
{CHALLENGE_CONFIGS[key].name}
</option>
))}
</select>
{/* Account size dropdown populated from the selected challenge */}
<h2>Account Size</h2>
<select value={selectedSize} onChange={(e) => onSizeChange(Number(e.target.value))}>
{currentConfig.accountSizes.map((size) => (
<option key={size} value={size}>
${size.toLocaleString()}
</option>
))}
</select>
</div>
);
}
export default ChallengeSelector;
What does this component do?
ChallengeSelector receives the current selection and two callback props from App. It reads the challenge keys from CHALLENGE_CONFIGS to populate the first dropdown. The second dropdown pulls from the selected challenge's accountSizes array.
The dropdowns exist, but they will not appear until you wire the component into App.jsx. Next you will replace App.jsx so the selector renders on screen.
- Open src/App.jsx and replace its entire contents with the following to wire ChallengeSelector into the layout:
import { useState } from 'react';
import { CHALLENGE_CONFIGS } from './challengeConfig';
import ChallengeSelector from './components/ChallengeSelector';
import './App.css';
function App() {
// Track which challenge type and account size are selected
const [selectedChallenge, setSelectedChallenge] = useState('stellar-1-step');
const [selectedSize, setSelectedSize] = useState(
CHALLENGE_CONFIGS['stellar-1-step'].accountSizes[0]
);
// Reset account size when challenge type changes
const handleChallengeChange = (challenge) => {
setSelectedChallenge(challenge);
setSelectedSize(CHALLENGE_CONFIGS[challenge].accountSizes[0]);
};
return (
<div className="app">
<ChallengeSelector
selectedChallenge={selectedChallenge}
selectedSize={selectedSize}
onChallengeChange={handleChallengeChange}
onSizeChange={setSelectedSize}
/>
</div>
);
}
export default App;
What's happening here?
App owns two pieces of React state: selectedChallenge and selectedSize. When the trader picks a different challenge type, handleChallengeChange also resets the size to that challenge's first available option so the two dropdowns stay in sync.
- Save both ChallengeSelector.jsx and App.jsx.
- Switch to your browser at http://localhost:5173. You should see two dropdowns: one for challenge type showing "Stellar 1-Step" and one for account size showing "$6,000".
- Try switching the challenge type to "Stellar Lite". The account size dropdown should update to show different options starting at $5,000.
Don't see the dropdowns?
- Check the browser console for import errors. Make sure ChallengeSelector.jsx imports from '../challengeConfig' (with two dots, since it is one folder up).
- Make sure you saved all three files: challengeConfig.js, ChallengeSelector.jsx, and App.jsx.
- help me debug why my ChallengeSelector dropdowns are not rendering
The dropdowns work, but traders also need to see the loaded rules at a glance.
- Return to src/components/ChallengeSelector.jsx. Find the closing </select> for the account size dropdown (the second one).
- Right below that </select>, paste the rules display section:
<div className="rules-display">
<h3>Challenge Rules</h3>
<p>Daily Loss Limit: {currentConfig.dailyLossPercent}%</p>
<p>Max Loss Limit: {currentConfig.maxLossPercent}%</p>
<p>Profit Target: {currentConfig.profitTargetPercent}%</p>
<p>Min Trading Days: {currentConfig.minTradingDays}</p>
</div>
- Save ChallengeSelector.jsx and check your browser.
- Below the two dropdowns you should now see a "Challenge Rules" panel showing percentages for daily loss limit, max loss limit, profit target, and minimum trading days.
- Switch to "Stellar 2-Step" and confirm the rules update to 5% daily loss, 10% max loss, and 8% profit target.
Build the trade logger with localStorage persistence
The selector tells the dashboard which rules to enforce. Now the trader needs a way to log individual trades so the dashboard has data to work with.
- Open src/components/TradeLogger.jsx and paste the component's state and form submission handler:
import { useState } from 'react';
function TradeLogger({ trades, onAddTrade, onDeleteTrade, onResetTrades, onExportTrades }) {
// Local form state for the three input fields
const [pnl, setPnl] = useState('');
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
const [note, setNote] = useState('');
// Build a trade object and pass it up to App
const handleSubmit = (e) => {
e.preventDefault();
if (!pnl) return;
onAddTrade({
id: Date.now(),
pnl: parseFloat(pnl),
date,
note,
});
setPnl('');
setNote('');
};
// Confirm before wiping all trades
const handleReset = () => {
if (window.confirm('Are you sure you want to delete all trades? This cannot be undone.')) {
onResetTrades();
}
};
What does this code do?
- Three useState hooks hold the form's current P&L, date, and note values. The date defaults to today.
- handleSubmit creates a trade object with a unique id from Date.now(), then calls onAddTrade to send it up to App. It resets the P&L and note fields after submission.
- handleReset shows a browser confirmation dialog before wiping all trades, preventing accidental data loss.
- Continue in the same file. Right below the handleReset function, paste the JSX return with the trade form:
return (
<div className="trade-logger">
<h2>Log a Trade</h2>
<form onSubmit={handleSubmit}>
<div className="form-row">
<label>
P&L ($)
<input
type="number"
step="0.01"
value={pnl}
onChange={(e) => setPnl(e.target.value)}
placeholder="-500 or 1200"
required
/>
</label>
<label>
Date
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</label>
- Continue right below the Date label. Paste the Note field, submit button, trade actions, and the trade table:
<label>
Note
<input
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Optional note"
/>
</label>
</div>
<button type="submit">Add Trade</button>
</form>
{/* Export and reset action buttons */}
<div className="trade-actions">
<button onClick={onExportTrades} className="btn-secondary">Export CSV</button>
<button onClick={handleReset} className="btn-danger">Reset All Trades</button>
</div>
- Continue right below the trade actions div. Paste the trade table and close out the component:
{/* Trade history table */}
<table className="trade-table">
<thead>
<tr>
<th>Date</th>
<th>P&L</th>
<th>Note</th>
<th></th>
</tr>
</thead>
<tbody>
{trades.map((trade) => (
<tr key={trade.id}>
<td>{trade.date}</td>
<td className={trade.pnl >= 0 ? 'profit' : 'loss'}>
{trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)}
</td>
<td>{trade.note}</td>
<td>
<button onClick={() => onDeleteTrade(trade.id)} className="btn-delete">X</button>
</td>
</tr>
))}
{trades.length === 0 && (
<tr>
<td colSpan="4" className="empty-state">No trades logged yet</td>
</tr>
)}
</tbody>
</table>
</div>
);
}
export default TradeLogger;
What does the trade table do?
- Each trade row shows the date, P&L amount, note, and a delete button.
- P&L values are color-coded using the profit and loss CSS classes. Green for positive, red for negative.
- When the trades array is empty, a single row shows "No trades logged yet" so the table is never blank.
- Save TradeLogger.jsx.
TradeLogger is ready, but App.jsx does not yet import it, manage the trades array, or persist anything to localStorage. Time to wire everything together.
- Open src/App.jsx and replace its entire contents with the full wiring. Start with the imports and state management:
import { useState, useEffect } from 'react';
import { getAccountLimits, CHALLENGE_CONFIGS } from './challengeConfig';
import ChallengeSelector from './components/ChallengeSelector';
import TradeLogger from './components/TradeLogger';
import './App.css';
// Build a unique localStorage key per challenge + size combination
function getStorageKey(challenge, size) {
return `fn-trades-${challenge}-${size}`;
}
function App() {
const [selectedChallenge, setSelectedChallenge] = useState('stellar-1-step');
const [selectedSize, setSelectedSize] = useState(
CHALLENGE_CONFIGS['stellar-1-step'].accountSizes[0]
);
const [trades, setTrades] = useState([]);
const limits = getAccountLimits(selectedChallenge, selectedSize);
Why a unique localStorage key?
getStorageKey produces keys like fn-trades-stellar-1-step-6000. This keeps trade data separate per challenge and account size. A trader can track a $100K Stellar 2-Step and a $25K Stellar Lite simultaneously without mixing data.
- Continue in the same file. Right below the limits line, paste the useEffect hooks and handler functions:
// Load trades from localStorage when challenge or size changes
useEffect(() => {
const key = getStorageKey(selectedChallenge, selectedSize);
const saved = localStorage.getItem(key);
setTrades(saved ? JSON.parse(saved) : []);
}, [selectedChallenge, selectedSize]);
// Save trades to localStorage whenever the array changes
useEffect(() => {
const key = getStorageKey(selectedChallenge, selectedSize);
localStorage.setItem(key, JSON.stringify(trades));
}, [trades, selectedChallenge, selectedSize]);
const handleAddTrade = (trade) => {
setTrades((prev) => [trade, ...prev]);
};
const handleDeleteTrade = (id) => {
setTrades((prev) => prev.filter((t) => t.id !== id));
};
const handleResetTrades = () => {
setTrades([]);
};
How does localStorage persistence work?
The first useEffect runs whenever selectedChallenge or selectedSize changes, loading the saved trades for that combination. The second useEffect runs whenever the trades array changes, writing the updated data back to localStorage.
This two-effect pattern means the trader can close the browser, reopen it, and pick up exactly where they left off.
- Continue in the same file. Below handleResetTrades, paste the CSV export, challenge change handler, and the return JSX:
// Download trades as a CSV file
const handleExportTrades = () => {
if (trades.length === 0) return;
const header = 'Date,P&L,Note\n';
const rows = trades.map((t) => `${t.date},${t.pnl},"${t.note || ''}"`).join('\n');
const blob = new Blob([header + rows], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `fundednext-trades-${selectedChallenge}-${selectedSize}.csv`;
a.click();
URL.revokeObjectURL(url);
};
// Reset size to first option when challenge type changes
const handleChallengeChange = (challenge) => {
setSelectedChallenge(challenge);
setSelectedSize(CHALLENGE_CONFIGS[challenge].accountSizes[0]);
};
const uniqueDays = new Set(trades.map((t) => t.date)).size;
- Finally, paste the return statement that renders the two-column layout. Add this right below the uniqueDays line:
return (
<div className="app">
<main className="dashboard-grid">
<div className="left-column">
<ChallengeSelector
selectedChallenge={selectedChallenge}
selectedSize={selectedSize}
onChallengeChange={handleChallengeChange}
onSizeChange={setSelectedSize}
/>
<div className="summary">
<p>{trades.length} trades logged, {uniqueDays} unique trading days</p>
</div>
</div>
<div className="right-column">
<TradeLogger
trades={trades}
onAddTrade={handleAddTrade}
onDeleteTrade={handleDeleteTrade}
onResetTrades={handleResetTrades}
onExportTrades={handleExportTrades}
/>
</div>
</main>
</div>
);
}
export default App;
What's happening in the layout?
The dashboard-grid class creates a two-column layout. The left column holds the challenge selector and a summary line showing how many trades are logged. The right column holds the trade logger form and table.
The components are wired, but the layout needs CSS to look right. Time to add the dashboard styles.
- Open src/App.css. Keep the existing :root variables and base styles. At the bottom of the file, paste the layout and selector styles:
/* Two-column dashboard grid, collapses to single column on mobile */
.dashboard-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
@media (max-width: 768px) {
.dashboard-grid { grid-template-columns: 1fr; }
}
.left-column, .right-column { display: flex; flex-direction: column; gap: 20px; }
/* Card-style containers for each section */
.challenge-selector, .trade-logger, .risk-gauges, .summary {
background: var(--bg-secondary);
border-radius: 12px;
padding: 20px;
}
.challenge-selector select {
width: 100%; padding: 10px; margin-bottom: 16px;
background: var(--bg-tertiary); color: var(--text-primary);
border: 1px solid var(--bg-tertiary); border-radius: 8px; font-size: 0.95rem;
}
.rules-display { margin-top: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; }
.rules-display p { color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 4px; }
.summary p { color: var(--text-secondary); font-size: 0.9rem; }
- Continue at the bottom of src/App.css. Paste the form and trade table styles:
/* Trade form layout */
.form-row { display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 12px; }
.form-row label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--text-secondary); flex: 1; min-width: 120px; }
.form-row input { padding: 10px; background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--bg-tertiary); border-radius: 8px; font-size: 0.95rem; }
button[type='submit'] { width: 100%; padding: 10px; background: var(--accent-blue); color: white; border: none; border-radius: 8px; font-size: 0.95rem; cursor: pointer; }
.trade-actions { display: flex; gap: 8px; margin-top: 12px; margin-bottom: 16px; }
.btn-secondary { flex: 1; padding: 8px; background: var(--bg-tertiary); color: var(--text-primary); border: none; border-radius: 8px; cursor: pointer; font-size: 0.85rem; }
.btn-danger { flex: 1; padding: 8px; background: transparent; color: var(--accent-red); border: 1px solid var(--accent-red); border-radius: 8px; cursor: pointer; font-size: 0.85rem; }
- Paste the final batch of table and utility styles at the bottom of src/App.css:
/* Trade history table */
.trade-table { width: 100%; border-collapse: collapse; }
.trade-table th { text-align: left; padding: 8px; color: var(--text-secondary); font-size: 0.85rem; border-bottom: 1px solid var(--bg-tertiary); }
.trade-table td { padding: 8px; font-size: 0.9rem; border-bottom: 1px solid var(--bg-tertiary); }
.trade-table tbody tr:nth-child(even) { background: var(--bg-tertiary); }
/* Color-coded P&L values */
.profit { color: var(--accent-green); font-weight: 600; }
.loss { color: var(--accent-red); font-weight: 600; }
.btn-delete { background: transparent; color: var(--text-secondary); border: none; cursor: pointer; font-size: 0.85rem; }
.empty-state { text-align: center; color: var(--text-secondary); padding: 20px; }
- Save App.jsx and App.css.
Before you check the browser, what do you expect to see? Think about what the two-column layout should look like with a selector on the left and a trade form on the right.
- Check your browser. You should see a dark-themed two-column layout. The left column shows the challenge selector with rules and a summary line reading "0 trades logged, 0 unique trading days". The right column shows the trade form with P&L, Date, and Note fields.
- Select "Stellar 1-Step" from the dropdown and enter 500 in the P&L field. Click **Add Trade**.
- You should see the trade appear in the table below the form with today's date, a green "+$500.00", and the summary update to "1 trades logged, 1 unique trading days".
Trade not appearing in the table?
- Check the browser console for errors. A common issue is the import path for TradeLogger. It should be './components/TradeLogger' in App.jsx.
- Make sure the five handler functions in App.jsx are all present: handleAddTrade, handleDeleteTrade, handleResetTrades, handleExportTrades, and handleChallengeChange.
- help me debug why trades are not showing up in my TradeLogger table
Now test that localStorage persistence works.
- Close the browser tab completely.
- Open a new tab and navigate back to http://localhost:5173. Your trade from earlier should still be in the table.
- Click the **X** button next to the trade to delete it. The table should go back to showing "No trades logged yet".
✔️ Awesome, I've got everything!
Great. Double check you have saved all your files before moving on.
ⓧ I'd like to double check the full code
Your project has several files that changed in this step. Scroll up and confirm each of these chunks is present in the correct file, in order:
- src/challengeConfig.js: CHALLENGE_CONFIGS object followed by getAccountLimits function.
- src/components/ChallengeSelector.jsx: Two dropdowns (challenge type, account size) plus the rules display div.
- src/components/TradeLogger.jsx: State and handlers, then the form with three inputs, then the trade actions, then the trade table.
- src/App.jsx: Imports, getStorageKey, state, limits, two useEffect hooks, five handlers, uniqueDays, and the two-column return JSX.
- src/App.css: Original variables and base styles, plus grid layout, selector styles, form styles, and table styles.
Your dashboard now accepts trades and remembers them across sessions. But raw numbers in a table do not tell a trader how close they are to breaching a limit. Next up, you will build visual risk gauges that make danger obvious at a glance.
Add Risk Gauges
Your challenge selector loads the correct rules and the trade logger records every position. But look at the dashboard right now. Can you tell at a glance whether a trader is one bad trade away from breaching their daily loss limit?
In this step, you will build three visual risk gauges using Recharts bar charts. Each gauge fills with color as the trader approaches a limit. Green means safe, yellow means caution, red means danger.
In this step, get ready to:
- Build risk calculations that track daily loss, max drawdown, and profit progress from logged trades.
- Render three color-coded bar chart gauges that update instantly when trades change.
- Wire the gauges into the dashboard with warning banners for dangerous thresholds.
Build the risk calculation logic
The gauges need to answer three questions: how much of today's daily loss limit has been used, how deep is the overall drawdown compared to the max loss limit, and how close is the trader to hitting the profit target.
- Start building the calculation logic by copying the following code into src/components/RiskGauges.jsx:
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Cell, ResponsiveContainer } from 'recharts';
// Returns a color based on how close a gauge is to its limit
function getColor(percentage) {
if (percentage >= 80) return '#ef4444';
if (percentage >= 50) return '#f59e0b';
return '#22c55e';
}
function RiskGauges({ trades, limits }) {
// Get today's date to filter trades for the daily gauge
const today = new Date().toISOString().split('T')[0];
// Sum today's and all-time P&L from the trades array
const todayPnL = trades.filter((t) => t.date === today).reduce((sum, t) => sum + t.pnl, 0);
const totalPnL = trades.reduce((sum, t) => sum + t.pnl, 0);
const uniqueDays = new Set(trades.map((t) => t.date)).size;
// Convert losses to percentages of each limit
const dailyLossUsed = Math.abs(Math.min(todayPnL, 0));
const dailyLossPercent = (dailyLossUsed / limits.dailyLossLimit) * 100;
const maxDrawdownUsed = Math.abs(Math.min(totalPnL, 0));
const maxDrawdownPercent = (maxDrawdownUsed / limits.maxLossLimit) * 100;
const profitProgress = Math.max(totalPnL, 0);
const profitPercent = (profitProgress / limits.profitTarget) * 100;
What does this code do?
- getColor() maps a percentage to a traffic-light color. Under 50% is green (safe), 50-80% is yellow (caution), and 80%+ is red (danger).
- todayPnL filters trades to today's date and sums their P&L. totalPnL sums every trade ever logged.
- Math.abs(Math.min(todayPnL, 0)) converts a negative P&L (a loss) into a positive dollar amount. If the trader is profitable today, Math.min returns 0, so the daily loss gauge stays empty.
- Each percentage divides the dollar loss by the dollar limit. For example, $2,000 lost against a $5,000 daily limit produces 40%.
- Add the rendering logic by copying the following code directly below what you just pasted in src/components/RiskGauges.jsx:
const gaugeData = [
{ name: 'Daily Loss', value: Math.min(dailyLossPercent, 100), label: `$${dailyLossUsed.toFixed(0)} / $${limits.dailyLossLimit.toFixed(0)}` },
{ name: 'Max Drawdown', value: Math.min(maxDrawdownPercent, 100), label: `$${maxDrawdownUsed.toFixed(0)} / $${limits.maxLossLimit.toFixed(0)}` },
{ name: 'Profit Target', value: Math.min(profitPercent, 100), label: `$${profitProgress.toFixed(0)} / $${limits.profitTarget.toFixed(0)}` },
];
const showDailyWarning = dailyLossPercent >= 80;
const showMaxWarning = maxDrawdownPercent >= 80;
return (
<div className="risk-gauges">
<h2>Risk Dashboard</h2>
{showDailyWarning && <div className="warning-banner">Warning: You are approaching your daily loss limit!</div>}
{showMaxWarning && <div className="warning-banner">Warning: You are approaching your max loss limit!</div>}
{gaugeData.map((gauge, index) => (
<div key={gauge.name} className="gauge-container">
<h3>{gauge.name}</h3>
<p className="gauge-label">{gauge.label}</p>
<ResponsiveContainer width="100%" height={50}>
<BarChart data={[gauge]} layout="vertical" margin={{ top: 0, right: 20, bottom: 0, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" horizontal={false} />
<XAxis type="number" domain={[0, 100]} tickFormatter={(v) => `${v}%`} />
<YAxis type="category" dataKey="name" hide />
<Tooltip formatter={(v) => `${v.toFixed(1)}%`} />
<Bar dataKey="value" barSize={30}>
<Cell fill={index === 2 ? (profitPercent >= 100 ? '#22c55e' : '#3b82f6') : getColor(gauge.value)} />
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
))}
<div className="trading-days">
<h3>Trading Days</h3>
<p className={uniqueDays >= limits.minTradingDays ? 'days-met' : 'days-remaining'}>{uniqueDays} / {limits.minTradingDays} minimum trading days completed</p>
</div>
</div>
);
}
export default RiskGauges;
What does the rendering do?
- gaugeData builds an array of three objects, one per gauge. Each has a name, a value (percentage capped at 100), and a label showing dollars used vs. the dollar limit.
- Each gauge renders a horizontal BarChart with a single bar that fills from 0% to 100%. The Cell component sets the fill color dynamically.
- The Profit Target gauge uses blue instead of the red/yellow/green scale. It turns green only when the target is fully met.
- Warning banners appear when either loss gauge hits 80% or higher.
- Save src/components/RiskGauges.jsx.
- Check your terminal where the dev server is running. You should see no compilation errors.
Seeing a syntax error in the terminal?
- Make sure you pasted the second code block directly below the first one, inside the same file. The function must open in the first block and close in the second.
- Check that the file ends with export default RiskGauges; on its own line.
Why is my RiskGauges component not compiling?
Connect the gauges to the dashboard
The component exists but the dashboard does not render it yet. You need to import it into App.jsx, add styles for the gauge containers, and place it in the left column.
- Add the gauge styles to the bottom of src/App.css, below the existing .summary p rule:
/* Gauge chart containers and warning styles */
.gauge-container { margin-bottom: 16px; }
.gauge-container h3 { font-size: 0.95rem; margin-bottom: 4px; }
.gauge-label { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 4px; }
.warning-banner { background: rgba(239,68,68,0.15); border: 1px solid var(--accent-red); color: var(--accent-red); padding: 10px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 0.9rem; font-weight: 600; }
.trading-days { margin-top: 12px; padding: 12px; background: var(--bg-tertiary); border-radius: 8px; }
.trading-days h3 { font-size: 0.95rem; margin-bottom: 4px; }
.days-met { color: var(--accent-green); }
.days-remaining { color: var(--accent-yellow); }
- Save src/App.css.
- Switch to src/App.jsx. Find this import line near the top:
import TradeLogger from './components/TradeLogger';
- Add this new import line directly below it:
import RiskGauges from './components/RiskGauges';
- Scroll down to the JSX. Find the closing </div> tag of the summary section:
<div className="summary">
<p>{trades.length} trades logged, {uniqueDays} unique trading days</p>
</div>
- Add the RiskGauges component directly below that </div>, still inside the left column:
<RiskGauges trades={trades} limits={limits} />
Why does this make the gauges reactive?
You are passing the trades array and limits object as props. Every time a trade is added or deleted, React re-renders the RiskGauges component with the updated data.
The gauges recalculate automatically because they derive all their values from the props on every render. No extra wiring is needed.
- Save src/App.jsx.
Before you check the browser, take a moment to predict. On a $100,000 Stellar 2-Step challenge, the daily loss limit is 5% ($5,000). If you log a trade with a P&L of -2000, what percentage of the Daily Loss gauge do you expect to see filled?
- Switch to your browser at http://localhost:5173.
- Select Stellar 2-Step from the challenge type dropdown.
- Select $100,000 from the account size dropdown.
- In the trade logger, enter -2000 in the P&L field.
- Click Add Trade.
The Daily Loss gauge fills to 40% ($2,000 out of the $5,000 limit) and the bar is green. The Max Drawdown gauge shows 20% ($2,000 out of the $10,000 limit) in green. The Profit Target gauge stays at 0% because there is no profit yet.
Below the gauges, the Trading Days counter shows 1 out of 5 minimum trading days in yellow, since you have only traded on one date so far.
Gauges not appearing?
- Check that you added import RiskGauges from './components/RiskGauges'; near the top of src/App.jsx.
- Make sure <RiskGauges trades={trades} limits={limits} /> is inside the left-column div, after the summary div.
- If the terminal shows an error about recharts, confirm you installed it earlier. You can re-run npm install recharts react-is to be safe.
Help me debug my risk gauges
✔️ Awesome, I've got everything!
Great. Make sure you have saved all three files: src/components/RiskGauges.jsx, src/App.css, and src/App.jsx.
ⓧ I'd like to double check the full code
Since the files are long, scroll up and confirm each section is present in order.
In src/components/RiskGauges.jsx, confirm you have:
- The Recharts import line and the getColor helper function at the top.
- The RiskGauges function with calculation variables (todayPnL, totalPnL, dailyLossUsed, etc.) and the gaugeData array.
- The return statement rendering warning banners, three BarChart gauges via map, and the trading days counter.
- export default RiskGauges; at the bottom.
In src/App.jsx, confirm you have:
- import RiskGauges from './components/RiskGauges'; near the top.
- <RiskGauges trades={trades} limits={limits} /> in the left-column div, after the summary div.
In src/App.css, confirm the new styles for .gauge-container, .gauge-label, .warning-banner, .trading-days, .days-met, and .days-remaining are at the bottom of the file.
Now your dashboard tells a visual story. In the next step, you will add affiliate CTAs that turn this free tool into a revenue channel, and polish the layout with a dark trading-terminal theme.
Integrate Affiliate CTAs and Polish the Dashboard
Your risk gauges now show traders exactly how close they are to breaching their challenge limits. The dashboard delivers genuine value to anyone navigating a FundedNext Stellar challenge.
Right now, though, a trader can use your dashboard and then navigate to FundedNext directly to start a challenge. You get no credit for the referral. This step adds affiliate CTA buttons that embed your FundedNext partner code into every link, and polishes the dashboard with a proper header, footer, and refined styles.
In this step, get ready to:
- Create affiliate CTA components linking to FundedNext with your referral code.
- Add a secondary CTA for visitors who don't have a challenge yet.
- Polish the dashboard with a header, footer, and refined CSS styles.
Create the affiliate CTA components
FundedNext's Partners Program tracks referrals through a ?fpr= query parameter appended to any fundednext.com link. When someone clicks your link and purchases a challenge, you earn a commission. The component you build here embeds that parameter in a prominent call-to-action button.
- Create a new file at src/components/AffiliateCTA.jsx and add the following code:
function AffiliateCTA() {
return (
<div className="affiliate-cta">
<a
href="https://fundednext.com?fpr=your-code-here"
target="_blank"
rel="noopener noreferrer"
className="cta-button"
>
Start Your FundedNext Challenge
</a>
</div>
);
}
export default AffiliateCTA;
What does this code do?
The component renders a single anchor tag styled as a button. The href points to fundednext.com with ?fpr=your-code-here appended. You will replace your-code-here with your actual FundedNext partner code once you are approved.
The target="_blank" attribute opens the link in a new tab so traders stay on your dashboard. The rel="noopener noreferrer" attribute is a security best practice for external links.
- Save src/components/AffiliateCTA.jsx.
The component is saved but nothing changes in the browser yet. You need to import it into App.jsx and render it.
- In src/App.jsx, find this import line:
import RiskGauges from './components/RiskGauges';
- Add this import directly below it:
import AffiliateCTA from './components/AffiliateCTA';
- Still in src/App.jsx, scroll down to the return statement. Find this line:
<div className="app">
- Add a header block directly after it, above <main>:
<header className="app-header">
<h1>FundedNext Challenge Dashboard</h1>
<AffiliateCTA />
</header>
- Now find the closing </main> tag near the bottom of the return. Add a footer directly after it, above the closing </div>:
<footer className="app-footer">
<p>This tool is for educational purposes. Past performance does not guarantee future results.</p>
</footer>
What do the header and footer do?
The <header> places the dashboard title and the primary affiliate CTA side by side at the top of the page. Every visitor sees the "Start Your FundedNext Challenge" button immediately.
The <footer> adds a standard financial disclaimer at the bottom. This is good practice for any tool that displays trading-related data.
- Save src/App.jsx.
- Check your browser. You should see "FundedNext Challenge Dashboard" as a heading at the top, a "Start Your FundedNext Challenge" link next to it, and the disclaimer text at the bottom of the page.
Don't see the header or footer?
- Make sure the import AffiliateCTA line is above the import './App.css' line.
- Confirm the <header> block is inside <div className="app"> but above <main>. The <footer> should be after </main> but before the final </div>.
- Check the terminal for errors. A missing closing tag or a typo in className will prevent the page from rendering.
Still stuck? Help me debug why my AffiliateCTA component is not showing up in the header.
Now add the secondary CTA for visitors who don't have a FundedNext challenge yet. This goes inside the challenge selector panel, right below the rules display.
- In src/components/ChallengeSelector.jsx, find the closing </div> of the rules-display div. Add this block directly after it, but before the component's final closing </div>:
<div className="affiliate-cta-secondary">
<p>Don't have a challenge yet?</p>
<a
href="https://fundednext.com?fpr=your-code-here"
target="_blank"
rel="noopener noreferrer"
>
View FundedNext Plans
</a>
</div>
What does the secondary CTA do?
The primary CTA at the top targets traders who are ready to buy. The secondary CTA targets visitors who are still browsing. Both links use the same ?fpr=your-code-here parameter so every click is tracked to your partner account.
- Save src/components/ChallengeSelector.jsx.
- Check your browser. Below the Challenge Rules panel you should see "Don't have a challenge yet?" with a "View FundedNext Plans" link.
Don't see the secondary CTA?
- Make sure the new <div className="affiliate-cta-secondary"> block is placed after the rules-display closing </div>, not inside it.
- Verify the component's final </div> still closes the challenge-selector wrapper. An extra or missing </div> tag will break the layout.
Need help? Help me figure out why the secondary affiliate CTA is not appearing below the challenge rules.
Style the header, CTA button, and footer
Your new header, footer, and CTA elements are rendering but they lack styling. Time to add the CSS that makes them look like part of a polished trading terminal.
- In src/App.css, find this line:
.app { max-width: 1200px; margin: 0 auto; padding: 20px; }
- Add these styles directly after it:
/* Header layout */
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--bg-tertiary);
}
.app-header h1 { font-size: 1.5rem; }
/* CTA button */
.cta-button {
display: inline-block;
padding: 12px 24px;
background: var(--cta-gradient);
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
transition: opacity 0.2s;
}
.cta-button:hover { opacity: 0.9; }
What do these styles do?
The .app-header uses flexbox to push the title to the left and the CTA button to the right. A subtle bottom border separates the header from the dashboard content.
The .cta-button uses the --cta-gradient CSS variable you defined earlier. This gives the button a blue-to-purple gradient that draws the eye without clashing with the dark theme.
- Save src/App.css.
- Check your browser. The header should now show the dashboard title on the left and a gradient-styled CTA button on the right, separated from the content below by a thin line.
Add the remaining polish styles
The secondary CTA, footer, section headings, and button hover states still need their styles. You also need to update the responsive breakpoint so the header stacks properly on mobile.
- In src/App.css, find the media query line:
@media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } }
- Replace it with this updated version that also stacks the header on small screens:
@media (max-width: 768px) { .dashboard-grid { grid-template-columns: 1fr; } .app-header { flex-direction: column; gap: 12px; } }
- Now scroll to the very end of src/App.css and add these styles after the last rule:
/* Section heading sizes */
.challenge-selector h2,
.trade-logger h2,
.risk-gauges h2 { font-size: 1.1rem; margin-bottom: 12px; }
/* Rules display heading */
.rules-display h3 { font-size: 0.95rem; margin-bottom: 8px; }
/* Secondary affiliate CTA */
.affiliate-cta-secondary { margin-top: 16px; text-align: center; }
.affiliate-cta-secondary p { color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 6px; }
.affiliate-cta-secondary a { color: var(--accent-blue); text-decoration: none; font-size: 0.9rem; }
.affiliate-cta-secondary a:hover { text-decoration: underline; }
/* Hover states */
button[type='submit']:hover { opacity: 0.9; }
.btn-delete:hover { color: var(--accent-red); }
/* Footer */
.app-footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--bg-tertiary); text-align: center; }
.app-footer p { color: var(--text-secondary); font-size: 0.8rem; }
What do these styles do?
- The section heading rules standardize the size and spacing of every <h2> across the challenge selector, trade logger, and risk gauges panels.
- The .affiliate-cta-secondary rules center the secondary CTA text and style the link in blue to match the overall accent color.
- The hover states give the submit button and delete button visual feedback when the user hovers over them.
- The .app-footer rules add a top border and center the disclaimer text in a smaller, muted font.
- Save src/App.css.
- Check your browser. The secondary CTA below the Challenge Rules should be centered with a blue link. The footer at the bottom should have small muted text centered above a subtle top border. Hover over the "Add Trade" button and the delete (X) buttons to confirm they respond to your cursor.
Styles not applying?
- Make sure the CSS class names match exactly. The CTA wrapper is affiliate-cta-secondary (with hyphens, not underscores).
- Check that you placed the new rules after the last existing rule in the file, not inside another rule's curly braces.
- If the header looks wrong on a narrow browser, confirm you updated the media query to include the .app-header flexbox override.
Still stuck? Help me debug why my CSS styles for the header, CTA, or footer are not applying correctly.
Time to test the full affiliate flow. Before you click, predict what you expect to happen when you click the CTA button.
- Click the Start Your FundedNext Challenge button in the header.
A new browser tab opens at https://fundednext.com?fpr=your-code-here. That ?fpr= parameter is what tells FundedNext this visitor came from your referral. Once you are approved as a partner, you replace your-code-here with your actual partner code.
- Click the View FundedNext Plans link below the Challenge Rules panel to verify the secondary CTA also opens FundedNext in a new tab with the same ?fpr= parameter.
How does the FundedNext Partners Program work?
FundedNext captures the fpr value into a first-party cookie with a 60-day attribution window. If the visitor purchases a challenge within 60 days, you earn a commission. The Silver tier (open to anyone with a FundedNext account) pays 10% on CFD first purchases.
To join, apply at fundednext.com/partners. After approval, copy your unique partner code from the Partner Dashboard and replace your-code-here in both CTA components.
✔️ Awesome, I've got everything!
Great. Double check you have saved all four files: AffiliateCTA.jsx, ChallengeSelector.jsx, App.jsx, and App.css.
ⓧ I'd like to double check the full code
Scroll up through each code chunk you added and confirm these are present in your project:
- AffiliateCTA.jsx: a single function component that renders an anchor tag with className="cta-button" linking to https://fundednext.com?fpr=your-code-here.
- App.jsx: imports AffiliateCTA, renders a <header className="app-header"> with <h1> and <AffiliateCTA /> above <main>, and a <footer className="app-footer"> with the disclaimer after </main>.
- ChallengeSelector.jsx: a <div className="affiliate-cta-secondary"> block after the rules-display div, with "Don't have a challenge yet?" text and a "View FundedNext Plans" link.
- App.css: .app-header and .cta-button rules after .app. Updated media query with .app-header column stacking. Section heading sizes, .affiliate-cta-secondary styles, hover states, and .app-footer rules at the end.
Your dashboard now earns you money. Every visitor sees the affiliate CTA, and both links track referrals to your partner code. Next up, the Secret Mission challenges you to add a position size calculator that makes this dashboard even more useful for traders.
Secret mission
Build a Position Size Calculator
Your risk gauges show how close you are to breaching limits. But they do not answer the most practical question a trader faces before every trade: how many lots can I safely open right now? Build a calculator that reads from your live dashboard state and answers that in real time.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with no cloud resources or paid services. There are no ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The fundednext-dashboard project folder on your Desktop (contains all source code and node_modules).
- The Vite dev server process running at http://localhost:5173.
- Trade data stored in your browser's localStorage under keys like fn-trades-stellar-1-step-6000.
Keep everything running
No action needed. Choose this if you are still actively building or plan to deploy the dashboard to a static hosting service like Netlify, Vercel, or GitHub Pages.
- Your dev server stays running at http://localhost:5173.
- Your trade data persists in localStorage across browser sessions.
- Your project folder remains on your Desktop, ready for further development.
Pause - I'll come back to this later
Shut down the dev server to free up your terminal. Your project files and trade data stay intact so you can pick up where you left off.
- Switch to the terminal where the dev server is running.
- Press Ctrl+C to stop the dev server.
- When you are ready to resume, open a terminal in the fundednext-dashboard folder and run npm run dev to start the server again.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your source code, dependencies, and any trade data stored in localStorage.
- Switch to the terminal where the dev server is running and press Ctrl+C to stop it.
- Navigate back to your Desktop by running:
cd ~/Desktop
- Delete the project folder. On macOS/Linux, run:
rm -rf fundednext-dashboard
On Windows, run:
rmdir /s /q fundednext-dashboard
- To clear your saved trade data, open your browser's DevTools by pressing F12.
- Go to the Application tab (Chrome) or Storage tab (Firefox).
- Expand Local Storage, select http://localhost:5173, and click Clear All to remove the stored trade entries.
Nice Work!
Nice Work!
Nice work! You just built a full trading risk management dashboard from scratch using React and Vite that helps traders survive FundedNext challenges while earning you affiliate commissions.
You've learned how to:
- Built a React dashboard with challenge selection, a trade logger with full localStorage persistence, and CSV export so traders can track P&L across FundedNext's Stellar 1-Step, 2-Step, and Lite challenges.
- Created live risk gauges using Recharts that visually track daily loss, max drawdown, and profit target progress with color-coded warning thresholds.
- Integrated FundedNext affiliate CTAs using the ?fpr= referral parameter so you earn commissions on sign-ups driven by the value your dashboard provides.
- Secret Mission: Added a position size calculator that computes safe lot sizes based on remaining daily loss budget and stop-loss distance.
Ready to quiz yourself?