Build a Mini Learning Platform with React
Build a React app with components, state, and localStorage persistence.
Introduction
30 Second Summary
When you're learning something new, having a clear list of steps to follow makes all the difference. A good learning platform lets you track exactly where you are and what's left to do.
In this project, you will build a mini learning platform called LearnFlow using React. You will browse a catalog of tech projects, click into each one to follow its steps, and check off your progress as you go.
What You'll Build
Picture browsing a grid of project cards in your browser, clicking one to reveal its step-by-step guide, and checking off each step as you complete it.
By the end of this project, you'll have:
- A project catalog homepage with cards you can browse, each showing a title, difficulty badge, duration, and a progress bar that fills as you complete steps.
- A detail view for each project where you read through its steps and tick off checkboxes to mark each one complete.
- Persistent progress tracking powered by localStorage, so your checked-off steps stay checked even after you close the browser.
- Secret Mission: Add search and difficulty filtering to find projects instantly in your catalog.
Do I need any prior experience?
No prior web development or React experience is needed. You already have everything you need (VS Code and Node.js) installed.
Before We Start
Before diving into code, take a moment to lock in what you're about to build and why it matters to you.
Set Up Your React Project
To build LearnFlow, you need a working development environment. React apps require a build tool that compiles your code and serves it to the browser during development.
Vite is a modern build tool that scaffolds a ready-to-run React project in seconds. It includes hot module replacement, so every change you save shows up in the browser instantly.
In this step, get ready to:
- Scaffold a new React project using Vite.
- Install the project dependencies and start the dev server.
- Verify the app runs in your browser.
Scaffold a new React project
- 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
- Scaffold your React project by running this command:
npm create vite@latest learnflow -- --template react
What does this command do?
npm create vite@latest downloads and runs the latest version of Vite's project scaffolder.
learnflow is the name of your project folder. The --template react flag tells Vite to set up a React project with all the starter files you need.
- Confirm the project folder was created by running:
ls
You should see learnflow listed in the output.
Don't see the learnflow folder?
- Make sure you typed the command exactly as shown, including the two -- separators before --template.
- Check that your Node.js version is 20.19 or higher by running node --version. Vite requires this minimum version to run.
- Help me troubleshoot the Vite scaffold command
Install dependencies and start the dev server
- Move into your project folder and install its dependencies by running these commands:
cd learnflow
npm install
You should see output confirming that packages were added successfully.
- Start the development server by running:
npm run dev
What is the dev server?
The dev server compiles your React code and serves it to your browser at http://localhost:5173. It watches your files for changes and refreshes the browser automatically.
This command takes over your terminal. Keep it running for the rest of the project. To stop it later, press Ctrl+C.
You should see output in your terminal that includes Local: http://localhost:5173/.
Dev server not starting?
- Make sure you ran npm install before npm run dev. The dev server needs the installed packages to run.
- Check that you are inside the learnflow folder. Run pwd to confirm your current directory.
- Help me get the Vite dev server running
Open the project in VS Code and verify it runs
Your dev server is running, so now open the project files in your editor and check the app in your browser.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Visual Studio Code and press Enter to open it.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop in the file dialog.
- Select the learnflow folder.
- Click Open.
What files did Vite create?
In the Explorer sidebar in VS Code, you can see the files Vite scaffolded for you. The important ones are src/App.jsx (your main component), src/App.css (its styles), and src/index.css (global styles).
You will replace the contents of these files in the next step when you build the project catalog.
Before you check the browser, what do you think you will see at http://localhost:5173?
- Open your browser and go to http://localhost:5173.
You should see the Vite + React starter page with logos and a counter button. Click the counter button a few times to confirm the app is interactive.
Not seeing the starter page?
- Make sure the dev server is still running in your terminal. You should see the Local: http://localhost:5173/ line in the output.
- Double-check the URL in your browser address bar. It should be http://localhost:5173, not https.
- If you accidentally closed the terminal, open a new one, navigate back to the learnflow folder on your Desktop, and run npm run dev again.
- Help me see my Vite React app in the browser
Your React project is up and running. Next up, you will replace this starter page with a project catalog full of learning cards.
Build the Project Catalog
Your dev environment is ready, and the Vite starter page is running in the browser. Now it is time to build the actual homepage of your learning platform.
The catalog is the first thing a visitor sees. Building it teaches you three core React concepts at once: creating components, passing data with props, and rendering lists from arrays.
In this step, get ready to:
- Create a data file with sample project information.
- Build a reusable ProjectCard component.
- Wire up the catalog grid with styles.
Create the project data
Every learning platform needs a catalog of projects to display. You will store this catalog as a JavaScript array of objects, with each object holding a project's title, description, difficulty, duration, and steps.
- In VS Code, right-click the src folder in the Explorer sidebar.
- Select New File and name it data.js.
- Paste the first part of the projects array into src/data.js:
const projects = [
{
id: 1,
title: "Deploy a Static Website",
description:
"Learn to deploy your first website to the cloud using AWS S3.",
difficulty: "Easy",
duration: "30 min",
steps: [
{
id: 1,
title: "Create an S3 Bucket",
content:
"Navigate to the AWS S3 console and create a new bucket. Choose a unique name and select your preferred region.",
},
{
id: 2,
title: "Upload Your Files",
content:
"Upload your HTML, CSS, and JavaScript files to the bucket. Make sure your entry point is named index.html.",
},
{
id: 3,
title: "Enable Static Hosting",
content:
"In the bucket properties, enable static website hosting. Set index.html as your index document.",
},
{
id: 4,
title: "Set Permissions",
content:
"Update the bucket policy to allow public read access so visitors can view your website.",
},
],
},
- Add the second project right below the first:
{
id: 2,
title: "Build a REST API",
description:
"Create a REST API using Node.js and Express to serve data to a frontend application.",
difficulty: "Medium",
duration: "45 min",
steps: [
{
id: 1,
title: "Initialize the Project",
content:
"Create a new directory, run npm init, and install Express as a dependency.",
},
{
id: 2,
title: "Create the Server",
content:
"Write a basic Express server that listens on port 3000 and responds to requests.",
},
{
id: 3,
title: "Define API Routes",
content:
"Create GET and POST endpoints that return and accept JSON data.",
},
{
id: 4,
title: "Test Your Endpoints",
content:
"Use curl or a browser to send requests and verify your API returns the correct responses.",
},
],
},
- Add the third project below:
{
id: 3,
title: "Set Up a CI/CD Pipeline",
description:
"Automate your deployments with GitHub Actions to build and deploy on every push.",
difficulty: "Medium",
duration: "40 min",
steps: [
{
id: 1,
title: "Create a Repository",
content:
"Push your project to a new GitHub repository so Actions can detect changes.",
},
{
id: 2,
title: "Write the Workflow",
content:
"Create a .github/workflows/deploy.yml file with your build and test steps.",
},
{
id: 3,
title: "Configure Triggers",
content:
"Set the workflow to run on pushes to the main branch.",
},
{
id: 4,
title: "Verify the Pipeline",
content:
"Push a change and watch the Actions tab to confirm your pipeline runs successfully.",
},
],
},
- Add the final project, close the array, and export it:
{
id: 4,
title: "Containerize an Application",
description:
"Package your app into a Docker container for consistent, portable deployments.",
difficulty: "Hard",
duration: "50 min",
steps: [
{
id: 1,
title: "Install Docker",
content:
"Download and install Docker Desktop on your machine.",
},
{
id: 2,
title: "Write a Dockerfile",
content:
"Create a Dockerfile that specifies your app's base image, dependencies, and start command.",
},
{
id: 3,
title: "Build the Image",
content:
"Run docker build to create a container image from your Dockerfile.",
},
{
id: 4,
title: "Run the Container",
content:
"Start a container from your image and verify your app is accessible.",
},
],
},
];
export default projects;
What is this data file?
The projects array holds four objects, each representing a learning project. Every project has an id, title, description, difficulty, duration, and a steps array.
The export default at the bottom lets other files import this array. Your components will read from this data to render the catalog.
- Save src/data.js.
✔️ 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
Scroll up through your src/data.js and confirm these sections are present, in order:
- The array declaration starting with const projects = [
- Project 1: "Deploy a Static Website" (Easy, 30 min, 4 steps).
- Project 2: "Build a REST API" (Medium, 45 min, 4 steps).
- Project 3: "Set Up a CI/CD Pipeline" (Medium, 40 min, 4 steps).
- Project 4: "Containerize an Application" (Hard, 50 min, 4 steps).
- The closing ]; and export default projects; at the bottom.
Build the ProjectCard component
A React component is a reusable function that returns a piece of UI. You will create a ProjectCard component that receives a project object as a prop and renders a card with its details.
- In the Explorer sidebar, right-click the src folder.
- Select New Folder and name it components.
- Right-click the new components folder, select New File, and name it ProjectCard.jsx.
- Paste the following code into src/components/ProjectCard.jsx:
function ProjectCard({ project, completedSteps, onSelect }) {
// Calculate the progress percentage for the progress bar
const totalSteps = project.steps.length;
const progressPercent = Math.round((completedSteps / totalSteps) * 100);
return (
<div className="project-card" onClick={() => onSelect(project)}>
<div className="card-header">
<span className={`difficulty ${project.difficulty.toLowerCase()}`}>
{project.difficulty}
</span>
<span className="duration">{project.duration}</span>
</div>
<h2>{project.title}</h2>
<p>{project.description}</p>
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${progressPercent}%` }}
></div>
</div>
<span className="progress-text">
{completedSteps}/{totalSteps} steps completed
</span>
</div>
);
}
export default ProjectCard;
What does this component do?
The function signature { project, completedSteps, onSelect } uses destructuring to pull three props out of the object React passes in. Props are how a parent component sends data down to a child.
Inside the return, curly braces like {project.title} inject JavaScript values into the JSX markup. The template literal on the difficulty className dynamically applies a CSS class based on the project's difficulty level.
- Save src/components/ProjectCard.jsx.
Nothing changes in the browser yet because no file imports this component. The next substep wires everything together.
Seeing a red squiggly underline in VS Code?
If VS Code underlines the JSX syntax, it may not recognize .jsx files yet. Make sure the file extension is .jsx (not .js). VS Code highlights JSX correctly when the extension is right.
Why is my ProjectCard not showing anything?
✔️ Awesome, I've got everything!
Great. Make sure the file is saved before continuing.
ⓧ I'd like to double check the full code
function ProjectCard({ project, completedSteps, onSelect }) {
const totalSteps = project.steps.length;
const progressPercent = Math.round((completedSteps / totalSteps) * 100);
return (
<div className="project-card" onClick={() => onSelect(project)}>
<div className="card-header">
<span className={`difficulty ${project.difficulty.toLowerCase()}`}>
{project.difficulty}
</span>
<span className="duration">{project.duration}</span>
</div>
<h2>{project.title}</h2>
<p>{project.description}</p>
<div className="progress-bar-container">
<div
className="progress-bar"
style={{ width: `${progressPercent}%` }}
></div>
</div>
<span className="progress-text">
{completedSteps}/{totalSteps} steps completed
</span>
</div>
);
}
export default ProjectCard;
Wire up the catalog in App.jsx
Your data and component exist, but nothing connects them yet. The App component is the root of your application. You will replace the Vite starter code with a catalog that imports the data and maps over it to render one ProjectCard per project.
- Open src/App.jsx in VS Code.
- Select all the existing code (Cmd+A on macOS or Ctrl+A on Windows) and replace it with:
import ProjectCard from "./components/ProjectCard";
import projects from "./data";
import "./App.css";
function App() {
return (
<div className="app">
<header className="header">
<h1>LearnFlow</h1>
<p>Project-based learning, one step at a time.</p>
</header>
<main className="catalog">
{/* Loop through each project and render a card */}
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
completedSteps={0}
onSelect={() => {}}
/>
))}
</main>
</div>
);
}
export default App;
What does this code do?
- The three import lines pull in the ProjectCard component, the projects data array, and the CSS file.
- projects.map() loops through the array and returns a <ProjectCard /> for each project. This is how React renders lists.
- The key={project.id} prop gives React a unique identifier for each card so it can efficiently update the list.
- completedSteps={0} and onSelect={() => {}} are placeholder values. You will replace them with real logic in later steps.
- Save src/App.jsx.
- Switch to your browser. Vite hot-reloads on save, so you should see a "LearnFlow" heading with project titles and descriptions below it.
The content is there but it looks unstyled. The CSS you are about to add will transform it into a polished card grid.
Seeing a blank page or an error?
- Check that src/data.js ends with export default projects;. A missing export causes an import error.
- Make sure the file is named ProjectCard.jsx (capital P, capital C) inside the src/components/ folder. The import path is case-sensitive.
- Look at the terminal where the dev server is running. If you see a red error, it will tell you which file and line to fix.
Help me debug my blank page or import error.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file.
ⓧ I'd like to double check the full code
import ProjectCard from "./components/ProjectCard";
import projects from "./data";
import "./App.css";
function App() {
return (
<div className="app">
<header className="header">
<h1>LearnFlow</h1>
<p>Project-based learning, one step at a time.</p>
</header>
<main className="catalog">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
completedSteps={0}
onSelect={() => {}}
/>
))}
</main>
</div>
);
}
export default App;
Style the catalog
The raw content is rendering, but it needs CSS to look like a real platform. You will replace the default stylesheets with a clean base reset and card grid styles.
- Open src/index.css, select all, and replace the contents with:
:root {
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background-color: #f0f2f5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
}
What does this reset do?
The :root block sets global font and color defaults. The * { margin: 0; padding: 0; } rule strips browser-default spacing so every element starts from zero.
- Save src/index.css and check your browser. The background should turn to a light gray (#f0f2f5) and the default spacing disappears.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved.
ⓧ I'd like to double check the full code
:root {
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background-color: #f0f2f5;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
}
- Now open src/App.css, select all, and replace the contents with the layout and header styles:
.app {
max-width: 900px;
margin: 0 auto;
padding: 2rem 1rem;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2.5rem;
color: #6c5ce7;
margin-bottom: 0.25rem;
}
.header p {
color: #636e72;
font-size: 1.1rem;
}
.catalog {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
- Save src/App.css and check your browser. You should see the "LearnFlow" heading centered in purple and the project content arranged in a grid.
- Add the card and card-header styles below the existing code in src/App.css:
.project-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
border: 1px solid #e0e0e0;
}
.project-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.difficulty {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
}
- Save and check your browser. The cards should now have white backgrounds, rounded corners, and a lift effect when you hover over them.
- Add the difficulty badge color variants and card text styles below:
.difficulty.easy {
background: #d4edda;
color: #155724;
}
.difficulty.medium {
background: #fff3cd;
color: #856404;
}
.difficulty.hard {
background: #f8d7da;
color: #721c24;
}
.duration {
color: #636e72;
font-size: 0.85rem;
}
.project-card h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
color: #1a1a2e;
}
.project-card p {
color: #636e72;
font-size: 0.9rem;
margin-bottom: 1rem;
}
- Save and check. Each card's difficulty badge should now be color-coded: green for Easy, yellow for Medium, red for Hard.
- Finish src/App.css by adding the progress bar styles at the bottom:
.progress-bar-container {
width: 100%;
height: 6px;
background: #e9ecef;
border-radius: 3px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-bar {
height: 100%;
background: #6c5ce7;
border-radius: 3px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.8rem;
color: #636e72;
}
What do these styles do together?
- The .catalog grid uses auto-fill so cards automatically wrap to new rows as the window shrinks.
- The .project-card:hover rule lifts the card upward and adds a shadow, giving a clickable feel.
- The .progress-bar width is set dynamically by the component's inline style. Right now it is 0% because no steps are completed.
- Save src/App.css and check your browser. Every card should now show a thin gray progress bar at the bottom with "0/4 steps completed" beneath it.
Cards not styled or layout looks broken?
- Make sure you replaced all of the old CSS in src/App.css. Leftover Vite starter styles can conflict with your new classes.
- Check that the class names in your CSS match the ones in your JSX exactly. For example, className="project-card" must match .project-card in CSS.
- Confirm you saved both src/index.css and src/App.css. Unsaved files will not hot-reload.
Help me fix my catalog styling.
✔️ Awesome, I've got everything!
Great. Double check both CSS files are saved.
ⓧ I'd like to double check the full code
Scroll through your src/App.css and confirm these style groups are present, in order:
- Layout and header styles: .app, .header, .header h1, .header p, .catalog.
- Card styles: .project-card, .project-card:hover, .card-header, .difficulty.
- Difficulty variants: .difficulty.easy, .difficulty.medium, .difficulty.hard, plus .duration, .project-card h2, and .project-card p.
- Progress bar styles: .progress-bar-container, .progress-bar, .progress-text.
Confirm the catalog in your browser
Before you run the final check, what do you expect to see in the browser right now? Think about how many cards should appear and what information each card shows.
- Switch to your browser at http://localhost:5173.
You should see a "LearnFlow" header and four project cards in a responsive grid. Each card displays a title, description, difficulty badge, duration, a progress bar, and "0/4 steps completed".
- Try resizing your browser window narrower and wider. The grid should adjust, moving cards between rows as the window shrinks.
- Hover over a card. It should lift slightly with a shadow.
- Click a card. Nothing happens yet. That is expected. You will add click behavior in the next step.
Your catalog homepage is live. Next up, you will make those cards interactive so clicking one reveals its step-by-step guide.
Add the Project Detail View
In Step 2, you built a catalog of project cards that display titles, descriptions, and difficulty badges. Your homepage looks great, but clicking a card does nothing.
A learning platform needs to let you drill into a project and see its steps. This step introduces useState and conditional rendering so your app can switch between the catalog view and a detail view when you click a card.
In this step, get ready to:
- Create a ProjectDetail component that displays a project's steps.
- Add state and navigation logic so clicking a card opens the detail view.
- Style the detail view and step cards.
Create the ProjectDetail component and connect it
The detail view needs its own component. It receives a project as a prop and renders the project's title, description, a back button, and a progress bar.
- Create a new file called ProjectDetail.jsx inside your src/components/ folder and paste this code:
function ProjectDetail({ project, progress, onToggleStep, onBack }) {
// Count how many steps are completed
const completedSteps = project.steps.filter(
(step) => progress[`${project.id}-${step.id}`]
).length;
const totalSteps = project.steps.length;
const progressPercent = Math.round((completedSteps / totalSteps) * 100);
return (
<div className="app">
<header className="detail-header">
<button className="back-button" onClick={onBack}>
Back to Projects
</button>
<h1>{project.title}</h1>
<p>{project.description}</p>
<div className="detail-progress">
<div className="progress-bar-container">
<div className="progress-bar"
style={{ width: `${progressPercent}%` }}></div>
</div>
<span className="progress-text">
{completedSteps}/{totalSteps} steps completed
</span>
</div>
</header>
</div>
);
}
export default ProjectDetail;
What does this code do?
This component accepts four props: project (the data to display), progress (which steps are done), onToggleStep (a callback for checking steps), and onBack (a callback for the back button).
The progress and onToggleStep props will receive placeholder values for now. You will wire them up with real tracking logic in the next step.
Now you need to tell your app when to show this new component. React's useState hook lets you store a value that, when changed, causes the component to re-render with the new value.
- Replace the entire contents of src/App.jsx with the code in the next two blocks. Start with this first block:
import { useState } from "react";
import ProjectCard from "./components/ProjectCard";
import ProjectDetail from "./components/ProjectDetail";
import projects from "./data";
import "./App.css";
function App() {
// Track which project the user clicked
const [selectedProject, setSelectedProject] = useState(null);
// Set the selected project when a card is clicked
function handleSelectProject(project) {
setSelectedProject(project);
}
// Clear the selection to return to the catalog
function handleBack() {
setSelectedProject(null);
}
// Show detail view if a project is selected
if (selectedProject) {
return (
<ProjectDetail
project={selectedProject}
progress={{}}
onToggleStep={() => {}}
onBack={handleBack}
/>
);
}
- Paste this second block directly below, so it continues the App function:
// Catalog view (default when no project is selected)
return (
<div className="app">
<header className="header">
<h1>LearnFlow</h1>
<p>Project-based learning, one step at a time.</p>
</header>
<main className="catalog">
{projects.map((project) => (
<ProjectCard
key={project.id}
project={project}
completedSteps={0}
onSelect={handleSelectProject}
/>
))}
</main>
</div>
);
}
export default App;
What's happening in App.jsx now?
- selectedProject starts as null. When it is null, the catalog renders. When it holds a project object, the detail view renders instead.
- handleSelectProject stores the clicked project in state. handleBack resets it to null so the catalog reappears.
- progress={{}} and onToggleStep={() => {}} are temporary placeholders. You will replace them with real progress tracking in Step 4.
Before you click a card, what do you think will happen to the catalog grid?
- Save both ProjectDetail.jsx and App.jsx.
- Click any project card in the browser. The catalog disappears and you see the project's title, description, a progress bar, and a Back to Projects button.
- Click Back to Projects. The catalog reappears.
Card clicks doing nothing?
- Make sure you saved both files. Unsaved changes won't appear in the browser.
- Check that your App.jsx imports useState from "react" on the first line. A missing import causes a runtime error.
- Verify that onSelect={handleSelectProject} is set on each ProjectCard. If it still says onSelect={() => {}}, clicks are being swallowed by the empty function.
Still stuck? Ask for help.
✔️ Awesome, I've got everything!
Great. Double check you have saved both App.jsx and ProjectDetail.jsx.
ⓧ I'd like to double check the full code
Your App.jsx should contain, in order:
- Imports for useState, ProjectCard, ProjectDetail, projects, and App.css.
- The App function with selectedProject state, handleSelectProject, and handleBack functions.
- The if (selectedProject) block returning ProjectDetail with placeholder props.
- The catalog JSX with onSelect={handleSelectProject} on each ProjectCard.
Add the step list to ProjectDetail
The detail view shows the header, but no steps. Each project in your data has a steps array, and you need to map over it to render each step as a card.
- In src/components/ProjectDetail.jsx, find the closing </header> tag. Add this code between </header> and the closing </div>:
{/* Map over each step and render it as a card */}
<main className="steps">
{project.steps.map((step) => {
const isCompleted = progress[`${project.id}-${step.id}`];
return (
<div
key={step.id}
className={`step ${isCompleted ? "completed" : ""}`}
>
<label className="step-checkbox">
<input
type="checkbox"
checked={isCompleted || false}
onChange={() => onToggleStep(project.id, step.id)}
/>
<span className="step-number">Step {step.id}</span>
</label>
<div className="step-content">
<h3>{step.title}</h3>
<p>{step.content}</p>
</div>
</div>
);
})}
</main>
What does this code do?
- project.steps.map() loops through the project's steps and renders a card for each one with its number, title, and description.
- Each step card includes a checkbox wired to onToggleStep. Right now that prop is a no-op, so clicking the checkbox does nothing. You will connect it in Step 4.
- Save ProjectDetail.jsx.
- Click any project card in the browser. You should now see the project's four steps listed below the header, each with a step number, title, and description.
Steps not appearing?
- Make sure the new code sits between </header> and </div>. Placing it inside the <header> or after the closing </div> will break the layout.
- Check the browser console for errors. A missing closing brace or mismatched JSX tag will prevent the component from rendering.
Help me find the issue in my ProjectDetail component.
✔️ Awesome, I've got everything!
Great. Double check you have saved ProjectDetail.jsx.
ⓧ I'd like to double check the full code
Your ProjectDetail.jsx should contain, in order:
- The function signature receiving project, progress, onToggleStep, and onBack props, with computed completedSteps and progressPercent variables.
- The header section with a back button, title, description, and progress bar.
- The <main className="steps"> section mapping over each step with a checkbox, step number, title, and content.
- export default ProjectDetail at the bottom.
Style the detail view
The detail view works, but everything is unstyled. Time to add CSS for the header, back button, and step cards.
- Open src/App.css and add these styles at the very bottom of the file:
/* Detail view header and navigation */
.detail-header {
margin-bottom: 2rem;
}
.back-button {
background: none;
border: none;
color: #6c5ce7;
font-size: 1rem;
cursor: pointer;
margin-bottom: 1rem;
padding: 0;
font-weight: 600;
}
.back-button:hover {
text-decoration: underline;
}
What do these styles do?
The .back-button class strips the default button styling and applies the purple brand color. The hover state adds an underline so the learner knows it is clickable.
- Continue adding these styles below the previous block:
/* Detail view title and progress */
.detail-header h1 {
font-size: 2rem;
color: #1a1a2e;
margin-bottom: 0.5rem;
}
.detail-header p {
color: #636e72;
margin-bottom: 1rem;
}
.detail-progress {
margin-bottom: 1rem;
}
.steps {
display: flex;
flex-direction: column;
gap: 1rem;
}
What do these styles do?
These styles give the title and description clean spacing. The .steps class uses flexbox to stack the step cards vertically with consistent gaps between them.
- Save App.css and click a project card in the browser.
- The back button should now be purple text, and the title and description should have clean spacing. The steps below are still unstyled.
- Continue adding these styles below to define the step card layout:
.step { /* Individual step card layout */
background: white;
border-radius: 12px;
padding: 1.5rem;
border: 1px solid #e0e0e0;
display: flex;
gap: 1rem;
align-items: flex-start;
transition: border-color 0.2s, background 0.2s;
}
.step.completed {
border-color: #6c5ce7;
background: #f8f7ff;
}
.step-checkbox {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
cursor: pointer;
}
.step-checkbox input[type="checkbox"] {
width: 20px;
height: 20px;
accent-color: #6c5ce7;
cursor: pointer;
}
- Add the final block of styles below:
/* Step text labels and content */
.step-number {
font-size: 0.75rem;
color: #636e72;
white-space: nowrap;
}
.step-content h3 {
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: #1a1a2e;
}
.step-content p {
color: #636e72;
font-size: 0.9rem;
line-height: 1.6;
}
What do these step styles do?
- .step gives each step a white card with a border. The display: flex puts the checkbox and text content side by side.
- .step.completed changes the border to purple and adds a light purple background. This will become visible once progress tracking works in Step 4.
- .step-checkbox stacks the checkbox and step number vertically, centering them in a compact column.
What do you expect the detail view to look like now that all the styles are in place?
- Save App.css.
- Click any project card. You should see the project title, description, and four steps displayed as white cards. Each card has a checkbox on the left with a step number beneath it, and the step title and content on the right.
- Click Back to Projects to return to the catalog.
Steps look unstyled or misaligned?
- Make sure every CSS block was pasted outside and after the previous block's closing brace. Nesting one selector inside another will break the layout.
- Check that .step-checkbox input[type="checkbox"] includes the quotes around checkbox. Missing quotes will cause the selector to fail silently.
Help me fix my detail view styling.
✔️ Awesome, I've got everything!
Great. Double check you have saved App.css.
ⓧ I'd like to double check the full code
Your App.css should contain all the styles from Step 2 (the catalog, card, difficulty badge, progress bar styles) plus the new styles you just added. Scroll up and confirm these sections appear at the bottom in order:
- .detail-header and .back-button styles (header spacing, purple link-style button).
- .detail-header h1, .detail-header p, .detail-progress, and .steps styles (title sizing, vertical step layout).
- .step, .step.completed, .step-checkbox styles (card layout, completion highlight, checkbox sizing).
- .step-number, .step-content h3, and .step-content p styles (text sizing and colors).
Your app can now switch between two views. Clicking a card drills into the project's steps, and the back button returns you to the catalog. You have the checkboxes in place, but clicking them does nothing yet. In the next step, you will wire up progress tracking with localStorage so those checkboxes actually stick.
Track Your Learning Progress
Your platform can display projects and let you read through their steps. But try clicking a checkbox right now and you will notice a problem.
Nothing happens. The checkboxes are there, but they are wired to placeholder values that do nothing. In this step, you will add real progress tracking that saves to the browser so your checked-off steps survive page refreshes.
In this step, get ready to:
- Add a progress state object backed by localStorage so progress persists across refreshes.
- Wire up the checkboxes so clicking them toggles a step between complete and incomplete.
- Connect progress to the catalog cards so each card shows a live progress bar.
See why progress doesn't work yet
Before writing any code, take a moment to experience the gap yourself.
- Switch back to your browser at http://localhost:5173.
- Click any project card to open its detail view.
Before you click a checkbox, what do you think will happen? Will the step highlight as complete?
- Click any checkbox next to a step.
Nothing happens. The checkbox does not toggle and no purple highlight appears. That is exactly the gap you are about to fix.
Why don't the checkboxes work?
In App.jsx, the detail view receives progress={{}} (an empty object) and onToggleStep={() => {}} (a function that does nothing). These were placeholder props that let the components render without crashing. Now you will replace them with real state management.
Add progress state and make checkboxes work
React's useEffect hook lets you run code whenever a piece of state changes. You will pair it with useState to create a progress object that automatically saves to the browser's localStorage every time it updates.
- In src/App.jsx, find this import line at the top:
import { useState } from "react";
- Replace it with this line to also import useEffect:
import { useState, useEffect } from "react";
- Next, find the selectedProject state line inside the App function. Add the following code directly below it:
const [progress, setProgress] = useState(() => {
// Load any saved progress from the browser
const saved = localStorage.getItem("learnflow-progress");
return saved ? JSON.parse(saved) : {};
});
useEffect(() => {
// Save progress to the browser whenever it changes
localStorage.setItem("learnflow-progress", JSON.stringify(progress));
}, [progress]);
What does this code do?
The useState call uses a lazy initializer (the arrow function inside it). On the very first render, React calls that function to check localStorage for any previously saved progress. If found, it parses it. If not, it starts with an empty object.
The useEffect hook watches the progress object (that is the [progress] at the end). Every time progress changes, it saves the updated object back to localStorage.
- Now find the handleBack function. Add the handleToggleStep function directly below it:
function handleToggleStep(projectId, stepId) {
// Create a unique key like "1-3" for project 1, step 3
const key = `${projectId}-${stepId}`;
setProgress((prev) => ({
...prev,
[key]: !prev[key],
}));
}
What does this code do?
Each step gets a unique key combining its project ID and step ID, like "1-3". The !prev[key] flips the value between true and false each time you click. The spread operator ...prev keeps all your other saved progress intact.
The state and functions are ready. Now you need to connect them to the detail view by replacing the placeholder props.
- In the same file, scroll down to the if (selectedProject) block. Find these two lines inside the <ProjectDetail> tag:
progress={{}}
onToggleStep={() => {}}
- Replace them with the real state and function:
progress={progress}
onToggleStep={handleToggleStep}
- Save App.jsx.
- Switch back to your browser. Click into any project and click a checkbox.
The checkbox toggles on and the step card highlights with a purple border and light purple background. The progress bar at the top of the detail view updates in real time as you check off steps.
Checkboxes still not working?
- Make sure you saved App.jsx after making all three changes (import, state/effect, and props).
- Check that the props say progress={progress} (without the double curly braces). The double curly braces progress={{}} was the old placeholder.
- Confirm the useEffect import is on the same line as useState: import { useState, useEffect } from "react".
- If the terminal shows an error, help me debug my progress tracking in App.jsx.
Connect progress to the catalog cards
The checkboxes work inside the detail view, but the catalog cards still show "0/4 steps completed" no matter how many steps you check off. You need a helper function that counts completed steps for each project.
- In src/App.jsx, find the handleToggleStep function you just added. Add the getProjectProgress function directly below it:
function getProjectProgress(projectId, totalSteps) {
// Count how many steps are marked complete for this project
let completed = 0;
for (let i = 1; i <= totalSteps; i++) {
if (progress[`${projectId}-${i}`]) {
completed++;
}
}
return completed;
}
What does this code do?
This function loops through every step in a project and checks the progress object for a matching key. If project 1 has 4 steps, it checks keys "1-1" through "1-4" and counts how many are true.
Now connect this function to the catalog cards by replacing the placeholder value.
- Scroll down to the return statement at the bottom of the App function. Inside the projects.map(), find this line inside the <ProjectCard> tag:
completedSteps={0}
- Replace it with a call to your new helper function:
completedSteps={getProjectProgress(
project.id,
project.steps.length
)}
- Save App.jsx.
- In your browser, click into a project and check off two steps. Then click Back to Projects to return to the catalog.
The project card now shows "2/4 steps completed" with a filled purple progress bar matching the steps you checked off.
Progress bar still showing 0?
- Make sure you replaced completedSteps={0} with the getProjectProgress call, not just added a new line alongside it.
- Check that the getProjectProgress function is defined inside the App function, not outside it. It needs access to the progress state variable.
- If you are stuck, help me debug my catalog progress bars.
Now for the real test. Does your progress survive a page refresh?
Before you refresh, do you think your checked-off steps will still be there? Think about what the lazy initializer does on the first render.
- Press Cmd+R (macOS) or Ctrl+R (Windows) to refresh the page.
- Click into the same project you checked steps off in.
Your checked steps are still checked. The progress bar on the catalog card still shows "2/4 steps completed." The lazy initializer loaded your saved progress from localStorage on page load, so nothing was lost.
✔️ Awesome, I've got everything!
Great. Double check that you have saved App.jsx.
ⓧ I'd like to double check the full code
Scroll up through your work in this step and confirm each piece is in place, in this order inside your App function:
- The import line includes both useState and useEffect.
- The progress state with lazy initializer is below selectedProject.
- The useEffect hook saves to localStorage with the key "learnflow-progress".
- The handleToggleStep function is below handleBack.
- The getProjectProgress function is below handleToggleStep.
- The ProjectDetail tag passes progress={progress} and onToggleStep={handleToggleStep}.
- Each ProjectCard receives completedSteps={getProjectProgress(project.id, project.steps.length)}.
Secret mission
Add Search and Difficulty Filtering
Your catalog works, but with only four projects it is easy to scan. A real platform would have dozens. Can you add a search bar and difficulty filter buttons so users can find exactly what they need?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your project files to continue building on them, or delete them entirely.
Resources you used:
- The learnflow project folder on your Desktop.
- The Vite dev server running in your terminal.
- Browser localStorage data stored under the learnflow-progress key.
Keep everything running
No action needed. Choose this if you are still actively building on your LearnFlow app or want to keep experimenting with React.
- Your dev server stays running in the terminal. You can access your app at http://localhost:5173 anytime.
- Your project files remain in the learnflow folder on your Desktop.
- Your progress data stays in your browser's localStorage.
Pause - I'll come back to this later
Stop the dev server to free up your terminal. Your project files and browser data stay intact so you can pick up where you left off.
- Switch to the terminal where your dev server is running.
- Press Ctrl+C to stop the server.
- When you are ready to come back, open a terminal in the learnflow 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 browser data to start completely fresh.
- Switch to the terminal where your dev server is running and press Ctrl+C to stop it.
- Delete the project folder by running the following commands (macOS/Linux):
cd ~/Desktop
rm -rf learnflow
On Windows, run this instead:
cd ~\Desktop
rmdir /s /q learnflow
- Clear your saved progress from the browser. Open the browser you used for the project, press F12 to open DevTools, click the Console tab, and run:
localStorage.removeItem("learnflow-progress")
- Confirm the folder is gone by running ls ~/Desktop (macOS/Linux) or dir ~\Desktop (Windows). You should no longer see learnflow in the output.
Nice Work!
Nice Work!
You just built a fully functional learning platform from scratch using React and Vite. LearnFlow lets you browse a catalog of projects, click into step-by-step guides, and track your progress across browser sessions.
You've learned how to:
- Built reusable React components (ProjectCard, ProjectDetail) and passed data between them using props.
- Used useState and conditional rendering to switch between a project catalog and a detail view with a back button.
- Persisted learning progress with useEffect and localStorage so checked-off steps survive page refreshes.
- Secret Mission: Added search and difficulty filtering to the catalog using controlled inputs and array filtering.
Ready to quiz yourself?