LeetCode Analytics Dashboard

Build a React dashboard that visualizes your LeetCode stats with charts.

Introduction

30 Second Summary

You've been grinding LeetCode problems, but your profile page only shows raw numbers. You can't tell if you're avoiding Hard problems, which algorithm topics you've barely touched, or whether your practice is consistent or full of gaps.

In this project, you will build a local analytics dashboard that pulls your real LeetCode solving data and transforms it into interactive charts. You will see exactly where you are strong, where you are weak, and how consistent your practice has been.

What You'll Build

Enter your LeetCode username and instantly see a full visual breakdown of your solving history, with charts you can hover over and explore.

By the end of this project, you'll have:

  • A difficulty breakdown bar chart showing your Easy, Medium, and Hard progress against the total problems available on LeetCode.
  • A topic radar chart revealing your strongest and weakest algorithm categories at a glance.
  • An activity grid showing your daily submission history over the past year, with streaks and total active days displayed above it.
  • A backend proxy server that handles CORS restrictions when querying external APIs from the browser.
  • Secret Mission: Add a contest rating history line chart showing your competitive performance over time.

Are there any prerequisites?

No prior experience with React or Express is required. This guide walks you through setting up every tool from scratch, including VS Code and Node.js.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. Think about what patterns in your LeetCode practice you want to uncover.

Set Up the Project

Your dashboard has two halves: a React frontend that renders charts, and an Express backend that fetches data from LeetCode on your behalf.

In this step you will scaffold both halves inside one project folder. Vite creates the React app instantly, and Express gives you a lightweight server in just a few files.

In this step, get ready to:
  • Scaffold a React frontend with Vite and install Recharts for charting.
  • Create the Express backend folder with Express and cors packages installed.
  • Verify the frontend starts and shows the Vite starter page.
Scaffold the React frontend with Vite and install Recharts

Vite scaffolds a complete React starter app in seconds. You will also install Recharts, the charting library you will use to build every visualization in this project.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir leetcode-dashboard
cd leetcode-dashboard
  • Scaffold the React frontend by running this command:
npm create vite@latest client -- --template react

What does this command do?

npm create vite@latest client -- --template react tells npm to download the create-vite scaffolding tool, create a folder called client, and fill it with a ready-to-run React project.

The -- --template react part skips the interactive framework selector and picks React directly.

Seeing a prompt about installing create-vite?

If npm asks "Need to install the following packages: create-vite. Ok to proceed? (y)" type y and press Enter. This only appears the first time you use create-vite on your machine.

  • Confirm the client folder was created by running:
ls

You should see client listed in the output.

  • Move into the client folder and install the default dependencies by running:
cd client
npm install
  • Install Recharts by running:
npm install recharts

Your terminal shows "added X packages" confirming Recharts is installed.

  • Start the dev server to confirm the scaffold works by running:
npm run dev

Open http://localhost:5173 in your browser. You should see the default Vite + React starter page with a spinning React logo and a counter button.

Don't see the page?

  • Make sure the terminal is still showing the dev server output. If you see an error, check that npm install completed successfully before you ran npm run dev.
  • Try opening the URL in a different browser or in an incognito window.
  • help me debug why my Vite dev server is not starting
  • Stop the dev server by pressing Ctrl+C in your terminal.

Now you will replace two scaffold files with cleaner base versions for your project.

  • Open the leetcode-dashboard folder in VS Code. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar, type VS Code, and press Enter.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the leetcode-dashboard folder on your Desktop, then click Open.
  • In the Explorer sidebar, expand client then src, and open index.css.
  • Select all the existing content and replace it with this base stylesheet:
:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #1a1a1a;
  background-color: #f5f5f5;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
}

What does this stylesheet do?

This replaces Vite's default styles with a minimal reset. It sets a system font, a light gray background, and removes default margins and padding so your dashboard components control their own spacing.

  • Now open main.jsx in the same src folder.
  • Select all existing content and replace it with this clean entry point:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);

What does this file do?

main.jsx is the entry point for your React app. It mounts the App component into the HTML page and imports the base stylesheet you just created.

  • Save both files with Cmd+S (macOS) or Ctrl+S (Windows).
Create the Express backend folder

Your backend will be a small Express server that proxies requests to LeetCode's API. You will set it up in a separate server folder next to client.

  • In your terminal, navigate back to the project root by running:
cd ~/Desktop/leetcode-dashboard
  • Create the server folder and move into it by running:
mkdir server
cd server
  • Create a package.json file for the server. In VS Code, right-click the server folder in the Explorer sidebar and select New File.
  • Name the file package.json and paste this content:
{
  "name": "leetcode-analytics-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^5.2.1",
    "cors": "^2.8.6"
  }
}

Why type module?

Setting "type": "module" lets you use modern import/export syntax in your server code instead of the older require() style. This keeps both your frontend and backend using the same module system.

  • Save package.json.
  • Install the server dependencies by running this command in your terminal (make sure you are inside the server folder):
npm install

You should see output confirming that express and cors packages were added.

  • Confirm the install worked by running:
ls

You should see node_modules, package.json, and package-lock.json listed.

Seeing errors during npm install?

  • Make sure your terminal is inside the server folder. Run pwd to confirm your path ends with leetcode-dashboard/server.
  • Check that package.json is saved correctly. A missing comma or bracket causes parse errors.
  • help me fix npm install errors in my Express server setup
Verify the frontend starts

Both folders are set up. Time to confirm everything runs.

  • Switch to the client folder and start the dev server by running:
cd ~/Desktop/leetcode-dashboard/client
npm run dev

Before you check the browser, what do you expect to see at http://localhost:5173?

Open http://localhost:5173 in your browser. You should see the Vite + React starter page. The background is now light gray (#f5f5f5) from your new base stylesheet.

Page looks unchanged or broken?

  • If the background is still white, confirm you saved index.css in VS Code. Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows).
  • If the page does not load at all, check the terminal for error messages. A common cause is another process already using port 5173.
  • help me get my Vite dev server running on localhost:5173

✔️ Awesome, I've got everything!

Great. Double check that you have saved all your files before moving on.

ⓧ I'd like to double check the full code

Here are the three key files that should exist at this point. Compare yours against these:

:root {
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.5;
  font-weight: 400;
  color: #1a1a1a;
  background-color: #f5f5f5;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  min-width: 320px;
  min-height: 100vh;
}
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './index.css';

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
);
{
  "name": "leetcode-analytics-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^5.2.1",
    "cors": "^2.8.6"
  }
}

Your project skeleton is ready. Next up, you will build the Express backend that fetches your real LeetCode data.

Build the Backend Proxy

Your React frontend is running, but it cannot talk directly to LeetCode's servers. LeetCode's GraphQL API blocks requests that come from a browser due to CORS restrictions.

The fix is a backend proxy. Your React app sends requests to your own Express server, which then forwards them to LeetCode and returns the results. The browser never touches LeetCode directly, so CORS never triggers.

In this step, get ready to:
  • Create an Express server that proxies requests to LeetCode's GraphQL endpoint.
  • Wire up CORS middleware so your React frontend can call the proxy.
  • Test the proxy endpoint manually to confirm real data flows through.
Create the server file with CORS and the query helper

The server needs three things before it can proxy anything: Express to handle HTTP requests, the cors middleware to allow cross-origin requests from your React app, and a helper function that knows how to query LeetCode's GraphQL endpoint.

  • In VS Code, right-click the server folder in the Explorer sidebar and select New File.
  • Name it server.js.
  • Paste the following code into server.js to set up Express, enable CORS, and create the LeetCode query helper:
import express from 'express';
import cors from 'cors';

const app = express();
const PORT = 3001;

// Allow cross-origin requests from the React frontend
app.use(cors());
app.use(express.json());

// Reusable helper that sends a GraphQL query to LeetCode
async function queryLeetCode(query, variables) {
  const response = await fetch('https://leetcode.com/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  if (!response.ok) {
    throw new Error(`LeetCode API error: ${response.status}`);
  }
  return response.json();
}

app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

What does this code do?

  • app.use(cors()) tells Express to add CORS headers to every response, so your React app on port 5173 can call this server on port 3001 without the browser blocking it.
  • queryLeetCode() is a reusable helper. It POSTs a GraphQL query to https://leetcode.com/graphql and returns the parsed JSON response.
  • app.listen(PORT) starts the server on port 3001 and prints a confirmation message.
  • Save server.js.
  • Open a new terminal in VS Code by pressing Ctrl+` (backtick key) or clicking Terminal then New Terminal.
  • Navigate to the server folder and start the server by running:
cd server
node server.js

You should see Server running on http://localhost:3001 printed in the terminal.

Don't see the message?

  • Make sure you ran npm install inside the server folder in Step 1. Without installed dependencies, Node cannot find Express.
  • Check that server/package.json has "type": "module" set. Without it, the import statements fail.
  • If port 3001 is already in use, stop whatever process is using it and try again.

help me fix my Express server startup error

Add the user profile route

The first route fetches a LeetCode user's core stats: their solved count by difficulty, their ranking, and their topic-level breakdown. This single endpoint gives the frontend everything it needs for the stats panel, bar chart, and radar chart.

  • Stop the running server by pressing Ctrl+C in the terminal.
  • In server.js, find the app.listen line at the bottom.
  • Paste the following route above app.listen to define the route handler and its GraphQL query:
app.get('/api/user/:username', async (req, res) => {
  const { username } = req.params;

  // GraphQL query for profile, solve stats, and topic tags
  const query = `
    query getUserProfile($username: String!) {
      allQuestionsCount {
        difficulty
        count
      }
      matchedUser(username: $username) {
        username
        profile {
          ranking
          reputation
        }
        submitStatsGlobal {
          acSubmissionNum {
            difficulty
            count
            submissions
          }
        }
        tagProblemCounts {
          advanced {
            tagName
            problemsSolved
          }
          intermediate {
            tagName
            problemsSolved
          }
          fundamental {
            tagName
            problemsSolved
          }
        }
      }
    }
  `;
  • Directly below the closing `; of the query string, paste the error-handling logic that completes the route:
  // Send query to LeetCode and handle errors
  try {
    const data = await queryLeetCode(query, { username });
    if (!data.data.matchedUser) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(data.data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch LeetCode data' });
  }
});

What does this route do?

When the frontend calls /api/user/someUsername, this handler runs.

  • The query variable holds a GraphQL query requesting total question counts, the user's profile, their acceptance stats by difficulty, and their topic-level problem counts.
  • queryLeetCode() sends this query to LeetCode's endpoint and returns the response.
  • If matchedUser is null, the username does not exist and the route returns a 404. If the request to LeetCode itself fails, the catch block returns a 500.
  • Save server.js and restart the server by running:
node server.js

Before you test, what do you think will happen if you visit http://localhost:3001/api/user/leetcode in your browser? Will you see JSON data, or an error?

  • Open your browser and navigate to http://localhost:3001/api/user/leetcode.

You should see a JSON response containing fields like allQuestionsCount and matchedUser with real LeetCode data. The leetcode username is LeetCode's own official account, so it always has data.

Seeing an error in the browser?

  • If you see Cannot GET /api/user/leetcode, the route was not added correctly. Make sure the app.get block sits above app.listen in server.js.
  • If you see Failed to fetch LeetCode data, your network might be blocking the request. Check your internet connection and try again.
  • If the browser shows a page-not-reachable error, the server is not running. Go back to the terminal and run node server.js again.

help me debug why my /api/user route is not returning LeetCode data

Add the calendar data route

The second route fetches a user's submission calendar. This data powers the activity grid that will show solving streaks and consistency patterns.

  • Stop the server with Ctrl+C.
  • In server.js, paste the following route below the /api/user/:username route and above app.listen:
app.get('/api/user/:username/calendar', async (req, res) => {
  const { username } = req.params;

  // Query for submission history and streak data
  const query = `
    query userProfileCalendar($username: String!) {
      matchedUser(username: $username) {
        userCalendar {
          activeYears
          streak
          totalActiveDays
          submissionCalendar
        }
      }
    }
  `;

  try {
    const data = await queryLeetCode(query, { username });
    if (!data.data.matchedUser) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json(data.data.matchedUser.userCalendar);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch calendar data' });
  }
});

What does this route return?

This route queries LeetCode's userCalendar field, which returns the user's current streak, total active days, and a submissionCalendar string.

The submissionCalendar is a JSON string mapping Unix timestamps (start of each day) to submission counts for that day. The React frontend will parse this into a visual grid later.

  • Save server.js and restart the server:
node server.js
  • In your browser, navigate to http://localhost:3001/api/user/leetcode/calendar.

You should see JSON containing streak, totalActiveDays, and a long submissionCalendar string filled with timestamp-count pairs.

Why two separate routes instead of one?

Each route sends a focused GraphQL query requesting only the data it needs. Splitting them means the frontend can fetch profile stats and calendar data in parallel, and each response stays small and fast.

  • Test error handling by visiting http://localhost:3001/api/user/thisuserdoesnotexist99999 in your browser.

You should see {"error":"User not found"} with a 404 status. This confirms your error handling catches invalid usernames gracefully.

Calendar route returning an error?

  • Make sure the new route is placed between the first app.get block and app.listen. Route order matters.
  • Confirm you restarted the server after saving. Changes to server.js only take effect after a restart.

help me figure out why my calendar route is not working

✔️ Awesome, I've got everything!

Great. Double check you have saved server.js and the server is running on port 3001.

ⓧ I'd like to double check the full code

Your server/server.js file should contain these sections. Scroll through and confirm each is present in this order:

  • Imports, Express setup, CORS middleware, and the queryLeetCode helper function.
  • The /api/user/:username route with the getUserProfile GraphQL query and try/catch error handling.
  • The /api/user/:username/calendar route with the userProfileCalendar GraphQL query and try/catch error handling.
  • app.listen(PORT) at the very bottom.

Your backend proxy is live and serving real LeetCode data. Next up, you will wire your React frontend to this proxy and display your actual stats on screen.

Connect React to the API and Show Raw Stats

Your backend proxy is live and returning real LeetCode data. Now you need to confirm that data can flow all the way from LeetCode, through your proxy, and into React components on screen.

In this step, you will build the React frontend that talks to your backend and displays your solving stats as plain text. The numbers will look bare for now. That gap between raw numbers and actionable insight is exactly what motivates the charts in the next step.

In this step, get ready to:
  • Create a username input component and a fetch hook that calls your backend proxy.
  • Display the raw stats (total solved, per-difficulty counts, ranking) in a simple stats panel.
  • Style the dashboard with CSS so the data is easy to scan.
Create the username input and wire up the fetch logic

Your React app needs two things to get data on screen: an input where the learner types a LeetCode username, and a function that sends that username to your backend proxy.

  • In VS Code, open the integrated terminal for your client folder.
  • Create a components directory inside src by running this command:
mkdir src/components
  • Create a new file at src/components/UsernameInput.jsx and paste the following code:
import { useState } from 'react';

function UsernameInput({ onSubmit, loading }) {
  const [username, setUsername] = useState('');

  function handleSubmit(e) {
    e.preventDefault();
    if (username.trim()) {
      onSubmit(username.trim());
    }
  }

  return (
    <form className="username-form" onSubmit={handleSubmit}>
      <input
        type="text"
        value={username}
        onChange={(e) => setUsername(e.target.value)}
        placeholder="Enter LeetCode username"
        disabled={loading}
      />
      <button type="submit" disabled={loading || !username.trim()}>
        {loading ? 'Loading...' : 'Analyze'}
      </button>
    </form>
  );
}

export default UsernameInput;

What does this component do?

This is a controlled form component. React owns the input value through useState, so you always know what the user typed.

When the form submits, it calls the onSubmit prop (which will be your fetch function) with the trimmed username. The loading prop disables the input and button while data is being fetched.

Now you need the main App.jsx file that manages state and calls your backend. This is where the fetch logic lives.

  • Open src/App.jsx and replace its entire contents with the following code. Start with the imports, state, and fetch function:
import { useState } from 'react';
import UsernameInput from './components/UsernameInput';

function App() {
  // Store fetched data and UI state
  const [userData, setUserData] = useState(null);
  const [calendarData, setCalendarData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  // Fetch both endpoints in parallel
  async function fetchUserData(username) {
    setLoading(true);
    setError(null);
    try {
      const [userRes, calendarRes] = await Promise.all([
        fetch(`http://localhost:3001/api/user/${username}`),
        fetch(`http://localhost:3001/api/user/${username}/calendar`),
      ]);
      if (!userRes.ok) {
        throw new Error('User not found');
      }
      const user = await userRes.json();
      const calendar = await calendarRes.json();
      setUserData(user);
      setCalendarData(calendar);
    } catch (err) {
      setError(err.message);
      setUserData(null);
      setCalendarData(null);
    } finally {
      setLoading(false);
    }
  }

What does this code do?

  • Promise.all fires both API calls at the same time instead of waiting for one to finish before starting the other.
  • If the user endpoint returns a non-OK status (like 404 for an invalid username), the code throws an error that gets caught and stored in state.
  • The finally block always resets loading to false, whether the request succeeded or failed.
  • Directly below the fetchUserData function (still inside the App component), add the return statement and export:
  return (
    <div className="app">
      <h1>LeetCode Analytics</h1>
      <UsernameInput onSubmit={fetchUserData} loading={loading} />
      {error && <p className="error">{error}</p>}
    </div>
  );
}

export default App;

What does this render?

The JSX renders a heading, the username form, and conditionally shows an error paragraph if something goes wrong. The {error && ...} pattern only renders the <p> tag when error is not null.

  • Save both UsernameInput.jsx and App.jsx.
  • Switch to your browser at http://localhost:5173.

You should see a heading that says "LeetCode Analytics" and an input field with an "Analyze" button. Type any text and notice the button becomes clickable.

Don't see the form?

  • Check that your file is saved as src/components/UsernameInput.jsx (not src/UsernameInput.jsx).
  • Make sure the Vite dev server is still running in your terminal. If it stopped, run npm run dev again from the client folder.
  • Look for red error text in the browser or terminal. A typo in the import path is the most common cause.

help me debug why my UsernameInput component isn't rendering

Build the stats overview panel

The fetch logic is ready but there is nothing to display the data yet. You need a component that takes the API response and renders the key numbers.

  • Create a new file at src/components/StatsOverview.jsx and paste the following code:
function StatsOverview({ data }) {
  // Extract solved counts from the API response
  const stats = data.matchedUser.submitStatsGlobal.acSubmissionNum;
  const totalSolved = stats.find((s) => s.difficulty === 'All')?.count || 0;
  const easySolved = stats.find((s) => s.difficulty === 'Easy')?.count || 0;
  const mediumSolved = stats.find((s) => s.difficulty === 'Medium')?.count || 0;
  const hardSolved = stats.find((s) => s.difficulty === 'Hard')?.count || 0;
  const ranking = data.matchedUser.profile.ranking;

  return (
    <div className="stats-overview">
      <div className="stat-card">
        <div className="value">{totalSolved}</div>
        <div className="label">Total Solved</div>
      </div>
      <div className="stat-card">
        <div className="value" style={{ color: '#00b8a3' }}>{easySolved}</div>
        <div className="label">Easy</div>
      </div>
      <div className="stat-card">
        <div className="value" style={{ color: '#ffc01e' }}>{mediumSolved}</div>
        <div className="label">Medium</div>
      </div>
      <div className="stat-card">
        <div className="value" style={{ color: '#ff375f' }}>{hardSolved}</div>
        <div className="label">Hard</div>
      </div>
    </div>
  );
}

export default StatsOverview;

What does this component do?

  • It receives the full API response as the data prop and digs into acSubmissionNum to find the count for each difficulty level.
  • The ?. optional chaining prevents a crash if a difficulty level is missing from the response.
  • Each stat card uses inline style to color-code the numbers: green for Easy, yellow for Medium, red for Hard.

Now wire StatsOverview into App.jsx so it renders when data arrives.

  • In src/App.jsx, find the import line for UsernameInput:
import UsernameInput from './components/UsernameInput';
  • Add a new import for StatsOverview directly below it, so your imports look like this:
import UsernameInput from './components/UsernameInput';
import StatsOverview from './components/StatsOverview';
  • In the return statement, find the line with the error paragraph:
{error && <p className="error">{error}</p>}
  • Add the following block directly below it, before the closing </div>:
      {userData && (
        <div className="dashboard">
          <StatsOverview data={userData} />
        </div>
      )}

What does this conditional render do?

The {userData && (...)} pattern only renders the dashboard section when userData is not null. Before the user submits a username, nothing shows below the form.

  • Save both StatsOverview.jsx and App.jsx.
  • Switch to your browser and enter a real LeetCode username (for example, leetcode), then click Analyze.

You should see four numbers appear below the form: your total solved count, Easy count, Medium count, and Hard count. They are unstyled plain text for now.

See "User not found" instead of stats?

  • Make sure your Express server is still running in its own terminal. The React app calls http://localhost:3001 which needs the backend to respond.
  • Double-check the username is a real LeetCode profile. Try leetcode as a test (it is LeetCode's own official account).

help me debug why my React app can't fetch data from the backend

Style the dashboard

The numbers work but they are hard to read as a plain list. A few CSS rules will turn them into clean stat cards with a centered layout.

  • In src/App.jsx, find the import for StatsOverview:
import StatsOverview from './components/StatsOverview';
  • Add the CSS import directly below it:
import StatsOverview from './components/StatsOverview';
import './App.css';
  • Create a new file at src/App.css and paste the following layout styles:
.app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
  font-family: system-ui, -apple-system, sans-serif;
}

.app h1 {
  text-align: center;
  margin-bottom: 2rem;
  color: #1a1a1a;
}

.error {
  color: #dc3545;
  text-align: center;
  padding: 1rem;
  background: #fee;
  border-radius: 8px;
}

.dashboard {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  margin-top: 2rem;
}

.charts-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2rem;
}
  • Continue adding to App.css below the previous styles. Paste the card styles:
.chart-card {
  background: #fff;
  border-radius: 12px;
  padding: 1.5rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.chart-card h3 {
  margin: 0 0 1rem 0;
  color: #333;
}

.stats-overview {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1rem;
}

.stat-card {
  background: #fff;
  border-radius: 12px;
  padding: 1.5rem;
  text-align: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.stat-card .value {
  font-size: 2rem;
  font-weight: 700;
  color: #1a1a1a;
}

.stat-card .label {
  font-size: 0.875rem;
  color: #666;
  margin-top: 0.25rem;
}
  • Save App.css and App.jsx, then refresh your browser.

You should see a centered heading, and if you enter a username again, the stats now display as four white cards in a grid row.

  • Continue adding to App.css. Paste the activity section styles (these prepare for a later step):
.activity-section {
  background: #fff;
  border-radius: 12px;
  padding: 1.5rem;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

.activity-header {
  display: flex;
  gap: 2rem;
  margin-bottom: 1rem;
}

.activity-header .stat {
  font-size: 0.875rem;
  color: #666;
}

.activity-header .stat strong {
  color: #1a1a1a;
}

.activity-grid {
  display: grid;
  grid-template-columns: repeat(52, 1fr);
  gap: 2px;
}

.activity-cell {
  width: 100%;
  aspect-ratio: 1;
  border-radius: 2px;
  background: #ebedf0;
}
  • Add the activity color levels and form styles below the previous block:
.activity-cell.level-1 { background: #9be9a8; }
.activity-cell.level-2 { background: #40c463; }
.activity-cell.level-3 { background: #30a14e; }
.activity-cell.level-4 { background: #216e39; }

.username-form {
  display: flex;
  justify-content: center;
  gap: 0.5rem;
}

.username-form input {
  padding: 0.75rem 1rem;
  border: 2px solid #e0e0e0;
  border-radius: 8px;
  font-size: 1rem;
  width: 280px;
  outline: none;
  transition: border-color 0.2s;
}

.username-form input:focus {
  border-color: #ffa116;
}

.username-form button {
  padding: 0.75rem 1.5rem;
  background: #ffa116;
  color: #fff;
  border: none;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}
  • Finally, add the button hover and disabled states at the bottom of App.css:
.username-form button:hover {
  background: #e89200;
}

.username-form button:disabled {
  background: #ccc;
  cursor: not-allowed;
}
  • Save App.css and refresh your browser.

Before you test, what do you expect to see when you enter an invalid username like zzzznotarealuser999?

  • Type zzzznotarealuser999 into the input and click Analyze.

You should see a red error box that says "User not found". The orange input and button are now fully styled with hover and disabled states.

  • Now enter a real LeetCode username and click Analyze.

Your total solved, Easy, Medium, and Hard counts appear in four white stat cards with large colored numbers. Your ranking is not displayed yet as a card, but the data is flowing correctly.

Styles not appearing?

  • Confirm App.jsx has the line import './App.css' near the top. Without this import, Vite does not load the stylesheet.
  • Check that your App.css file is saved inside src/ (not in the project root).
  • If only some styles work, scroll through App.css and look for missing closing braces.

help me figure out why my CSS styles are not applying to my React dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved all four files: App.jsx, App.css, UsernameInput.jsx, and StatsOverview.jsx.

ⓧ I'd like to double check the full code

Scroll up and confirm each of these files matches what you pasted:

  • src/components/UsernameInput.jsx (the controlled form component).
  • src/components/StatsOverview.jsx (extracts and displays solved counts).
  • src/App.jsx (state, fetchUserData, conditional rendering of UsernameInput, error, and StatsOverview).
  • src/App.css (all layout, card, activity, and form styles in one file).

If your App.jsx does not import StatsOverview and './App.css', add those imports now.

You have real LeetCode data flowing from the API through your proxy into styled React components. The numbers are correct, but they do not reveal patterns. Next up, you will transform them into interactive charts that make your strengths and weaknesses instantly visible.

Add Interactive Charts

Your dashboard shows real numbers, but plain text hides patterns. Looking at "Easy: 150, Medium: 80, Hard: 20" does not instantly reveal whether you are neglecting harder problems or which algorithm topics you have barely touched.

A bar chart makes difficulty imbalance obvious at a glance. A radar chart exposes your weakest topics in a shape you can read instantly. In this step, you will build both using Recharts and wire them into the dashboard.

In this step, get ready to:
  • Build a DifficultyChart component with a bar chart showing solved vs total for each difficulty level.
  • Build a TopicRadar component with a radar chart showing your top algorithm categories.
  • Wire both charts into App.jsx inside a responsive grid layout.
Build the DifficultyChart component

This component takes the raw solved-count data and renders a grouped bar chart. Each difficulty level gets a colored bar for your solved count and a grey bar for the total available.

  • In VS Code, create a new file at client/src/components/DifficultyChart.jsx.
  • Add the import and data preparation logic by pasting this code:
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell } from 'recharts';

function DifficultyChart({ data }) {
  // Extract solved counts and total questions from API response
  const stats = data.matchedUser.submitStatsGlobal.acSubmissionNum;
  const allQuestions = data.allQuestionsCount;

  // Build an array with solved vs total for each difficulty
  const chartData = ['Easy', 'Medium', 'Hard'].map((difficulty) => {
    const solved = stats.find((s) => s.difficulty === difficulty)?.count || 0;
    const total = allQuestions.find((q) => q.difficulty === difficulty)?.count || 0;
    return { difficulty, solved, total };
  });

  // Map each difficulty to its signature color
  const colors = { Easy: '#00b8a3', Medium: '#ffc01e', Hard: '#ff375f' };

What does this code do?

The chartData array transforms the raw API response into the shape Recharts needs. Each entry has a difficulty label, a solved count (your progress), and a total count (how many exist on LeetCode).

The colors object maps each difficulty to the same green/yellow/red scheme LeetCode uses, so the chart feels familiar.

  • Directly below the code you just pasted, add the JSX that renders the bar chart:
  // Render a grouped bar chart with colored solved bars and grey total bars
  return (
    <div className="chart-card">
      <h3>Difficulty Breakdown</h3>
      <ResponsiveContainer width="100%" height={250}>
        <BarChart data={chartData} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="difficulty" />
          <YAxis />
          <Tooltip />
          <Bar dataKey="solved" name="Solved">
            {chartData.map((entry) => (
              <Cell key={entry.difficulty} fill={colors[entry.difficulty]} />
            ))}
          </Bar>
          <Bar dataKey="total" name="Total" fill="#e0e0e0" />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

export default DifficultyChart;

What does the JSX do?

ResponsiveContainer makes the chart resize with its parent. Inside it, BarChart renders the axes and grid lines.

The first Bar uses a Cell per entry to color each difficulty differently. The second Bar renders the total available in neutral grey so you can see progress against the full set.

  • Save DifficultyChart.jsx.
  • In the VS Code Explorer sidebar, confirm you see DifficultyChart.jsx listed inside client/src/components/.

Don't see the file in your sidebar?

Make sure you created the file inside the components folder, not at the project root. The path should be client/src/components/DifficultyChart.jsx.

help me find where my DifficultyChart.jsx file ended up

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your DifficultyChart.jsx should contain both chunks in order:

  • The import line and DifficultyChart function with chartData and colors defined (first chunk).
  • The return statement with BarChart, both Bar elements, and the export default at the bottom (second chunk).

Scroll up and confirm each chunk is present, in order, with no gaps between them.

Build the TopicRadar component

The radar chart plots your top 8 algorithm tags in a web shape. Tags where you have solved more problems push the shape outward. Weak spots collapse inward, making imbalances immediately visible.

  • Create a new file at client/src/components/TopicRadar.jsx.
  • Add the import, data processing, and empty-state fallback by pasting this code:
import { RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer, Tooltip } from 'recharts';

function TopicRadar({ data }) {
  // Merge all tag categories into one array
  const tags = data.matchedUser.tagProblemCounts;
  const allTags = [
    ...(tags.fundamental || []),
    ...(tags.intermediate || []),
    ...(tags.advanced || []),
  ];

  // Sort by most solved and take the top 8 for readability
  const sorted = allTags.sort((a, b) => b.problemsSolved - a.problemsSolved);
  const top8 = sorted.slice(0, 8).map((tag) => ({
    topic: tag.tagName,
    solved: tag.problemsSolved,
  }));

  // Handle users with no topic data
  if (top8.length === 0) {
    return (
      <div className="chart-card">
        <h3>Topic Strengths</h3>
        <p>No topic data available</p>
      </div>
    );
  }

What does this code do?

LeetCode groups algorithm tags into three tiers: fundamental, intermediate, and advanced. This code merges all three into one flat array, sorts by problems solved, and keeps only the top 8 so the radar chart stays readable.

The empty-state check prevents Recharts from crashing when a user has no topic data at all.

  • Directly below the code you just pasted, add the radar chart JSX:
  // Render the radar chart with topic labels around the perimeter
  return (
    <div className="chart-card">
      <h3>Topic Strengths</h3>
      <ResponsiveContainer width="100%" height={250}>
        <RadarChart data={top8}>
          <PolarGrid />
          <PolarAngleAxis dataKey="topic" tick={{ fontSize: 11 }} />
          <PolarRadiusAxis />
          <Tooltip />
          <Radar dataKey="solved" stroke="#ffa116" fill="#ffa116" fillOpacity={0.5} />
        </RadarChart>
      </ResponsiveContainer>
    </div>
  );
}

export default TopicRadar;

What does this JSX do?

PolarAngleAxis places topic names around the perimeter like a clock face. Radar draws a filled polygon whose vertices extend outward proportional to your solved count for each topic.

The orange fill with 50% opacity makes the shape easy to read without obscuring the grid lines underneath.

  • Save TopicRadar.jsx.
  • Confirm TopicRadar.jsx appears in your client/src/components/ folder in the Explorer sidebar.

Seeing a syntax error in the file?

Make sure the second chunk starts immediately after the closing brace of the if (top8.length === 0) block. There should be no gap or duplicate closing brace between the two chunks.

help me fix a syntax error in my TopicRadar component

✔️ Awesome, I've got everything!

Great. Make sure you have saved the file before moving on.

ⓧ I'd like to double check the full code

Your TopicRadar.jsx should contain both chunks in order:

  • The import line, the TopicRadar function with allTags, sorted, top8, and the empty-state if block (first chunk).
  • The main return statement with RadarChart and the export default at the bottom (second chunk).

Scroll up and confirm each chunk is present, in order, with no gaps between them.

Wire the charts into your dashboard

Both chart components exist but the app does not render them yet. You need to import them and place them inside a grid container so they appear side by side.

  • In App.jsx, find the import section at the top of the file:
import UsernameInput from './components/UsernameInput';
import StatsOverview from './components/StatsOverview';
import './App.css';
  • Add the two new imports between StatsOverview and the CSS import, so the section looks like this:
import UsernameInput from './components/UsernameInput';
import StatsOverview from './components/StatsOverview';
import DifficultyChart from './components/DifficultyChart';
import TopicRadar from './components/TopicRadar';
import './App.css';
  • Now find the return statement in the same file. Locate the <StatsOverview data={userData} /> line inside the dashboard div.
  • Replace the entire return block with the following, which adds the charts-grid container below StatsOverview:
  return (
    <div className="app">
      <h1>LeetCode Analytics</h1>
      <UsernameInput onSubmit={fetchUserData} loading={loading} />

      {error && <p className="error">{error}</p>}

      {userData && (
        <div className="dashboard">
          <StatsOverview data={userData} />
          <div className="charts-grid">
            <DifficultyChart data={userData} />
            <TopicRadar data={userData} />
          </div>
        </div>
      )}
    </div>
  );

What changed?

You wrapped DifficultyChart and TopicRadar in a charts-grid div. The CSS for this class already exists in your App.css from a previous step. It uses a two-column grid so the bar chart and radar chart sit side by side.

  • Save App.jsx.

Before you check the browser, what do you expect to see when you enter a LeetCode username now?

  • Switch to your browser at http://localhost:5173 and enter a LeetCode username.

You should see the stats overview cards at the top, followed by two charts side by side. The left chart is a grouped bar chart showing your Easy, Medium, and Hard solved counts in color against grey totals. The right chart is a radar polygon plotting your top 8 algorithm tags.

Charts not appearing?

  • Check your browser console for import errors. A typo in the file path (e.g. ./components/DifficultyChart vs ./component/DifficultyChart) will cause a module-not-found error.
  • Make sure both chart files are saved and have the export default line at the bottom.
  • If the radar chart shows "No topic data available", try a more active LeetCode username that has solved problems across multiple tags.

help me debug why my Recharts charts are not rendering

Your dashboard now transforms raw API data into visual insight. Next up, you will add an activity grid that shows your daily solving consistency over the past year.

Visualize Your Activity Patterns

Your dashboard already shows what you've solved and which topics you're strongest in. But it doesn't answer a crucial question: are you actually practicing consistently?

Consistency matters more than volume. In this step, you'll build a calendar heatmap that reveals your daily submission activity over the past year. Streaks, gaps, and momentum will all become visible at a glance.

In this step, get ready to:
  • Parse LeetCode's submission calendar into daily activity cells.
  • Render a colored grid showing submission intensity for each day of the past year.
  • Display your current streak and total active days as headline stats.
Parse the submission calendar data

The calendar endpoint you built earlier returns a submissionCalendar field. This is a JSON string mapping Unix timestamps (the start of each day) to how many problems you submitted that day.

You need to parse this string, iterate over every day in the past year, and assign each day an intensity level based on how many submissions it had.

  • Create a new file at client/src/components/ActivityGrid.jsx and add the following data-parsing logic:
function ActivityGrid({ data }) {
  const { streak, totalActiveDays, submissionCalendar } = data;
  // Parse the JSON string of timestamps into a usable object
  const calendar = JSON.parse(submissionCalendar || '{}');

  const today = new Date();
  const oneYearAgo = new Date(today);
  oneYearAgo.setFullYear(today.getFullYear() - 1);

  // Build an array of cells, one per day for the past year
  const cells = [];
  const current = new Date(oneYearAgo);

  while (current <= today) {
    // Convert JS milliseconds to Unix seconds, snap to day start
    const timestamp = Math.floor(current.getTime() / 1000);
    const dayStart = timestamp - (timestamp % 86400);
    const count = calendar[dayStart] || 0;

    // Assign intensity level based on submission count
    let level = 0;
    if (count >= 5) level = 4;
    else if (count >= 3) level = 3;
    else if (count >= 2) level = 2;
    else if (count >= 1) level = 1;

    cells.push({ date: current.toISOString().split('T')[0], count, level });
    current.setDate(current.getDate() + 1);
  }

What does this code do?

The submissionCalendar from LeetCode is a JSON string like {"1720137600": 3, "1720224000": 1}. Each key is a Unix timestamp for the start of a day, and the value is how many problems you submitted.

The while loop walks day-by-day from one year ago to today. For each day, it snaps the timestamp to midnight (using modulo 86400 seconds) and looks up how many submissions you made. It then assigns a level from 0 (no activity) to 4 (5+ submissions) for coloring.

Render the activity grid and streak stats

Now that you have an array of cells with intensity levels, you need JSX that renders them as a visual grid. Each cell gets a CSS class like level-2 that maps to a shade of green.

  • In the same ActivityGrid.jsx file, add the return statement and export directly below the closing } of the while loop:
  // Render streak stats and the colored grid
  return (
    <div className="activity-section">
      <h3>Activity</h3>
      <div className="activity-header">
        <span className="stat">
          Current Streak: <strong>{streak} days</strong>
        </span>
        <span className="stat">
          Total Active Days: <strong>{totalActiveDays}</strong>
        </span>
      </div>
      <div className="activity-grid">
        {cells.map((cell) => (
          <div
            key={cell.date}
            className={`activity-cell level-${cell.level}`}
            title={`${cell.date}: ${cell.count} submissions`}
          />
        ))}
      </div>
    </div>
  );
}

export default ActivityGrid;

What does this code do?

  • The activity-header div shows your current streak and total active days as headline stats above the grid.
  • The activity-grid div maps each cell to a tiny square. The class activity-cell level-${cell.level} picks the right shade of green from your existing CSS.
  • The title attribute shows the date and submission count when the learner hovers over a cell.
  • Save ActivityGrid.jsx.

Seeing a syntax error?

Make sure the return statement is inside the ActivityGrid function but after the while loop's closing brace. The export default line should be outside and below the function's final }.

help me fix a syntax error in my ActivityGrid component

✔️ Awesome, I've got everything!

Great. Double check you have saved ActivityGrid.jsx before moving on.

ⓧ I'd like to double check the full code

Since the file is longer than 30 lines, scroll up and confirm you have both chunks in order:

  • The function ActivityGrid({ data }) declaration with the parsing logic and while loop.
  • The return statement rendering the activity-section div with header stats and the grid.
  • The export default ActivityGrid at the bottom.
Wire ActivityGrid into App

The component exists, but your app does not render it yet. You need to import it and conditionally display it when calendar data is available.

  • In App.jsx, find the import for TopicRadar:
import TopicRadar from './components/TopicRadar';
  • Add this import directly below it:
import ActivityGrid from './components/ActivityGrid';
  • Now scroll down to the JSX. Find the closing </div> of the charts-grid div:
          </div>
        </div>
      )}
  • Add the ActivityGrid render between the charts-grid closing </div> and the dashboard closing </div>, so the section now looks like this:
          </div>
          {calendarData && <ActivityGrid data={calendarData} />}
        </div>
      )}

Why the conditional render?

The {calendarData && <ActivityGrid ... />} pattern only renders the component when calendarData is not null. This prevents the component from crashing while the API call is still in progress.

  • Save App.jsx.

Before you test, what do you expect to see below the bar chart and radar chart when you enter your username?

  • Switch to your browser at http://localhost:5173 and enter your LeetCode username.

You should see a grid of small colored cells appear below your charts, representing your daily submission activity over the past year. Above the grid, your current streak and total active days are displayed as headline stats.

Don't see the activity grid?

  • Check that you imported ActivityGrid in App.jsx and that the import path matches ./components/ActivityGrid exactly.
  • Confirm the render line uses calendarData (not userData) as both the condition and the prop.
  • Open your browser's DevTools console. If you see a JSON parse error, check that your backend's /api/user/:username/calendar endpoint is still running.

help me debug why my ActivityGrid is not showing up

Your dashboard now paints a full picture of your LeetCode journey. You can see what you've solved, where your strengths lie, and whether your practice is consistent. Next up, you could take this further with the Secret Mission and track your contest performance over time.

Secret mission

Contest Rating History

Your dashboard tracks solving stats, topics, and daily activity. But if you compete in LeetCode contests, there is one more dimension to your journey: your competitive rating over time. Can you add a line chart that shows how your contest performance has evolved?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing cloud costs or subscriptions to worry about.

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • A local Vite dev server running on port 5173 (React frontend).
  • A local Express server running on port 3001 (backend proxy).
  • Project files in the client/ and server/ folders on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively using the dashboard to track your LeetCode progress.

  • Both servers continue running in your terminal windows.
  • Visit http://localhost:5173 anytime to use your dashboard.

Pause - I'll come back to this later

Stop both servers to free up memory and terminal sessions. Your project files stay on disk so you can restart anytime.

  • Switch to the terminal running your Vite dev server and press Ctrl+C to stop it.
  • Switch to the terminal running your Express server and press Ctrl+C to stop it.
  • To restart later, open a terminal in the client/ folder and run npm run dev for the frontend.
  • Open another terminal in the server/ folder and run node server.js for the backend.

Delete - I don't want to use this again

Remove all project files and stop both servers. Nothing else to clean up since everything runs locally.

  • Stop both servers by pressing Ctrl+C in each terminal window.
  • Delete the project folders by running these commands (macOS/Linux):
cd ~/Desktop
rm -rf client server
  • On Windows, run this instead:
cd ~\Desktop
rmdir /s /q client server
  • Confirm the folders are gone by running ls (macOS/Linux) or dir (Windows) on your Desktop. You should no longer see client or server listed.

Nice Work!

Nice Work!

You just built a full-stack analytics dashboard that pulls your real LeetCode data and transforms it into interactive visualizations. From a blank project to bar charts, radar charts, and activity grids, you now have a personal tool that reveals patterns in your practice.

You've learned how to:

  • Built a full-stack React application with an Express backend proxy server that forwards requests to an external API.
  • Queried LeetCode's public GraphQL API and transformed the response into structured data for rendering.
  • Created interactive bar charts, radar charts, and an activity grid using Recharts.
  • Understood why CORS exists and how backend proxying solves it for browser-based applications.
  • Secret Mission: Added a contest rating line chart with time-series data visualization showing competitive performance over time.

Ready to quiz yourself?