Build a React Task Dashboard
Build a React task dashboard with state, filters, and browser storage.
Introduction
30 Second Summary
Keeping a busy day on track gets harder when a task list cannot show what is finished or what still needs attention. Every small update becomes another manual check.
In this project, you will build FocusFlow, an interactive task dashboard with React. You will connect every form, filter, task status, and progress update to one shared task list.
What You'll Build
You will open FocusFlow in Microsoft Edge to add tasks, tick them off, filter your sprint, and watch the progress card respond immediately.
By the end of this project, you'll have:
- An interactive sprint list where you add validated tasks and see each new item appear without a page reload.
- Independent completion controls that switch each task between Done and In progress while updating its styling.
- Coordinated filters and progress where visible rows, completed counts, percentages, and empty states stay in step with every task change.
- Secret Mission: Save the task list in browser local storage so added tasks and completion states survive a refresh.
Are there any prerequisites?
You should be comfortable with JavaScript functions, arrays, objects, destructuring, spread syntax, map(), and filter(). Prior experience with React state is not required.
Before We Start
This is your moment to commit to building FocusFlow as an interactive React task dashboard. You will use it to learn the client-side patterns that prepare you to add a backend later.
Set Up a Local React Workspace
FocusFlow needs a dependable local loop before its dashboard can grow. A compatible Node.js runtime keeps setup problems from stealing time from React.
Vite provides a small development loop for learning React fundamentals. This step turns that loop into a running starter page in Microsoft Edge.
In this step, get ready to:
- Confirm that Node.js satisfies the version required by Vite.
- Create the FocusFlow files with pinned project dependencies.
- Launch the starter app in Microsoft Edge.
Verify the Windows development tools
Vite 8.3.0 requires Node.js 20.19+ or 22.12+. Checking the runtime first protects the installation from compatibility errors.
Why Vite for this project?
Vite keeps this first project focused on components and state. Its development server also gives you a quick feedback loop while you learn React.
Full-stack frameworks add concerns such as routing and server rendering. Those become useful after the client-side patterns in FocusFlow feel familiar.
- Press the Windows key to open Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to open Visual Studio Code.
- Open the integrated terminal from the Terminal menu.
- Select PowerShell as the terminal profile if another shell appears.
- Check the installed Node.js and npm versions by running:
node --version
npm --version
What do these checks confirm?
- The first command prints the Node.js runtime version used by Vite.
- The second command confirms that npm is available for installing the project dependencies.
✔️ I see a compatible Node.js version
Your Node.js result meets the Vite requirement. A version number from npm confirms that the package manager is ready too.
ⓧ I see an older version
A Node.js result outside 20.19+ or 22.12+ needs an update before Vite can run reliably.
- Open the official Node.js download page.
- Choose the official Node.js Windows LTS installer.
- Run the downloaded installer.
- Complete the installation using its default options.
- Close Visual Studio Code after the installation finishes.
Why reopen Visual Studio Code?
A fresh Visual Studio Code process picks up the updated Node.js installation. Its new terminal can then find the current runtime.
- Press the Windows key to reopen Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to reopen Visual Studio Code.
ⓧ Command not found
Windows cannot currently find Node.js. Installing the Windows LTS release also installs npm.
- Open the official Node.js download page.
- Choose the official Node.js Windows LTS installer.
- Run the downloaded installer.
- Complete the installation using its default options.
- Close Visual Studio Code after the installation finishes.
Why reopen Visual Studio Code?
Visual Studio Code reads the available commands when it starts. Reopening it lets the integrated terminal discover Node.js and npm.
- Press the Windows key to reopen Windows search.
- Type Visual Studio Code into the search field.
- Press Enter to reopen Visual Studio Code.
- Open a fresh integrated terminal from the Terminal menu.
- Make the final compatibility check by running:
node --version
npm --version
What should the final check show?
The Node.js line should satisfy 20.19+ or 22.12+. The npm line should contain a version number.
That compatibility check is complete. Your Windows runtime is ready for the pinned FocusFlow dependencies.
Create the pinned project files
A manual workspace keeps every dependency version visible. It also avoids changes from a generated template.
- In Visual Studio Code from the tool check, select File from the top menu.
- Select Open Folder....
- Choose your Desktop in the Windows folder picker.
- Use the folder creation control to create focusflow-react.
- Select the new focusflow-react folder.
- Confirm the folder selection to open it as the workspace.
The Explorer sidebar now shows focusflow-react as the open workspace. Every project file you add stays inside this folder.
- Select the focusflow-react folder in the Explorer sidebar.
- Click New File... in the Explorer toolbar.
- Enter package.json as the file name.
- Add the pinned project definition by pasting:
{
"name": "focusflow-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
Why pin every dependency?
The file installs React and React DOM at 19.3.0. It also installs Vite 8.3.0 with @vitejs/plugin-react 6.1.1.
Exact versions give every learner the same development environment. The scripts also define the project's development and production commands.
- Save package.json.
- Open the integrated terminal from the Terminal menu.
- Install the exact dependency versions by running:
npm install
What does this command create?
- npm installs the declared packages inside node_modules.
- npm records the resolved dependency tree inside package-lock.json.
Your dependency layer is now locked in. The terminal returns to a PowerShell prompt after the installation completes.
Did the dependency installation fail?
Confirm that the terminal path ends with focusflow-react. An installation started from another folder cannot find this project's package.json.
Repeat the Node.js version check if the output mentions an unsupported runtime. Vite needs one of the compatible version ranges verified above.
Help me troubleshoot this npm installation.
- Select the focusflow-react folder in the Explorer sidebar.
- Use New File... to create vite.config.js.
- Configure the React plugin by pasting:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
What does this configuration do?
- The configuration registers the official React plugin with Vite.
- The plugin enables React's JSX handling and development refresh behavior.
- Save vite.config.js.
- Use New File... to create index.html.
- Define the browser entry page by pasting:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FocusFlow</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
How does the page connect to React?
The root element gives React a place to render FocusFlow. The module script loads src/main.jsx as the JavaScript entry point.
- Save index.html.
- Confirm the top-level files by running:
Get-ChildItem
What does this check show?
PowerShell lists the files and folders inside focusflow-react. This gives you a visible checkpoint before adding the source files.
You should see index.html, package.json, package-lock.json, vite.config.js, and node_modules in the output.
- Select the focusflow-react folder in the Explorer sidebar.
- Use New Folder... to create src.
- Select the new src folder.
- Use New File... to create App.css.
- Add the starter stylesheet marker by pasting:
/* Starter stylesheet; replaced with the FocusFlow dashboard stylesheet in the next step. */
Why start with one CSS comment?
The stylesheet already exists for the component import. Its full visual design belongs to the dashboard work in the next step.
- Save src/App.css.
- Select the src folder in the Explorer sidebar.
- Use New File... to create App.jsx.
- Create the starter component by pasting:
import './App.css'
export default function App() {
return <h1>FocusFlow is ready</h1>
}
What does the App component do?
The App component returns one heading through JSX. That heading gives the first server launch a clear visual result.
- Save src/App.jsx.
- Confirm the first source files by running:
Get-ChildItem -Path .\src
What does this source check show?
PowerShell lists the files inside src without searching through the installed packages. You should see App.css and App.jsx.
- Select the src folder in the Explorer sidebar.
- Use New File... to create main.jsx.
- Connect React to the browser page by pasting:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
How does React reach the page?
- The createRoot() call connects React to the root element in index.html.
- Strict Mode enables extra development checks around the rendered App component.
- Save src/main.jsx.
- Save every remaining open file in Visual Studio Code.
✔️ Awesome, I've got everything!
Your pinned workspace now has every file needed for its first browser run.
ⓧ I'd like to double check the full code
These references show the exact file contents for the completed workspace.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FocusFlow</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
What to compare in index.html
The page contains the root element and loads /src/main.jsx as a module.
{
"name": "focusflow-react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "19.3.0",
"react-dom": "19.3.0"
},
"devDependencies": {
"@vitejs/plugin-react": "6.1.1",
"vite": "8.3.0"
}
}
What to compare in package.json
The dependency names and pinned versions should match this reference exactly.
/* Starter stylesheet; replaced with the FocusFlow dashboard stylesheet in the next step. */
What to compare in App.css
The starter stylesheet contains one comment that marks where the dashboard styles belong.
import './App.css'
export default function App() {
return <h1>FocusFlow is ready</h1>
}
What to compare in App.jsx
The component imports its stylesheet and returns the starter heading.
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
What to compare in main.jsx
The entry point renders App inside StrictMode.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
})
What to compare in vite.config.js
The configuration imports the React plugin and adds it to the Vite plugin list.
Launch the starter app
The Vite development server reads the files you created and serves them from a local address. The server stays attached to its terminal while FocusFlow is available.
- Keep the integrated PowerShell terminal available for the development server.
- Start the installed Vite server by running:
npx vite
What does this command do?
The command starts the locally installed Vite development server. It prints the address where your browser can reach FocusFlow.
The terminal remains occupied while the server runs. Keeping that process active lets Vite continue serving your files.
The terminal should print the local address http://localhost:5173 when the default port is available.
- Press the Windows key to open Windows search.
- Type Microsoft Edge into the search field.
- Press Enter to open Microsoft Edge.
Before you load the local address, which heading do you expect the React component to render?
- Enter http://localhost:5173 in the Microsoft Edge address bar.
- Press Enter to load the app.
You should see the heading FocusFlow is ready on the page. This proves that Vite loaded the entry point and React rendered the App component.
Does the starter page fail to load?
Confirm that the integrated terminal still shows the running Vite process. A closed terminal stops the local server.
Check that index.html points to /src/main.jsx. Also confirm that every import uses the exact file name shown in the reference tab.
Help me troubleshoot the FocusFlow starter page.
Your first visible win is running in Microsoft Edge. FocusFlow now has a working local development loop.
Your local React workspace is ready. Next, you will replace the starter heading with a styled task dashboard built from reusable components.
Render the Dashboard Components
Your local React loop is already serving the FocusFlow starter heading. The next job is to turn that single line into a dashboard driven by task data.
JSX and props can transform a JavaScript array into reusable task rows. This first dashboard lets you test how far that read-only data flow goes when you interact with it.
In this step, get ready to:
- Model the starter task data with stable IDs.
- Render the dashboard through reusable components and props.
- Style the dashboard before testing its read-only controls.
Model and render the task data
A component can render the same structure for different data. FocusFlow starts with three task objects so each row receives its own title and completion value.
- In the Explorer sidebar in Visual Studio Code, select src/App.jsx.
- Find the import './App.css' line.
- Add the task data and TaskItem component below that line by copying this code:
const initialTasks = [
{ id: 1, title: 'Create the dashboard layout', completed: true },
{ id: 2, title: 'Connect task interactions', completed: false },
{ id: 3, title: 'Plan the first API endpoint', completed: false },
]
function TaskItem({ task }) {
return (
<li className={`task-item ${task.completed ? 'completed' : ''}`}>
<label>
<input type="checkbox" checked={task.completed} readOnly />
<span>{task.title}</span>
</label>
<span className="task-status">{task.completed ? 'Done' : 'In progress'}</span>
</li>
)
}
What does this code do?
- initialTasks holds three task objects with stable numeric IDs.
- TaskItem receives one task object through a prop.
- checked={task.completed} makes each checkbox display the completion value from its task.
- readOnly keeps the controlled checkbox tied to the task data in this version.
- The conditional expressions select the completed class and status label from the same value.
- Save src/App.jsx.
- Return to Microsoft Edge.
The starter heading still renders without an error overlay. This confirms Vite accepted the new data and component definitions.
Seeing an error overlay?
Check that the new code sits below the stylesheet import. Confirm that every bracket and parenthesis matches the snippet.
Help me fix the new task data or TaskItem component.
Compose the visible dashboard
The task objects now need a parent layout. The App component calculates progress before mapping every task into a TaskItem.
- In src/App.jsx, find the existing App function that returns the starter heading.
- Replace that function with this dashboard component:
export default function App() {
const completedCount = initialTasks.filter((task) => task.completed).length
const progress = Math.round((completedCount / initialTasks.length) * 100)
return (
<main className="app-shell">
<section className="dashboard">
<header className="dashboard-header">
<div>
<p className="eyebrow">React learning project</p>
<h1>FocusFlow</h1>
<p className="subtitle">Turn today's plan into visible progress.</p>
</div>
<div className="progress-card">
<div className="progress-summary"><span>{completedCount} of {initialTasks.length} complete</span><strong>{progress}%</strong></div>
<div className="progress-track"><span style={{ width: `${progress}%` }} /></div>
</div>
</header>
<section className="task-panel">
<div className="panel-heading"><div><p className="eyebrow">Current workload</p><h2>Today's sprint</h2></div></div>
<ul className="task-list">{initialTasks.map((task) => <TaskItem key={task.id} task={task} />)}</ul>
</section>
</section>
</main>
)
}
How does the dashboard come together?
- completedCount filters the array to count completed tasks.
- progress converts that count into a rounded percentage.
- initialTasks.map() creates one TaskItem for each task object.
- key={task.id} gives React a stable identity for every rendered row.
- task={task} passes the current task object into its row component.
- Save src/App.jsx.
- Return to Microsoft Edge.
Microsoft Edge refreshes to show the FocusFlow header. You also see a progress summary above three plain task rows.
Dashboard missing or incomplete?
Check that the new App function replaced the starter function. Confirm that initialTasks.map() appears inside the task list.
Help me debug the FocusFlow component tree.
✔️ Awesome, I've got everything!
Your src/App.jsx file now contains the complete read-only dashboard component tree.
ⓧ I'd like to double check the full code
import './App.css'
const initialTasks = [
{ id: 1, title: 'Create the dashboard layout', completed: true },
{ id: 2, title: 'Connect task interactions', completed: false },
{ id: 3, title: 'Plan the first API endpoint', completed: false },
]
function TaskItem({ task }) {
return (
<li className={`task-item ${task.completed ? 'completed' : ''}`}>
<label>
<input type="checkbox" checked={task.completed} readOnly />
<span>{task.title}</span>
</label>
<span className="task-status">{task.completed ? 'Done' : 'In progress'}</span>
</li>
)
}
export default function App() {
const completedCount = initialTasks.filter((task) => task.completed).length
const progress = Math.round((completedCount / initialTasks.length) * 100)
return (
<main className="app-shell">
<section className="dashboard">
<header className="dashboard-header">
<div>
<p className="eyebrow">React learning project</p>
<h1>FocusFlow</h1>
<p className="subtitle">Turn today's plan into visible progress.</p>
</div>
<div className="progress-card">
<div className="progress-summary"><span>{completedCount} of {initialTasks.length} complete</span><strong>{progress}%</strong></div>
<div className="progress-track"><span style={{ width: `${progress}%` }} /></div>
</div>
</header>
<section className="task-panel">
<div className="panel-heading"><div><p className="eyebrow">Current workload</p><h2>Today's sprint</h2></div></div>
<ul className="task-list">{initialTasks.map((task) => <TaskItem key={task.id} task={task} />)}</ul>
</section>
</section>
</main>
)
}
Style and test the static dashboard
The component tree supplies the dashboard structure. CSS now turns that structure into the finished FocusFlow layout while Vite refreshes each saved change.
- In the Explorer sidebar, select src/App.css.
- Replace the starter comment with these global and app-shell styles:
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2ff;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* { box-sizing: border-box; }
body { min-width: 320px; min-height: 100vh; margin: 0; }
button, input { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.55; }
.app-shell {
display: grid;
min-height: 100vh;
place-items: center;
padding: 32px 16px;
background: radial-gradient(circle at top left, rgba(124, 58, 237, 0.2), transparent 36%), linear-gradient(135deg, #eef2ff 0%, #f8fafc 55%, #ecfeff 100%);
}
What do these foundation styles control?
- :root establishes the dashboard font and page colours.
- The shared element rules normalize sizing and control states.
- .app-shell centers the dashboard against a layered page background.
- Save src/App.css.
- Return to Microsoft Edge.
The dashboard content moves to the center of a pale gradient background. The page now uses the shared system font.
Background still plain?
Confirm that src/App.jsx still imports ./App.css. Check that the starter comment was replaced with the new rules.
Help me find why the FocusFlow foundation styles are missing.
- Add the dashboard card and header styles at the end of src/App.css by copying this code:
.dashboard {
width: min(920px, 100%);
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 28px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 28px 70px rgba(30, 41, 59, 0.16);
backdrop-filter: blur(14px);
}
.dashboard-header {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 28px;
align-items: end;
padding: 36px;
color: #fff;
background: linear-gradient(135deg, #5b21b6, #7c3aed 55%, #0891b2);
}
.eyebrow { margin: 0 0 8px; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
.dashboard-header h1, .panel-heading h2 { margin: 0; }
.dashboard-header h1 { font-size: clamp(2.2rem, 7vw, 4.6rem); line-height: 0.95; }
.subtitle { max-width: 460px; margin: 14px 0 0; color: rgba(255, 255, 255, 0.82); font-size: 1.05rem; }
How does the dashboard frame work?
- .dashboard creates the rounded white surface around the app.
- .dashboard-header divides the header into title and progress columns.
- The header typography gives FocusFlow a clear visual hierarchy.
- Save src/App.css.
- Return to Microsoft Edge.
You see a rounded dashboard card with a purple-to-teal header. The FocusFlow title now dominates the left side.
Header missing its card layout?
Check that the new rules sit after the closing brace for .app-shell. Confirm that the class names match the JSX exactly.
Help me debug the dashboard card and header CSS.
- Add the progress and form styles at the end of src/App.css by copying this code:
.progress-card { padding: 20px; border: 1px solid rgba(255, 255, 255, 0.28); border-radius: 20px; background: rgba(255, 255, 255, 0.14); }
.progress-summary { display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 12px; }
.progress-summary span { color: rgba(255, 255, 255, 0.82); }
.progress-summary strong { font-size: 1.7rem; }
.progress-track { height: 10px; overflow: hidden; border-radius: 999px; background: rgba(255, 255, 255, 0.2); }
.progress-track span { display: block; height: 100%; border-radius: inherit; background: #fff; transition: width 180ms ease; }
.task-form { padding: 28px 36px 0; }
.task-form label { display: block; margin-bottom: 9px; color: #475569; font-size: 0.85rem; font-weight: 800; }
.task-form-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; }
.task-form input { min-width: 0; padding: 14px 16px; border: 1px solid #cbd5e1; border-radius: 14px; outline: none; background: #fff; }
.task-form input:focus { border-color: #7c3aed; box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.12); }
.task-form button, .filters button { border: 0; border-radius: 12px; font-weight: 800; }
.task-form button { padding: 0 22px; color: #fff; background: #5b21b6; }
What do these controls styles prepare?
- The progress rules turn the completion percentage into a translucent summary card.
- .progress-track span uses the inline width from JSX to draw the completed portion.
- The form rules prepare consistent input and button styling for the dashboard's next control.
- Save src/App.css.
- Return to Microsoft Edge.
The progress summary becomes a translucent card. Its white bar fills to match the displayed percentage.
Progress bar not visible?
Confirm that .progress-track span includes a white background. Check that the JSX still supplies the inline width style.
Help me fix the FocusFlow progress card styling.
- Add the panel and task-row styles at the end of src/App.css by copying this code:
.task-panel { padding: 30px 36px 38px; }
.panel-heading { display: flex; justify-content: space-between; gap: 20px; align-items: end; margin-bottom: 18px; }
.panel-heading .eyebrow { color: #7c3aed; }
.panel-heading h2 { font-size: 1.5rem; }
.filters { display: flex; gap: 8px; padding: 5px; border-radius: 14px; background: #f1f5f9; }
.filters button { padding: 8px 12px; color: #64748b; background: transparent; }
.filters button.active { color: #5b21b6; background: #fff; box-shadow: 0 4px 12px rgba(30, 41, 59, 0.1); }
.task-list { display: grid; gap: 10px; padding: 0; margin: 0; list-style: none; }
.task-item { display: flex; justify-content: space-between; gap: 18px; align-items: center; padding: 17px 18px; border: 1px solid #e2e8f0; border-radius: 16px; background: #fff; transition: border-color 160ms ease, transform 160ms ease; }
.task-item:hover { border-color: #c4b5fd; transform: translateY(-1px); }
.task-item label { display: flex; gap: 12px; align-items: center; min-width: 0; font-weight: 700; }
.task-item input { width: 19px; height: 19px; accent-color: #7c3aed; }
.task-item.completed label span { color: #94a3b8; text-decoration: line-through; }
.task-status { flex: none; padding: 6px 10px; border-radius: 999px; color: #9a3412; background: #ffedd5; font-size: 0.75rem; font-weight: 800; }
.task-item.completed .task-status { color: #166534; background: #dcfce7; }
.empty-state { margin: 0; padding: 30px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #f8fafc; }
How do task states become visible?
- .task-list arranges the rendered components into evenly spaced rows.
- .task-item gives each task a bordered card layout.
- .task-item.completed changes the title and status styling for completed data.
- The remaining rules prepare filters and an empty state for later dashboard views.
- Save src/App.css.
- Return to Microsoft Edge.
The sprint becomes three separate task cards. The completed task has struck-through text and a green Done badge.
Task rows still look like bullets?
Confirm that .task-list includes list-style: none. Check that each row uses the task-item class.
Help me debug the FocusFlow task-row styles.
- Add the first responsive breakpoint at the end of src/App.css by copying this code:
@media (max-width: 700px) {
.dashboard-header { grid-template-columns: 1fr; }
.panel-heading { align-items: stretch; flex-direction: column; }
.filters { align-self: flex-start; }
}
What changes below 700 pixels?
The dashboard header switches to one column. The panel heading also stacks its contents for a narrower screen.
- Save src/App.css.
- Resize Microsoft Edge to less than 700px wide.
The progress card moves beneath the FocusFlow title. The sprint heading remains above the task list.
Header still has two columns?
Confirm that the media query sits outside every earlier selector. Check that its closing brace is present.
Help me fix the 700px responsive breakpoint.
- Add the narrow-screen breakpoint at the end of src/App.css by copying this code:
@media (max-width: 520px) {
.dashboard-header, .task-form, .task-panel { padding-right: 20px; padding-left: 20px; }
.task-form-row { grid-template-columns: 1fr; }
.task-form button { min-height: 46px; }
.task-item { align-items: flex-start; flex-direction: column; }
}
What changes on a narrow screen?
The dashboard uses smaller side padding. Each task row stacks its status below the task title.
- Save src/App.css.
- Resize Microsoft Edge to less than 520px wide.
Each task status moves beneath its checkbox and title. The narrower padding leaves more room for task text.
Task rows still stay horizontal?
Check that the 520px media query comes after the 700px query. Confirm that .task-item sets a column direction.
Help me fix the narrow FocusFlow task layout.
✔️ Awesome, I've got everything!
Your src/App.css file now contains the complete FocusFlow dashboard stylesheet.
ⓧ I'd like to double check the full code
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: #172033;
background: #eef2ff;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
* { box-sizing: border-box; }
body { min-width: 320px; min-height: 100vh; margin: 0; }
button, input { font: inherit; }
button { cursor: pointer; }
button:disabled { cursor: not-allowed; opacity: 0.55; }
.app-shell {
display: grid;
min-height: 100vh;
place-items: center;
padding: 32px 16px;
background: radial-gradient(circle at top left, rgba(124, 58, 237, 0.2), transparent 36%), linear-gradient(135deg, #eef2ff 0%, #f8fafc 55%, #ecfeff 100%);
}
.dashboard {
width: min(920px, 100%);
overflow: hidden;
border: 1px solid rgba(148, 163, 184, 0.28);
border-radius: 28px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 28px 70px rgba(30, 41, 59, 0.16);
backdrop-filter: blur(14px);
}
.dashboard-header {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 28px;
align-items: end;
padding: 36px;
color: #fff;
background: linear-gradient(135deg, #5b21b6, #7c3aed 55%, #0891b2);
}
.eyebrow { margin: 0 0 8px; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.12em; text-transform: uppercase; }
.dashboard-header h1, .panel-heading h2 { margin: 0; }
.dashboard-header h1 { font-size: clamp(2.2rem, 7vw, 4.6rem); line-height: 0.95; }
.subtitle { max-width: 460px; margin: 14px 0 0; color: rgba(255, 255, 255, 0.82); font-size: 1.05rem; }
.progress-card { padding: 20px; border: 1px solid rgba(255, 255, 255, 0.28); border-radius: 20px; background: rgba(255, 255, 255, 0.14); }
.progress-summary { display: flex; justify-content: space-between; gap: 16px; align-items: baseline; margin-bottom: 12px; }
.progress-summary span { color: rgba(255, 255, 255, 0.82); }
.progress-summary strong { font-size: 1.7rem; }
.progress-track { height: 10px; overflow: hidden; border-radius: 999px; background: rgba(255, 255, 255, 0.2); }
.progress-track span { display: block; height: 100%; border-radius: inherit; background: #fff; transition: width 180ms ease; }
.task-form { padding: 28px 36px 0; }
.task-form label { display: block; margin-bottom: 9px; color: #475569; font-size: 0.85rem; font-weight: 800; }
.task-form-row { display: grid; grid-template-columns: 1fr auto; gap: 12px; }
.task-form input { min-width: 0; padding: 14px 16px; border: 1px solid #cbd5e1; border-radius: 14px; outline: none; background: #fff; }
.task-form input:focus { border-color: #7c3aed; box-shadow: 0 0 0 4px rgba(124, 58, 237, 0.12); }
.task-form button, .filters button { border: 0; border-radius: 12px; font-weight: 800; }
.task-form button { padding: 0 22px; color: #fff; background: #5b21b6; }
.task-panel { padding: 30px 36px 38px; }
.panel-heading { display: flex; justify-content: space-between; gap: 20px; align-items: end; margin-bottom: 18px; }
.panel-heading .eyebrow { color: #7c3aed; }
.panel-heading h2 { font-size: 1.5rem; }
.filters { display: flex; gap: 8px; padding: 5px; border-radius: 14px; background: #f1f5f9; }
.filters button { padding: 8px 12px; color: #64748b; background: transparent; }
.filters button.active { color: #5b21b6; background: #fff; box-shadow: 0 4px 12px rgba(30, 41, 59, 0.1); }
.task-list { display: grid; gap: 10px; padding: 0; margin: 0; list-style: none; }
.task-item { display: flex; justify-content: space-between; gap: 18px; align-items: center; padding: 17px 18px; border: 1px solid #e2e8f0; border-radius: 16px; background: #fff; transition: border-color 160ms ease, transform 160ms ease; }
.task-item:hover { border-color: #c4b5fd; transform: translateY(-1px); }
.task-item label { display: flex; gap: 12px; align-items: center; min-width: 0; font-weight: 700; }
.task-item input { width: 19px; height: 19px; accent-color: #7c3aed; }
.task-item.completed label span { color: #94a3b8; text-decoration: line-through; }
.task-status { flex: none; padding: 6px 10px; border-radius: 999px; color: #9a3412; background: #ffedd5; font-size: 0.75rem; font-weight: 800; }
.task-item.completed .task-status { color: #166534; background: #dcfce7; }
.empty-state { margin: 0; padding: 30px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; text-align: center; background: #f8fafc; }
@media (max-width: 700px) {
.dashboard-header { grid-template-columns: 1fr; }
.panel-heading { align-items: stretch; flex-direction: column; }
.filters { align-self: flex-start; }
}
@media (max-width: 520px) {
.dashboard-header, .task-form, .task-panel { padding-right: 20px; padding-left: 20px; }
.task-form-row { grid-template-columns: 1fr; }
.task-form button { min-height: 46px; }
.task-item { align-items: flex-start; flex-direction: column; }
}
Before you test the controls, do you think an incomplete checkbox can retain a new value with the current data flow?
- Resize Microsoft Edge to show the full dashboard.
- Click the unchecked box beside Connect task interactions.
The checkbox remains unchecked. Its status stays In progress.
That shortfall is intentional. JSX and props describe the current task data without retaining a user change.
Your component tree now turns task data into a polished dashboard. Next, you will give those controls memory while keeping one shared task list in charge.
Make Task Completion Interactive
The styled React dashboard is in place. Its checkbox test exposed the gap: clicking cannot change the fixed array.
Now App will own the shared task list as state. Each TaskItem will send checkbox changes back to the parent.
That parent-owned event handler gives every row one source of truth.
In this step, get ready to:
- Store the task array in React state.
- Update the selected task with an immutable array update.
- Pass the toggle handler through props to make each checkbox interactive.
Give the dashboard memory
React state gives a component memory between renders. App will own the shared array because it is the closest common parent for every task row.
The snippets in this step show the exact lines that change inside src/App.jsx. Every unmentioned line remains in place.
- In Visual Studio Code, select src/App.jsx in the Explorer sidebar.
- Replace the existing stylesheet import with these two imports:
import { useState } from 'react'
import './App.css'
What do these imports provide?
- The named useState import gives App access to React state.
- The ./App.css import keeps the existing dashboard styles connected.
- Save src/App.jsx.
- Return to Microsoft Edge.
You will see the dashboard reload through Vite with all three task rows still visible.
- Inside App, replace the opening calculations with this state-backed snippet:
const [tasks, setTasks] = useState(initialTasks)
const completedCount = tasks.filter((task) => task.completed).length
const progress = Math.round((completedCount / tasks.length) * 100)
How does the state setup work?
- The tasks value holds the current task array for this render.
- The setTasks function gives App a controlled way to replace that array.
- The completed count reads from tasks so the summary follows the same source of truth.
- Save src/App.jsx.
- Switch back to Microsoft Edge.
You will see 1 of 3 complete with 33% in the progress card. The intact dashboard confirms that its summary now renders from state.
Seeing a blank dashboard?
Check that useState uses the same capitalization in the import and state declaration. Confirm that the state declaration sits inside App.
Also check that both calculations use tasks instead of a misspelled variable name.
Help me fix my React state setup in src/App.jsx.
Update tasks without mutation
An immutable update creates a new array for React to compare. map() returns every unmatched task as-is.
The matching task is copied with spread syntax. Its completed value is inverted inside that copy.
- Inside App, add this handler directly below the tasks state declaration:
function handleToggleTask(taskId) {
setTasks((currentTasks) => currentTasks.map((task) => task.id === taskId ? { ...task, completed: !task.completed } : task))
}
What does the toggle handler do?
- The taskId parameter identifies the row that triggered the update.
- The functional setTasks updater receives the latest task array.
- The ternary expression copies the matching task with the opposite completed value.
- Every unmatched task keeps its existing object reference.
- Replace the current task-list line with this state-backed mapping:
<ul className="task-list">{tasks.map((task) => <TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />)}</ul>
How does the handler reach each row?
- The mapping now reads from the current tasks state array.
- The stable task.id remains the key for each rendered row.
- The onToggleTask prop carries the parent handler into every TaskItem.
- Save src/App.jsx.
- Return to Microsoft Edge.
You will see all three task rows after the refresh. This confirms that App can map its state and pass the handler through the component tree.
Missing the task rows?
Check that the list maps tasks with a lowercase first letter. Confirm that the handler prop uses the exact name onToggleTask.
Make sure the handleToggleTask function sits inside App before its return statement.
Help me find why my FocusFlow task rows disappeared after connecting the toggle handler.
Connect the child interaction
The child now receives onToggleTask from its parent. Its controlled checkbox can call that function whenever the checked value changes.
- Replace the existing TaskItem component with this interactive version:
function TaskItem({ task, onToggleTask }) {
return (
<li className={`task-item ${task.completed ? 'completed' : ''}`}>
<label>
<input type="checkbox" checked={task.completed} onChange={() => onToggleTask(task.id)} />
<span>{task.title}</span>
</label>
<span className="task-status">{task.completed ? 'Done' : 'In progress'}</span>
</li>
)
}
How does the checkbox update its task?
- The component accepts onToggleTask beside its existing task prop.
- The checked value keeps the checkbox synchronized with task.completed.
- The onChange handler sends the stable task ID back to App.
- The next render updates the row styling and status label from the new state.
- Replace the current progress-summary line with this state-backed version:
<div className="progress-summary"><span>{completedCount} of {tasks.length} complete</span><strong>{progress}%</strong></div>
Why update the task total?
The summary now reads both its completed count and total count from tasks. Every progress value therefore comes from the same state array.
- Save src/App.jsx.
- Switch back to Microsoft Edge.
Before you click a checkbox, what should change after the first click and reverse after the second click?
- Click the unchecked box beside Connect task interactions.
You will see that row gain completed styling. Its status changes to Done. The progress card updates to 2 of 3 complete and 67%.
- Click the same checkbox again.
You will see the row return to In progress. The progress card returns to 1 of 3 complete and 33%.
- Click the unchecked box beside Plan the first API endpoint.
That row updates independently while the other rows keep their current values. You have connected child interaction to one parent-owned task state.
Checkbox still not changing?
Check that TaskItem accepts onToggleTask in its parameter list. Confirm that the checkbox uses onChange instead of readOnly.
Make sure the handler receives task.id. A different value will prevent the matching task from being found.
Help me debug why my FocusFlow checkbox does not toggle its task state.
✔️ Awesome, I've got everything!
Your src/App.jsx file is saved. Each task can now toggle independently through the parent-owned handler.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import './App.css'
const initialTasks = [
{ id: 1, title: 'Create the dashboard layout', completed: true },
{ id: 2, title: 'Connect task interactions', completed: false },
{ id: 3, title: 'Plan the first API endpoint', completed: false },
]
function TaskItem({ task, onToggleTask }) {
return (
<li className={`task-item ${task.completed ? 'completed' : ''}`}>
<label>
<input type="checkbox" checked={task.completed} onChange={() => onToggleTask(task.id)} />
<span>{task.title}</span>
</label>
<span className="task-status">{task.completed ? 'Done' : 'In progress'}</span>
</li>
)
}
export default function App() {
const [tasks, setTasks] = useState(initialTasks)
function handleToggleTask(taskId) {
setTasks((currentTasks) => currentTasks.map((task) => task.id === taskId ? { ...task, completed: !task.completed } : task))
}
const completedCount = tasks.filter((task) => task.completed).length
const progress = Math.round((completedCount / tasks.length) * 100)
return (
<main className="app-shell"><section className="dashboard"><header className="dashboard-header"><div><p className="eyebrow">React learning project</p><h1>FocusFlow</h1><p className="subtitle">Turn today's plan into visible progress.</p></div><div className="progress-card"><div className="progress-summary"><span>{completedCount} of {tasks.length} complete</span><strong>{progress}%</strong></div><div className="progress-track"><span style={{ width: `${progress}%` }} /></div></div></header><section className="task-panel"><div className="panel-heading"><div><p className="eyebrow">Current workload</p><h2>Today's sprint</h2></div></div><ul className="task-list">{tasks.map((task) => <TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />)}</ul></section></section></main>
)
}
How to use this reference
This is the complete src/App.jsx file for this step. Compare its imports, component props, handler, calculations, and JSX with your saved file.
Task completion now flows from each checkbox into the shared task array. Next, you will add a controlled form that creates new tasks from user input.
Add Tasks with a Controlled Form
Your task rows now respond independently because React keeps the shared array in App. FocusFlow still only works with the starter tasks.
A controlled form keeps its field synchronized with state. A submit handler sends validated titles to App for insertion into the shared array.
In this step, get ready to:
- Build a controlled input that stores its draft title in local state.
- Validate submitted text before it reaches the parent.
- Append each accepted task to the shared array with a stable ID.
Build the controlled form
The form needs local state because the draft title only matters while someone is typing. Its submit handler cleans that title before sharing it with the dashboard.
- In src/App.jsx, place this component below initialTasks by copying the code:
function TaskForm({ onAddTask }) {
const [title, setTitle] = useState('')
function handleSubmit(event) {
event.preventDefault()
const trimmedTitle = title.trim()
if (!trimmedTitle) return
onAddTask(trimmedTitle)
setTitle('')
}
return <form className="task-form" onSubmit={handleSubmit}><label htmlFor="task-title">New task</label><div className="task-form-row"><input id="task-title" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="e.g. Build a login form" /><button type="submit" disabled={!title.trim()}>Add task</button></div></form>
}
What does this component do?
- The title state stores the current field text.
- The value={title} binding makes that state control the input.
- The onChange handler updates the state after each input event.
- The disabled={!title.trim()} condition prevents button submission when the field contains no meaningful text.
- The event.preventDefault() call keeps the browser on the current page.
- The trimmedTitle value removes surrounding whitespace.
- The empty-title guard stops invalid text before it reaches the parent.
- The onAddTask prop passes an accepted title upward.
- The setTitle('') call clears the field after a successful submission.
- Save src/App.jsx.
Your existing FocusFlow dashboard should remain available without a compile error. This confirms the new component has valid syntax.
Seeing a compile error?
Make sure TaskForm sits after the closing ] for initialTasks. Keep it outside every other function.
If Vite still reports a problem, help me compare my TaskForm component with the required FocusFlow code..
Connect task creation to App
The parent creates each task because it owns tasks. A stable numeric ID lets React track the new row through future updates.
- In src/App.jsx, add this helper between initialTasks and TaskForm by copying the code:
function getNextTaskId(tasks) {
return tasks.reduce((highestId, task) => Math.max(highestId, task.id), 0) + 1
}
How is the next ID chosen?
- The reduce() call checks every existing task ID.
- The Math.max call keeps the highest ID found so far.
- Adding 1 produces the next stable numeric ID.
- In src/App.jsx, find the line that starts with function TaskItem.
- Replace everything from that line to the end of the file with the code below:
function TaskItem({ task, onToggleTask }) {
return <li className={`task-item ${task.completed ? 'completed' : ''}`}><label><input type="checkbox" checked={task.completed} onChange={() => onToggleTask(task.id)} /><span>{task.title}</span></label><span className="task-status">{task.completed ? 'Done' : 'In progress'}</span></li>
}
export default function App() {
const [tasks, setTasks] = useState(initialTasks)
function handleAddTask(title) { setTasks((currentTasks) => [...currentTasks, { id: getNextTaskId(currentTasks), title, completed: false }]) }
function handleToggleTask(taskId) { setTasks((currentTasks) => currentTasks.map((task) => task.id === taskId ? { ...task, completed: !task.completed } : task)) }
const completedCount = tasks.filter((task) => task.completed).length
const progress = tasks.length === 0 ? 0 : Math.round((completedCount / tasks.length) * 100)
return <main className="app-shell"><section className="dashboard"><header className="dashboard-header"><div><p className="eyebrow">React learning project</p><h1>FocusFlow</h1><p className="subtitle">Turn today's plan into visible progress.</p></div><div className="progress-card"><div className="progress-summary"><span>{completedCount} of {tasks.length} complete</span><strong>{progress}%</strong></div><div className="progress-track"><span style={{ width: `${progress}%` }} /></div></div></header><TaskForm onAddTask={handleAddTask} /><section className="task-panel"><div className="panel-heading"><div><p className="eyebrow">Current workload</p><h2>Today's sprint</h2></div></div><ul className="task-list">{tasks.map((task) => <TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />)}</ul></section></section></main>
}
How does the parent update work?
- The handleAddTask function receives a validated title from TaskForm.
- The functional setTasks updater receives the latest task array.
- The spread syntax creates a new array without mutating the existing state.
- The appended object gets a stable ID from getNextTaskId.
- The new task starts with completed: false so it appears unchecked.
- The onAddTask={handleAddTask} prop connects the child submission to the parent update.
- The progress calculation now handles an empty task array safely.
- The TaskItem prop flow remains intact so every checkbox still toggles independently.
- Save src/App.jsx.
- Return to Microsoft Edge.
You'll see a New task field above the sprint panel. You'll also see an Add task button beside it.
Form missing from the dashboard?
Make sure <TaskForm onAddTask={handleAddTask} /> appears between the dashboard header and the task panel. Confirm that getNextTaskId appears above TaskForm.
If the form still does not render, help me trace the TaskForm and handleAddTask connection in my FocusFlow app..
✔️ Awesome, I've got everything!
Your file now contains the controlled form and its parent update path.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import './App.css'
const initialTasks = [
{ id: 1, title: 'Create the dashboard layout', completed: true },
{ id: 2, title: 'Connect task interactions', completed: false },
{ id: 3, title: 'Plan the first API endpoint', completed: false },
]
function getNextTaskId(tasks) {
return tasks.reduce((highestId, task) => Math.max(highestId, task.id), 0) + 1
}
function TaskForm({ onAddTask }) {
const [title, setTitle] = useState('')
function handleSubmit(event) {
event.preventDefault()
const trimmedTitle = title.trim()
if (!trimmedTitle) return
onAddTask(trimmedTitle)
setTitle('')
}
return <form className="task-form" onSubmit={handleSubmit}><label htmlFor="task-title">New task</label><div className="task-form-row"><input id="task-title" value={title} onChange={(event) => setTitle(event.target.value)} placeholder="e.g. Build a login form" /><button type="submit" disabled={!title.trim()}>Add task</button></div></form>
}
function TaskItem({ task, onToggleTask }) {
return <li className={`task-item ${task.completed ? 'completed' : ''}`}><label><input type="checkbox" checked={task.completed} onChange={() => onToggleTask(task.id)} /><span>{task.title}</span></label><span className="task-status">{task.completed ? 'Done' : 'In progress'}</span></li>
}
export default function App() {
const [tasks, setTasks] = useState(initialTasks)
function handleAddTask(title) { setTasks((currentTasks) => [...currentTasks, { id: getNextTaskId(currentTasks), title, completed: false }]) }
function handleToggleTask(taskId) { setTasks((currentTasks) => currentTasks.map((task) => task.id === taskId ? { ...task, completed: !task.completed } : task)) }
const completedCount = tasks.filter((task) => task.completed).length
const progress = tasks.length === 0 ? 0 : Math.round((completedCount / tasks.length) * 100)
return <main className="app-shell"><section className="dashboard"><header className="dashboard-header"><div><p className="eyebrow">React learning project</p><h1>FocusFlow</h1><p className="subtitle">Turn today's plan into visible progress.</p></div><div className="progress-card"><div className="progress-summary"><span>{completedCount} of {tasks.length} complete</span><strong>{progress}%</strong></div><div className="progress-track"><span style={{ width: `${progress}%` }} /></div></div></header><TaskForm onAddTask={handleAddTask} /><section className="task-panel"><div className="panel-heading"><div><p className="eyebrow">Current workload</p><h2>Today's sprint</h2></div></div><ul className="task-list">{tasks.map((task) => <TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />)}</ul></section></section></main>
}
Test valid and empty submissions
Prediction: will the new title appear without a page reload.
- Type Build a login form into the New task field.
- Click Add task.
You'll see Build a login form as a new unchecked row. The input clears after submission.
The task total rises by one. The progress percentage updates from the same array.
Prediction: can an empty title reach the task list.
- Press Enter while the New task field is empty.
The task list stays the same. The task total also stays the same.
- Select the checkbox beside Build a login form.
You'll see the new row gain completed styling. Its status changes to Done.
Task not added correctly?
If the page reloads, confirm that the form uses onSubmit={handleSubmit}. Check that handleSubmit calls event.preventDefault().
If no row appears, confirm that handleAddTask appends the new object inside the functional setTasks updater. You can also ask for help debugging the form submission flow..
You've crossed an important line: FocusFlow now accepts your own tasks without a reload.
Your dashboard now accepts validated input while preserving its independent task toggles. Next, you'll derive filtered views and live progress from the same task array.
Filter Tasks and Calculate Progress
Your controlled form can now add tasks to the shared React state. Each checkbox can also update its own task without disturbing the other rows.
Separate stored totals or filtered copies would eventually drift away from that task array. This step keeps one source of truth by deriving every view from the current tasks.
In this step, get ready to:
- Add status filters backed by their own selected-filter state.
- Derive the visible rows from the current tasks.
- Connect accessible progress information to the live task data.
Add filter state and controls
The selected filter is a user choice that must persist between renders. A small filter state tells the dashboard which task view to display.
- In src/App.jsx, add the filter options directly below the initialTasks array by copying this code:
const filters = [
{ id: 'all', label: 'All' },
{ id: 'active', label: 'Active' },
{ id: 'completed', label: 'Completed' },
]
What do the filter options represent?
- Each id supplies the value that the filtering logic checks.
- Each label supplies the text shown on its button.
- Keeping these options in one array lets the interface render every filter with map().
- Add the FilterBar component between TaskForm and TaskItem by copying this code:
function FilterBar({ currentFilter, onFilterChange }) {
return (
<div className="filters" aria-label="Filter tasks">
{filters.map((filterOption) => (
<button
key={filterOption.id}
type="button"
className={currentFilter === filterOption.id ? 'active' : ''}
onClick={() => onFilterChange(filterOption.id)}
>
{filterOption.label}
</button>
))}
</div>
)
}
What does this component do?
- The currentFilter prop identifies the selected option.
- The active class gives the selected button its existing highlighted style.
- The onFilterChange prop sends a clicked filter ID back to App.
- Stable filterOption.id values give the mapped JSX buttons reliable keys.
- Save src/App.jsx.
- Return to Microsoft Edge.
Your dashboard remains rendered without an error overlay. This confirms the new array and component compile successfully.
Seeing an error overlay?
Check that the filters array closes with ]. Confirm that FilterBar sits outside every other component.
Compare the capitalization of currentFilter and onFilterChange with the code above.
Help me fix the filter array or FilterBar component in my React app.
- Inside App(), add the filter state directly below the task state by copying these lines:
const [tasks, setTasks] = useState(initialTasks)
const [filter, setFilter] = useState('all')
Why store the selected filter?
The filter value records the current view. Calling setFilter causes React to render the dashboard again with the new selection.
The initial value all keeps every task visible when the dashboard first loads.
- Replace the existing panel-heading block with this version:
<div className="panel-heading">
<div>
<p className="eyebrow">Current workload</p>
<h2>Today's sprint</h2>
</div>
<FilterBar currentFilter={filter} onFilterChange={setFilter} />
</div>
How do the controls reach state?
The FilterBar receives the current value through currentFilter. It receives the state setter through onFilterChange.
This prop flow keeps the selected value inside App while the child component handles the buttons.
- Save src/App.jsx.
- Switch back to Microsoft Edge.
You should see All, Active, and Completed buttons beside the sprint heading. The All button should have the selected style.
Do the filter buttons look unselected?
Confirm that the filter state starts with all. Check that the conditional class uses currentFilter === filterOption.id.
Make sure the component receives currentFilter={filter} from App.
Help me troubleshoot why the selected FocusFlow filter is not highlighted.
Derive the visible task list
The selected filter describes a view of the existing task array. A derived list can calculate that view during each render without creating another state value to synchronize.
- Inside App(), add the visible-task calculation directly below the existing progress calculation by copying this code:
const visibleTasks = tasks.filter((task) => {
if (filter === 'active') return !task.completed
if (filter === 'completed') return task.completed
return true
})
How does the derived list work?
- The active branch keeps tasks whose completed value is false.
- The completed branch keeps tasks whose completed value is true.
- The final return keeps every task for the all view.
- The original tasks array remains the source of truth for every view.
- Replace the existing task-list element with this conditional rendering block:
{visibleTasks.length > 0 ? (
<ul className="task-list">
{visibleTasks.map((task) => (
<TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />
))}
</ul>
) : (
<p className="empty-state">No tasks match this filter.</p>
)}
What does the condition display?
The list renders when visibleTasks contains at least one task. Every TaskItem keeps its stable task.id key.
An empty result activates conditional rendering and shows the empty-state message.
- Save src/App.jsx.
- Return to Microsoft Edge.
- Click the Active filter.
You should see only incomplete tasks. The Active button should now have the selected style.
- Mark every visible task as complete.
- Select the Active filter again.
You should see No tasks match this filter. in place of the list. This proves the empty state comes from the derived result.
- Select the All filter.
Every task should return with completed styling. The rows still use the same underlying task objects.
Do all filters show the same rows?
Confirm that the task list maps over visibleTasks instead of tasks. Check that each comparison uses the lowercase IDs from the filters array.
Verify that the active branch returns !task.completed while the completed branch returns task.completed.
Help me debug why my FocusFlow filters show the wrong tasks.
Connect accessible progress and test the workflow
The dashboard already calculates completedCount and progress from the task array. Adding progress-bar attributes exposes the same live percentage to assistive technology.
- In the progress card inside src/App.jsx, replace the opening progress-track element with this version:
<div
className="progress-track"
role="progressbar"
aria-label="Task completion"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progress}
>
Why add these progress attributes?
- The progressbar role identifies the element as a progress indicator.
- The accessible label describes what the percentage measures.
- The minimum and maximum establish the percentage range.
- The aria-valuenow value follows the same derived progress number shown on screen.
- Save src/App.jsx.
Before the final test, the question is whether one task change updates every dashboard view together.
- Return to Microsoft Edge.
- Select the All filter.
- Enter Review sprint demo in the New task field.
- Click Add task.
You should see the new unchecked row immediately. The task total should increase while the completed count stays fixed, so the percentage should fall.
- Select the checkbox beside Review sprint demo.
The row should gain completed styling and show Done. The completed count and percentage should increase from the same task update.
- Select the Active filter.
You should see the empty-state message because every task is complete. The progress card should continue showing the totals for the full task array.
- Select the Completed filter.
You should see every completed task including Review sprint demo.
- Select the All filter.
You should see the full list again with the same completed count and percentage. That completes the dashboard loop: adding, completing, filtering, and progress all agree with one task state array.
Does the progress disagree with the list?
Confirm that completedCount filters the full tasks array. Confirm that progress also uses tasks.length.
The visible rows should use visibleTasks, while the summary should use tasks.
Help me find why my FocusFlow progress summary disagrees with the filtered list.
✔️ Awesome, I've got everything!
Great work. Save src/App.jsx once more so your completed dashboard code is ready for the checkpoint.
ⓧ I'd like to double check the full code
import { useState } from 'react'
import './App.css'
const initialTasks = [
{ id: 1, title: 'Create the dashboard layout', completed: true },
{ id: 2, title: 'Connect task interactions', completed: false },
{ id: 3, title: 'Plan the first API endpoint', completed: false },
]
const filters = [
{ id: 'all', label: 'All' },
{ id: 'active', label: 'Active' },
{ id: 'completed', label: 'Completed' },
]
function getNextTaskId(tasks) {
return tasks.reduce((highestId, task) => Math.max(highestId, task.id), 0) + 1
}
function TaskForm({ onAddTask }) {
const [title, setTitle] = useState('')
function handleSubmit(event) {
event.preventDefault()
const trimmedTitle = title.trim()
if (!trimmedTitle) return
onAddTask(trimmedTitle)
setTitle('')
}
return (
<form className="task-form" onSubmit={handleSubmit}>
<label htmlFor="task-title">New task</label>
<div className="task-form-row">
<input
id="task-title"
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder="e.g. Build a login form"
/>
<button type="submit" disabled={!title.trim()}>Add task</button>
</div>
</form>
)
}
function FilterBar({ currentFilter, onFilterChange }) {
return (
<div className="filters" aria-label="Filter tasks">
{filters.map((filterOption) => (
<button
key={filterOption.id}
type="button"
className={currentFilter === filterOption.id ? 'active' : ''}
onClick={() => onFilterChange(filterOption.id)}
>
{filterOption.label}
</button>
))}
</div>
)
}
function TaskItem({ task, onToggleTask }) {
return (
<li className={`task-item ${task.completed ? 'completed' : ''}`}>
<label>
<input
type="checkbox"
checked={task.completed}
onChange={() => onToggleTask(task.id)}
/>
<span>{task.title}</span>
</label>
<span className="task-status">{task.completed ? 'Done' : 'In progress'}</span>
</li>
)
}
export default function App() {
const [tasks, setTasks] = useState(initialTasks)
const [filter, setFilter] = useState('all')
function handleAddTask(title) {
setTasks((currentTasks) => [
...currentTasks,
{ id: getNextTaskId(currentTasks), title, completed: false },
])
}
function handleToggleTask(taskId) {
setTasks((currentTasks) =>
currentTasks.map((task) =>
task.id === taskId ? { ...task, completed: !task.completed } : task,
),
)
}
const completedCount = tasks.filter((task) => task.completed).length
const progress = tasks.length === 0 ? 0 : Math.round((completedCount / tasks.length) * 100)
const visibleTasks = tasks.filter((task) => {
if (filter === 'active') return !task.completed
if (filter === 'completed') return task.completed
return true
})
return (
<main className="app-shell">
<section className="dashboard">
<header className="dashboard-header">
<div>
<p className="eyebrow">React learning project</p>
<h1>FocusFlow</h1>
<p className="subtitle">Turn today's plan into visible progress.</p>
</div>
<div className="progress-card">
<div className="progress-summary">
<span>{completedCount} of {tasks.length} complete</span>
<strong>{progress}%</strong>
</div>
<div
className="progress-track"
role="progressbar"
aria-label="Task completion"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow={progress}
>
<span style={{ width: `${progress}%` }} />
</div>
</div>
</header>
<TaskForm onAddTask={handleAddTask} />
<section className="task-panel">
<div className="panel-heading">
<div>
<p className="eyebrow">Current workload</p>
<h2>Today's sprint</h2>
</div>
<FilterBar currentFilter={filter} onFilterChange={setFilter} />
</div>
{visibleTasks.length > 0 ? (
<ul className="task-list">
{visibleTasks.map((task) => (
<TaskItem key={task.id} task={task} onToggleTask={handleToggleTask} />
))}
</ul>
) : (
<p className="empty-state">No tasks match this filter.</p>
)}
</section>
</section>
</main>
)
}
Secret mission
Keep Tasks After a Refresh
FocusFlow remembers tasks while its page stays open. In this mission, you will synchronize the task array with browser local storage so additions and completion states survive a refresh.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep FocusFlow available, pause its local server, or remove the project completely. Everything runs on your Windows computer, so keeping these resources creates no ongoing charges.
Resources you used:
- The focusflow-react folder containing the complete project.
- The generated package-lock.json file plus node_modules directory inside focusflow-react.
- The Vite development server process still running in the integrated terminal.
- The focusflow-tasks entry in Microsoft Edge local storage for http://localhost:5173.
Keep everything running
No action needed. Choose this if you plan to keep improving FocusFlow.
- Keep the Vite terminal process running while you continue building.
- Retain the focusflow-react folder in its current location.
- Leave focusflow-tasks in browser local storage so your tasks survive future refreshes.
Pause - I'll come back to this later
Stop the running development server to free local memory. Your files plus saved tasks remain ready for your next session.
- Return to the integrated terminal in Visual Studio Code from earlier.
- Press Ctrl+C to stop the Vite process.
- Keep the focusflow-react folder in its current location.
- Leave the focusflow-tasks entry in browser local storage.
The terminal prompt returns after the server stops. Your dashboard becomes available again when you restart Vite from the same project folder.
Delete - I don't want to use this again
This option clears the saved browser data plus removes the local project. The Vite process must stop before Windows can release every project file.
Save Anything You Need
Deleting focusflow-react removes your working copy of the dashboard. Clearing focusflow-tasks removes the tasks saved for this app origin.
- Return to Microsoft Edge at http://localhost:5173.
- Open the browser console for the FocusFlow page.
- Remove the saved task data by running this command:
localStorage.removeItem("focusflow-tasks")
What Does This Command Do?
The command removes only the focusflow-tasks key for http://localhost:5173. The rest of Microsoft Edge's stored website data stays in place.
- Return to the Visual Studio Code integrated terminal from earlier.
- Press Ctrl+C to stop the running Vite process.
- Right-click focusflow-react in the Visual Studio Code Explorer sidebar.
- Select Reveal in File Explorer.
- Close Visual Studio Code.
- Return to File Explorer.
- Right-click the selected focusflow-react folder.
- Select Delete.
- Confirm that focusflow-react no longer appears in its original location.
Cleanup complete. Your project folder plus its saved browser task data are removed.
Nice Work!
Nice Work!
You did it! FocusFlow is now a working task dashboard built with React and served locally by Vite.
You've learned how to:
- Compose reusable React components with JSX and props so App can pass task data into each focused part of the dashboard.
- Manage one shared task array with useState and immutable updates. Connect a controlled form and checkbox handlers to that state so every task change redraws the dashboard immediately.
- Derive filtered task views from the current task array. Calculate the live progress summary from that same source of truth.
- Secret Mission: Synchronize task state with browser local storage through useEffect so added and completed tasks survive a refresh.
Ready to quiz yourself?